---
title: 'Classes Action, services et DTO dans Laravel | DevSense'
description: 'Comment alléger les contrôleurs et modèles obèses dans Laravel : classes Action, couche service, DTO en classes readonly, frontières de modules, transactions et événements — et quand tout cela relève de la sur-ingénierie.'
faq:
    - { question: 'Quelle est la différence entre une classe Action et un service dans Laravel ?', answer: "Une Action décrit un seul scénario métier (passer une commande, inviter un membre dans une équipe) et expose une seule méthode publique. Un service regroupe des opérations autour d'une ressource ou d'une intégration (passerelle de paiement, calculateur de prix) et ignore le scénario dans son ensemble. L'Action appelle les services, et non l'inverse." }
    - { question: "Pourquoi des DTO alors qu'on a $request->validated() et les tableaux ?", answer: "Un tableau ne dit pas quelles clés il contient ni de quels types elles sont : une faute de frappe dans une clé ne se révèle qu'à l'exécution, l'IDE ne propose rien et l'analyse statique est impuissante. Un DTO en classe readonly fige le contrat : le type de chaque champ, son caractère obligatoire et son immuabilité. La même Action peut alors être appelée depuis un contrôleur, une commande console, un job de file d'attente et un test." }
    - { question: 'Peut-on faire circuler des modèles Eloquent entre modules ?', answer: "À l'intérieur d'un module, oui. Entre modules, mieux vaut transmettre des DTO ou des identifiants : un modèle entraîne avec lui ses relations, des requêtes paresseuses et la possibilité de modifier les données d'autrui via save(). Une frontière faite de DTO rend les dépendances explicites et permet de faire évoluer le schéma des tables d'un module sans casser ses voisins." }
    - { question: 'Quand les classes Action et les DTO relèvent-ils de la sur-ingénierie ?', answer: "Pour du CRUD sans règles métier, des back-offices et des prototypes. Si un contrôleur de cinq lignes valide la requête et enregistre le modèle, extraire une Action et un DTO ajoutera trois fichiers et aucune règle. Les couches deviennent rentables lorsque le scénario acquiert des invariants, plusieurs points d'entrée ou des effets de bord." }
published: '2026-10-03'
---
# Classes Action, services et DTO dans Laravel : alléger les contrôleurs et les modèles

Presque tous les projets Laravel passent par la même étape. `OrderController@store` grossit jusqu'à deux cents lignes : validation, calcul du prix avec code promo, réservation du stock, débit du paiement, e-mail au client, webhook vers le CRM. Puis arrive l'API pour l'application mobile, et ces deux cents lignes sont copiées dans un second contrôleur. Puis une commande console pour importer les commandes d'une marketplace : troisième copie. Six mois plus tard, un bug d'arrondi est corrigé dans l'une des copies, mais pas dans les deux autres.

En parallèle, le modèle `Order` grossit lui aussi : il contient les méthodes `send()`, `refund()`, `syncWithCrm()`, des gestionnaires d'événements et quelques requêtes HTTP. On ne peut le tester qu'en passant par HTTP, et la moindre modification fait peur. Cet article explique comment répartir ce code en couches : des **classes Action** pour les scénarios, des **services** pour les opérations réutilisables, des **DTO** pour les données qui circulent entre eux. Et où s'arrêter pour ne pas transformer un simple CRUD en monolithe d'entreprise à quarante interfaces.

**Voir aussi :** [Antipatterns de conception](design-antipatterns) · [Patterns structurels du GoF](structural-design-patterns) · [Intégrations résilientes](resilient-external-integrations) · [Files d'attente Laravel en production](laravel-queues-production)

## Sommaire

* [Symptômes du contrôleur obèse et du modèle obèse](#symptoms)
* [Carte des couches : qui fait quoi](#layers)
* [DTO : un contrat plutôt qu'un tableau](#dto)
* [Classes Action : un scénario, une classe](#actions)
* [Couche service : quand elle est vraiment utile](#services)
* [Le contrôleur après refactoring](#thin-controller)
* [Une logique, plusieurs points d'entrée : commandes, jobs, tests](#reuse)
* [Ce qui reste dans le modèle](#models)
* [Frontières de modules : des DTO entre contextes](#modules)
* [Quand c'est de la sur-ingénierie](#when-not)
* [Refactorer progressivement](#migration)
* [Erreurs fréquentes](#common-mistakes)
* [Checklist](#checklist)
* [Quiz d'auto-évaluation](#self-test-quiz)

---

<a id="symptoms"></a>
## Symptômes du contrôleur obèse et du modèle obèse

Le problème n'est pas le nombre de lignes, mais le fait qu'un même endroit mélange plusieurs raisons de changer. Signes qu'il est temps de répartir le code :

* **La logique est copiée entre les points d'entrée.** Le contrôleur web, le contrôleur API et la commande console font « presque la même chose ».
* **Tester un scénario exige une requête HTTP.** Pour vérifier le calcul d'une remise, il faut s'authentifier, construire un formulaire et décortiquer une redirection.
* **On ne sait pas où se situe la frontière de transaction.** La commande est enregistrée, la réservation du stock a échoué, et il reste en base une commande sans réservation.
* **Les effets de bord se produisent avant le commit.** L'e-mail « commande validée » est parti, mais la transaction a été annulée.
* **Le modèle sait tout faire.** `Order` envoie des e-mails, appelle le CRM et calcule les taxes. Chaque tâche oblige à modifier le même fichier, et les conflits Git deviennent la norme.
* **Des tableaux à la forme inconnue.** `$data['items'][0]['qty']` ou `$data['items'][0]['quantity']` ? Seul le débogueur connaît la réponse.

---

<a id="layers"></a>
## Carte des couches : qui fait quoi

| Couche | Responsable de | Ne doit pas |
|------|-------------|-----------|
| **Controller** | Recevoir la requête HTTP, appeler le scénario, renvoyer la réponse | Calculer des prix, appeler des API externes |
| **FormRequest** | Autorisation et validation des entrées, construction du DTO | Enregistrer des données |
| **DTO** | Transport de données typées entre les couches | Contenir de la logique métier ni accéder à la base |
| **Action** | Un scénario métier, les frontières de transaction, l'ordre des étapes | Connaître HTTP, la session, les redirections |
| **Service** | Une opération ou une intégration réutilisable (prix, paiements, stock) | Piloter le scénario dans son ensemble |
| **Model** | Données, relations, casts, scopes, valeurs dérivées simples | Envoyer des e-mails, faire des requêtes HTTP |
| **Job / Event** | Effets de bord différés et asynchrones | Renvoyer un résultat à la requête |

Les dépendances vont dans un seul sens : contrôleur → Action → services et modèles. Un service n'appelle jamais une Action, et le modèle ne connaît ni les uns ni les autres.

---

<a id="dto"></a>
## DTO : un contrat plutôt qu'un tableau

Un DTO (Data Transfer Object) est un objet sans comportement qui transporte des données à travers la frontière d'une couche. En PHP moderne, il tient en quelques lignes grâce aux classes readonly ([PHP 8.2](../php/8.2)) et à la promotion des propriétés du constructeur :

```php
// app/Domain/Orders/Data/OrderLineData.php
namespace App\Domain\Orders\Data;

final readonly class OrderLineData
{
    public function __construct(
        public int $productId,
        public int $quantity,
    ) {}
}
```

```php
// app/Domain/Orders/Data/PlaceOrderData.php
namespace App\Domain\Orders\Data;

use App\Domain\Orders\Enums\DeliveryMethod;

final readonly class PlaceOrderData
{
    /**
     * @param  list<OrderLineData>  $lines
     */
    public function __construct(
        public int $customerId,
        public array $lines,
        public DeliveryMethod $delivery,
        public ?string $promoCode = null,
    ) {}
}
```

Ce que cela apporte par rapport à un tableau :

* **Les types.** `quantity` est toujours un `int`, `delivery` une valeur d'enum et non la chaîne `"courrier"` avec une faute de frappe.
* **L'immuabilité.** Aucune étape du scénario ne peut « discrètement » corriger les données pour la suivante.
* **L'autocomplétion de l'IDE et l'analyse statique.** PHPStan/Psalm repèrent l'accès à un champ inexistant avant même l'exécution.
* **L'indépendance vis-à-vis de la source.** L'Action ignore si les données viennent d'un formulaire, d'une API JSON, d'un import CSV ou d'un test.

La validation reste dans le `FormRequest` : c'est son rôle. Le DTO est construit à partir de données déjà validées. Il est pratique de placer cette construction à côté des règles :

```php
// app/Http/Requests/PlaceOrderRequest.php
public function rules(): array
{
    return [
        'lines' => ['required', 'array', 'min:1'],
        'lines.*.product_id' => ['required', 'integer', 'exists:products,id'],
        'lines.*.quantity' => ['required', 'integer', 'min:1', 'max:100'],
        'delivery' => ['required', Rule::enum(DeliveryMethod::class)],
        'promo_code' => ['nullable', 'string', 'max:32'],
    ];
}

public function toData(): PlaceOrderData
{
    $validated = $this->validated();

    return new PlaceOrderData(
        customerId: $this->user()->id,
        lines: array_map(
            fn (array $line) => new OrderLineData((int) $line['product_id'], (int) $line['quantity']),
            $validated['lines'],
        ),
        delivery: DeliveryMethod::from($validated['delivery']),
        promoCode: $validated['promo_code'] ?? null,
    );
}
```

> [!NOTE]
> **Faut-il un package ?** Pour une dizaine de DTO, des classes readonly écrites à la main suffisent. Des packages comme `spatie/laravel-data` ajoutent la construction automatique depuis la requête, la validation par attributs et la transformation en JSON. C'est pratique quand il y a des centaines de DTO qui servent aussi de ressources d'API, mais cela lie la couche domaine au package. Commencez par des classes simples.

Stockez les montants dans les DTO sous forme d'entier en plus petite unité (centimes) ou d'objet `Money`, jamais en `float` : en PHP, `0.1 + 0.2` n'est pas égal à `0.3`.

---

<a id="actions"></a>
## Classes Action : un scénario, une classe

Une Action est une classe dotée d'une seule méthode publique, qui exécute un scénario métier de bout en bout. Son nom est un verbe issu du langage du domaine : `PlaceOrder`, `CancelSubscription`, `InviteTeamMember`.

```php
// app/Domain/Orders/Actions/PlaceOrder.php
namespace App\Domain\Orders\Actions;

use App\Domain\Orders\Data\PlaceOrderData;
use App\Domain\Orders\Events\OrderPlaced;
use App\Domain\Orders\Services\PriceCalculator;
use App\Domain\Orders\Services\StockReservations;
use App\Models\Order;
use Illuminate\Support\Facades\DB;

final class PlaceOrder
{
    public function __construct(
        private PriceCalculator $prices,
        private StockReservations $stock,
    ) {}

    public function handle(PlaceOrderData $data): Order
    {
        return DB::transaction(function () use ($data): Order {
            $quote = $this->prices->quote($data->lines, $data->promoCode);

            $order = Order::create([
                'customer_id' => $data->customerId,
                'delivery' => $data->delivery,
                'total_cents' => $quote->totalCents,
                'discount_cents' => $quote->discountCents,
            ]);

            $order->lines()->createMany($quote->linesForStorage());

            // Throws when stock is insufficient, which rolls back the whole order.
            $this->stock->reserve($order);

            // The event implements ShouldDispatchAfterCommit,
            // so listeners never see an order that was rolled back.
            OrderPlaced::dispatch($order->id);

            return $order;
        });
    }
}
```

Les décisions clés de ce code :

* **La frontière de transaction est dans l'Action.** Le scénario sait quelles étapes doivent s'exécuter de façon atomique. Le contrôleur n'a pas à s'en soucier, les services non plus.
* **Les dépendances passent par le constructeur.** Le conteneur de Laravel construit lui-même `PriceCalculator` et `StockReservations`, et dans un test il est facile de les remplacer.
* **Les effets de bord après le commit.** Un événement implémentant `ShouldDispatchAfterCommit` n'est émis qu'une fois la transaction validée. Pour les jobs de file d'attente, `ShouldQueueAfterCommit` ou l'option `after_commit` de la connexion font la même chose.
* **On renvoie un résultat, pas une réponse HTTP.** L'Action ignore ce qui suit : redirection, JSON ou ligne dans la console.

```php
// app/Domain/Orders/Events/OrderPlaced.php
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;

final class OrderPlaced implements ShouldDispatchAfterCommit
{
    use Dispatchable;

    public function __construct(public int $orderId) {}
}
```

Le nom de la méthode — `handle()`, `execute()` ou `__invoke()` — est une convention d'équipe. L'important est qu'il n'y en ait qu'une et qu'elle soit identique dans tout le projet. `__invoke()` permet de passer l'Action comme callable, `handle()` se lit mieux lors d'un appel explicite.

> [!NOTE]
> **Une Action peut en appeler une autre** si cela fait partie du scénario : `PlaceOrder` peut appeler `ApplyLoyaltyPoints`. Mais attention aux transactions : un `DB::transaction()` imbriqué crée un savepoint, et l'annulation de la transaction interne n'annule pas l'externe si l'exception est interceptée.

---

<a id="services"></a>
## Couche service : quand elle est vraiment utile

Un service est une classe construite autour d'une responsabilité unique, utilisée par plusieurs scénarios : calcul des prix, gestion du stock, passerelle de paiement, génération de PDF. Il ne sait pas pourquoi on l'appelle.

```php
// app/Domain/Payments/Contracts/PaymentGateway.php
interface PaymentGateway
{
    public function charge(int $amountCents, string $currency, string $idempotencyKey): PaymentResult;

    public function refund(string $paymentId, int $amountCents): RefundResult;
}
```

```php
// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
}
```

Une interface se justifie lorsqu'il existe plus d'une implémentation (la vraie passerelle et un fake pour les tests, deux prestataires selon les pays) ou lorsqu'il s'agit d'une frontière avec le monde extérieur. Pour un `PriceCalculator` interne doté d'une seule implémentation, l'interface est un fichier de trop : une classe concrète se remplace tout aussi facilement dans un test via `$this->mock()` ou `$this->app->instance()`.

Le principal danger de la couche service, c'est le **God Service**. Un `OrderService` avec les méthodes `create`, `cancel`, `refund`, `export`, `notify`, `recalculate` et quarante dépendances dans le constructeur n'est rien d'autre que le contrôleur obèse, rangé dans un autre dossier. Si un service compte plus de cinq à sept méthodes publiques sans données communes, ce sont plusieurs classes Action collées en une seule.

---

<a id="thin-controller"></a>
## Le contrôleur après refactoring

```php
// app/Http/Controllers/OrderController.php
public function store(PlaceOrderRequest $request, PlaceOrder $placeOrder): RedirectResponse
{
    try {
        $order = $placeOrder->handle($request->toData());
    } catch (InsufficientStock $e) {
        return back()->withErrors(['lines' => $e->getMessage()])->withInput();
    }

    return to_route('orders.show', $order);
}
```

Le contrôleur traduit HTTP en appel de scénario, puis le résultat du scénario en HTTP. L'exception métier `InsufficientStock` devient une erreur de formulaire. Dans un contrôleur API, la même exception deviendra une réponse `422`, sans que la logique de commande change.

---

<a id="reuse"></a>
## Une logique, plusieurs points d'entrée : commandes, jobs, tests

La même Action est appelée sans modification depuis la commande console d'import :

```php
// app/Console/Commands/ImportMarketplaceOrders.php
public function handle(MarketplaceClient $client, PlaceOrder $placeOrder): int
{
    foreach ($client->newOrders() as $external) {
        $placeOrder->handle(MarketplaceOrderMapper::toData($external));
    }

    return self::SUCCESS;
}
```

Et elle se teste sans HTTP, sans session ni CSRF :

```php
public function test_placing_an_order_reserves_stock(): void
{
    $customer = User::factory()->create();
    $product = Product::factory()->create(['stock' => 10, 'price_cents' => 1500]);

    $order = app(PlaceOrder::class)->handle(new PlaceOrderData(
        customerId: $customer->id,
        lines: [new OrderLineData($product->id, 2)],
        delivery: DeliveryMethod::Courier,
    ));

    $this->assertSame(3000, $order->total_cents);
    $this->assertSame(8, $product->fresh()->stock);
}

public function test_order_is_rolled_back_when_stock_is_insufficient(): void
{
    $product = Product::factory()->create(['stock' => 1]);

    $this->expectException(InsufficientStock::class);

    try {
        app(PlaceOrder::class)->handle(new PlaceOrderData(
            customerId: User::factory()->create()->id,
            lines: [new OrderLineData($product->id, 5)],
            delivery: DeliveryMethod::Pickup,
        ));
    } finally {
        $this->assertDatabaseCount('orders', 0);
    }
}
```

Les tests du contrôleur deviennent alors minces : ils vérifient la validation, l'autorisation et la justesse de la réponse. Les règles métier sont testées une seule fois, au niveau de l'Action.

---

<a id="models"></a>
## Ce qui reste dans le modèle

Le but n'est pas d'obtenir un « modèle anémique » sans la moindre méthode. Un modèle Eloquent est le bon endroit pour tout ce qui décrit **les données elles-mêmes** :

* les relations (`lines()`, `customer()`), les casts (`'delivery' => DeliveryMethod::class`), les scopes (`scopePaid()`) ;
* des valeurs dérivées simples sans effet de bord : `isPaid()`, `canBeCancelledBy(User $user)`, un accesseur `total` calculé à partir de `total_cents` ;
* les invariants d'une seule entité qui ne nécessitent aucun autre service.

Ce qui n'a rien à faire dans le modèle : l'envoi d'e-mails, les requêtes HTTP, la manipulation des files d'attente, une logique complexe dans les observers. Les observers sont particulièrement sournois : ils se déclenchent lors des imports, dans les seeders et dans les tests, et restent invisibles depuis le code du scénario. Si un effet de bord fait partie du scénario, sa place est dans l'Action.

---

<a id="modules"></a>
## Frontières de modules : des DTO entre contextes

Quand le projet grandit, il est utile de regrouper le code non plus par couches techniques (`Controllers`, `Models`, `Services`), mais par domaines métier :

```
app/
├── Domain/
│   ├── Orders/
│   │   ├── Actions/        PlaceOrder, CancelOrder
│   │   ├── Data/           PlaceOrderData, OrderLineData
│   │   ├── Enums/          DeliveryMethod, OrderStatus
│   │   ├── Events/         OrderPlaced
│   │   └── Services/       PriceCalculator, StockReservations
│   ├── Billing/
│   │   ├── Actions/        ChargeOrder, IssueRefund
│   │   ├── Contracts/      PaymentGateway
│   │   └── Data/           PaymentResult
│   └── Catalog/
├── Http/                   controllers and form requests stay framework-shaped
└── Models/
```

La règle aux frontières : **le module `Billing` n'accepte pas le modèle `Order` du module `Orders`.** Il reçoit un DTO ou un identifiant :

```php
// Inside the Orders module: a listener translates the event into a Billing call.
final class ChargePlacedOrder implements ShouldQueue
{
    public function handle(OrderPlaced $event, ChargeOrder $charge, OrderSummaryQuery $orders): void
    {
        $summary = $orders->summary($event->orderId); // returns a DTO, not a model

        $charge->handle(new ChargeRequestData(
            reference: "order-{$summary->id}",
            amountCents: $summary->totalCents,
            currency: $summary->currency,
        ));
    }
}
```

Pourquoi pas le modèle :

* le modèle permet d'appeler `$order->update()` depuis un module tiers, et personne ne saura qui a changé le statut ;
* les relations paresseuses transforment un accès à `$order->customer->address` en requêtes dont le module `Billing` ne soupçonne pas l'existence ;
* le schéma de la table `orders` devient une API publique : impossible de le modifier sans vérifier tous les modules.

C'est le même principe que lors du [passage du monolithe aux microservices](monolith-to-microservices-architecture) : d'abord des frontières explicites au sein d'une même application, et seulement ensuite à travers le réseau, si cela s'avère un jour nécessaire.

---

<a id="when-not"></a>
## Quand c'est de la sur-ingénierie

Les couches ont un coût : plus de fichiers, plus de navigation dans le code, plus de conventions. Elles ne sont pas rentables si :

* **C'est du CRUD sans règles.** Un formulaire d'édition de profil qui valide et enregistre cinq champs n'a besoin ni de `UpdateProfileAction` ni de `UpdateProfileData`. `$user->update($request->validated())` est du code tout à fait correct.
* **C'est un back-office sous Filament ou Nova.** Le framework impose déjà sa propre structure, et des classes Action par-dessus dupliquent souvent ses ressources.
* **C'est un prototype.** Tant que vous cherchez votre produit, la vitesse de changement compte plus que l'architecture. Le refactoring en couches viendra quand le scénario se sera stabilisé.
* **Le scénario n'a qu'un point d'entrée et aucun effet de bord.** Il n'y a rien à réutiliser ni à isoler.

Bons signaux qu'il est temps : un deuxième point d'entrée vers le même scénario, le premier « l'e-mail est parti mais la transaction a été annulée », ou le premier test impossible à écrire sans HTTP.

---

<a id="migration"></a>
## Refactorer progressivement

Réécrire tout le projet « en Actions » est inutile et dangereux. Une démarche qui fonctionne :

1. **Les nouveaux scénarios directement dans le nouveau style.** Cela pose un modèle sans risque pour l'ancien code.
2. **Avant l'extraction, un test de caractérisation.** On écrit un test fonctionnel (feature test) sur le comportement actuel du contrôleur, même s'il est étrange. Il détecte les régressions pendant le déplacement.
3. **Extrayez un scénario à la fois.** Le corps de la méthode du contrôleur migre presque tel quel dans le `handle()` de l'Action, et le contrôleur se met à l'appeler. Le test doit rester vert.
4. **Améliorez seulement ensuite.** Introduisez des DTO à la place des tableaux, déplacez les effets de bord après le commit, extrayez des services à partir des fragments répétés.
5. **Supprimez les copies.** Le deuxième et le troisième contrôleur appellent désormais la même Action, et les doublons disparaissent.

Chaque étape est une petite PR distincte, que l'on peut annuler.

---

<a id="common-mistakes"></a>
## Erreurs fréquentes

**1. L'Action reçoit une `Request`.**
Le scénario est de nouveau lié à HTTP, et impossible de l'appeler depuis une commande ou un job sans fausse requête. Passez un DTO.

**2. Un DTO avec de la logique métier.**
Une méthode `calculateTotal()` dans un DTO, c'est un service caché dans un objet de données. Le DTO se contente de transporter des données.

**3. Des DTO mutables.**
Des propriétés publiques non readonly permettent à une étape de modifier les données pour une autre. Utilisez `readonly`.

**4. Un God Service à la place du contrôleur obèse.**
Un `OrderService` de deux mille lignes, c'est le même problème dans un nouveau dossier. Les scénarios vont dans des classes Action, les services restent étroits.

**5. Des effets de bord à l'intérieur de la transaction.**
Les e-mails, webhooks et jobs envoyés avant le commit se déclenchent même en cas d'annulation. Utilisez `ShouldDispatchAfterCommit`, `ShouldQueueAfterCommit` ou `afterCommit()`.

**6. Une interface pour chaque classe.**
Un `PriceCalculatorInterface` avec une seule implémentation et sans frontière externe, c'est un niveau d'indirection superflu. Laravel sait remplacer des classes concrètes dans les tests.

**7. Des couches pour le plaisir des couches.**
`UpdateUserNameAction` avec `UpdateUserNameData` pour une seule ligne `$user->update()`, c'est de la cérémonie, pas de l'architecture.

---

<a id="checklist"></a>
## Checklist

1. Le contrôleur se contente de traduire HTTP en appel de scénario, et le résultat en HTTP.
2. Validation et autorisation sont dans le `FormRequest`, de même que la construction du DTO.
3. Un scénario métier = une classe Action avec une seule méthode publique.
4. La frontière de transaction est définie dans l'Action.
5. Les effets de bord (e-mails, webhooks, jobs) sont émis après le commit.
6. Les DTO sont `final readonly`, typés et sans logique ; les montants ne sont pas en `float`.
7. Les services sont étroits ; les interfaces sont réservées aux frontières externes et aux implémentations multiples.
8. Entre modules circulent des DTO ou des identifiants, pas des modèles.
9. Les règles métier sont couvertes par des tests au niveau de l'Action, sans HTTP.

---

## En résumé

Les classes Action, les services et les DTO ne visent pas à avoir de jolis dossiers, mais à donner à chaque scénario métier un emplacement unique, une frontière de transaction unique et un jeu de tests unique. Partez de la douleur — duplication, effets de bord avant le commit, impossibilité de tester — et introduisez exactement autant de couches que nécessaire pour la supprimer. Qu'un simple CRUD reste simple.

---

<a id="self-test-quiz"></a>
## Quiz d'auto-évaluation

### Question 1 : Où doit se situer la frontière de transaction lors de la création d'une commande ?
- A) Dans le contrôleur, autour de l'appel à l'Action.
- B) Dans la classe Action, qui sait quelles étapes du scénario doivent s'exécuter de façon atomique.
- C) Dans chaque service séparément, avec une transaction par appel.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
Le scénario sait que la commande, ses lignes et la réservation du stock doivent être enregistrées ensemble. Le contrôleur ne doit pas connaître ces détails, et des transactions séparées dans les services ne garantissent pas l'atomicité du scénario dans son ensemble.
</details>

### Question 2 : À l'intérieur de `DB::transaction()`, on envoie l'e-mail « Commande validée », puis la réservation du stock échoue avec une exception. Que se passe-t-il sans mesure supplémentaire ?
- A) L'e-mail ne part pas, car la transaction a été annulée.
- B) L'e-mail part (ou le job d'envoi est placé dans la file d'attente), alors que la commande n'existe pas en base.
- C) Laravel annule automatiquement l'e-mail via l'événement de rollback.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
L'annulation d'une transaction ne concerne que la base de données. Les effets de bord externes doivent être émis après le commit : via `ShouldDispatchAfterCommit` pour les événements, `ShouldQueueAfterCommit` ou `afterCommit()` pour les jobs.
</details>

### Question 3 : Le module `Billing` doit débiter le paiement d'une commande. Que vaut-il mieux lui transmettre depuis le module `Orders` ?
- A) Le modèle `Order` complet, pour que `Billing` puisse charger lui-même les relations nécessaires.
- B) Un DTO avec le montant, la devise et la référence de la commande, ou l'identifiant de la commande.
- C) Le tableau `$request->all()` de la requête d'origine.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
Le DTO fixe les données dont le module a besoin et l'empêche de modifier les enregistrements d'autrui ou de déclencher des requêtes cachées via les relations. Le schéma de la table `orders` cesse d'être une API publique pour les autres modules.
</details>