---
title: 'Classi Action, servizi e DTO in Laravel | DevSense'
description: 'Come alleggerire controller e model troppo grassi in Laravel: classi Action, service layer, DTO con classi readonly, confini tra moduli, transazioni ed eventi — e quando tutto questo è overengineering.'
faq:
    - { question: "Che differenza c'è tra una classe Action e un servizio in Laravel?", answer: "Una Action descrive un singolo scenario di business (effettuare un ordine, invitare un membro nel team) e ha un solo metodo pubblico. Un servizio raggruppa operazioni attorno a una risorsa o a un'integrazione (gateway di pagamento, calcolatore di prezzi) e non conosce lo scenario nel suo insieme. È la Action a chiamare i servizi, non il contrario." }
    - { question: 'A cosa servono i DTO se ci sono $request->validated() e gli array?', answer: "Un array non dice quali chiavi contiene né di che tipo sono: un refuso in una chiave emerge solo a runtime, l'IDE non suggerisce nulla e l'analisi statica è impotente. Un DTO basato su una classe readonly fissa il contratto: il tipo di ogni campo, l'obbligatorietà e l'immutabilità. La stessa Action può essere invocata da un controller, da un comando console, da un job in coda e da un test." }
    - { question: 'Si possono passare model Eloquent tra moduli diversi?', answer: "All'interno di un modulo, sì. Tra moduli è meglio passare DTO o identificativi: il model si porta dietro relazioni, query lazy e la possibilità di modificare dati altrui tramite save(). Un confine fatto di DTO rende esplicite le dipendenze e permette di cambiare lo schema delle tabelle di un modulo senza rompere i vicini." }
    - { question: 'Quando classi Action e DTO sono overengineering?', answer: 'Per CRUD senza regole di business, pannelli di amministrazione e prototipi. Se un controller di cinque righe valida la richiesta e salva il model, spostare tutto in una Action e un DTO aggiunge tre file e nessuna regola. I livelli si ripagano quando lo scenario acquisisce invarianti, più punti di ingresso o effetti collaterali.' }
published: '2026-10-03'
---
# Classi Action, servizi e DTO in Laravel: come alleggerire controller e model

Quasi ogni progetto Laravel attraversa la stessa fase. `OrderController@store` cresce fino a duecento righe: validazione, calcolo del prezzo con codice promozionale, prenotazione della merce, addebito, email al cliente, webhook verso il CRM. Poi arriva l'API per l'app mobile, e quelle duecento righe vengono copiate in un secondo controller. Poi un comando console per importare gli ordini da un marketplace: terza copia. Sei mesi dopo, in una delle copie viene corretto un bug di arrotondamento, nelle altre due no.

In parallelo cresce il model `Order`: contiene i metodi `send()`, `refund()`, `syncWithCrm()`, gli handler degli eventi e un paio di richieste HTTP. Lo si può testare solo via HTTP, e ogni modifica fa paura. Questo articolo spiega come distribuire questo codice su più livelli: **classi Action** per gli scenari, **servizi** per le operazioni riutilizzabili, **DTO** per i dati che passano tra loro. E dove fermarsi per non trasformare un semplice CRUD in un monolite enterprise con quaranta interfacce.

**Materiali correlati:** [Antipattern di progettazione](design-antipatterns) · [Pattern strutturali GoF](structural-design-patterns) · [Integrazioni resilienti](resilient-external-integrations) · [Code di Laravel in produzione](laravel-queues-production)

## Indice

* [Sintomi di controller e model troppo grassi](#symptoms)
* [Mappa dei livelli: chi è responsabile di cosa](#layers)
* [DTO: un contratto al posto di un array](#dto)
* [Classi Action: uno scenario, una classe](#actions)
* [Service layer: quando serve davvero](#services)
* [Il controller dopo il refactoring](#thin-controller)
* [Una logica, molti ingressi: comandi, job, test](#reuse)
* [Cosa resta nel model](#models)
* [Confini tra moduli: DTO tra contesti](#modules)
* [Quando è overengineering](#when-not)
* [Come fare refactoring in modo graduale](#migration)
* [Errori comuni](#common-mistakes)
* [Checklist](#checklist)
* [Quiz di autovalutazione](#self-test-quiz)

---

<a id="symptoms"></a>
## Sintomi di controller e model troppo grassi

Il problema non è il numero di righe, ma il fatto che in un unico punto si mescolano ragioni di cambiamento diverse. Segnali che è ora di ridistribuire il codice:

* **La logica viene copiata tra i punti di ingresso.** Il controller web, il controller API e il comando console fanno «quasi la stessa cosa».
* **Testare uno scenario richiede una richiesta HTTP.** Per verificare il calcolo di uno sconto bisogna fare login, compilare un form e interpretare un redirect.
* **Non è chiaro dove sia il confine della transazione.** L'ordine è salvato, la prenotazione della merce è fallita, e nel database resta un ordine senza prenotazione.
* **Gli effetti collaterali avvengono prima del commit.** L'email «ordine effettuato» è partita, ma la transazione è stata annullata.
* **Il model sa tutto.** `Order` invia email, chiama il CRM e calcola le tasse. Ogni task richiede di modificare lo stesso file, e i conflitti in Git diventano la norma.
* **Array dalla forma sconosciuta.** `$data['items'][0]['qty']` o `$data['items'][0]['quantity']`? La risposta sta solo nel debugger.

---

<a id="layers"></a>
## Mappa dei livelli: chi è responsabile di cosa

| Livello | È responsabile di | Non deve |
|------|-------------|-----------|
| **Controller** | Ricevere la richiesta HTTP, invocare lo scenario, restituire la risposta | Calcolare prezzi, chiamare API esterne |
| **FormRequest** | Autorizzazione e validazione dell'input, costruzione del DTO | Salvare dati |
| **DTO** | Trasportare dati tipizzati tra i livelli | Contenere logica di business e accedere al DB |
| **Action** | Un singolo scenario di business, confini della transazione, ordine dei passi | Sapere di HTTP, sessione, redirect |
| **Service** | Un'operazione o un'integrazione riutilizzabile (prezzi, pagamenti, magazzino) | Gestire lo scenario nel suo insieme |
| **Model** | Dati, relazioni, cast, scope, semplici valori derivati | Inviare email, fare richieste HTTP |
| **Job / Event** | Effetti collaterali differiti e asincroni | Restituire un risultato alla richiesta |

La direzione delle dipendenze è una sola: controller → Action → servizi e model. Un servizio non chiama mai una Action, e il model non conosce né le une né gli altri.

---

<a id="dto"></a>
## DTO: un contratto al posto di un array

Un DTO (Data Transfer Object) è un oggetto senza comportamento che trasporta dati attraverso il confine di un livello. Nel PHP moderno bastano poche righe grazie alle classi readonly ([PHP 8.2](../php/8.2)) e alla promozione delle proprietà nel costruttore:

```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,
    ) {}
}
```

Cosa offre rispetto a un array:

* **Tipi.** `quantity` è sempre un `int`, `delivery` è un valore dell'enum e non la stringa `"courrier"` con un refuso.
* **Immutabilità.** Nessun passo dello scenario può «ritoccare in silenzio» i dati per il passo successivo.
* **Suggerimenti dell'IDE e analisi statica.** PHPStan/Psalm vedono l'accesso a un campo inesistente ancora prima dell'esecuzione.
* **Indipendenza dalla sorgente.** La Action non sa se i dati arrivano da un form, da un'API JSON, da un import CSV o da un test.

La validazione resta nella `FormRequest`: è il suo lavoro. Il DTO viene costruito a partire dai dati già validati. È comodo tenere la costruzione accanto alle regole:

```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]
> **Serve un pacchetto?** Per una decina di DTO bastano classi readonly scritte a mano. Pacchetti come `spatie/laravel-data` aggiungono la costruzione automatica dalla richiesta, la validazione tramite attributi e la trasformazione in JSON. È comodo quando i DTO sono centinaia e fungono anche da API resource, ma lega il livello di dominio al pacchetto. Partite da classi semplici.

Nei DTO conservate gli importi come interi nell'unità minima (centesimi) o come oggetto `Money`, non come `float`: in PHP `0.1 + 0.2` non è uguale a `0.3`.

---

<a id="actions"></a>
## Classi Action: uno scenario, una classe

Una Action è una classe con un solo metodo pubblico che esegue un singolo scenario di business dall'inizio alla fine. Il nome è un verbo del linguaggio del dominio: `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;
        });
    }
}
```

Le decisioni chiave in questo codice:

* **Il confine della transazione sta nella Action.** Lo scenario sa quali passi devono essere eseguiti in modo atomico. Il controller non se ne preoccupa, e nemmeno i servizi.
* **Dipendenze tramite costruttore.** Il container di Laravel costruisce da solo `PriceCalculator` e `StockReservations`, e nei test è facile sostituirli.
* **Effetti collaterali dopo il commit.** Un evento che implementa `ShouldDispatchAfterCommit` viene inviato solo dopo il commit della transazione. Per i job in coda fa lo stesso `ShouldQueueAfterCommit` oppure l'opzione `after_commit` della connessione.
* **Restituisce un risultato, non una risposta HTTP.** La Action non sa cosa succede dopo: un redirect, un JSON o una riga in 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) {}
}
```

Come chiamare il metodo — `handle()`, `execute()` o `__invoke()` — è una convenzione di team. L'importante è che sia uno solo e uguale in tutto il progetto. `__invoke()` permette di passare la Action come callable, `handle()` si legge meglio in una chiamata esplicita.

> [!NOTE]
> **Una Action può chiamare un'altra Action** se fa parte dello scenario: `PlaceOrder` può chiamare `ApplyLoyaltyPoints`. Ma attenzione alle transazioni: un `DB::transaction()` annidato crea un savepoint, e il rollback di quello interno non annulla quello esterno se l'eccezione viene intercettata.

---

<a id="services"></a>
## Service layer: quando serve davvero

Un servizio è una classe costruita attorno a una singola responsabilità usata da più scenari: calcolo dei prezzi, gestione del magazzino, gateway di pagamento, generazione di PDF. Non sa perché è stato chiamato.

```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);
}
```

Un'interfaccia è giustificata quando le implementazioni sono più di una (il gateway reale e un fake per i test, due provider in paesi diversi) o quando rappresenta il confine con il mondo esterno. Per un `PriceCalculator` interno con un'unica implementazione l'interfaccia è un file in più: la classe concreta si sostituisce nei test altrettanto facilmente con `$this->mock()` o `$this->app->instance()`.

Il pericolo principale del service layer è il **God Service**. Un `OrderService` con i metodi `create`, `cancel`, `refund`, `export`, `notify`, `recalculate` e quaranta dipendenze nel costruttore è lo stesso controller grasso, solo in un'altra cartella. Se un servizio ha più di cinque-sette metodi pubblici non legati da dati comuni, si tratta di diverse classi Action incollate in una sola.

---

<a id="thin-controller"></a>
## Il controller dopo il 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);
}
```

Il controller traduce l'HTTP in una chiamata allo scenario e il risultato dello scenario di nuovo in HTTP. L'eccezione di dominio `InsufficientStock` diventa un errore del form. In un controller API la stessa eccezione diventerà una risposta `422`, mentre la logica dell'ordine non cambia.

---

<a id="reuse"></a>
## Una logica, molti ingressi: comandi, job, test

La stessa Action, senza modifiche, viene invocata dal comando console di 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;
}
```

E si verifica con un test senza HTTP, sessioni e 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);
    }
}
```

A questo punto i test del controller diventano sottili: verificano validazione, autorizzazione e correttezza della risposta. Le regole di business si verificano una volta sola, a livello di Action.

---

<a id="models"></a>
## Cosa resta nel model

L'obiettivo non è un «model anemico» senza un solo metodo. Il model Eloquent è il posto giusto per tutto ciò che descrive **i dati stessi**:

* relazioni (`lines()`, `customer()`), cast (`'delivery' => DeliveryMethod::class`), scope (`scopePaid()`);
* semplici valori derivati senza effetti collaterali: `isPaid()`, `canBeCancelledBy(User $user)`, l'accessor `total` a partire da `total_cents`;
* invarianti di una singola entità che non richiedono altri servizi.

Cosa non deve stare nel model: invio di email, richieste HTTP, interazione con le code, logica complessa negli observer. Gli observer sono particolarmente insidiosi: scattano durante gli import, nei seeder e nei test, e dal codice dello scenario non si vedono. Se un effetto collaterale fa parte dello scenario, il suo posto è nella Action.

---

<a id="modules"></a>
## Confini tra moduli: DTO tra contesti

Quando il progetto cresce, conviene raggruppare il codice non per livelli tecnici (`Controllers`, `Models`, `Services`) ma per aree di dominio:

```
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 regola per i confini: **il modulo `Billing` non accetta il model `Order` del modulo `Orders`.** Riceve un DTO o un identificativo:

```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,
        ));
    }
}
```

Perché non il model:

* il model permette di chiamare `$order->update()` da un modulo esterno, e nessuno saprà chi ha cambiato lo stato;
* le relazioni lazy trasformano l'accesso a `$order->customer->address` in query di cui il modulo `Billing` non sospetta nulla;
* lo schema della tabella `orders` diventa un'API pubblica: non si può modificare senza verificare tutti i moduli.

È lo stesso principio del [passaggio dal monolite ai microservizi](monolith-to-microservices-architecture): prima confini espliciti all'interno di un'unica applicazione, e solo dopo, se mai servirà, attraverso la rete.

---

<a id="when-not"></a>
## Quando è overengineering

I livelli costano tempo: più file, più salti nel codice, più convenzioni. Non si ripagano se:

* **È un CRUD senza regole.** Un form di modifica del profilo che valida e salva cinque campi non ha bisogno di `UpdateProfileAction` e `UpdateProfileData`. `$user->update($request->validated())` è codice perfettamente normale.
* **È un pannello di amministrazione su Filament o Nova.** Il framework impone già la propria struttura, e le classi Action sopra di essa spesso duplicano le sue risorse.
* **È un prototipo.** Finché state cercando il prodotto, la velocità di cambiamento conta più dell'architettura. Il refactoring in livelli arriva quando lo scenario si è stabilizzato.
* **Lo scenario ha un solo punto di ingresso e nessun effetto collaterale.** Non c'è niente da riutilizzare né da isolare.

Un buon segnale che è il momento: il secondo ingresso nello stesso scenario, il primo «l'email è partita ma la transazione è stata annullata» o il primo test impossibile da scrivere senza HTTP.

---

<a id="migration"></a>
## Come fare refactoring in modo graduale

Riscrivere l'intero progetto «ad Action» non è necessario ed è pericoloso. Il percorso che funziona:

1. **I nuovi scenari nascono subito nel nuovo stile.** Questo fissa un modello senza rischi per il codice esistente.
2. **Prima di estrarre, un test di caratterizzazione.** Si scrive un feature test sul comportamento attuale del controller, anche se è strano. Intercetta le regressioni durante lo spostamento.
3. **Estraete uno scenario alla volta.** Il corpo del metodo del controller si sposta nell'`handle()` della Action quasi senza modifiche, e il controller inizia a chiamarla. Il test deve restare verde.
4. **Solo dopo migliorate.** Introducete DTO al posto degli array, spostate gli effetti collaterali dopo il commit, estraete servizi dai pezzi ripetuti.
5. **Eliminate le copie.** Il secondo e il terzo controller iniziano a chiamare la stessa Action, e i duplicati spariscono.

Ogni passo è una PR separata e piccola, che si può annullare.

---

<a id="common-mistakes"></a>
## Errori comuni

**1. La Action accetta una `Request`.**
Lo scenario è di nuovo legato all'HTTP, e non si può invocarlo da un comando o da un job senza una richiesta fittizia. Passate un DTO.

**2. DTO con logica di business.**
Un metodo `calculateTotal()` in un DTO è un servizio nascosto in un oggetto dati. Il DTO trasporta solo dati.

**3. DTO mutabili.**
Proprietà pubbliche non readonly permettono a un passo di modificare i dati per un altro. Usate `readonly`.

**4. Un God Service al posto del controller grasso.**
Un `OrderService` di duemila righe è lo stesso problema in una nuova cartella. Gli scenari vanno nelle classi Action, i servizi restano focalizzati.

**5. Effetti collaterali dentro la transazione.**
Email, webhook e job in coda inviati prima del commit scattano anche in caso di rollback. Usate `ShouldDispatchAfterCommit`, `ShouldQueueAfterCommit` o `afterCommit()`.

**6. Un'interfaccia per ogni classe.**
Una `PriceCalculatorInterface` con un'unica implementazione e senza confine esterno è un livello di indirezione superfluo. Laravel sa sostituire le classi concrete nei test.

**7. Livelli per il gusto dei livelli.**
`UpdateUserNameAction` con `UpdateUserNameData` per una sola riga di `$user->update()` è cerimonia, non architettura.

---

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

1. Il controller si limita a tradurre l'HTTP in una chiamata allo scenario e il risultato di nuovo in HTTP.
2. Validazione e autorizzazione stanno nella `FormRequest`, insieme alla costruzione del DTO.
3. Un singolo scenario di business corrisponde a una classe Action con un solo metodo pubblico.
4. Il confine della transazione è definito nella Action.
5. Gli effetti collaterali (email, webhook, job) vengono inviati dopo il commit.
6. I DTO sono `final readonly`, tipizzati e senza logica; gli importi non sono `float`.
7. I servizi sono focalizzati; le interfacce solo per confini esterni e implementazioni multiple.
8. Tra moduli si passano DTO o identificativi, non model.
9. Le regole di business sono coperte da test a livello di Action, senza HTTP.

---

## Conclusione

Classi Action, servizi e DTO non riguardano l'estetica delle cartelle, ma il fatto che ogni scenario di business abbia un unico posto, un unico confine di transazione e un unico insieme di test. Partite dal dolore — duplicazione, effetti collaterali prima del commit, impossibilità di testare — e introducete esattamente i livelli necessari per eliminarlo. Un CRUD semplice lasciatelo semplice.

---

<a id="self-test-quiz"></a>
## Quiz di autoverifica

### Domanda 1: Dove va definito il confine della transazione quando si effettua un ordine?
- A) Nel controller, attorno alla chiamata alla Action.
- B) Nella classe Action, che sa quali passi dello scenario devono essere eseguiti in modo atomico.
- C) In ogni servizio separatamente: una transazione per ogni chiamata.

<details>
<summary><b>Mostra la risposta</b></summary>

**Risposta: B**
Lo scenario sa che l'ordine, le sue righe e la prenotazione della merce devono essere salvati insieme. Il controller non deve conoscere questi dettagli, e transazioni separate nei servizi non garantiscono l'atomicità dell'intero scenario.
</details>

### Domanda 2: All'interno di `DB::transaction()` viene inviata l'email «Ordine effettuato», poi la prenotazione della merce fallisce con un'eccezione. Cosa succede senza misure aggiuntive?
- A) L'email non parte, perché la transazione è stata annullata.
- B) L'email parte (o il job di invio finisce in coda), anche se l'ordine nel database non c'è.
- C) Laravel annulla automaticamente l'email tramite un evento di rollback.

<details>
<summary><b>Mostra la risposta</b></summary>

**Risposta: B**
Il rollback della transazione riguarda solo il database. Gli effetti collaterali esterni vanno inviati dopo il commit: tramite `ShouldDispatchAfterCommit` per gli eventi, `ShouldQueueAfterCommit` o `afterCommit()` per i job.
</details>

### Domanda 3: Il modulo `Billing` deve addebitare l'importo di un ordine. Cosa conviene passargli dal modulo `Orders`?
- A) L'intero model `Order`, così che `Billing` possa caricare da solo le relazioni necessarie.
- B) Un DTO con importo, valuta e riferimento all'ordine, oppure l'identificativo dell'ordine.
- C) L'array `$request->all()` della richiesta originale.

<details>
<summary><b>Mostra la risposta</b></summary>

**Risposta: B**
Il DTO fissa quali dati servono al modulo e gli impedisce di modificare record altrui o di eseguire query nascoste tramite le relazioni. Lo schema della tabella `orders` smette di essere un'API pubblica per gli altri moduli.
</details>