Questions d'entretien Laravel et PHP : Top 25 en 2026

Les 25 questions d'entretien Laravel et PHP les plus posées. Eloquent ORM, middleware, artisan, queues, tests et architecture avec réponses détaillées et exemples de code.

Questions d'entretien Laravel et PHP - Guide complet

Les entretiens Laravel évaluent la maîtrise du framework PHP le plus populaire, la compréhension de l'ORM Eloquent, l'architecture MVC, et la capacité à construire des applications robustes et maintenables. Ce guide couvre les 25 questions les plus posées, des fondamentaux Laravel jusqu'aux patterns avancés de déploiement production.

Conseil pour l'entretien

Les recruteurs apprécient les candidats qui expliquent les choix architecturaux de Laravel. Comprendre pourquoi le framework adopte certaines conventions (Convention over Configuration, Service Container) fait la différence en entretien.

Fondamentaux Laravel

Question 1 : Expliquez le cycle de vie d'une requête dans Laravel

Le cycle de vie d'une requête Laravel traverse plusieurs couches avant d'atteindre le contrôleur. Cette compréhension est essentielle pour le debugging et l'optimisation des performances.

public/index.phpphp
// Point d'entrée de toutes les requêtes HTTP
require __DIR__.'/../vendor/autoload.php';

// Chargement de l'application Laravel
$app = require_once __DIR__.'/../bootstrap/app.php';

// Le kernel HTTP traite la requête
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);

Le cycle complet : index.php → autoload → bootstrap → service providers → middlewares → routing → controller → response → terminate. Chaque étape peut être interceptée et personnalisée.

Question 2 : Qu'est-ce que le Service Container et comment fonctionne l'injection de dépendances ?

Le Service Container est le cœur de Laravel. Il gère l'instanciation des classes et résout automatiquement les dépendances via l'injection de constructeur.

app/Services/PaymentService.phpphp
// Service avec dépendances injectées automatiquement
namespace App\Services;

use App\Contracts\PaymentGatewayInterface;
use App\Repositories\OrderRepository;
use Illuminate\Support\Facades\Log;

class PaymentService
{
    public function __construct(
        private PaymentGatewayInterface $gateway, // Interface résolue par le container
        private OrderRepository $orders           // Classe concrète auto-résolue
    ) {}

    public function processPayment(int $orderId, float $amount): bool
    {
        $order = $this->orders->find($orderId);

        try {
            $result = $this->gateway->charge($amount, $order->customer);
            $order->markAsPaid($result->transactionId);
            return true;
        } catch (PaymentException $e) {
            Log::error('Payment failed', ['order' => $orderId, 'error' => $e->getMessage()]);
            return false;
        }
    }
}
app/Providers/AppServiceProvider.phpphp
// Binding d'une interface à une implémentation concrète
public function register(): void
{
    // Binding simple : nouvelle instance à chaque injection
    $this->app->bind(
        PaymentGatewayInterface::class,
        StripeGateway::class
    );

    // Singleton : même instance partagée partout
    $this->app->singleton(
        CacheService::class,
        fn($app) => new CacheService($app['config']['cache.driver'])
    );
}

L'injection de dépendances permet de découpler les classes et facilite les tests unitaires via le mocking.

Question 3 : Quelle est la différence entre Facades et injection de dépendances ?

Les Facades fournissent une syntaxe statique pour accéder aux services du container, tandis que l'injection de dépendances rend les dépendances explicites.

php
// Utilisation des Facades : syntaxe concise mais dépendances implicites
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class ReportController extends Controller
{
    public function generate()
    {
        // Facades : accès statique aux services
        $data = Cache::remember('report_data', 3600, fn() => $this->fetchData());
        Log::info('Report generated');
        return view('report', compact('data'));
    }
}

// Injection de dépendances : dépendances explicites et testables
use Illuminate\Contracts\Cache\Repository as CacheContract;
use Psr\Log\LoggerInterface;

class ReportController extends Controller
{
    public function __construct(
        private CacheContract $cache,
        private LoggerInterface $logger
    ) {}

    public function generate()
    {
        // Même fonctionnalité, dépendances explicites
        $data = $this->cache->remember('report_data', 3600, fn() => $this->fetchData());
        $this->logger->info('Report generated');
        return view('report', compact('data'));
    }
}

Préférer l'injection de dépendances dans les classes métier pour la testabilité. Les Facades conviennent pour les helpers et le code ponctuel.

Question 4 : Comment fonctionnent les Service Providers dans Laravel ?

Les Service Providers sont le point central de configuration de l'application. Chaque provider enregistre des services, configure des bindings et initialise des composants.

app/Providers/PaymentServiceProvider.phpphp
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\PaymentService;
use App\Contracts\PaymentGatewayInterface;
use App\Gateways\StripeGateway;

class PaymentServiceProvider extends ServiceProvider
{
    // Méthode register : bindings et enregistrements
    // Ne pas accéder aux autres services ici (pas encore chargés)
    public function register(): void
    {
        $this->app->singleton(PaymentGatewayInterface::class, function ($app) {
            return new StripeGateway(
                config('services.stripe.key'),
                config('services.stripe.secret')
            );
        });
    }

    // Méthode boot : initialisation après tous les providers
    // Accès complet à tous les services de l'application
    public function boot(): void
    {
        // Enregistrement de macros, event listeners, routes, etc.
        $this->loadRoutesFrom(__DIR__.'/../routes/payment.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'payment');

        // Publication de fichiers pour les packages
        $this->publishes([
            __DIR__.'/../config/payment.php' => config_path('payment.php'),
        ], 'payment-config');
    }
}

L'ordre d'exécution : tous les register() d'abord, puis tous les boot(). Cet ordre garantit que les dépendances sont disponibles lors du boot.

Eloquent ORM

Question 5 : Expliquez les relations Eloquent et leurs différences

Eloquent propose plusieurs types de relations pour modéliser les associations entre tables. Chaque type a ses cas d'usage spécifiques.

app/Models/User.phpphp
class User extends Model
{
    // Un utilisateur a un seul profil (1:1)
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }

    // Un utilisateur a plusieurs articles (1:N)
    public function articles(): HasMany
    {
        return $this->hasMany(Article::class);
    }

    // Un utilisateur a plusieurs rôles via table pivot (N:N)
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class)
            ->withPivot('assigned_at')        // Colonnes supplémentaires du pivot
            ->withTimestamps();                // created_at/updated_at sur le pivot
    }

    // Un utilisateur a plusieurs commentaires via articles (HasManyThrough)
    public function comments(): HasManyThrough
    {
        return $this->hasManyThrough(
            Comment::class,    // Modèle final
            Article::class     // Modèle intermédiaire
        );
    }
}

// app/Models/Article.php
class Article extends Model
{
    // Un article appartient à un utilisateur (inverse de hasMany)
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    // Relation polymorphique : un article peut avoir des tags, comme d'autres modèles
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Les relations polymorphiques (morphOne, morphMany, morphToMany) permettent à un modèle d'être associé à plusieurs autres types de modèles via une seule relation.

Question 6 : Qu'est-ce que le problème N+1 et comment le résoudre avec Eager Loading ?

Le problème N+1 survient quand une requête principale génère N requêtes supplémentaires pour charger les relations. C'est la cause la plus fréquente de lenteur dans les applications Laravel.

php
// ❌ PROBLÈME : N+1 requêtes
// 1 requête pour les articles + 1 requête PAR article pour l'auteur
$articles = Article::all();
foreach ($articles as $article) {
    echo $article->author->name; // Requête SQL à chaque itération !
}

// ✅ SOLUTION 1 : with() - Eager Loading
// 2 requêtes seulement (articles + users avec IN clause)
$articles = Article::with('author')->get();
foreach ($articles as $article) {
    echo $article->author->name; // Déjà chargé, pas de requête
}

// ✅ SOLUTION 2 : Eager Loading imbriqué
// Charge les articles, leurs auteurs, et les rôles des auteurs
$articles = Article::with(['author.roles', 'comments.user'])->get();

// ✅ SOLUTION 3 : Eager Loading avec contraintes
$articles = Article::with([
    'comments' => function ($query) {
        $query->where('approved', true)
              ->orderBy('created_at', 'desc')
              ->limit(5);
    }
])->get();

// ✅ SOLUTION 4 : Eager Loading par défaut dans le modèle
class Article extends Model
{
    // Ces relations sont toujours chargées automatiquement
    protected $with = ['author', 'category'];
}

Utiliser php artisan telescope:prune avec Laravel Telescope pour détecter les problèmes N+1 en développement.

Question 7 : Comment créer des Query Scopes et quand les utiliser ?

Les Query Scopes encapsulent des conditions de requête réutilisables au niveau du modèle, rendant le code plus lisible et DRY.

app/Models/Article.phpphp
class Article extends Model
{
    // Global Scope : appliqué automatiquement à TOUTES les requêtes
    protected static function booted(): void
    {
        // Exclut les articles supprimés soft-delete par défaut
        static::addGlobalScope('published', function (Builder $builder) {
            $builder->where('status', 'published');
        });
    }

    // Local Scope : appelé explicitement via scopeNomDuScope
    public function scopePopular(Builder $query, int $minViews = 1000): Builder
    {
        return $query->where('view_count', '>=', $minViews);
    }

    public function scopeByAuthor(Builder $query, User $author): Builder
    {
        return $query->where('user_id', $author->id);
    }

    public function scopeRecent(Builder $query, int $days = 7): Builder
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }

    public function scopeWithStats(Builder $query): Builder
    {
        return $query->withCount('comments')
                     ->withSum('reactions', 'score');
    }
}

// Utilisation : chaînage fluide des scopes
$articles = Article::popular(500)
    ->recent(30)
    ->byAuthor($user)
    ->withStats()
    ->orderByDesc('comment_count')
    ->paginate(20);

// Ignorer un Global Scope
$allArticles = Article::withoutGlobalScope('published')->get();

Les scopes locaux améliorent la lisibilité et centralisent la logique de requête. Les global scopes conviennent pour le multi-tenancy ou le soft delete.

Prêt à réussir tes entretiens Laravel ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Middleware et Routing

Question 8 : Comment fonctionnent les middlewares dans Laravel ?

Les middlewares filtrent les requêtes HTTP entrantes et peuvent modifier les réponses sortantes. Chaque requête traverse une pile de middlewares.

app/Http/Middleware/CheckSubscription.phpphp
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckSubscription
{
    public function handle(Request $request, Closure $next, string $plan = 'basic'): Response
    {
        $user = $request->user();

        // Vérification avant le contrôleur
        if (!$user || !$user->hasActiveSubscription($plan)) {
            if ($request->expectsJson()) {
                return response()->json(['error' => 'Subscription required'], 403);
            }
            return redirect()->route('subscription.plans');
        }

        // Passe au middleware suivant ou au contrôleur
        $response = $next($request);

        // Modification de la réponse après le contrôleur
        $response->headers->set('X-Subscription-Plan', $user->subscription->plan);

        return $response;
    }
}

// bootstrap/app.php (Laravel 11+)
return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Middleware global (toutes les requêtes)
        $middleware->append(LogRequestMiddleware::class);

        // Alias pour utilisation dans les routes
        $middleware->alias([
            'subscription' => CheckSubscription::class,
            'role' => EnsureUserHasRole::class,
        ]);

        // Groupes de middlewares
        $middleware->group('api', [
            ThrottleRequests::class.':api',
            SubstituteBindings::class,
        ]);
    });
routes/web.phpphp
// Application des middlewares aux routes
Route::middleware(['auth', 'subscription:premium'])->group(function () {
    Route::get('/dashboard', DashboardController::class);
    Route::resource('projects', ProjectController::class);
});

L'ordre des middlewares est important : ils s'exécutent de haut en bas à l'entrée et de bas en haut à la sortie.

Question 9 : Expliquez le Route Model Binding et ses variantes

Le Route Model Binding injecte automatiquement des modèles Eloquent dans les contrôleurs en fonction des paramètres d'URL.

routes/web.phpphp
// Binding implicite : Laravel résout automatiquement par ID
Route::get('/articles/{article}', [ArticleController::class, 'show']);

// Binding par slug au lieu de l'ID
Route::get('/articles/{article:slug}', [ArticleController::class, 'show']);

// Binding avec relation (scope automatique)
Route::get('/users/{user}/articles/{article}', function (User $user, Article $article) {
    // Laravel vérifie automatiquement que l'article appartient à l'user
    return $article;
})->scopeBindings();
app/Models/Article.phpphp
class Article extends Model
{
    // Personnalisation de la clé de résolution par défaut
    public function getRouteKeyName(): string
    {
        return 'slug'; // Résout par slug au lieu de id
    }

    // Personnalisation de la requête de résolution
    public function resolveRouteBinding($value, $field = null): ?Model
    {
        return $this->where($field ?? 'slug', $value)
                    ->where('status', 'published')
                    ->firstOrFail();
    }
}
app/Providers/RouteServiceProvider.phpphp
// Binding explicite personnalisé
public function boot(): void
{
    Route::bind('article', function (string $value) {
        return Article::where('slug', $value)
            ->published()
            ->with('author')
            ->firstOrFail();
    });
}

Le Route Model Binding réduit le code boilerplate et centralise la logique de résolution.

Queues et Jobs

Question 10 : Comment implémenter les Jobs et Queues dans Laravel ?

Les queues permettent de différer l'exécution de tâches lourdes en arrière-plan, améliorant la réactivité de l'application.

app/Jobs/ProcessPodcast.phpphp
namespace App\Jobs;

use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // Nombre de tentatives avant échec définitif
    public int $tries = 3;

    // Timeout en secondes
    public int $timeout = 600;

    // Délai entre les tentatives (backoff exponentiel)
    public array $backoff = [30, 60, 120];

    public function __construct(
        public Podcast $podcast
    ) {}

    public function handle(AudioProcessor $processor): void
    {
        // Le job s'exécute en arrière-plan
        $processor->transcode($this->podcast->audio_path);
        $processor->generateWaveform($this->podcast);

        $this->podcast->update(['status' => 'processed']);
    }

    // Gestion des échecs
    public function failed(\Throwable $exception): void
    {
        $this->podcast->update(['status' => 'failed']);
        // Notification à l'admin, logging, etc.
    }

    // Conditions pour relancer le job
    public function retryUntil(): \DateTime
    {
        return now()->addHours(24);
    }
}
php
// Dispatch du job
ProcessPodcast::dispatch($podcast);                    // File par défaut
ProcessPodcast::dispatch($podcast)->onQueue('audio'); // File spécifique
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10)); // Délai

// Chaînage de jobs (exécution séquentielle)
Bus::chain([
    new ProcessPodcast($podcast),
    new GenerateThumbnail($podcast),
    new NotifySubscribers($podcast),
])->dispatch();

// Batch de jobs (exécution parallèle avec suivi)
Bus::batch([
    new ProcessPodcast($podcast1),
    new ProcessPodcast($podcast2),
    new ProcessPodcast($podcast3),
])->then(function (Batch $batch) {
    // Tous les jobs réussis
})->catch(function (Batch $batch, \Throwable $e) {
    // Premier échec
})->finally(function (Batch $batch) {
    // Tous les jobs terminés (succès ou échec)
})->dispatch();

Lancer le worker avec php artisan queue:work --queue=high,default pour traiter plusieurs files par priorité.

Question 11 : Quelle est la différence entre Jobs, Events et Listeners ?

Les trois concepts servent à découpler le code, mais avec des intentions différentes.

php
// Jobs : tâche unique à exécuter
// Utilisé pour les opérations lourdes ou différées
class SendWelcomeEmail implements ShouldQueue
{
    public function handle(Mailer $mailer): void
    {
        $mailer->send(new WelcomeEmail($this->user));
    }
}

// Events : notification que quelque chose s'est produit
// L'event ne contient que les données, pas la logique
class UserRegistered
{
    public function __construct(
        public User $user,
        public string $source
    ) {}
}

// Listeners : réagissent aux events
// Un event peut avoir plusieurs listeners
class SendWelcomeNotification implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        $event->user->notify(new WelcomeNotification());
    }
}

class TrackRegistration
{
    public function handle(UserRegistered $event): void
    {
        Analytics::track('user_registered', [
            'user_id' => $event->user->id,
            'source' => $event->source,
        ]);
    }
}
app/Providers/EventServiceProvider.phpphp
protected $listen = [
    UserRegistered::class => [
        SendWelcomeNotification::class,  // Queue
        TrackRegistration::class,         // Sync
        CreateDefaultSettings::class,     // Sync
    ],
];

// Déclenchement de l'event
event(new UserRegistered($user, 'web'));
// Ou
UserRegistered::dispatch($user, 'web');

Les events favorisent l'architecture découplée : le code qui déclenche l'event ignore ses conséquences.

Sécurité et Authentification

Question 12 : Comment Laravel protège-t-il contre les attaques CSRF ?

Laravel génère automatiquement un token CSRF unique par session et vérifie ce token sur chaque requête POST, PUT, PATCH, DELETE.

php
// Dans les formulaires Blade
<form method="POST" action="/profile">
    @csrf  {{-- Génère un champ hidden avec le token --}}
    @method('PUT')  {{-- Spoofing de méthode HTTP --}}
    <input type="text" name="name" value="{{ $user->name }}">
    <button type="submit">Mettre à jour</button>
</form>

// Pour les requêtes AJAX, le token est dans la meta tag
<meta name="csrf-token" content="{{ csrf_token() }}">

// Configuration Axios pour envoyer automatiquement le token
axios.defaults.headers.common['X-CSRF-TOKEN'] =
    document.querySelector('meta[name="csrf-token"]').content;
bootstrap/app.phpphp
// Exclure des routes de la vérification CSRF (webhooks externes)
return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->validateCsrfTokens(except: [
            'stripe/webhook',      // Webhook Stripe authentifié par signature
            'api/*',               // API authentifiée par token
        ]);
    });

Ne jamais désactiver la protection CSRF globalement. Utiliser les exceptions uniquement pour les endpoints authentifiés autrement.

Question 13 : Comment implémenter l'authentification avec Laravel Sanctum ?

Laravel Sanctum fournit une authentification légère pour les SPAs, applications mobiles et APIs avec tokens.

php
// Installation et configuration
// php artisan install:api (Laravel 11+)

// app/Models/User.php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}
app/Http/Controllers/AuthController.phpphp
class AuthController extends Controller
{
    public function login(Request $request): JsonResponse
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        if (!Auth::attempt($credentials)) {
            return response()->json(['message' => 'Invalid credentials'], 401);
        }

        $user = Auth::user();

        // Création d'un token avec abilities (permissions)
        $token = $user->createToken('api-token', [
            'articles:read',
            'articles:write',
            'profile:update',
        ]);

        return response()->json([
            'user' => $user,
            'token' => $token->plainTextToken,
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        // Révoque le token actuel
        $request->user()->currentAccessToken()->delete();

        // Ou révoquer tous les tokens
        // $request->user()->tokens()->delete();

        return response()->json(['message' => 'Logged out']);
    }
}

// routes/api.php
Route::post('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn(Request $r) => $r->user());
    Route::post('/logout', [AuthController::class, 'logout']);

    // Vérification des abilities
    Route::middleware('ability:articles:write')->group(function () {
        Route::post('/articles', [ArticleController::class, 'store']);
    });
});

Sanctum supporte aussi l'authentification par cookie pour les SPAs du même domaine, offrant une protection CSRF automatique.

Question 14 : Comment sécuriser une API avec des Policies et Gates ?

Les Policies et Gates centralisent la logique d'autorisation, séparant les règles métier des contrôleurs.

app/Policies/ArticlePolicy.phpphp
namespace App\Policies;

use App\Models\Article;
use App\Models\User;

class ArticlePolicy
{
    // Pré-vérification : les admins ont tous les droits
    public function before(User $user, string $ability): ?bool
    {
        if ($user->isAdmin()) {
            return true; // Autorise tout
        }
        return null; // Continue vers la méthode spécifique
    }

    public function view(?User $user, Article $article): bool
    {
        // Articles publiés visibles par tous
        if ($article->status === 'published') {
            return true;
        }
        // Brouillons visibles uniquement par l'auteur
        return $user?->id === $article->user_id;
    }

    public function update(User $user, Article $article): bool
    {
        return $user->id === $article->user_id;
    }

    public function delete(User $user, Article $article): bool
    {
        return $user->id === $article->user_id
            && $article->comments()->count() === 0;
    }
}
app/Http/Controllers/ArticleController.phpphp
class ArticleController extends Controller
{
    public function update(Request $request, Article $article)
    {
        // Vérifie la policy, lance 403 si non autorisé
        $this->authorize('update', $article);

        $article->update($request->validated());
        return redirect()->route('articles.show', $article);
    }
}

// Dans Blade
@can('update', $article)
    <a href="{{ route('articles.edit', $article) }}">Modifier</a>
@endcan

// Gates pour les autorisations sans modèle
Gate::define('access-admin', function (User $user) {
    return $user->role === 'admin';
});

// Utilisation
if (Gate::allows('access-admin')) {
    // ...
}

Les Policies sont liées à un modèle, les Gates sont pour les autorisations générales.

Validation et Formulaires

Question 15 : Comment créer des règles de validation personnalisées ?

Laravel offre plusieurs façons de créer des validations personnalisées selon la complexité et la réutilisabilité requises.

app/Rules/StrongPassword.phpphp
// Règle personnalisée comme classe (réutilisable)
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class StrongPassword implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $errors = [];

        if (strlen($value) < 12) {
            $errors[] = 'au moins 12 caractères';
        }
        if (!preg_match('/[A-Z]/', $value)) {
            $errors[] = 'au moins une majuscule';
        }
        if (!preg_match('/[a-z]/', $value)) {
            $errors[] = 'au moins une minuscule';
        }
        if (!preg_match('/[0-9]/', $value)) {
            $errors[] = 'au moins un chiffre';
        }
        if (!preg_match('/[@$!%*?&#]/', $value)) {
            $errors[] = 'au moins un caractère spécial';
        }

        if (!empty($errors)) {
            $fail("Le mot de passe doit contenir : " . implode(', ', $errors) . '.');
        }
    }
}
app/Http/Requests/RegisterRequest.phpphp
// Form Request avec validation complexe
namespace App\Http\Requests;

use App\Rules\StrongPassword;
use Illuminate\Foundation\Http\FormRequest;

class RegisterRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Ou logique d'autorisation
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'confirmed', new StrongPassword()],
            'company' => ['required_if:account_type,business', 'string'],
            'vat_number' => [
                'nullable',
                'string',
                // Closure pour validation inline
                function ($attribute, $value, $fail) {
                    if ($value && !$this->isValidVatNumber($value)) {
                        $fail('Le numéro de TVA est invalide.');
                    }
                },
            ],
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique' => 'Cette adresse email est déjà utilisée.',
            'password.confirmed' => 'Les mots de passe ne correspondent pas.',
        ];
    }

    protected function prepareForValidation(): void
    {
        // Normalisation avant validation
        $this->merge([
            'email' => strtolower(trim($this->email)),
        ]);
    }
}

Les Form Requests centralisent validation, autorisation et messages d'erreur, allégeant les contrôleurs.

Prêt à réussir tes entretiens Laravel ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Testing

Question 16 : Comment structurer les tests dans Laravel ?

Laravel fournit PHPUnit avec des helpers dédiés pour tester les différentes couches de l'application.

tests/Feature/ArticleControllerTest.phpphp
namespace Tests\Feature;

use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ArticleControllerTest extends TestCase
{
    use RefreshDatabase; // Réinitialise la DB entre chaque test

    public function test_guest_can_view_published_articles(): void
    {
        $article = Article::factory()->published()->create();

        $response = $this->get('/articles/' . $article->slug);

        $response->assertStatus(200);
        $response->assertSee($article->title);
    }

    public function test_authenticated_user_can_create_article(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)
            ->post('/articles', [
                'title' => 'Mon nouvel article',
                'content' => 'Contenu de test',
            ]);

        $response->assertRedirect();
        $this->assertDatabaseHas('articles', [
            'title' => 'Mon nouvel article',
            'user_id' => $user->id,
        ]);
    }

    public function test_user_cannot_update_others_article(): void
    {
        $owner = User::factory()->create();
        $other = User::factory()->create();
        $article = Article::factory()->for($owner)->create();

        $response = $this->actingAs($other)
            ->put('/articles/' . $article->id, [
                'title' => 'Titre modifié',
            ]);

        $response->assertStatus(403);
    }
}
tests/Unit/Services/PaymentServiceTest.phpphp
namespace Tests\Unit\Services;

use App\Services\PaymentService;
use App\Contracts\PaymentGatewayInterface;
use App\Repositories\OrderRepository;
use Mockery;
use Tests\TestCase;

class PaymentServiceTest extends TestCase
{
    public function test_process_payment_charges_correct_amount(): void
    {
        // Mock des dépendances
        $gateway = Mockery::mock(PaymentGatewayInterface::class);
        $gateway->shouldReceive('charge')
            ->once()
            ->with(99.99, Mockery::any())
            ->andReturn((object) ['transactionId' => 'tx_123']);

        $orders = Mockery::mock(OrderRepository::class);
        $orders->shouldReceive('find')
            ->with(1)
            ->andReturn($this->createOrder());

        $service = new PaymentService($gateway, $orders);

        $result = $service->processPayment(1, 99.99);

        $this->assertTrue($result);
    }
}

Séparer les tests Feature (HTTP, intégration) des tests Unit (classes isolées avec mocks).

Question 17 : Comment utiliser les Factories et Seeders efficacement ?

Les Factories génèrent des données de test réalistes, les Seeders peuplent la base de données.

database/factories/ArticleFactory.phpphp
namespace Database\Factories;

use App\Models\Article;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class ArticleFactory extends Factory
{
    protected $model = Article::class;

    public function definition(): array
    {
        return [
            'user_id' => User::factory(),
            'title' => fake()->sentence(6),
            'slug' => fake()->unique()->slug(),
            'content' => fake()->paragraphs(5, true),
            'status' => 'draft',
            'view_count' => fake()->numberBetween(0, 10000),
            'created_at' => fake()->dateTimeBetween('-1 year'),
        ];
    }

    // États pour différentes configurations
    public function published(): static
    {
        return $this->state(fn(array $attr) => [
            'status' => 'published',
            'published_at' => fake()->dateTimeBetween('-6 months'),
        ]);
    }

    public function draft(): static
    {
        return $this->state(['status' => 'draft', 'published_at' => null]);
    }

    public function popular(): static
    {
        return $this->state(['view_count' => fake()->numberBetween(10000, 100000)]);
    }

    // Configuration des relations
    public function configure(): static
    {
        return $this->afterCreating(function (Article $article) {
            // Créer des tags après la création de l'article
            $article->tags()->attach(
                \App\Models\Tag::factory()->count(3)->create()
            );
        });
    }
}

// Utilisation dans les tests
$article = Article::factory()->published()->create();
$articles = Article::factory()->count(10)->for($user)->create();
$articleWithComments = Article::factory()
    ->has(Comment::factory()->count(5))
    ->create();

Les states permettent de créer des variations sans dupliquer la logique de factory.

Architecture et Patterns

Question 18 : Comment implémenter le Repository Pattern dans Laravel ?

Le Repository Pattern abstrait l'accès aux données et facilite les tests en permettant de mocker les requêtes.

app/Contracts/ArticleRepositoryInterface.phpphp
namespace App\Contracts;

use App\Models\Article;
use Illuminate\Pagination\LengthAwarePaginator;

interface ArticleRepositoryInterface
{
    public function find(int $id): ?Article;
    public function findBySlug(string $slug): ?Article;
    public function getPublished(int $perPage = 20): LengthAwarePaginator;
    public function getByAuthor(int $userId, int $perPage = 20): LengthAwarePaginator;
    public function create(array $data): Article;
    public function update(Article $article, array $data): Article;
    public function delete(Article $article): bool;
}
app/Repositories/EloquentArticleRepository.phpphp
namespace App\Repositories;

use App\Contracts\ArticleRepositoryInterface;
use App\Models\Article;
use Illuminate\Pagination\LengthAwarePaginator;

class EloquentArticleRepository implements ArticleRepositoryInterface
{
    public function __construct(
        private Article $model
    ) {}

    public function find(int $id): ?Article
    {
        return $this->model->with('author')->find($id);
    }

    public function findBySlug(string $slug): ?Article
    {
        return $this->model
            ->where('slug', $slug)
            ->with(['author', 'tags'])
            ->firstOrFail();
    }

    public function getPublished(int $perPage = 20): LengthAwarePaginator
    {
        return $this->model
            ->published()
            ->with('author')
            ->orderByDesc('published_at')
            ->paginate($perPage);
    }

    public function create(array $data): Article
    {
        return $this->model->create($data);
    }

    public function update(Article $article, array $data): Article
    {
        $article->update($data);
        return $article->fresh();
    }

    public function delete(Article $article): bool
    {
        return $article->delete();
    }
}

// Binding dans le ServiceProvider
$this->app->bind(
    ArticleRepositoryInterface::class,
    EloquentArticleRepository::class
);

Le Repository Pattern est utile pour les applications complexes mais peut être excessif pour les projets simples. Évaluer le rapport coût/bénéfice.

Question 19 : Comment gérer les transactions et la concurrence dans Laravel ?

Les transactions garantissent l'intégrité des données lors d'opérations multiples. Laravel simplifie leur gestion.

app/Services/OrderService.phpphp
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function processOrder(Cart $cart, User $user): Order
    {
        // Transaction avec closure : rollback automatique en cas d'exception
        return DB::transaction(function () use ($cart, $user) {
            // Création de la commande
            $order = Order::create([
                'user_id' => $user->id,
                'total' => $cart->total(),
                'status' => 'pending',
            ]);

            // Création des lignes de commande
            foreach ($cart->items as $item) {
                $order->items()->create([
                    'product_id' => $item->product_id,
                    'quantity' => $item->quantity,
                    'price' => $item->product->price,
                ]);

                // Décrémentation du stock avec verrouillage pessimiste
                $product = Product::lockForUpdate()->find($item->product_id);

                if ($product->stock < $item->quantity) {
                    throw new InsufficientStockException($product);
                }

                $product->decrement('stock', $item->quantity);
            }

            // Vider le panier
            $cart->clear();

            return $order;
        }, attempts: 3); // 3 tentatives en cas de deadlock
    }

    public function updateOrderStatus(Order $order, string $status): void
    {
        // Verrouillage optimiste avec version/timestamp
        $updated = DB::table('orders')
            ->where('id', $order->id)
            ->where('updated_at', $order->updated_at) // Vérification de version
            ->update([
                'status' => $status,
                'updated_at' => now(),
            ]);

        if ($updated === 0) {
            throw new ConcurrencyException('La commande a été modifiée entre-temps');
        }
    }
}

lockForUpdate() empêche les lectures concurrentes pendant la transaction. Utiliser avec parcimonie pour éviter les deadlocks.

Question 20 : Comment implémenter le Caching efficacement dans Laravel ?

Le caching améliore drastiquement les performances en évitant les requêtes répétitives.

php
// Stratégies de caching
use Illuminate\Support\Facades\Cache;

class ArticleService
{
    public function getPopularArticles(): Collection
    {
        // Cache-Aside : vérifie le cache, sinon charge et stocke
        return Cache::remember('articles:popular', 3600, function () {
            return Article::published()
                ->popular()
                ->with('author')
                ->limit(10)
                ->get();
        });
    }

    public function getArticle(string $slug): Article
    {
        // Cache par clé dynamique
        return Cache::remember("article:{$slug}", 1800, function () use ($slug) {
            return Article::where('slug', $slug)
                ->with(['author', 'comments.user'])
                ->firstOrFail();
        });
    }

    public function updateArticle(Article $article, array $data): Article
    {
        $article->update($data);

        // Invalidation du cache après modification
        Cache::forget("article:{$article->slug}");
        Cache::forget('articles:popular');

        // Tags pour invalidation groupée (Redis uniquement)
        Cache::tags(['articles', "user:{$article->user_id}"])->flush();

        return $article;
    }

    public function getArticleWithLock(int $id): Article
    {
        // Atomic lock pour éviter le cache stampede
        return Cache::lock("article-lock:{$id}", 10)->block(5, function () use ($id) {
            return Cache::remember("article:{$id}", 3600, fn() => Article::findOrFail($id));
        });
    }
}
config/cache.php - Configuration Redis recommandée en productionphp
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'cache',
        'lock_connection' => 'default',
    ],
],

Utiliser les tags de cache pour une invalidation groupée efficace. Attention aux cache stampedes lors de l'expiration simultanée.

Performance et Optimisation

Question 21 : Comment optimiser les performances d'une application Laravel ?

L'optimisation couvre plusieurs niveaux : requêtes, cache, configuration et infrastructure.

1. Optimisation des requêtes Eloquentphp
$articles = Article::query()
    ->select(['id', 'title', 'slug', 'published_at', 'user_id']) // Colonnes spécifiques
    ->with(['author:id,name,avatar'])  // Eager loading sélectif
    ->withCount('comments')             // Comptage en une requête
    ->published()
    ->latest('published_at')
    ->cursorPaginate(20);               // Pagination par curseur (plus performante)

// 2. Chunking pour les opérations de masse
Article::query()
    ->where('status', 'published')
    ->chunkById(1000, function ($articles) {
        foreach ($articles as $article) {
            // Traitement par lots de 1000
            ProcessArticle::dispatch($article);
        }
    });

// 3. Mise à jour de masse sans modèles
Article::where('published_at', '<', now()->subYear())
    ->update(['status' => 'archived']); // Une seule requête SQL
bash
# Commandes d'optimisation pour la production
php artisan config:cache    # Cache la configuration
php artisan route:cache     # Cache les routes
php artisan view:cache      # Compile les vues Blade
php artisan event:cache     # Cache les mappings d'events
php artisan optimize        # Exécute toutes les optimisations

# Autoloader optimisé
composer install --optimize-autoloader --no-dev

Ces optimisations peuvent réduire le temps de boot de 50% ou plus en production.

Question 22 : Comment débugger et profiler une application Laravel ?

Laravel offre plusieurs outils pour identifier les problèmes de performance et les bugs.

php
// Laravel Telescope pour le debugging en développement
// Capture les requêtes, jobs, exceptions, etc.

// Queries debugging
DB::enableQueryLog();
$articles = Article::with('author')->get();
$queries = DB::getQueryLog();
dump($queries); // Affiche toutes les requêtes SQL

// Debug bar intégré à Blade
@dump($variable)   // Affiche et continue
@dd($variable)     // Dump and die

// Logging structuré
use Illuminate\Support\Facades\Log;

Log::channel('slack')->critical('Payment failed', [
    'user_id' => $user->id,
    'amount' => $amount,
    'error' => $exception->getMessage(),
    'trace' => $exception->getTraceAsString(),
]);

// Context logging
Log::withContext(['request_id' => request()->id()]);
Log::info('Processing order', ['order_id' => $order->id]);
config/logging.php - Configuration des channelsphp
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'slack'],
        'ignore_exceptions' => false,
    ],
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'debug',
        'days' => 14,
    ],
],

Utiliser Telescope en développement et un APM (New Relic, Datadog) en production pour le monitoring continu.

Déploiement et Production

Question 23 : Comment gérer les migrations en production sans downtime ?

Les migrations en production nécessitent une attention particulière pour éviter les interruptions de service.

database/migrations/2026_01_30_add_role_to_users_table.phpphp
// Migration safe : ajoute une colonne nullable d'abord
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        // Étape 1 : Ajouter la colonne nullable
        $table->string('role')->nullable()->after('email');
    });
}

// Étape 2 : Migration de données (job séparé)
// php artisan tinker
// User::whereNull('role')->update(['role' => 'user']);

// Étape 3 : Seconde migration pour la contrainte
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('role')->nullable(false)->default('user')->change();
    });
}
php
// Pour les suppressions de colonnes (3 déploiements)
// Déploiement 1 : Arrêter d'utiliser la colonne dans le code
// Déploiement 2 : Supprimer la colonne
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('deprecated_field');
});

// Migration avec timeout pour les grandes tables
public function up(): void
{
    DB::statement('SET lock_timeout TO \'5s\'');

    Schema::table('large_table', function (Blueprint $table) {
        $table->index('status'); // Index concurrent si PostgreSQL
    });
}

La stratégie "expand-contract" permet d'ajouter des colonnes sans downtime : ajouter nullable → migrer les données → rendre non-nullable.

Question 24 : Comment configurer Laravel pour la haute disponibilité ?

Une architecture haute disponibilité nécessite la séparation des composants stateless et la gestion du state partagé.

config/session.php - Sessions partagées entre instancesphp
'driver' => env('SESSION_DRIVER', 'redis'),
'connection' => 'session',

// config/cache.php - Cache partagé
'default' => env('CACHE_DRIVER', 'redis'),

// config/queue.php - Queues Redis pour la distribution
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
        'queue' => 'default',
        'retry_after' => 90,
        'block_for' => null,
    ],
],

// config/filesystems.php - Storage S3 pour les fichiers
'disks' => [
    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
    ],
],
php
// Health check endpoint pour le load balancer
Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
        Cache::store('redis')->ping();
        return response()->json(['status' => 'healthy']);
    } catch (\Exception $e) {
        return response()->json(['status' => 'unhealthy'], 503);
    }
});

Chaque instance doit être stateless. Les sessions, cache et queues doivent utiliser Redis ou un store partagé.

Question 25 : Quelles sont les bonnes pratiques de déploiement Laravel ?

Un déploiement robuste combine automatisation, vérifications et rollback facile.

bash
# deploy.sh - Script de déploiement typique
#!/bin/bash
set -e

echo "Pulling latest code..."
git pull origin main

echo "Installing dependencies..."
composer install --no-dev --optimize-autoloader

echo "Running migrations..."
php artisan migrate --force

echo "Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

echo "Restarting queue workers..."
php artisan queue:restart

echo "Clearing old cache..."
php artisan cache:clear

echo "Deployment complete!"
.env.production - Variables critiquesphp
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:... # Généré avec php artisan key:generate

LOG_CHANNEL=stack
LOG_LEVEL=warning

# Ne jamais exposer les credentials en clair
# Utiliser des secrets managers (Vault, AWS Secrets Manager)

Checklist de déploiement :

  • php artisan config:cache - Cache la configuration
  • php artisan route:cache - Cache les routes
  • php artisan view:cache - Compile les vues
  • composer install --no-dev - Dependencies production
  • ✅ Tests automatisés avant déploiement
  • ✅ Health checks configurés
  • ✅ Monitoring et alerting en place

Conclusion

Ces 25 questions couvrent l'essentiel des entretiens Laravel et PHP, des fondamentaux du Service Container aux patterns de déploiement production.

Checklist de préparation :

  • ✅ Service Container et injection de dépendances
  • ✅ Eloquent ORM : relations, scopes, eager loading
  • ✅ Middleware, routing et sécurité
  • ✅ Queues, jobs et events asynchrones
  • ✅ Testing : Feature tests, Unit tests, Factories
  • ✅ Patterns avancés : Repository, Transactions, Caching
  • ✅ Déploiement : migrations, optimisation, haute disponibilité
Aller plus loin

Chaque question mérite un approfondissement avec la documentation officielle de Laravel. Les recruteurs valorisent les candidats qui connaissent les subtilités du framework et savent justifier leurs choix techniques.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#laravel
#php
#interview
#eloquent
#entretien technique

Partager

Articles similaires