Aller au contenu
Lantorian

Laravel10 min de lecture

Architecture Laravel propre

Chaque classe a un seul rôle. Le contrôleur reçoit, l'Action décide, la Resource répond. Résultat : du code lisible, réutilisable et simple à tester.

Le trajet d'une requête

Exemple fil rouge : créer une facture avec POST /api/v1/invoices. Clique sur une étape pour voir le fichier concerné et ce qu'il ne doit jamais faire.

Route

1 / 7

routes/api.php

Associe l'URL et le verbe HTTP à une méthode de contrôleur. Versionne l'API (v1) et applique l'authentification Sanctum au groupe.

Jamais : de closure avec de la logique dedans.

Sept étapes, sept responsabilités. Si une classe fait le travail de sa voisine, c'est un signal de refactorisation.

L'arborescence standard

On garde la structure Laravel par type de classe, avec un sous-dossier par domaine métier. Survole un fichier pour savoir pourquoi il est là.

Qui fait quoi

Garde ce tableau ouvert pendant tes premières PR. La plupart des remarques en revue viennent d'une responsabilité mal placée.

CoucheFaitNe fait jamais
FormRequestValide, autorise, produit un DTOÉcrire en base
ContrôleurRelie la requête, l'Action et la ResourceContenir des if métier
ActionExécute un cas d'usage en transactionLire request() ou auth()
ModèleRelations, casts, scopesEnvoyer des mails, appeler une API
ResourceFormate le JSON de sortieLancer des requêtes (utilise whenLoaded)
PolicyDécide des droitsModifier des données
JobExécute un travail lent en fileRecevoir un modèle entier sérialisé inutilement

Une fonctionnalité complète

Tout le code de la création de facture. Les lignes surlignées sont celles qu'on regarde en premier en revue.

routes/api.php
<?php

use App\Http\Controllers\Api\V1\InvoiceController;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')
    ->middleware('auth:sanctum')
    ->group(function () {
        Route::apiResource('invoices', InvoiceController::class);
        Route::post('invoices/{invoice}/pay', [InvoiceController::class, 'pay'])
            ->name('invoices.pay');
    });

Contrôleur fin

Le premier réflexe d'un stagiaire est souvent de tout écrire dans le contrôleur. Ça fonctionne, mais rien n'est réutilisable ni testable isolément.

À éviter

InvoiceController.php
public function store(Request $request)
{
    // validation inline, no authorization
    $request->validate(['customer_id' => 'required']);

    $invoice = new Invoice();
    $invoice->customer_id = $request->customer_id;
    $invoice->status = 'draft';           // magic string
    $invoice->save();

    foreach ($request->lines as $line) {  // no transaction
        $invoice->lines()->create($line);
    }

    Mail::to($invoice->customer)->send(new InvoiceMail($invoice));

    return $invoice;                      // raw model: leaks columns
}

À faire

InvoiceController.php
public function store(
    StoreInvoiceRequest $request,
    CreateInvoice $createInvoice,
): InvoiceResource {
    $invoice = $createInvoice->handle(
        $request->user(),
        $request->toData(),
    );

    return InvoiceResource::make($invoice);
}

Eloquent sans surprise

Le problème N+1 est la première cause de lenteur. On active le mode strict en développement pour qu'il lève une exception au lieu de ralentir en silence.

À éviter

N+1
// 1 query for invoices + 1 query per invoice
$invoices = Invoice::all();

foreach ($invoices as $invoice) {
    echo $invoice->customer->name;
}

À faire

eager loading
// 2 queries total, only needed columns, paginated
$invoices = Invoice::query()
    ->select(['id', 'customer_id', 'total', 'status'])
    ->with('customer:id,name')
    ->paginate(20);
app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use App\Services\Payment\PaymentGateway;
use App\Services\Payment\StripeGateway;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Code depends on the interface, tests swap the implementation
        $this->app->bind(PaymentGateway::class, StripeGateway::class);
    }

    public function boot(): void
    {
        // Lazy loading, silently discarded attributes, missing attributes: throw in dev
        Model::shouldBeStrict(! $this->app->isProduction()); 

        // No migrate:fresh or db:wipe in production
        DB::prohibitDestructiveCommands($this->app->isProduction()); 

        Date::use(CarbonImmutable::class);
    }
}

Principes à retenir

  • Typage partout : types de retour, propriétés typées, génériques en PHPDoc pour Larastan.
  • Injection de dépendances plutôt que new ou les façades dans les Actions : c'est ce qui rend les tests simples.
  • Classes finales et readonly par défaut, héritage seulement quand il simplifie vraiment.
  • Pas de logique dans les migrations ni dans les seeders de production.
  • Configuration via config(), jamais env() en dehors du dossier config/.