---
title: 'Action класове, сървиси и DTO в Laravel | DevSense'
description: 'Как да разтоварите дебелите контролери и модели в Laravel: Action класове, сървисен слой, DTO с readonly класове, граници на модулите, транзакции и събития — и кога всичко това е overengineering.'
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 са overengineering?', answer: 'При CRUD без бизнес правила, админ панели и прототипи. Ако контролер от пет реда валидира заявката и записва модела, изнасянето в Action и DTO ще добави три файла и нито едно правило. Слоевете се изплащат, когато сценарият придобие инварианти, няколко входни точки или странични ефекти.' }
published: '2026-10-03'
---
# Action класове, сървиси и DTO в Laravel: как да разтоварим контролерите и моделите

Почти всеки Laravel проект минава през един и същ етап. `OrderController@store` нараства до двеста реда: валидация, изчисляване на цената с промокод, резервиране на стоката, теглене на парите, имейл до клиента, webhook към 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)
* [Кога това е overengineering](#when-not)
* [Как да рефакторирате постепенно](#migration)
* [Чести грешки](#common-mistakes)
* [Чеклист](#checklist)
* [Тест за самопроверка](#self-test-quiz)

---

<a id="symptoms"></a>
## Симптоми на дебелия контролер и дебелия модел

Проблемът не е в броя редове, а в това, че на едно място са смесени различни причини за промяна. Признаци, че е време да подредите кода:

* **Логиката се копира между входните точки.** Уеб контролерът, API контролерът и конзолната команда правят „почти едно и също“.
* **Тестът на сценария изисква HTTP заявка.** За да проверите изчисляването на отстъпката, трябва да се логнете, да сглобите формата и да разчетете редиректа.
* **Не е ясно къде е границата на транзакцията.** Поръчката е записана, а резервирането на стоката е гръмнало — и в базата е останала поръчка без резервация.
* **Страничните ефекти се случват преди commit.** Имейлът „поръчката е приета“ е изпратен, а транзакцията е върната назад.
* **Моделът знае за всичко.** `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** | Данни, релации, cast-ове, scope-ове, прости производни стойности | Да праща имейли, да прави HTTP заявки |
| **Job / Event** | Отложени и асинхронни странични ефекти | Да връща резултат към заявката |

Посоката на зависимостите е една: контролер → Action → сървиси и модели. Сървисът никога не извиква Action, а моделът не знае нито за едните, нито за другите.

---

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

DTO (Data Transfer Object) е обект без поведение, който пренася данни през границата на слоя. В съвременния PHP това са няколко реда благодарение на readonly класовете ([PHP 8.2](../php/8.2)) и constructor property promotion:

```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`, а в тест е лесно да ги подмените.
* **Странични ефекти след commit.** Събитие с интерфейса `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()`), cast-ове (`'delivery' => DeliveryMethod::class`), scope-ове (`scopePaid()`);
* прости производни стойности без странични ефекти: `isPaid()`, `canBeCancelledBy(User $user)`, accessor `total` от `total_cents`;
* инварианти на една същност, които не изискват други сървиси.

Какво не трябва да има в модела: изпращане на имейли, HTTP заявки, работа с опашки, сложна логика в observers. Observers са особено коварни: те се задействат и при импорт, и в seeder-ите, и в тестовете, а от кода на сценария не се виждат. Ако страничният ефект е част от сценария, мястото му е в 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>
## Кога това е overengineering

Слоевете струват време: повече файлове, повече преходи из кода, повече договорки. Те не се изплащат, ако:

* **Това е CRUD без правила.** Форма за редакция на профил, която валидира и записва пет полета, няма нужда от `UpdateProfileAction` и `UpdateProfileData`. `$user->update($request->validated())` е нормален код.
* **Това е админ панел на Filament или Nova.** Фреймуъркът вече задава своя структура и Action класовете върху нея често дублират ресурсите му.
* **Това е прототип.** Докато търсите продукта, скоростта на промените е по-важна от архитектурата. Рефакторинг към слоеве — когато сценарият се е стабилизирал.
* **Сценарият има една входна точка и няма странични ефекти.** Няма какво да се преизползва и няма какво да се изолира.

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

---

<a id="migration"></a>
## Как да рефакторирате постепенно

Не е нужно, а и е опасно, да пренаписвате целия проект „на Actions“. Работещият път:

1. **Новите сценарии — веднага в новия стил.** Това задава образец без риск за стария код.
2. **Преди изнасянето — характеризиращ тест.** Пише се feature тест за текущото поведение на контролера, дори ако то е странно. Той хваща регресии при преноса.
3. **Изнасяйте по един сценарий.** Тялото на метода на контролера се премества в `handle()` на Action почти без промени, а контролерът започва да го извиква. Тестът трябва да остане зелен.
4. **Едва след това подобрявайте.** Въвеждайте DTO вместо масив, премествайте страничните ефекти след commit, отделяйте сървиси от повтарящите се парчета.
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. Странични ефекти вътре в транзакцията.**
Имейли, webhook-ове и задачи в опашка, изпратени преди commit, се задействат дори при rollback. Използвайте `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. Страничните ефекти (имейли, webhook-ове, задачи) се изпращат след commit.
6. DTO е `final readonly`, с типове и без логика; парите не са във `float`.
7. Сървисите са тесни; интерфейси — само за външни граници и при няколко реализации.
8. Между модулите се предават DTO или идентификатори, а не модели.
9. Бизнес правилата са покрити с тестове на ниво Action, без HTTP.

---

## Обобщение

Action класовете, сървисите и DTO не са за красиви папки, а за това всеки бизнес сценарий да има едно място, една граница на транзакцията и един набор от тестове. Започнете от болката — дублиране, странични ефекти преди commit, невъзможност за тестване — и въвеждайте точно толкова слоеве, колкото са нужни, за да я премахнете. Простият CRUD нека си остане прост.

---

<a id="self-test-quiz"></a>
## Тест за самопроверка

### Въпрос 1: Къде трябва да се определя границата на транзакцията при оформяне на поръчка?
- А) В контролера, около извикването на Action.
- Б) В Action класа, който знае кои стъпки от сценария трябва да се изпълнят атомарно.
- В) Във всеки сървис поотделно — собствена транзакция за всяко извикване.

<details>
<summary>Покажи правилния отговор</summary>

**Правилен отговор: Б**
Сценарият знае, че поръчката, нейните редове и резервацията на стоката трябва да се запишат заедно. Контролерът не трябва да знае такива детайли, а отделните транзакции в сървисите не дават атомарност на целия сценарий.
</details>

### Въпрос 2: Вътре в `DB::transaction()` се изпраща имейл „Поръчката е приета“, след което резервирането на стоката гърми с изключение. Какво ще се случи без допълнителни мерки?
- А) Имейлът няма да тръгне, защото транзакцията е върната назад.
- Б) Имейлът ще тръгне (или задачата за изпращането му ще попадне в опашката), въпреки че поръчката я няма в базата.
- В) Laravel автоматично ще отмени имейла чрез събитие за rollback.

<details>
<summary>Покажи правилния отговор</summary>

**Правилен отговор: Б**
Връщането назад на транзакцията засяга само базата данни. Външните странични ефекти трябва да се изпращат след commit: чрез `ShouldDispatchAfterCommit` при събитията, `ShouldQueueAfterCommit` или `afterCommit()` при задачите.
</details>

### Въпрос 3: Модулът `Billing` трябва да таксува поръчка. Какво е по-добре да му се подаде от модула `Orders`?
- А) Целия модел `Order`, за да може `Billing` сам да зареди нужните релации.
- Б) DTO със сумата, валутата и референция към поръчката или идентификатора на поръчката.
- В) Масива `$request->all()` от изходната заявка.

<details>
<summary>Покажи правилния отговор</summary>

**Правилен отговор: Б**
DTO фиксира какви данни са нужни на модула и не му позволява да променя чужди записи или да прави скрити заявки през релациите. Схемата на таблицата `orders` престава да бъде публичен API за другите модули.
</details>