DTO, или объекты переноса домена, можно использовать для очень многих целей. С момента выпуска PHP 8 создание этих фантастических классов в ваших проектах стало еще проще.
От экранирования базовой конструкции массива до добавления безопасности типов к тому, что раньше было просто старым массивом. В версиях до PHP 8 все было возможно; для этого требовалось гораздо больше стандартного кода, и он никогда не казался стоящим.
С приближением PHP 8.2 наши возможности в экосистеме PHP открываются все больше и больше. Отличная книга для чтения была бы Руководство по стилю объектного дизайна от Matthias Noback, который я рекомендую всем разработчикам прочитать хотя бы раз.
Однако я не называю эти DTO, поскольку я не просто использую их в коде своего домена. Вместо этого я называю эти Объекты Данных, поскольку именно они и есть. В оставшейся части этого руководства я буду называть их объектами данных.
При создании объектов данных мне нравится делать все свойства доступными только для чтения, поскольку их следует только читать, а не записывать — это лишает их смысла. Это дает мне неизменяемую структуру, которую я могу передать через свое приложение, чтобы сохранить контекст и безопасность типов, что я называю беспроигрышной ситуацией.
Давайте посмотрим на пример. Я позаимствую идею из Учебный лагерь Laravel и создайте чириканье. У нашего чирика есть две вещи, о которых ему нужно заботиться: его сообщение и пользователь, который его создал. В наши дни при создании приложений я использую либо UUID, либо ULID, в зависимости от приложения. В этом я буду использовать ULID.
Поэтому мы хотим провести рефакторинг кодовой базы Bootcamp, чтобы упростить управление в долгосрочной перспективе — веб-интерфейс, API, CLI и т. д. Поэтому мы стремимся перейти от встроенной логики в нашем приложении к общим классам. Давайте посмотрим, как это выглядит.
$validated = $request->validate([
'message' => 'required|string|max:255',
]);
$request->user()->chirps()->create($validated);
return redirect(route('chirps.index'));
Мы можем реорганизовать это, чтобы мы выполнили проверку в запросе формы и переместили создание во что-то другое.
public function __invoke(StoreRequest $request): Response
{
return new JsonResponse(
data: $this->command->handle(
chirp: $request->validated(),
),
status: Http::CREATED->value,
);
}
Здесь мы возвращаем и обрабатываем все за один раз — это может немного затруднить чтение, поэтому давайте разделим это.
$chirp = $this->command->handle(
chirp: $request->validated(),
);
Это нормально, и нет причин, по которым вам нужно идти дальше этого. Однако, если вы хотите сделать больше и начать добавлять контекст, вы можете начать добавлять объекты данных, которые, на мой взгляд, приятны в использовании.
Как должен выглядеть наш щебет? Что было бы полезно для нас? Давайте посмотрим, что я использовал, и обсудим процесс принятия решения.
final class ChirpObject implements DataObjectContract
{
public function __construct(
public readonly string $user,
public readonly string $message,
) {}
public function toArray(): array
{
return [
'message' => $this->message,
'user_id' => $this->user,
];
}
}
Итак, в типичной манере Стива, это последний урок. Он реализует интерфейс, называемый Контракт Объекта Данных
, который поступает из одного из пакетов Laravel, которые я обычно включаю в проект. Каждое свойство общедоступно и доступно за пределами класса, но они также доступны только для чтения, поэтому мой контекст не может измениться, как только объект будет создан. Затем у меня есть метод, называемый toArray
, который обеспечивается интерфейсом, и это способ реализовать способ отправки этого объекта в Eloquent.
Использование этого подхода позволяет мне использовать контекстный объект и добавлять в приложение дополнительную безопасность типов. Это означает, что я могу быть спокойным при передаче данных по моему приложению. Как теперь выглядит наш контроллер?
public function __invoke(StoreRequest $request): Response
{
return new JsonResponse(
data: $this->command->handle(
chirp: new ChirpObject(
user: strval(auth()->id()),
message: strval($request->get('message')),
),
),
status: Http::CREATED->value,
);
}
Этот код для меня идеален. Мы могли бы захотеть обернуть наш код в блок try-catch, чтобы отловить любые потенциальные проблемы, но это не совсем то, что я пытаюсь донести прямо сейчас.
До сих пор самая большая проблема, которую я обнаружил, заключается в том, что создание объектов данных иногда доставляет некоторую боль, особенно когда они становятся больше. Если я работаю в более крупном приложении, где объекты данных больше, я буду использовать немного другой подход. В этом примере я бы не стал его использовать. Однако, чтобы показать вам, как вы можете его использовать, я покажу вам это сейчас:
final class StoreController
{
public function __construct(
private readonly ChirpFactoryContract $factory,
private readonly CreateNewChirpContract $command,
) {}
public function __invoke(StoreRequest $request): Response
{
return new JsonResponse(
data: $this->command->handle(
chirp: $this->factory(
data: [
...$request->validated(),
'user' => strval(auth()->id()),
]
),
),
status: Http::CREATED->value,
);
}
}
Создание фабрики объектов данных позволит нам контролировать, как создаются объекты данных, и позволит нам преобразовать входящий запрос во что-то более близкое к тому, как мы хотим работать в нашем приложении. Давайте посмотрим, как будет выглядеть фабрика объектов данных.
final class ChirpFactory implements ChirpFactoryContract
{
public function make(array $data): DataObjectContract
{
return new ChirpObject(
user: strval(data_get($data, 'user')),
message: strval(data_get($data, 'message')),
);
}
}
Это всего лишь простые классы, которые берут массив запроса, чтобы превратить его в объект, но по мере увеличения полезной нагрузки запроса они помогают очистить код вашего контроллера.
Вы нашли интересные способы использования объектов данных? Как вы относитесь к их созданию? Раньше я добавлял статические методы создания к своим объектам данных, но мне казалось, что я смешиваю назначение самого объекта данных. Дайте нам знать ваши мысли на Twitter!