Wednesday 5 March 2014

Adventures in Zend Framework 2: Episode 3: Major Features

ZF2 Structure

A quick glance at the ZF2 directory structure instantly reveals its primary focus as a framework designed with web applications in mind. In addition to regular requirements for software development like caching, authentication, logging, cryptography and MVC components, you will so find classes for forms, http connections, soap and XmlRpc.

Zend Framework 2 directory structure
In this post, I'm going to give an overview of two of ZF2's most important components, the Event Manager and the Service Manager, an understanding of which is key to expanding the usefulness of the framework to your application.

The Event Manager

Perhaps the biggest change from ZF1 to ZF2 was the focus on event driven architecture. The whole application flow depends on events - a change in the application triggers an event and listeners can be attached to each event so that further changes can be made when desired. Each listener is usually passed information about the event, thus allowing it to set logic as to how to proceed given different event variables. Thus for example, one could attach listeners to the dispatch.error event which could log information about the error or send an e-mail to a system administrator.

Most of the events listeners attach to will be associated with the MVC flow but it is possible to create custom events and listeners which can be triggered manually and listened to accordingly. Custom events require their own event manager and are triggered with a name plus optional parameters for a target object, an array of arguments and a callback function.

use Zend\EventManager\EventManager

$event = new EventManager('star-wars');
$event->trigger('death star destruction', null, array('rebels' => array("Luke Skywalker", "Han Solo", "Princess Leia"));


Event listening is generally established during the Bootstrap MVC phrase, often in the OnBootstrap method of Module.php. As module boostrapping can require a lot of code, VuFind 2 adds a custom Bootstrapper class which allows it to create different methods for each initiation requirement such as forcing the user to an error page if the system is unavailable. A listener is added to an event via the attach method which accepts the event name (or an array of event names), an optional callback and a priority as arguments.

VuFind/Module
class Module {
...
    public function onBootstrap(MvcEvent $e)
    {
        $bootstrapper = new Bootstrapper($e);
        $bootstrapper->bootstrap();
    }

}

VuFind/Bootstrapper

class Boostrapper {

public function __construct(MvcEvent $event)
    {
        $this->event = $event;
        $this->events = event->getApplication()->getEventManager();
    }

    public function bootstrap()
    {
        // automatically call all methods starting with "init":
        $methods = get_class_methods($this);
        foreach ($methods as $method) {
            if (substr($method, 0, 4) == 'init') {
                $this->$method();
            }
        }

    }

    protected function initSystemStatus()
    {
        // If the system is unavailable, forward to a different place:
        if (isset($this->config->System->available)
            && !$this->config->System->available
        ) {
            $callback = function ($e) {
                $routeMatch = new RouteMatch(
                    array('controller' => 'Error', 'action' => 'Unavailable'), 1
                );
                $routeMatch->setMatchedRouteName('error-unavailable');
                $e->setRouteMatch($routeMatch);
            };
            $this->events->attach('route', $callback);
        }
    }
}

MVC events have their own event manager which is why in the code above, we can instantly call $this->events->attach(). If you have created your own events however, you need to access them via the shared event manager. Using the VuFind code as a base, we could therefore do something like:

    protected function initHolonet()
    {

        $callback = function ($e) {
            $rebels = $e->getParam('rebels');
            $sm = $e->getApplication()->getServiceManager();
            $holonet = $sm->get('holonet');

            return $holonet->transmit('palpatine@imperial.net', $rebels);
        };
        $sharedManager = $this->events->getSharedManager();
        $sharedManager->attach('star-wars',
'death star destruction', $callback);
    }

The priority argument is extremely useful as it allows us to stack our listeners according to the order in which they should run. Calling stopPropagation(true) on an event will also stop any subsequent listeners from executing, something which is very handy when an error occurs or a dependency is not available. The result of the listener callback is returned as an argument to the callback of the event trigger which allows for further processing if required.

The Service Manager

If the Event Manager determines when you run your code, the Service Manager is primarily designed to help with how you run it. The Service Manager is essentially a registry of services which can be intelligently initiated with a simple key. In the initHolonet example above, the Service Manager was accessed via
$sm = $e->getApplication()->getServiceManager();
and the holonet service instance was loaded with

$sm->get('holonet');

Though it is possible to define a service almost anywhere, it makes sense to do so either in the module configuration file or the module class itself for ease of reference.

The service manager accepts the following primary configuration keys:
1) invokeables
Classes which can be invokes without configuration
2) factories
Objects which require configuration, often with other service manager services
3) abstract Factories
Factories that can can create multiple services based on the name supplied
4) services
Objects which have already been instantiated
5) initializers
Services which initialise other services whenever they are created
6) aliases
Alias a key to a known service key
7) shared
Determines whether or not a service should be shared or if a new service should be created with every call
8) allow_override
Whether or not the services can be overridden by other modules
When defined in module.config.php, a service manager configuration might look something like this:
module.config.php
return array(
    ...
    

    service_manager => array(
    

    'invokables' => array(
        'Anakin' => '\Empire\Personel\AnakinSkywalker';

        'Empire\Transmitter' => \Empire\Resources\Transmitter
     ),

    'factories' => array(
        'holonet' => function ($sm) {
            $transmitter =
$sm->getServiceLocator()->get('Empire\Transmitter');
            return new \Empire\Services\Holonet($transmitter);
        }
    ),


    'abstract_factories' => array(
        'RebelAlliance\Services\PeronselManager',
    ),


    'services' => array(
        'light' => new \Force\LightSide(),

        'dark' => new \Force\DarkSide()
    ),


    'initializers' => array(
        function ($instance, $sm) {
            if ($instance instanceof
\Empire\Resources\Transmitter){
        $instance->setFrequencies($sm->get('ImperialFrequencies'));
            }
        },

        'RebelAlliance\Services\Initializers\Jedi',
        'Empire\Services\Initializers\Sith'
    ),

    'aliases' => array(
        'DarthVader' => 'Anakin';
    ),


    'shared' => array(
        'holonet' => false,
    ),
    

    )
);



View Helpers, Controllers, Controller Plugins and Form Elements effectively have their own service managers and their configurations needs to be returned with their own keys instead of "service_manager". These keys are "view_helpers", "controllers", "controller_plugins" and "form_elements".

If you wished to achieve something similar in the Module Class for a service manager configuration, you can use the getServiceConfig method to return a configuration array:


Module.php
 
public function getServiceConfig() {
   return array(
     'services' => array(
       'light' => new \Force\LightSide(),
       'dark' => new \Force\DarkSide()
      ),

  );
}


The getViewHelperConfig(), getControllerConfig(), getControllerPluginConfig() and getFormElementConfig() methods are also available for their service managers.

Service Manager: Basic Functionality


When  a call is made to the get method of the service manager with a service key, the service manager checks its registry of instances, invokables, factories, aliases and abstract factories for that key. If the key is found, it calls its create method with that key which in turn instantiates the service according to the service type (invokable, factory etc).

Zend/ServiceManager/ServiceManager

if (isset($this->instances[$cName])) {
    return $this->instances[$cName];

}

if (!$instance) {
    if (
        isset($this->invokableClasses[$cName])
        || isset($this->factories[$cName])
        || isset($this->aliases[$cName])
        || $this->canCreateFromAbstractFactory($cName, $name)
    ) {
        $instance = $this->create(array($cName, $name));
    } elseif ($usePeeringServiceManagers && !$this->retrieveFromPeeringManagerFirst) {
        $instance = $this->retrieveFromPeeringManager($name);
    }

}

Once instantiated, the service manager also applies the initializers to each instance, either through the initializer initialize method or via the supplied function.

Zend/ServiceManager/ServiceManager 

foreach ($this->initializers as $initializer) {
  if ($initializer instanceof InitializerInterface) {
     $initializer->initialize($instance, $this);
  } else {
     call_user_func($initializer, $instance, $this);
  }
}


The initalize method might therefore look something like:

public function initialize($instance, ServiceLocatorInterface $serviceLocator) {

    if ($instance instanceof \Force\ForceAwareInterface) {
         $instance->setLightSide($sm->get('light'));
         $instance->setDarkSide($sm->get('dark'));
    }

    if (method_exists($instance, 'setHolonet')) {
        $instance->setHolonet('holonet);
    }

}

Service Manager: Abstract Factories

Abstract Factories effectively allow you to create your own logic for instantiating service classes. They must implement Zend\ServiceManager\AbstractFactoryInterface which requires that you define a canCreateServiceWithName method and a createServiceWithName method. VuFind defines it's own AbstractPluginFactory as follows:

abstract class AbstractPluginFactory implements AbstractFactoryInterface
{
    protected $defaultNamespace;

    protected $classSuffix = '';

    protected function getClassName($name, $requestedName)
    {
        // If we have a FQCN, return it as-is; otherwise, prepend the default prefix:
        if (strpos($requestedName, '\\') !== false
            && class_exists($requestedName)
        ) {
            return $requestedName;
        }
        // First try the raw service name, then try a normalized version:
        $finalName = $this->defaultNamespace . '\\' . $requestedName
            . $this->classSuffix;
        if (!class_exists($finalName)) {
            $finalName = $this->defaultNamespace . '\\' . ucwords(strtolower($name))
                . $this->classSuffix;
        }
        return $finalName;
    }

    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator,
        $name, $requestedName
    ) {
        $className = $this->getClassName($name, $requestedName);
        return class_exists($className);
    }

    public function createServiceWithName(ServiceLocatorInterface $serviceLocator,
        $name, $requestedName
    ) {
        $class = $this->getClassName($name, $requestedName);
        return new $class();
    }
}



The key here is that the getClassName method is used to determine the class name, largely via a combination of default namespaces and class suffixes. This makes it easy to create a number of services which share either a common namespace, common class suffix or combination of both. It's also possible to pass in and instantiate a fully qualified namespace.

If desired, it would also be possible to initialise class dependencies as part of the createServiceWithName method. As the Service Locator is passed in as the first argument, it would be easy to access other services. Zend applications will be easier to debug and maintain if you stick to the same principles for all your services. If you decide to initialise dependencies as part of the createServiceWithName for one abstract factory, always initialise dependencies for your abstract factories there. For my money, the best place to perform your initialisation is with the specifically designed initialiser option of the Service Manager. You can either have one initializer responsible for all your instances or you can create multiple initializers according to instance type. If these are all referenced in module.config.php, they will be easy to find and understand.

Service Manager: Plugin Manager

The Plugin Manager is an extension of the service manager which allows users to establish a collection of services according to a specific set of criteria, perhaps via interfaces or ancestor class. It automatically registers an initializer which should be used to verify that a plugin instance is of a valid type, provides for a validatePlugin method and accepts an array of options for the constructor, which can be used to configure the plugin when retrieved.

VuFind extends the Zend Plugin manager to include a getExpectedInterface, thus forcing anything that extends it to implement that method. The validePlugin method then checks to make sure that the plugin is an instance of the value returned by the getExpectedInterface method to ensure plugin integrity.

public function validatePlugin($plugin)
{
    $expectedInterface = $this->getExpectedInterface();
    if (!($plugin instanceof $expectedInterface)) {
        throw new ServiceManagerRuntimeException(
            'Plugin ' . get_class($plugin) . ' does not belong to '
            . $expectedInterface
        );
    }
}

abstract protected function getExpectedInterface();

Once instantiated, a user can then call get on the plugin manager to retrieve the desired instance.

$serviceLocator()->get('RebelAlliance\ResourcesPluginManager')
    ->get('millennium-falcon);

Coming Up...

My next post will look at some of the basics of ZF2's MVC implementation.

No comments:

Post a Comment