<rmcreative>

RSS

Гидратор

4 июня 2017

В ноябре 2016 я написал и релизнул библиотеку Hydrator, но забыл её как надо анонсировать. Насколько знаю, термин «hydrator» первый раз был использован в Java ORM Hibernate. Задача гидратора — наполнить объект данными или получить данные из него. При этом важно не вызывать конструктор или геттеры-сеттеры. Это позволяет работать с private свойствами, которые сохраняются в базу или загружаются из неё, напрямую без открытия этих свойств. Интерфейс остаётся чистым.

Внутри используется PHP reflection.

Использование

Допустим, у нас есть сущность Post, представляющая запись в блоге. У неё есть заголовок (title) и текст (text). Для каждой записи генерируется уникальный id.

<?php
class Post
{
    private $id;
    protected $title;
    protected $text;
 
    public function __construct($title, $text)
    {
        $this->id = uniqid('post_', true);
        $this->title = $title;
        $this->text = $text;
    }
 
    public function getId()
    {
        return $this->id;
    }
 
    public function getTitle()
    {
        return $this->title;
    }
 
    public function setTitle($title)
    {
        $this->title = $title;
    }
 
    public function getText()
    {
        return $this->text;
    }
 
    public function setText()
    {
        return $this->text;
    }
}

Сохраняем пост в базу:

<?php
$post = new Post('First post', 'Hell, it is a first post.');
 
$postHydrator = new \samdark\hydrator\Hydrator([
    'id' => 'id',
    'title' => 'title',
    'text' => 'text',
]);
 
$data = $postHydrator->extract($post);
save_to_database($data);

Загружаем пост из базы:

<?php
$data = load_from_database();
 
$postHydrator = new \samdark\hydrator\Hydrator([
    'id' => 'id',
    'title' => 'title',
    'text' => 'text',
]);
 
$post = $postHydrator->hydrate($data, Post::class);
echo $post->getId();

Заполняем существующий объект:

<?php
$data = load_from_database();
 
$postHydrator = new \samdark\hydrator\Hydrator([
    'title' => 'title',
    'text' => 'text',
]);
 
$post = get_post();
$post = $postHydrator->hydrateInto($data, $post);
echo $post->getTitle();

Использование в Yii

Библиотека на данный момент не используется в самом фреймворке. Её можно использовать чтобы реализовать свой маппинг данных внутри репозитория в том случае, если вы решили делать проект следуя clean architecture и domain driven design.

Комментарии RSS

  1. №11010
    higimo
    higimo 05 июня 2017 г., 16:54:11

    В чём смысл использовать отражения, если можно вызвать конструктор и воспользоваться геттерами и сеттерами?

    Если всё равно Гидратор наполняет данными готовый класс, то что это даёт, кроме избавления от вызова сеттеров?

  2. №11011
    Stevad
    Stevad 05 июня 2017 г., 18:21:21

    @higimo Если в сеттерах есть сложная логика проверки данных, то при использовании гидратора для заполнения данными приходится повторно прогонять все эти проверки. Это с точки зрения производительности.

    С точки зрения логики, предполагается, что данные у нас корректные. И например, они были раньше сохранены в БД. Теперь мы загружаем данные из БД и заполняем класс гидратором. Логично же заполнить все напрямую, избежав лишних проверок в сеттерах?

    Еще другой момент связан с тем, как сеттеры будут выполняться. Гидратор не формирует названия методов для вызова и заполнения класса (типа setVariableName), т.к. не знает правильные имена. Да и делать такую логику - излишество, ИМХО.

    Если же сделать, чтобы гидратор напрямую делал вызовы в класс типа $object->variable_name, надеясь что у разработчика есть какая-то внутренняя магия с __set/__get - тоже глупая затея. Да и вызов этот больше времени занимает. Хотя, это надо проверять, что быстрее - использование магии __get/__set (со всеми неудобствами) или использовании Reflection (как в гидраторе).

  3. №11017
    delvin
    delvin 12 июня 2017 г., 10:24:57

    higimo Есть смысл, использовать, когда атрибуты read-only. Например:

    class Record
    {
        private $id;
        private $created_at;
        private $created_by;
        private $title;
    
        public function __construct($title)
        {
            $this->id = Uuid::v4()->toString();
            $this->created_at = new DateTimeImmutable();
            $this->created_by = Yii::$app->user->id;
        }
    
        public function getId()
        {
            return $this->id;
        }
    
        public function getCreatedAt()
        {
            return $this->created_at;
        }
    
        public function getCreatedBy()
        {
            return User::findByPk($this->created_by);
        }
    
        public function getTitle()
        {
            return $this->title;
        }
    
        public function setTitle(string $title)
        {
            $this->title = $title;
            return $this;
        }
    }
    
    

    Хотя, по идее, вместо гидратора можно, по необходимости, к классам добавлять статический метод для восстановления состояния

    public static function restoreState(array $state) : self {
        $record = new static($state['title']);
        $record->id = $state['id'];
        $record->created_at = new DateTimeImmutable($state['created_at']);
        $record->created_by = $state['created_by'];
        return $record;
    }
    
    

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

    Если, например в конструкторе еще идет обращение к диспетчеру событий, то без рефлексии, на сколько я знаю, не обойтись.

  4. №11018
    Oleg
    Oleg 16 июня 2017 г., 1:02:25

    @ Stevad во-первых, нельзя доверять данным, полученным извне (в том числе и из своей БД). Поэтому пропуск валидации данных и заполнение объекта "грязными" данными приводит к снижению безопасности приложения. Во-вторых, рефлексия достаточно дорогая операция и тут еще надо смотреть, что будет быстрее - рефлексия или сетеры с валидаторами

  5. №11019
    Oleg
    Oleg 16 июня 2017 г., 1:15:26

    А еще есть вот такой проект github.com/Ocramius/GeneratedHydrator поддерживающий несколько способов доступа к свойствам объекта и, по утверждению автора, очень быстрый

  6. №11020
    delvin
    delvin 16 июня 2017 г., 1:29:58

    Oleg

    По хорошему в сеттерах не нужно делать валидацию данных. Там достаточно, ну максимум, приведения типов. А валидация нужна на том уровне, где объект заполняется или создается - сервис, репозиторий и т.п.

  7. №11021
    Oleg
    Oleg 16 июня 2017 г., 9:55:54

    @delvin согласен, что валидацию стоит делать не в сеттерах. Просто @Stevad заговорил про сложную логику проверки данных в сеттерах, я и ответил

  8. №11023
    Stevad
    Stevad 21 июня 2017 г., 9:33:21

    @Oleg

    Нельзя доверять данным, полученным от пользователя в любом виде. А вот то, что вы сохраняете в базу, должно быть обязательно корректным. Иначе какой смысл выполнять двойную проверку - сначала перед сохранением, а потом еще и при выгрузке данных. Это будет еще больше только замедлять приложение.

  9. №11024
    Василий
    Василий 23 июня 2017 г., 8:56:43

    Stevad, к чему такая критика (причем высосанная из пальца)? Вас никто не принуждает использовать гидратор.

  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.