<-- home

Sistema de plugins zf2

O Zend Framework 2 utiliza uma interessante estrutura de plugins para seus controladores que vale o trabalho de uma segunda análise. Basicamente se você implementar o método setPluginManager em um controlador do tipo AbstractActionController ou AbstractRestfulController é lhe garantido o acesso a um conjunto de plugins pré-fabricados e a possibilidade de injetar novos. O objetivo deste post é criar uma estrutura similar para ser utilizada em um outro conjunto de classes que possam se beneficiar de plugins.

Um plugin é basicamente uma classe (ou conjunto de classes) que realizam uma tarefa específica para um ou mais requisitante(s). Entre os plugins do zend Framework constam:

  • AcceptableViewModelSelector seletor de ViewModel’s baseado em critérios do usuário
  • FlashMessenger um sistema de notificações para o usuário
  • Forward ajuda a construir um sistema widgetizável através de chamada de mais de um controller/action por requisição
  • Identity recupera uma instância da entidade de usuário (autenticado)
  • Layout auxilia em um conjunto de operações para o layout
  • Params acesso facilitado de parâmetros das mais diversas fontes
  • Redirect - Simplifica o processo de redirect (utilizando o sistema de rotas zf2)
  • Url gerador de URL’s

Para utilizar algum plugin em um controlador basta chamá-lo em uma das seguintes formas:

$plugin = $this->plugin('identity');
//ou
$plugin = $this->identity();

Esta forma de trabalhar é interessante pois ajuda a desacoplar código; já vi muito código de mentores da comunidade com coisas do tipo:

class Budget
{
...
    public function getIdentity($params)
    {
        return $this->getServiceLocator()->get('MyNamespace\Service\User')->getIdentity();
    }
...
}

Essa forma de trabalhar está longe de condizer com os padrões SOLID.

Single Responsability Principle - Budget não está mas cuidando exclusivamente de orçamentos, agora virou uma via para pegar identidades de usuários!

Open Closed Principle - este princípio - a meu ver - não se aplica;

Liskov substitution principle - como este método é usado como um facilitador ele acaba quebrando a interface do objeto (tudo bem se getIdentity for privado), certamente os objetos vão continuar a compartilhar sua interface herdada mas foi adicionada complexidade a uma classe que pode ou não ser herdada, em outras palavras a substituição fica relativizada;

Interface segregation principle - Se getIdentity estiver em uma interface o problema em potencial da substituição fica resolvido mas não é o caso geralmente;

Dependency inversion principle - n. a.

Com isso em vista, especialmente o problema da responsabilidade única, me parece urgente a busca por uma alterantiva mais elegante.


No projeto que desenvolvo, surgiu a necessidade de algo do tipo depois de os serviços (classes centrais da lógica de negócio) começarem a implementar um conjunto significante de interfaces para atenderem seus mais variados propósitos. Havia uma interface para injetar o serviço de traduções, outra para o identity, outra para cache e assim vai. Este problema foi solucionado herdando de AbstractPluginManager em AbstractService e configurando os plugins adequadamente. Talvez uma alternativa ainda melhor (em questões de OO) seja injetando uma instância de uma classe concreta de AbstractPluginManager, mas com injeção também haveria uma leve perda de performance , uma questão de trade-off’s.

O caminho da herança - ServiceAbstract como Gerenciador de Plugins

<?php

namespace Core\Service;

use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Zend\ServiceManager\AbstractPluginManager;

abstract class Service extends AbstractPluginManager implements ServiceManagerAwareInterface
{
    ...

    /**
     * plugin factories
     * @var array
     */
    protected $factories = [
        'identity' => 'Core\Service\Plugin\IdentityFactory',
        'translate' => 'Core\Service\Plugin\TranslateFactory',
        'cache' => 'Core\Service\Plugin\CacheFactory',
    ];
    
    /**
     * validate a  plugin
     */
    public function validatePlugin($plugin)
    {
        if ($plugin instanceof Plugin\PluginInterface) {
            return;
        }

        throw new \InvalidArgumentException(sprintf(
            'Plugin of type %s is invalid; must implement %s\Plugin\PluginInterface',
            (is_object($plugin) ? get_class($plugin) : gettype($plugin)),
            __NAMESPACE__
        ));
    }

    /**
     * Method overloading: return/call plugins
     *
     * If the plugin is a functor, call it, passing the parameters provided.
     * Otherwise, return the plugin instance.
     *
     * @param  string $method
     * @param  array  $params
     * @return mixed
     */
    public function __call($method, $params)
    {
        $plugin = $this->get($method);
        if (is_callable($plugin)) {
            return call_user_func_array($plugin, $params);
        }

        return $plugin;
    }
}

AbstractPluginManager define um método abstrato o: validatePlugin que deve ser implementado por seus filhos, neste caso validei se os plugins implementam a interface que defini para eles Plugin\PluginInterface. Além da interface a implementar, existem três variáveis relevantes a criação de plugins

  • AbstractPluginManager::$factories Recebe um array com os nomes Factories que retornam um plugin
  • AbstractPluginManager::$invokables Recebe um array com os nomes de Plugins que podem ser instanciados sem a ajuda de uma factory
  • AbstractPluginManager::$aliases Servem para dar apelidos aos plugins (por default os plugins são nomeados de acordo com o nome de suas classes)

Para criar um invokable:

	protected $aliases = [
		'identity' => '\Core\Service\Plugin\Identity'
	];

Para criar um alias:

	protected $aliases = [
		'identity' => 'useridentity'
	];

(A partir de então $this->identity() e $this->useridentity() fazem a mesma coisa )

O método __call é uma conveniência que nos possibilita chamar os plugins como se eles fossem membros da classe. A partir de então $this->identity() tem o mesmo resultado de $this->get('identity').

Uma interface para os plugins

<?php

namespace Core\Service\Plugin;

/**
 * base service plugin signature
 *
 * @category Application
 * @package  Service
 * @author   Jean Carlo Machado <contato@jeancarlomachado.com.br>
 */
interface PluginInterface
{
}

Mesmo deixando a interface vazia é bom depender em abstrações, sendo assim a mantive.

Nota O método __invoke() está sendo utilizado em todos os plugins exemplificados abaixo mesmo assim não faz parte da interface. Em plugins com múltiplas intenções (ex: FlashMessenger) um método principal como o __invoke sugere um objeitvo principal que pode não ser verdadeiro.

Plugin Identity

<?php

namespace Core\Service\Plugin;

/**
 * identity plugin
 *
 * @category Application
 * @package  Service
 * @author   Jean Carlo Machado <contato@jeancarlomachado.com.br>
 */
class Identity implements PluginInterface
{
    protected $authenticationService;

    /**
     * Retrieve the current identity through the auth service
     *
     * @return Application\Model\User
     */
    public function __invoke($token)
    {
        return $this->authenticationService->getIdentity($token);
    }

    public function getAuthenticationService()
    {
        return $this->authenticationService;
    }

    public function setAuthenticationService($authenticationService)
    {
        $this->authenticationService = $authenticationService;

        return $this;
    }
}

OBS: Utilizo o método mágico __invoke para executar a funcionalidade do plugin sem precisar utilizar métodos.

Factory Identity

<?php

namespace Core\Service\Plugin;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

/**
 * factory for identity plugin
 *
 * @category Application
 * @package  Service
 * @author   Jean Carlo Machado <contato@jeancarlomachado.com.br>
 */
class IdentityFactory implements FactoryInterface
{
    /**
     * {@inheritDoc}
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $services = $serviceLocator->getServiceLocator();
        $helper = new Identity();
        if ($services->has('Application\Service\Auth')) {
            $helper->setAuthenticationService($services->get('Application\Service\Auth'));
        }

        return $helper;
    }
}

O importante aqui é implementar o FactoryInterface, a partir de então o AbstractPluginManager se responsabiliza por injetar uma instância de ServiceManager na chamada createService que pode ser utilizado para incluir as dependências necessárias no plugin.

Plugin Cache

<?php

namespace Core\Service\Plugin;

/**
 * cache plugin
 *
 * @category Application
 * @package  Service
 * @author   Jean Carlo Machado <contato@jeancarlomachado.com.br>
 */
class Cache implements PluginInterface
{
    protected $cacheService;

    /**
     * Retrieve the cache service
     *
     * @return Zend\Cache\Storage\Adapter\AbstractAdapter
     */
    public function __invoke()
    {
        return $this->cacheService;
    }

    public function getCacheService()
    {
        return $this->cacheService;
    }

    public function setCacheService($cacheService)
    {
        $this->cacheService = $cacheService;

        return $this;
    }
}

No _invoke do plugin do cache retornei a própria instância de Cache, para facitilitar, visto que normalmente diversas operações são realizadas com esta classe em um serviço. Talvez o ideal seria implementar as chamadas mais utilizadas no plugin em detrimento de retornar todo o objeto pois as interface dos plugins devem prezar a simplicidade, segundo o SOL(I)D.


Você ainda pode configurar o seu PluginManager para pegar os array’s de configuração nos configs do Zend, e criar uma factory para injetar em cada atrituto. Pode também injetar dinamicamente plugins atravás do método setService($serviceName) do seu gerenciador de plugins.

Nota: Se você olhar a implementação de PluginManager você notará que o mesmo herda suas configurações de Zend\ServiceManager\ServiceManager, sendo assim não abuse pois este é um dos maiores objetos no Zend Framework2. Recomendo a estrutura somente quando lidando com classes chave ao projeto.

Recapitulando, o código fica muito mais organizado sem aqueles get*’s fora de escopo pulando em cada classe importante do projeto.

Sugestões, comentários e críticas são sempre bem vindos.