kuaukutsu/ds-dto

Data Structure: DTO (Data Transfer Object)

2.0.2 2023-10-27 13:23 UTC

This package is auto-updated.

Last update: 2024-03-29 20:34:57 UTC


README

Disclaimer: вставлю свои 5 копеек, не претендую на истинность, но мысли озвучу (в самых общих чертах).

Словарь используемых терминов:

  • Form получение данных извне (request: POST/GET/ARGS/Array), обработка (валидация, фильтрация), передача в сервисный слой (бизнес логика).
  • Model какое-то объектное представления данных в системе (инфрастркутура, repository).
  • Service различные сервисы (UoW, UseCase), обработка бизнес логики.
  • DTO транспорт, плюс схема данных (комментарии могут рассказывать о том, как и где используются данные). Есть возможность быстро организовать версионность массива данных.

Схема для описания примера

В самом простом исполнении получается следующая схема:

interface Form extends Dtoable
{
    /**
     * Конвертирует объект в массив.
     *
     * @param string[] $fields поля которые должны быть в исходном массиве
     * @return array<string, mixed>
     */
    public function toArray(array $fields = []): array;
    
    /**
     * Форма содержит данные, их нужно передать в сервис (например, Service). 
     *
     * @param class-string $dtoClassName
     */
    public function toDto(string $dtoClassName): DtoInterface;
}
interface Model 
{
    public function fromDto(DtoInterface $dto): self;
}
interface Service 
{
    public function save(DtoInterface $dto): bool;
}

Пример DTO

Подходы могут быть разные, но лично мне не нравится передавать данные через конструктор, возможно с приходом php8 и named arguments я поменяю точку зрения.

/**
 * @psalm-immutable
 */
final class ModelDto extends BaseDto
{
    public int $id;

    public string $name;

    /**
     * nullable в данном случае говорит что значение при Инициализации объекта может быть незадано.
     */
    public ?array $props = [];
}

В DTO можно использовать protected свойства (плюс геттеры), чтобы не было желания заполнять их как-то кроме как через hydrate(). Главное - иммутабельность и никакой логики, как только здесь появляется логика, это сразу превращается в Entity, или быть может какой-то иной подвид Value Object.

Может возникнуть вопрос, зачем здесь DTO, ведь в сервисный слой можно передавать форму напрямую ($form или в виде массива $form->toArray()), и так же получать из сервисного слоя напрямую модель. Суть в том, что Модель, как и Форма, это реализация некоторой логики, и в приложении могут быть несколько компонент, которые реализуют логику субъективно, по своему, с учётом требований БЛ, но сервисный слой для всех компонент один и тот же. Поэтому нужен механизм, который позволит 3-м разным формам работать с одним методом, как например $service->save(DTO). А так же потому что форма, как любой иной объект может менять своё состояние, и нет чётких гарантий неизменности данных.

class Service 
{
    public function save(DtoInterface $dto): bool
    {
        $data = $dto->toArray();

        ...
    
        return true;
    }
}

Docker

docker pull ghcr.io/kuaukutsu/php:8.1-cli

Container:

  • ghcr.io/kuaukutsu/php:${PHP_VERSION}-cli (default)
  • jakzal/phpqa:php${PHP_VERSION}

shell

docker run --init -it --rm -v "$(pwd):/app" -w /app ghcr.io/kuaukutsu/php:8.1-cli sh

Testing

Unit testing

The package is tested with PHPUnit. To run tests:

make phpunit

Static analysis

The code is statically analyzed with Psalm. To run static analysis:

make psalm

Code Sniffer

make phpcs

Rector

make rector