Spatie Laravel Data вместо FormRequest и Resource

Модуль Auto. На границе HTTP и CLI — тонкие адаптеры. Создание автомобиля — один хендлер. Общий ответ с id живёт в kernel, чтобы его не копировали по модулям.

kernel/
└── Data/
    └── IdResult.php
modules/
└── Auto/
    ├── Services/
    │   ├── Data/
    │   │   └── CreateAutoData.php
    │   └── CreateAutoHandler.php
    ├── Console/
    │   └── CreateAutoCommand.php
    └── Http/
        └── Controllers/
            └── CreateAutoAction.php

CreateAutoData — контракт создания. PHP-типы дают базовые правила (string, bool, nullable). Атрибуты — дополнительный HTTP-guard: длина строки номера, условная обязательность описания. Формат и смысл номера здесь не проверяем — это инвариант Value Object в домене.

<?php

class CreateAutoData extends Data
{
    public function __construct(
        
        #[Min(8)]#[Max(9)] // пример HTTP-guard, бизнес-правила номера будут в ValueObject.
        public readonly string $number,
        
        public readonly bool $isSelf,
        
        #[RequiredIf('isSelf', true)]
        public readonly ?string $description,
        
    ) {}
}

Имена свойств — ключи JSON. Клиент шлёт isSelf, не is_self. Иначе нужен MapInputName. Для JSON API это обычно проще, чем тащить snake_case через весь PHP.

Атрибуты срабатывают, когда объект собирается из request (type-hint в контроллере или CreateAutoData::from($request)). Обычный new CreateAutoData(...) валидацию Laravel не запускает.

Хендлер не знает про Request, консоль и HTTP-коды. На входе объект, на выходе IdResult. В примере запись через Eloquent — заглушка: в боевом коде здесь доменная модель и VO номера.

<?php

class CreateAutoHandler
{
    public function handle(CreateAutoData $data): IdResult
    {
        // Write через доменную модель, номер — в ValueObject.
        $auto = new Auto();
        $auto->number = $data->number;
        $auto->is_self = $data->isSelf;
        $auto->description = $data->description;
        $auto->save();
        return new IdResult((string) $auto->id);
    }
}

$data->number — свойство, не $data['number']. Переименовали поле — PHPStan и сам PHP это покажут, а не клиент на проде.

Invokable-экшен. Контейнер резолвит CreateAutoData из тела запроса, валидирует, при ошибке бросает ValidationException → 422. Хендлер получает уже собранный объект.

<?php

class CreateAutoAction
{
    public function __construct(private CreateAutoHandler $createAutoHandler) {}

    public function __invoke(CreateAutoData $data): IdResult
    {
        return $this->createAutoHandler->handle($data);
    }
}

Возвращаем Data — пакет сам сделает JsonResponse. Для POST статус 201, для GET/PUT/PATCH — 200. Отдельный API Resource не нужен.

<?php

class IdResult extends Data
{
    public function __construct(
        public readonly string $id
    ) {}
}

IdResult — маленький общий контракт. Не раздувайте его: список, карточка, создание — разные Data.

Команда собирает тот же CreateAutoData и зовёт тот же хендлер. Use case не ветвится «если это HTTP».

<?php

class CreateAutoCommand extends Command
{
    protected $signature = 'auto:create {number} {--self} {--description=}';

    protected $description = 'Create Auto';

    public function handle(CreateAutoHandler $createAutoHandler): int
    {
        $data = new CreateAutoData(
            number: (string) $this->argument('number'),
            isSelf: (bool) $this->option('self'),
            description: $this->option('description'),
        );
        $result = $createAutoHandler->handle($data);
        $this->info("Created. ID: {$result->id}");
        return self::SUCCESS;
    }
}

Здесь new, не request: Min/Max/RequiredIf не отработают. Для CLI это нормально — оператор не HTTP-клиент, формат номера всё равно проверит Value Object. Нужна та же attribute-валидация, что у API, — CreateAutoData::validateAndCreate([...]).

Чтение — отдельный query: не смешиваем с write. AutoData — форма карточки для клиента. Собираем именованными аргументами, без $auto->toArray() и без угадывания ключей.

<?php

class AutoData extends Data
{
    public function __construct(
        public readonly string $id,
        public readonly string $number,
        public readonly bool $isSelf,
        public readonly ?string $description,
    ) {}
}

class GetAutoQuery
{
    public function handle(string $id): AutoData
    {
        $auto = Auto::findOrFail($id);
        return new AutoData(
            id: (string) $auto->id,
            number: $auto->number,
            isSelf: $auto->is_self,
            description: $auto->description,
        );
    }
}

class GetAutoAction
{
    public function __construct(private GetAutoQuery $getAutoQuery) {}
    public function __invoke(string $id): AutoData
    {
        return $this->getAutoQuery->handle($id);
    }
}

GET → 200 и JSON. Spatie умеет AutoData::from($auto), но тогда либо имена свойств совпадают с колонками, либо настраиваете mapping. Явная сборка длиннее на десять строк и не зависит от магии.

Если объект только для ответа и валидация не нужна, в пакете есть Resource вместо Data — чуть дешевле. Для карточки автомобиля разницы почти нет.

  • В PHP-коде use case нет массивов и строковых ключей: везде объекты и свойства.
  • Один DTO на создание из API и из консоли, один хендлер.
  • FormRequest и API Resource не нужны: валидация на входе, JSON на выходе.
  • HTTP-атрибуты стерегут форму запроса. Смысл номера — в домене, не в Data.
  • Ответ собирается конструктором, а не toArray().

Пакет не заменяет доменную модель. Он убирает дублирование на границе приложения: request, CLI, JSON. Дальше — Entity, VO, хендлер.

Комментарии

Загрузка…

Войдите, чтобы оставить комментарий