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

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

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;
         }
    }

Фабрика картографов передает параметры в фабрику шлюзов таблиц, которая передает их фабрике гидраторов:

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, как задумано - в документации указано, что этот параметр должен быть для передачи параметров конструктора объекту, а не определять, какую логику фабрика должна использовать для загрузки зависимостей.

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

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;
    }
}
Другие вопросы по тегам