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

Структура

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

DTO для создания автомобиля с валидацией php-аттрибутами для request'а

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

Обработчик создания автомобиля, на вход DTO

<?php

class CreateAutoHandler
{
    public function handle(CreateAutoData $data): IdResult
    {
        // Тут должна быть write через доменную модель
        // c валидацией номера в ValueObject

        $auto = new Auto();
        $auto->number = $data->number; // типизированные свойства
        $auto->is_self = $data->isSelf;
        $auto->description = $data->description;
        $auto->save();

        return new IdResult($auto->id);
    }
}

Invokable-контроллер, на вход DTO, автоматическая валидация request'а с выбросом 422 ошибки, IdResult автоматически переводится в JsonResponse

<?php

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

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

Если вернуть Data в POST запросе, код будет 201 автоматически

<?php

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

Создание из консольной команды, тот же DTO

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

Например, если бы мы возвращали большую структуру данных. Если вернуть Data в GET/PUT/PATCH запросе, код будет 200 автоматически 

<?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: $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);
    }
}

  • Ни одного массива
  • Типизация, везде объекты
  • Нет текстовых ключей и значений
  • Одно DTO в request и консоли
  • Явная сборка ответа типизированными параметрами конструктора