Структуры данных в PHP. Data Transfer Object. Предметно-ориентированный Laravel

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

Все статьи серии

Предметно-ориентированный Laravel

Вы, наверное, и сами это заметили: в начале проекта вы начинаете не с создания контроллеров, а с создания моделей, как их называет Laravel. Крупные проекты выигрывают от создания ERDs и других видов диаграмм для концептуализации того, какие данные будут обрабатываться приложением. Только когда это ясно, вы можете начать создавать точки входа, которые работают с вашими данными.

В этой статье мы рассмотрим, как работать с данными в структурированном виде, чтобы все разработчики в вашей команде могли написать приложение для обработки этих данных предсказуемым и безопасным способом.

Возможно, вы сейчас думаете о моделях, но это не совсем так. Сначала нам нужно сделать еще несколько шагов назад.

Теория типов

Чтобы понять как использовать объекты передачи данных (DTO) вам нужно будет иметь некоторые базовые знания о типизации.

Давайте уточним несколько терминов, которыми я объясню, как я буду использовать типизацию в этой статье.

Сила типизации – сильная или слабая типизация – определяет, может ли переменная изменить свой тип после того, как она была определена.

Простой пример: дана строковая переменная $a = 'test'; слабая типизация позволяет повторно назначить эту переменную другому типу , например $a = 1;целому числу.

PHP – это слабо типизированный язык:

$id = '1'; // получили, например, из реста

function find(int $id): Model
{
    // id автоматически сконвертируется в число
}

find($id);

PHP имеет слабую типизацию. Будучи языком, который в основном работает с HTTP-запросом, практически все является строкой.

Вы можете подумать, что в современном PHP вы можете избежать слабой типизации с помощью функции включения строгой типизации declare(strict_types=1), но это не совсем так. Объявление строгой типизации предотвращает передачу других типов в функцию, но вы все равно можете изменить значение переменной в самой функции.

declare(strict_types=1);

function find(int $id): Model
{
    $id = '' . $id;

    /*
     * This is perfectly allowed in PHP
     * `$id` is a string now.
     */

    // …
}

find('1'); // Вызовет ошибку уровня TypeError.

find(1); // Все будет отлично

Даже при наличии включенной строгой типизации система типов PHP слаба. Подсказки типа обеспечивают только тип переменной в этот момент времени, без гарантии относительно любого будущего значения, которое может иметь эта переменная.

Как я уже говорил, PHP имеет слабую типизацию, поскольку все входные данные, с которыми он имеет дело, начинаются как строка. Однако у сильной типизации есть интересное свойство: данные приходят с гарантиями качества данных. Если переменная имеет тип, который не изменяется, то целый ряд неожиданных действий просто больше не может произойти.

Видите ли, математически доказуемо, что если строго типизированная программа компилируется, невозможно, чтобы у этой программы был целый ряд ошибок, которые могли бы существовать в слабо типизированных языках. Другими словами, сильная типизация дает программисту лучшую гарантию того, что код действительно ведет себя так, как он должен.

В качестве примечания: это не означает, что строго типизированный язык не может иметь ошибок! Вы прекрасно можете написать глючную реализацию. Но когда строго типизированная программа успешно компилируется, вы уверены, что определенный набор ошибок не может возникнуть в этой программе.

Статические и динамические типы

Есть еще одна концепция, которую мы должны рассмотреть: статические и динамические типы. И именно здесь все начинает становиться интересным.

Как вы, вероятно, знаете, PHP – это интерпретируемый язык. Это означает, что PHP-скрипт переводится в машинный код во время выполнения. Когда вы отправляете запрос на сервер, работающий на PHP, он возьмет эти простые .php файлы и проанализирует текст в них до того, как процессор сможет их выполнить.

Опять же, это одна из сильных сторон PHP: просто написал скрипт, обновил страницу и все работает. Это большая разница по сравнению с языком, который должен быть скомпилирован, прежде чем его можно будет запустить.

Очевидно, что существуют механизмы кэширования, которые оптимизируют это, поэтому приведенное выше утверждение является чрезмерным упрощением. Однако этого достаточно, чтобы понять суть дальше.

Опять же, есть и обратная сторона: поскольку PHP проверяет свои типы только во время выполнения, проверка типов программы может завершиться неудачей при запуске. Это означает, что у вас может быть более четкая ошибка для отладки, но все равно программа потерпела крах.

Эта проверка типов во время выполнения делает PHP динамически типизированным языком. Потому как статически типизированный язык программирования перед выполнением кода выполнит все проверки всех типов в программе при компиляции.

Начиная с PHP 7.0, его система типов была значительно улучшена. Настолько, что такие инструменты , как PHPStanphan и psalm, стали очень популярными в последнее время. Эти инструменты используют динамический язык PHP, но выполняют кучу статических анализов вашего кода.

Эти библиотеки могут предложить довольно много информации о вашем коде, без необходимости запускать или модульно тестировать его, IDE, такая как PhpStorm, также имеет множество встроенных статических проверок.

Имея в виду всю эту справочную информацию, пришло время вернуться к ядру нашего приложения: данным.

Структурирование неструктурированных данных

Приходилось ли вам когда-нибудь работать с “массивом данных”, который на самом деле был больше, чем просто список? Вы использовали ключи массива в качестве полей? И вы чувствовали боль от того, что не знали точно, что было в этом массиве? Не будучи уверенным, действительно ли данные в нем такие, какими вы их ожидаете увидеть, или какие поля доступны?

Давайте представим себе, о чем я говорю: работа с запросами Laravel. Рассмотрим этот пример как базовую операцию CRUD для обновления существующего клиента:

function store(CustomerRequest $request, Customer $customer) 
{
    $validated = $request->validated();
    
    $customer->name = $validated['name'];
    $customer->email = $validated['email'];
    
    // …
}

Возможно,вы уже видите возникающую проблему: мы не знаем точно, какие данные доступны в $validated массиве. Массивы в PHP являются универсальной и мощной структурой данных. Но как только мы начинаем использовать их как свалку данных, мы теряем всю эту мощь и направляемся ближе к тлену, ошибкам, непониманию.

Прежде чем искать решения этой проблемы, вот что вы можете сделать, чтобы понять что находится в этой свалке:

  • Прочтёте исходный код
  • Прочтёте документацию
  • Изучите свалку $validated через print_r
  • Или используете отладчик для этого

Теперь представьте себе на минуту, что вы работаете с командой из нескольких разработчиков над этим проектом, и что ваш коллега написал этот фрагмент кода пять месяцев назад: я могу гарантировать вам, что вы не будете знать, с какими данными вы работаете, не делая никаких громоздких вещей, перечисленных выше.

Оказывается, что строго типизированные системы в сочетании со статическим анализом могут быть большим подспорьем в понимании того, с чем именно мы имеем дело. Такие языки, как Rust, например, решают эту проблему просто:

struct CustomerData {
    name: String,
    email: String,
    birth_date: Date,
}

Структура – это то, что нам нужно! К сожалению, PHP не имеет структур.  Но у него есть массивы и объекты. Объектов может быть достаточно для решения нашей проблемы:

class CustomerData
{
    public string $name;
    public string $email;
    public Carbon $birth_date;
}

Типизированные свойства доступны только с PHP 7.4. В зависимости от того, когда вы читаете эту статью, вы можете еще не использовать их — у меня есть решение для вас позже в этой статье, продолжайте читать.

Для тех, кто может использовать PHP 7.4 или выше, вы можете сделать что-то вроде этого:

function store(CustomerRequest $request, Customer $customer) 
{
    $validated = CustomerData::fromRequest($request);
    
    $customer->name = $validated->name;
    $customer->email = $validated->email;
    $customer->birth_date = $validated->birth_date;
    
    // …
}

Статический анализатор, встроенный в вашу IDE, всегда сможет сообщить нам, с какими данными мы имеем дело.

Этот шаблон упаковки неструктурированных данных в типы, чтобы мы могли использовать наши данные надежным способом, называется “объекты передачи данных” или DTO (Data Transfer Object). Этот шаблон я настоятельно рекомендую вам использовать в ваших проектах большего размера. Для средних и маленьких проектов данный шаблон будет избыточен.

Обсуждая эту статью с коллегами, друзьями или в сообществе Laravel, вы можете столкнуться с людьми, которые не разделяют одно и то же видение сильной типизации. На самом деле есть много людей, которые предпочитают принимать динамическую и слабую типизацию PHP. И тут определенно есть что сказать в свое оправдание.

По моему опыту, однако, есть больше преимуществ в строго типизированном подходе при работе с командой из нескольких разработчиков над проектом в течение серьезного количества времени. Вы должны использовать любую возможность, чтобы уменьшить когнитивную нагрузку. Вы не хотите, чтобы разработчикам приходилось начинать отладку своего кода каждый раз, когда они хотят знать, что именно находится в переменной. Информация должна быть прямо под рукой, чтобы разработчики могли сосредоточиться на том, что важно: создании приложения.

Конечно, использование DTO имеет свою цену: существуют не только накладные расходы на определение этих классов; Вам также нужно сопоставить, например, запрос данных с DTO.

Преимущества использования DTO определенно перевешивают. Независимо от того, сколько времени вы потеряете, написав этот код, вы компенсируете его в долгосрочной перспективе.

Однако вопрос о построении DTO из “внешних” данных все еще нуждается в ответе.

Фабрики DTO

Как мы строим DTO? Я поделюсь с вами двумя возможностями, а также объясню, какая из них имеет мое личное предпочтение.

Первый-самый правильный: использование специальной фабрики.

class CustomerDataFactory
{
    public function fromRequest(
       CustomerRequest $request
    ): CustomerData {
        return new CustomerData([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'birth_date' => Carbon::make(
                $request->get('birth_date')
            ),
        ]);
    }
}

Наличие отдельной фабрики сохраняет ваш код чистым на протяжении всего проекта. Эта фабрика должна жить в прикладном слое.

Вы, вероятно, заметили, что чуть выше в коде я использовал следующий подход с созданием DTO: CustomerData::fromRequest.

Что плохого в таком подходе? Ну, во-первых: он добавляет специфичную для приложения логику в домен. DTO, который живет в домене, теперь должен знать о CustomerRequest классе, который живет в прикладном слое.

use Spatie\DataTransferObject\DataTransferObject;

class CustomerData extends DataTransferObject
{
    // …
    
    public static function fromRequest(
        CustomerRequest $request
    ): self {
        return new self([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'birth_date' => Carbon::make(
                $request->get('birth_date')
            ),
        ]);
    }
}

Очевидно, что смешивание кода конкретного приложения в рамках домена-не самая лучшая идея. Однако у него есть мое предпочтение. На то есть две причины.

Прежде всего: мы уже установили, что DTO являются точкой входа данных в кодовую базу. Как только мы работаем с данными извне, мы хотим преобразовать их в DTO. Нам нужно сделать это сопоставление где-то , так что мы могли бы также сделать это в классе, для которого оно предназначено.

Во-вторых, и это более важная причина; я предпочитаю этот подход, потому что одно из собственных ограничений PHP: он не поддерживает именованные параметры.

Видите ли, вы не хотите, чтобы ваши DTO в конечном итоге имели конструктор с индивидуальным параметром для каждого свойства: это не масштабируется и очень сбивает с толку при работе со свойствами nullable или default-value. Вот почему я предпочитаю подход передачи массива в DTO, и пусть он строит себя на основе данных в этом массиве. 

Поскольку именованные параметры не поддерживаются, статический анализ также недоступен, а это означает, что вы находитесь в неведении о том, какие данные необходимы при построении DTO. Я предпочитаю держать это “пребывание в темноте” в классе DTO, чтобы его можно было использовать без лишних мыслей извне.

Если бы PHP поддерживал что-то вроде именованных параметров, я бы использовал такой шаблон:

public function fromRequest(
    CustomerRequest $request
): CustomerData {
    return new CustomerData(
        'name' => $request->get('name'),
        'email' => $request->get('email'),
        'birth_date' => Carbon::make(
            $request->get('birth_date')
        ),
    );
}

Обратите внимание на отсутствие массива при построении CustomerData.

До тех пор, пока PHP не поддержит это, я бы предпочел прагматическое решение теоретически правильному. Но это зависит от тебя. Не стесняйтесь выбирать то, что подходит вашей команде лучше всего.

Альтернатива типизированным свойствам

Как я уже упоминал ранее, существует альтернатива использованию типизированных свойств для поддержки DTO: docblocks. Пакет DTO от Spatie, который выше применяли, также поддерживает их.

use Spatie\DataTransferObject\DataTransferObject;

class CustomerData extends DataTransferObject
{
    /** @var string */
    public $name;
    
    /** @var string */
    public $email;
    
    /** @var \Carbon\Carbon */
    public $birth_date;
}

Однако по умолчанию docblocks не дают никаких гарантий, что данные относятся к тому типу, о котором они говорят. К счастью, PHP имеет свой API рефлексии, и с ним возможно гораздо больше.

Решение, предоставляемое этим пакетом, можно рассматривать как расширение системы типизации PHP. Если вы не можете использовать PHP 7.4 и хотите немного больше уверенности в том, что ваши типы docblock действительно соблюдаются, этот пакет вам поможет.

Поскольку данные лежат в основе почти каждого проекта, это один из самых важных строительных блоков. Объекты передачи данных предлагают вам способ работы с данными структурированным, безопасным и предсказуемым способом.

Автор Spatie

Рейтинг
( 5 оценок, среднее 4.6 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.