Using Delegators in Zend Framework

  • Viewed 341×

  • Blog

Delegators (delegation pattern) in Zend Framework are very useful in situations when you need to add some configuration or dependency during instantiation of service class (service manager knows about that class). You will achive it with a delegator factory. The factory wraps your service and provides additional "decoration".

I'm often using delegators to set language or user identity in a model classes. For example, when model creates sql query where language is a parameter, I'm sure I have this parameter set (thanks to delegator factory) and I can just call getLanguage(). So there is no need to pass language parameter into model class from controller for example.
Let's make this use case.

Delegator factory

We will need an interface and delegator factory classes for that. I will put them into folder Service in the Application module.

MVC-application
└─ module
   └── Application
       ├── config
       │   ├── module.config.php
       │   └── template_map.config.php
       │
       ├── src
       │   ├── Controller
       │   ├── Entity
       │   ├── Form
       │   ├── Model
       │   ├── Service
       │   │   ├── LanguageInterface.php
       │   │   └── LanguageDelegatorFactory.php
       │   ├── View
       │   │   └── Helper
       │   └── Module.php 
       │    
       └── view

LanguageInterface.php

<?php
namespace Application\Service;

interface LanguageInterface
{

    /**
     *
     * @return string
     */
    public function getLanguage();

    /**
     *
     * @param string $language
     */
    public function setLanguage($language);
}

LanguageDelegatorFactory.php

<?php
namespace Application\Service;

use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
use Interop\Container\ContainerInterface;
use Zend\Session;

class LanguageDelegatorFactory implements DelegatorFactoryInterface
{

    public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
    {
        $serviceToWrap = $callback();

        if (! $serviceToWrap instanceof LanguageInterface) {
            return $serviceToWrap;
        }

	// distinguishing of language
        $request = $container->get('request');
        $uri = $request->getUri();
        $path = $uri->getPath();
        $urlParts = explode('/', $path);

        if (isset($urlParts[1]) && $urlParts[1] == \Admin\Module::SESSION_CONTAINER) {
            // admin
            $sessionContainerName = \Admin\Module::SESSION_CONTAINER;
        } else {
            // front
            $sessionContainerName = \Application\Module::SESSION_CONTAINER;
        }

        $session = new Session\Container($sessionContainerName);
        $serviceToWrap->setLanguage($session->offsetGet('language'));

        return $serviceToWrap;
    }

Our delegator is ready to be used. Lets have another module in our application named Item, where we will select items depending on language, besides. Our model ItemContentTable implements LanguageInterface so we need to define setLanguage() and getLanguage() methods.

<?php
namespace Item\Model;

use Werc\Db\AbstractTable;
use Zend\Db\TableGateway\TableGateway;
use Application\Service\LanguageInterface;
use Zend\Db\Sql\Sql;

class ItemContentTable extends AbstractTable implements LanguageInterface
{
    public $primary = 'id';

    protected $tableGateway;

    protected $language;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function getLanguage()
    {
        return $this->language;
    }

    public function setLanguage($language)
    {
        $this->language = $language;
    }

    /**
     * @param int $id
     * @return ArrayObject|NULL
     */
    public function selectOne($id)
    {
        $adapter = $this->tableGateway->getAdapter();
        $sql = new Sql($adapter, $this->tableGateway->getTable());
        $select = $sql->select();
        $select->columns(
            array(
                '*'
            ));
        $select->join('item', $this->tableGateway->getTable() . '.item_id = item.id',
            array(
                'created_at',
		'category'
            ), 'left');
        $select->where->equalTo('item_id', $id);
        $select->where->equalTo('lang', $this->getLanguage());
        $sqlString = $sql->buildSqlString($select);

        return $this->getResultSet($sqlString)->current();
    }
}

At last, in a module config we tell the service manager to "decorate" our model ItemContentTable with LanguageDelegatorFactory.

module.config.php

<?php
namespace Item;

return array(
    'controllers' => array(
       ...
    ),
    'service_manager' => array(
        'factories' => array(
            Model\ItemContentTable::class => Model\ItemContentTableFactory::class
        ),
        'delegators' => array(
            Model\ItemContentTable::class => array(
                \Application\Service\LanguageDelegatorFactory::class
            )
        )
    ),

	...

);

Similarly you can solve any cases when you need to refer some parameters to a class automatically.

Initializers

There is one more way how to solve similar principle in Zend Framework, initializers. Initializers were introduced in Zend Framework 2 and I'm not sure about a bright future. They have been primarily kept for backwards compatibility and have several disadvantages also. Marco Pivetta (Ocramius) has an excellent article on initializer performance problem. So keep distance from initializers and use delegator factories rather.

Footnotes

  1. Delegators, Zend Framework documentation.
  2. Initializers, Zend Framework documentation.
  3. Delegator Factories in Zend Framework 2, Ocramius blog.