ZF FactoryInterface — использование параметра options для настройки зависимостей загрузки

Мне интересно узнать о лучших методах загрузки сложных объектов. Для начала, я собираюсь обрисовать в общих чертах некоторые шаблоны, прежде чем перейти к проблеме. Предположим следующее: Простая модель предметной области. Клиент загружается с использованием tablegateway, а фабрики используются на каждом этапе для внедрения зависимостей:

namespace My\Model\Client;
class Client implements InputFilterProviderInterface
{
    /**@var integer*/
    protected $id;
    /**@var InputFilter*/
    protected $inputFilter;
    /**@var Preferences */
    protected $preferences;
    /**@var Orders*/
    protected $orders;
    /**@var Contacts*/
    protected $contacts;      
}

Фабрика для этого объекта Client:

namespace My\Model\Client;
class ClientFactory implements FactoryInterface
{
    public function($container, $requestedName, $options)
    {
        $client = new Client();
        $client->setInputFilter($container->get('InputFilterManager')->get('ClientInputFilter'));
        return $client;
    }
}

Далее фабрика сопоставления, использующая TableGateway:

namespace My\Model\Client\Mapper;
class ClientMapperFactory implements FactoryInterface
{
     public function __invoke($container, $requestedName, $options)
     {
        return new ClientMapper($container->get(ClientTableGateway::class));
     }
}

TableGatewayFactory:

namespace My\Model\Client\TableGateway
class ClientTableGatewayFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $hydrator = new ArraySerialisable();
        $rowObjectPrototype = $container->get(Client::class);
        $resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
        $tableGateway = new  TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
        return $tableGateway;

Обратите внимание на использование HydratingResultset для возврата полностью сформированных объектов Client из ResultSet. Все это прекрасно работает. Теперь объект Client имеет несколько связанных объектов в качестве свойств, поэтому при использовании HydratingResultSet я собираюсь добавить AggregateHydrator для их загрузки:

class ClientTableGatewayFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        **$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class);**
        $rowObjectPrototype = $container->get(Client::class);
        $resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
        $tableGateway = new  TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
        return $tableGateway;
    }

Наконец, завод гидраторов клиентов:

class ClientHydratorFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        //base ArraySerializable for Client object hydration
        $arrayHydrator = new ArraySerializable();
        $arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());

        $aggregateHydrator = new AggregateHydrator();
        $aggregateHydrator->add($arrayHydrator);

        $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsAddressHydrator::class));
        $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsOrdersHydrator::class));
        $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsPreferencesHydrator::class));
        $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsContactsHydrator::class));
        return $aggregateHydrator;
    }
}

... суть вышеперечисленных гидраторов такова:

class ClientsAddressHydrator implements HydratorInterface
{
    /** @var AddressMapper */
    protected $addressMapper;

    public function __construct(AddressMapper $addressMapper){
        $this->addressMapper = $addressMapper;
    }

    public function extract($object){return $object;}

    public function hydrate(array $data, $object)
    {
        if(!$object instanceof Client){
            return;
        }

        if(array_key_exists('id', $data)){
            $address = $this->addressMapper->findClientAddress($data['id']);
            if($address instanceof Address){
                $object->setAddress($address);
            }
        }
        return $object;
    }
}

Наконец мы подошли к делу. Вышеупомянутое работает отлично и довольно чисто загрузит объект Client со всеми полностью сформированными связанными объектами. Но у меня есть некоторые ресурсы, где весь граф объектов не нужен - например, при просмотре таблицы всех клиентов - нет необходимости загружать дополнительную информацию.

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

Решение 1 Фабрика для каждого варианта использования. Если нужны только данные клиента (без зависимостей), то создайте серию фабрик, т.е. ClientFactory, SimpleClientFactory, ComplexClientFactory, ClientWithAppointmentsFactory и т. д. Кажется избыточным и не очень пригодным для повторного использования.

Решение 2. Используйте параметр options, определенный в FactoryInterface, чтобы передать параметры «загрузки» в фабрику гидратора, например:

  class ViewClientDetailsControllerFactory implements FactoryInterface
    {
         //all Client info needed - full object graph
         public function __invoke($container, $requestedName, $options)
         {
            $controller = new ViewClientDetailsController();
            $loadDependencies = [
                'loadPreferences' => true,
                'loadOrders' => true,
                'loadContacts' => true
             ];
            $clientMapper = $container->get(ClientMapper::class, '', $loadDependencies);
            return $controller;
         }
    }



   class ViewAllClientsControllerFactory implements FactoryInterface
    {
         //Only need Client data - no related objects
         public function __invoke($container, $requestedName, $options)
         {
            $controller = new ViewAllClientsController();
            $loadDependencies = [
                'loadPreferences' => false,
                'loadOrders' => false,
                'loadContacts' => false
             ];
            $clientMapper = $container->get(ClientMapper::class, '', $loadDependencies);
            return $controller;
         }
    }

Фабрика сопоставления передает параметры фабрике tablegateway, которая передает их фабрике гидратора:

class ClientTableGatewayFactory implements FactoryInterface
{
     public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $hydrator = $container->get('HydratorManager')->get(ClientHydrator::class, '', $options);
        $rowObjectPrototype = $container->get(Client::class);
        $resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
        $tableGateway = new  TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
        return $tableGateway;
}

Наконец, здесь мы можем определить, сколько информации загружать в клиент:

class ClientHydratorFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            //base ArraySerializable for Client object hydration
            $arrayHydrator = new ArraySerializable();
            $arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());

            $aggregateHydrator = new AggregateHydrator();
            $aggregateHydrator->add($arrayHydrator);
            if($options['loadAddress'] === true){
                   $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsAddressHydrator::class));            
            }
            if($options['loadOrders'] === true){
                $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsOrdersHydrator::class));
            }
            if($options['loadPreferences'] === true){
                $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsPreferencesHydrator::class));
            }
            if($options['loadContacts'] === true){
                $aggregateHydrator->add($container->get('HydratorManager')->get(ClientsContactsHydrator::class));
            }
            return $aggregateHydrator;
        }
    }

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

Любые советы или альтернативные решения для достижения вышеизложенного были бы замечательными. Спасибо за чтение.


person John Crest    schedule 17.11.2019    source источник
comment
У меня есть для вас альтернатива: прекратите это безумие и начните использовать правильную форму, такую ​​как Doctrine, с помощью которой вы можете перестать беспокоиться о настойчивости.   -  person emix    schedule 18.11.2019
comment
Doctrine — это вариант, но я бы предпочел понять, как решать проблемы, используя компоненты из коробки. Этот тип проблемы возникает на каждом языке, и я не могу просто полагаться на использование ORM.   -  person John Crest    schedule 19.11.2019


Ответы (1)


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

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

Я бы тоже не советовал вам этот вариант. Я имею в виду, что это не так уж плохо, но у него есть серьезная проблема: каждый раз, когда вы создаете свой гидратор, вы должны не забывать передавать эти параметры, иначе вы получите «пустой гидратор». Та же логика применима ко всему, что использует эти гидраторы.

Поскольку вы на самом деле хотите удалить гидраторы, которые вам не нужны, я бы посоветовал избегать этого решения, потому что таким образом вы всегда вынуждены объявлять, какие гидраторы вам нужны ( и, честно говоря, я всегда буду забывать это делать.. ^^ ). Если вы добавите новый гидратор, вам придется просмотреть свой проект и добавить новые параметры. На самом деле не стоит усилий...

Вот почему я предлагаю вам следующее решение

Удаление ненужных гидраторов

В 99% случаев гидраторы используются картографами. Таким образом, я думаю, что было бы чище иметь картограф, который по умолчанию всегда возвращает данные одного и того же типа (-> один гидратор), но который можно модифицировать для удаления определенного набора гидраторов.

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

class DetachableAggregateHydrator extends AggregateHydrator 
{

    /**
     * List of all hydrators (as listeners)
     *
     * @var array
     */
    private $listeners = [];

    /**
     * {@inherit}
     */
    public function add(HydratorInterface $hydrator, int $priority = self::DEFAULT_PRIORITY): void 
    {
        $listener = new HydratorListener($hydrator);
        $listener->attach($this->getEventManager(), $priority);

        $this->listeners[get_class($hydrator)] = $listener;

    }

    /**
     * Remove a single hydrator and detach its listener
     * 
     * @param string $hydratorClass
     */
    public function detach($hydratorClass) 
    {
        $listener = $this->listeners[$hydratorClass];
        $listener->detach($this->getEventManager());
        unset($listener);
        unset($this->listeners[$hydratorClass]);

    }

}

Затем в TableGatewayFactory:

class ClientTableGatewayFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $hydrator = $container->get('HydratorManager')->get(ClientHydrator::class);
        $rowObjectPrototype = $container->get(Client::class);
        $resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
        $adapter = $container->get(Adapter::class);
        $tableGateway = new  TableGateway('clients', $adapter, null, $resultSet);
        return $tableGateway;
    }

}

И ClientHydratorFactory:

class ClientHydratorFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $aggregateHydrator = new DetachableAggregateHydrator();

        $arrayHydrator = new ArraySerializable();
        $arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
        $aggregateHydrator->add($arrayHydrator);

        $hydratorManager = $container->get('HydratorManager');
        $aggregateHydrator->add($hydratorManager->get(ClientsAddressHydrator::class));
        $aggregateHydrator->add($hydratorManager->get(ClientsOrdersHydrator::class));
        $aggregateHydrator->add($hydratorManager->get(ClientsPreferencesHydrator::class));
        $aggregateHydrator->add($hydratorManager->get(ClientsContactsHydrator::class));

        return $aggregateHydrator;
    }
}

Вам просто нужно сделать tablegateway доступным вне картографа:

class ClientMapper 
{

    private $tableGateway;

    // ..
    // Other methods
    // ..

    public function getTableGateway(): TableGateway 
    {
        return $this->tableGateway;
    }
}

И теперь вы можете выбрать, какие гидраторы вы не хотите прикреплять.

Допустим, у вас есть два контроллера:

  • ClientInfoController, где нужны клиенты и их адреса, предпочтения и контакты
  • ClientOrdersController, где нужны клиенты с их заказами

Их фабрики будут:

class ClientInfoController implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $clientMapper = $container->get(ClientMapper::class);

        // Orders are unnecessary
        $resultSetPrototype = $clientMapper->getTableGateway()->getResultSetPrototype();
        $resultSetPrototype->getHydrator()->detach(ClientsOrdersHydrator::class);

        return $aggregateHydrator;
    }
}

class ClientOrdersController implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $clientMapper = $container->get(ClientMapper::class);

        // Orders are unnecessary
        $resultSetPrototype = $clientMapper->getTableGateway()->getResultSetPrototype();
        $resultSetPrototype->getHydrator()->detach(ClientsAddressHydrator::class);
        $resultSetPrototype->getHydrator()->detach(ClientsPreferencesHydrator::class);
        $resultSetPrototype->getHydrator()->detach(ClientsContactsHydrator::class);

        return $aggregateHydrator;
    }
}
person Ermenegildo    schedule 20.11.2019
comment
Отличное решение, я не думал об экспонировании полностью загруженного гидратора через объект прототипа набора результатов, я думаю, что это очень чисто. Спасибо за подробный ответ, это именно то, что я искал. - person John Crest; 21.11.2019