---
title: 'Action-класи, сервіси та DTO в Laravel | DevSense'
description: 'Як розвантажити товсті контролери й моделі в Laravel: Action-класи, сервісний шар, DTO на readonly-класах, межі модулів, транзакції та події — і коли все це оверінжиніринг.'
faq:
    - { question: 'Чим Action-клас відрізняється від сервісу в Laravel?', answer: 'Action описує один бізнес-сценарій (оформити замовлення, запросити учасника до команди) і має один публічний метод. Сервіс групує операції навколо ресурсу чи інтеграції (платіжний шлюз, калькулятор цін) і не знає про сценарій загалом. Action викликає сервіси, а не навпаки.' }
    - { question: 'Навіщо DTO, якщо є $request->validated() і масиви?', answer: "Масив не каже, які ключі в ньому є і яких вони типів: одрук у ключі спливає лише в рантаймі, IDE не підказує, статичний аналіз безсилий. DTO на readonly-класі фіксує контракт — тип кожного поля, обов'язковість і незмінність. Той самий Action можна викликати з контролера, консольної команди, задачі черги й тесту." }
    - { question: 'Чи можна передавати Eloquent-моделі між модулями?', answer: "Усередині модуля — так. Між модулями краще передавати DTO або ідентифікатори: модель тягне за собою зв'язки, ліниві запити й можливість змінити чужі дані через save(). Межа з DTO робить залежності явними й дає змогу змінювати схему таблиць модуля, не ламаючи сусідів." }
    - { question: 'Коли Action-класи та DTO — це оверінжиніринг?', answer: "Для CRUD без бізнес-правил, адмінок і прототипів. Якщо контролер на п'ять рядків валідує запит і зберігає модель, винесення в Action і DTO додасть три файли й жодного правила. Шари окупаються, коли в сценарію з'являються інваріанти, кілька точок входу або побічні ефекти." }
published: '2026-10-03'
---
# Action-класи, сервіси та DTO в Laravel: як розвантажити контролери й моделі

Майже кожен Laravel-проєкт проходить ту саму стадію. `OrderController@store` розростається до двохсот рядків: валідація, розрахунок ціни з промокодом, резерв товару, списання коштів, лист клієнтові, вебхук у CRM. Потім з'являється API для мобільного застосунку, і ці двісті рядків копіюються в другий контролер. Потім — консольна команда для імпорту замовлень із маркетплейсу, третя копія. За пів року в одній із копій виправлено баг з округленням, а у двох інших — ні.

Паралельно росте модель `Order`: у ній методи `send()`, `refund()`, `syncWithCrm()`, обробники подій і кілька HTTP-запитів. Тестувати це можна лише через HTTP, а будь-яка зміна лякає. Ця стаття — про те, як розкласти такий код по шарах: **Action-класи** для сценаріїв, **сервіси** для операцій, що перевикористовуються, **DTO** для даних між ними. І про те, де зупинитися, щоб не перетворити простий CRUD на корпоративний моноліт із сорока інтерфейсів.

**Пов'язані матеріали:** [Антипатерни проєктування](design-antipatterns) · [Структурні патерни GoF](structural-design-patterns) · [Відмовостійкі інтеграції](resilient-external-integrations) · [Черги Laravel у продакшені](laravel-queues-production)

## Зміст

* [Симптоми товстого контролера й товстої моделі](#symptoms)
* [Карта шарів: хто за що відповідає](#layers)
* [DTO: контракт замість масиву](#dto)
* [Action-класи: один сценарій — один клас](#actions)
* [Сервісний шар: коли він справді потрібен](#services)
* [Контролер після рефакторингу](#thin-controller)
* [Одна логіка — багато входів: команди, задачі, тести](#reuse)
* [Що залишається в моделі](#models)
* [Межі модулів: DTO між контекстами](#modules)
* [Коли це оверінжиніринг](#when-not)
* [Як рефакторити поступово](#migration)
* [Часті помилки](#common-mistakes)
* [Чеклист](#checklist)
* [Квіз для самоперевірки](#self-test-quiz)

---

<a id="symptoms"></a>
## Симптоми товстого контролера й товстої моделі

Проблема не в кількості рядків, а в тому, що в одному місці змішано різні причини для змін. Ознаки, що час розкладати код:

* **Логіка копіюється між точками входу.** Веб-контролер, API-контролер і консольна команда роблять «майже те саме».
* **Тест сценарію потребує HTTP-запиту.** Щоб перевірити розрахунок знижки, доводиться логінитися, збирати форму й розбирати редирект.
* **Незрозуміло, де межа транзакції.** Замовлення збережено, а резерв товару впав — і в базі лишилося замовлення без резерву.
* **Побічні ефекти відбуваються до коміту.** Лист «замовлення оформлено» пішов, а транзакція відкотилася.
* **Модель знає про все.** `Order` надсилає листи, ходить у CRM і рахує податки. Будь-яка задача потребує правок того самого файлу, а конфлікти в Git стають нормою.
* **Масиви з невідомою формою.** `$data['items'][0]['qty']` чи `$data['items'][0]['quantity']`? Відповідь — лише у дебагері.

---

<a id="layers"></a>
## Карта шарів: хто за що відповідає

| Шар | Відповідає за | Не повинен |
|------|-------------|-----------|
| **Controller** | Прийняти HTTP-запит, викликати сценарій, повернути відповідь | Рахувати ціни, ходити в зовнішні API |
| **FormRequest** | Авторизацію та валідацію вхідних даних, збирання DTO | Зберігати дані |
| **DTO** | Перенесення типізованих даних між шарами | Містити бізнес-логіку та звертатися до БД |
| **Action** | Один бізнес-сценарій, межі транзакції, порядок кроків | Знати про HTTP, сесію, редиректи |
| **Service** | Операцію, що перевикористовується, або інтеграцію (ціни, платежі, склад) | Керувати сценарієм загалом |
| **Model** | Дані, зв'язки, касти, скоупи, прості похідні значення | Надсилати листи, робити HTTP-запити |
| **Job / Event** | Відкладені й асинхронні побічні ефекти | Повертати результат у запит |

Напрямок залежностей один: контролер → Action → сервіси й моделі. Сервіс ніколи не викликає Action, а модель не знає ні про ті, ні про інші.

---

<a id="dto"></a>
## DTO: контракт замість масиву

DTO (Data Transfer Object) — об'єкт без поведінки, який переносить дані через межу шару. У сучасному PHP це кілька рядків завдяки readonly-класам ([PHP 8.2](../php/8.2)) і просуванню властивостей конструктора:

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

Що це дає порівняно з масивом:

* **Типи.** `quantity` — завжди `int`, `delivery` — значення enum, а не рядок `"courrier"` з одруком.
* **Незмінність.** Жоден крок сценарію не може «тихо» підправити дані для наступного.
* **Підказки IDE та статичний аналіз.** PHPStan/Psalm бачать звернення до неіснуючого поля ще до запуску.
* **Незалежність від джерела.** Action не знає, чи прийшли дані з форми, JSON API, CSV-імпорту або тесту.

Валідація залишається у `FormRequest` — це його робота. DTO збирається вже з перевірених даних. Зручно тримати збирання поруч із правилами:

```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]
> **Чи потрібен пакет?** Для десятка DTO вистачає ручних readonly-класів. Пакети на кшталт `spatie/laravel-data` додають автоматичне збирання із запиту, валідацію за атрибутами й трансформацію в JSON. Це зручно, коли DTO сотні й вони ж слугують API-ресурсами, але прив'язує доменний шар до пакета. Починайте з простих класів.

Гроші в DTO зберігайте цілим числом у мінімальних одиницях (копійки, центи) або об'єктом `Money`, а не `float`: `0.1 + 0.2` у PHP не дорівнює `0.3`.

---

<a id="actions"></a>
## Action-класи: один сценарій — один клас

Action — клас з одним публічним методом, який виконує один бізнес-сценарій від початку до кінця. Назва — дієслово з мови предметної області: `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;
        });
    }
}
```

Ключові рішення в цьому коді:

* **Межа транзакції — в Action.** Сценарій знає, які кроки мають виконатися атомарно. Контролер про це не думає, сервіси — теж.
* **Залежності через конструктор.** Контейнер Laravel сам збере `PriceCalculator` і `StockReservations`, а в тесті їх легко підмінити.
* **Побічні ефекти після коміту.** Подія з інтерфейсом `ShouldDispatchAfterCommit` надсилається лише після фіксації транзакції. Для задач черги те саме робить `ShouldQueueAfterCommit` або опція `after_commit` з'єднання.
* **Повертається результат, а не HTTP-відповідь.** Action не знає, що далі — редирект, JSON чи рядок у консолі.

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

Як називати метод — `handle()`, `execute()` чи `__invoke()` — питання домовленості в команді. Важливо, щоб він був один і однаковий в усьому проєкті. `__invoke()` дає змогу передавати Action як callable, `handle()` краще читається при явному виклику.

> [!NOTE]
> **Action може викликати інший Action**, якщо це частина сценарію: `PlaceOrder` може викликати `ApplyLoyaltyPoints`. Але стежте за транзакціями: вкладений `DB::transaction()` створює savepoint, і відкат внутрішньої не відкочує зовнішню, якщо виняток перехоплено.

---

<a id="services"></a>
## Сервісний шар: коли він справді потрібен

Сервіс — це клас навколо однієї відповідальності, яку використовують кілька сценаріїв: розрахунок цін, робота зі складом, платіжний шлюз, генерація PDF. Він не знає, навіщо його викликали.

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

Інтерфейс виправданий, коли реалізацій більше ніж одна (бойовий шлюз і фейк для тестів, два провайдери в різних країнах) або коли це межа із зовнішнім світом. Для внутрішнього `PriceCalculator` з єдиною реалізацією інтерфейс — зайвий файл: конкретний клас так само легко підмінити в тесті через `$this->mock()` або `$this->app->instance()`.

Головна небезпека сервісного шару — **God Service**. `OrderService` з методами `create`, `cancel`, `refund`, `export`, `notify`, `recalculate` і сорока залежностями в конструкторі — це той самий товстий контролер, лише в іншій теці. Якщо в сервісі більше п'яти-семи публічних методів, не пов'язаних спільними даними, це кілька Action-класів, що злиплися в один.

---

<a id="thin-controller"></a>
## Контролер після рефакторингу

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

Контролер перетворює HTTP на виклик сценарію, а результат сценарію — назад на HTTP. Доменний виняток `InsufficientStock` стає помилкою форми. В API-контролері той самий виняток стане відповіддю `422`, а логіка замовлення при цьому не змінюється.

---

<a id="reuse"></a>
## Одна логіка — багато входів: команди, задачі, тести

Той самий Action без змін викликається з консольної команди імпорту:

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

І перевіряється тестом без HTTP, сесій і 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);
    }
}
```

Тести контролера після цього стають тонкими: перевіряють валідацію, авторизацію і те, що відповідь правильна. Бізнес-правила перевіряються один раз — на рівні Action.

---

<a id="models"></a>
## Що залишається в моделі

Мета не в «анемічній моделі» без жодного методу. Eloquent-модель — добре місце для всього, що описує **самі дані**:

* зв'язки (`lines()`, `customer()`), касти (`'delivery' => DeliveryMethod::class`), скоупи (`scopePaid()`);
* прості похідні значення без побічних ефектів: `isPaid()`, `canBeCancelledBy(User $user)`, аксесор `total` з `total_cents`;
* інваріанти однієї сутності, які не потребують інших сервісів.

Чого в моделі бути не повинно: надсилання листів, HTTP-запитів, роботи з чергами, складної логіки в observers. Observers особливо підступні: вони спрацьовують і під час імпорту, і в сидерах, і в тестах, а з коду сценарію їх не видно. Якщо побічний ефект — частина сценарію, йому місце в Action.

---

<a id="modules"></a>
## Межі модулів: DTO між контекстами

Коли проєкт росте, корисно групувати код не за технічними шарами (`Controllers`, `Models`, `Services`), а за предметними областями:

```
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/
```

Правило для меж: **модуль `Billing` не приймає модель `Order` з модуля `Orders`.** Він отримує DTO або ідентифікатор:

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

Чому не модель:

* модель дає змогу викликати `$order->update()` з чужого модуля — і ніхто не дізнається, хто змінив статус;
* ліниві зв'язки перетворюють звернення до `$order->customer->address` на запити, про які модуль `Billing` навіть не здогадується;
* схема таблиці `orders` стає публічним API — її не можна змінювати, не перевіривши всі модулі.

Це той самий принцип, що й під час [переходу від моноліту до мікросервісів](monolith-to-microservices-architecture): спершу явні межі всередині одного застосунку, і лише потім — через мережу, якщо це взагалі знадобиться.

---

<a id="when-not"></a>
## Коли це оверінжиніринг

Шари коштують часу: більше файлів, більше переходів по коду, більше домовленостей. Вони не окупаються, якщо:

* **Це CRUD без правил.** Форма редагування профілю, яка валідує й зберігає п'ять полів, не потребує `UpdateProfileAction` і `UpdateProfileData`. `$user->update($request->validated())` — нормальний код.
* **Це адмінка на Filament або Nova.** Фреймворк уже задає свою структуру, і Action-класи поверх неї часто дублюють його ресурси.
* **Це прототип.** Поки ви шукаєте продукт, швидкість змін важливіша за архітектуру. Рефакторинг у шари — коли сценарій устоявся.
* **У сценарію одна точка входу й немає побічних ефектів.** Нічого перевикористовувати й нічого ізолювати.

Добрий сигнал, що час: другий вхід у той самий сценарій, перше «лист пішов, а транзакція відкотилася» або перший тест, який неможливо написати без HTTP.

---

<a id="migration"></a>
## Як рефакторити поступово

Переписувати весь проєкт «на Actions» не потрібно й небезпечно. Робочий шлях:

1. **Нові сценарії — одразу в новому стилі.** Це задає зразок без ризику для старого коду.
2. **Перед винесенням — характеризаційний тест.** Пишеться feature-тест на поточну поведінку контролера, навіть якщо вона дивна. Він ловить регресії під час перенесення.
3. **Виносьте по одному сценарію.** Тіло методу контролера переїжджає в `handle()` Action майже без змін, контролер починає його викликати. Тест має лишитися зеленим.
4. **Лише потім покращуйте.** Запроваджуйте DTO замість масиву, переносьте побічні ефекти на після коміту, виділяйте сервіси з повторюваних фрагментів.
5. **Видаляйте копії.** Другий і третій контролери починають викликати той самий Action, дублікати зникають.

Кожен крок — окремий невеликий PR, який можна відкотити.

---

<a id="common-mistakes"></a>
## Типові помилки

**1. Action приймає `Request`.**
Сценарій знову прив'язаний до HTTP, і викликати його з команди чи задачі без фейкового запиту не можна. Передавайте DTO.

**2. DTO з бізнес-логікою.**
Метод `calculateTotal()` у DTO — це сервіс, захований в об'єкт даних. DTO лише переносить дані.

**3. Змінювані DTO.**
Публічні не-readonly властивості дають змогу одному кроку змінити дані для іншого. Використовуйте `readonly`.

**4. God Service замість товстого контролера.**
`OrderService` на дві тисячі рядків — та сама проблема в новій теці. Сценарії — в Action-класи, сервіси — вузькі.

**5. Побічні ефекти всередині транзакції.**
Листи, вебхуки й задачі черги, надіслані до коміту, спрацьовують навіть у разі відкату. Використовуйте `ShouldDispatchAfterCommit`, `ShouldQueueAfterCommit` або `afterCommit()`.

**6. Інтерфейс на кожен клас.**
`PriceCalculatorInterface` з єдиною реалізацією й без зовнішньої межі — зайвий рівень непрямості. Laravel уміє підмінювати конкретні класи в тестах.

**7. Шари заради шарів.**
`UpdateUserNameAction` з `UpdateUserNameData` для одного рядка `$user->update()` — це церемонія, а не архітектура.

---

<a id="checklist"></a>
## Чеклист

1. Контролер лише перетворює HTTP на виклик сценарію, а результат — назад на HTTP.
2. Валідація й авторизація — у `FormRequest`, там само збирання DTO.
3. Один бізнес-сценарій — один Action-клас з одним публічним методом.
4. Межа транзакції визначається в Action.
5. Побічні ефекти (листи, вебхуки, задачі) надсилаються після коміту.
6. DTO — `final readonly`, з типами й без логіки; гроші не у `float`.
7. Сервіси вузькі; інтерфейси — лише для зовнішніх меж і кількох реалізацій.
8. Між модулями передаються DTO або ідентифікатори, а не моделі.
9. Бізнес-правила покриті тестами на рівні Action, без HTTP.

---

## Підсумок

Action-класи, сервіси та DTO — не про красу тек, а про те, щоб у кожного бізнес-сценарію було одне місце, одна межа транзакції й один набір тестів. Починайте з болю — дублювання, побічних ефектів до коміту, неможливості протестувати — і запроваджуйте рівно стільки шарів, скільки потрібно, щоб його прибрати. Простий CRUD хай лишається простим.

---

<a id="self-test-quiz"></a>
## Квіз для самоперевірки

### Питання 1: Де має визначатися межа транзакції під час оформлення замовлення?
- А) У контролері, навколо виклику Action.
- Б) В Action-класі, який знає, які кроки сценарію мають виконатися атомарно.
- В) У кожному сервісі окремо — своя транзакція на кожен виклик.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
Сценарій знає, що замовлення, його рядки й резерв товару мають зберегтися разом. Контролер не повинен знати таких деталей, а окремі транзакції в сервісах не забезпечують атомарності всього сценарію.
</details>

### Питання 2: Усередині `DB::transaction()` надсилається лист «Замовлення оформлено», потім резерв товару падає з винятком. Що станеться без додаткових заходів?
- А) Лист не піде, бо транзакція відкотилася.
- Б) Лист піде (або задача на надсилання потрапить у чергу), хоча замовлення в базі немає.
- В) Laravel автоматично скасує лист через подію відкату.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
Відкат транзакції стосується лише бази даних. Зовнішні побічні ефекти потрібно надсилати після коміту: через `ShouldDispatchAfterCommit` у подій, `ShouldQueueAfterCommit` або `afterCommit()` у задач.
</details>

### Питання 3: Модулю `Billing` потрібно списати кошти за замовлення. Що краще передати йому з модуля `Orders`?
- А) Модель `Order` повністю, щоб `Billing` міг сам завантажити потрібні зв'язки.
- Б) DTO із сумою, валютою й посиланням на замовлення або ідентифікатор замовлення.
- В) Масив `$request->all()` з початкового запиту.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
DTO фіксує, які дані потрібні модулю, і не дає йому змінювати чужі записи чи робити приховані запити через зв'язки. Схема таблиці `orders` перестає бути публічним API для інших модулів.
</details>