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, хендлер.
Комментарии
Войдите, чтобы оставить комментарий