# 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. - Published: 2026-01-30 - Updated: 2026-03-28 - Author: SharpSkill - Tags: laravel, php, interview, eloquent, entretien technique - Reading time: 22 min --- 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. ```php // public/index.php // 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. ```php // app/Services/PaymentService.php // 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; } } } ``` ```php // app/Providers/AppServiceProvider.php // 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. ```php // app/Providers/PaymentServiceProvider.php 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. ```php // app/Models/User.php 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. ```php // app/Models/Article.php 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. ## 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. ```php // app/Http/Middleware/CheckSubscription.php 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, ]); }); ``` ```php // routes/web.php // 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. ```php // routes/web.php // 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(); ``` ```php // app/Models/Article.php 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(); } } ``` ```php // app/Providers/RouteServiceProvider.php // 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. ```php // app/Jobs/ProcessPodcast.php 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, ]); } } ``` ```php // app/Providers/EventServiceProvider.php 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
@csrf {{-- Génère un champ hidden avec le token --}} @method('PUT') {{-- Spoofing de méthode HTTP --}}
// Pour les requêtes AJAX, le token est dans la meta tag // Configuration Axios pour envoyer automatiquement le token axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content; ``` ```php // bootstrap/app.php // 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; } ``` ```php // app/Http/Controllers/AuthController.php 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. ```php // app/Policies/ArticlePolicy.php 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; } } ``` ```php // app/Http/Controllers/ArticleController.php 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) Modifier @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. ```php // app/Rules/StrongPassword.php // 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) . '.'); } } } ``` ```php // app/Http/Requests/RegisterRequest.php // 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. ## 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. ```php // tests/Feature/ArticleControllerTest.php 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); } } ``` ```php // tests/Unit/Services/PaymentServiceTest.php 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. ```php // database/factories/ArticleFactory.php 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. ```php // app/Contracts/ArticleRepositoryInterface.php 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; } ``` ```php // app/Repositories/EloquentArticleRepository.php 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. ```php // app/Services/OrderService.php 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)); }); } } ``` ```php // config/cache.php - Configuration Redis recommandée en production '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. ```php // 1. Optimisation des requêtes Eloquent $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]); ``` ```php // config/logging.php - Configuration des channels '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. ```php // database/migrations/2026_01_30_add_role_to_users_table.php // 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é. ```php // config/session.php - Sessions partagées entre instances '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!" ``` ```php // .env.production - Variables critiques 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/laravel/questions-entretien-laravel-php