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.

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.
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.
// 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.
// 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;
}
}
}// 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.
// 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.
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.
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.
// ❌ 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.
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.
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,
]);
});// 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.
// 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();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();
}
}// 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.
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);
}
}// 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.
// 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,
]);
}
}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.
// 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;// 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.
// 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;
}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.
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;
}
}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.
// 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) . '.');
}
}
}// 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.
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);
}
}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.
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.
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;
}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.
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.
// 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));
});
}
}'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.
$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# 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-devCes 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.
// 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]);'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.
// 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();
});
}// 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é.
'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'),
],
],// 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.
# 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!"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é
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
Partager
Articles similaires

Eloquent ORM : Patterns et optimisations pour Laravel
Maîtrisez Eloquent ORM avec les patterns avancés et techniques d'optimisation. Eager loading, query scopes, accessors, mutators et performance pour applications Laravel.

Laravel 11 : Créer une application complète de A à Z
Guide complet pour créer une application Laravel 11 avec authentification, API REST, Eloquent ORM et déploiement. Tutorial pratique pour débutants et intermédiaires.

Laravel Middleware en profondeur : Authentification, Rate Limiting et Middleware personnalisé
Exploration complète des middleware Laravel avec des exemples pratiques : gardes d'authentification, limitation de débit avec throttle, création de middleware personnalisé et patterns avancés pour les applications en production.