ZF2 – Service Manager – Zend\ServiceManager

The Service Manager is ZF2 is a type of intelligent registry where you pass in a string and receive an instance of the class that was defined in your application. For example, if you want the config service you would make the following call:

$serviceManager->get('config');

Zend\ServiceManager\ServiceManager implements the ServiceLocatorInterface interface.

The interface prescribes two methods: has() to determine if a service has been registered with the Service Manager, and get() to return an instance of that service.

namespace Zend\ServiceManager;

interface ServiceLocatorInterface
{
    public function get($name);
    public function has($name);
}

You can register a service using the setService() method of Zend\ServiceManager\ServiceManager passing in the name of the service as a string and the service itself.

$serviceManager->setService('my-foo', new stdClass());
$serviceManager->setService('my-settings', array('password' => 'super-secret'));

var_dump($serviceManager->get('my-foo')); // an instance of stdClass
var_dump($serviceManager->get('my-settings')); // array('password' => 'super-secret')

Configuration

The Service Manager can be configured in a couple of different different ways.

The first method, assuming you are using a ZF2 application, is by defining key/value pairs in a module’s configuration file.

<?php
// a module configuration, "module/SomeModule/config/module.config.php"
return array(
    'service_manager' => array(
        'abstract_factories' => array(
            // Valid values include names of classes implementing
            // AbstractFactoryInterface, instances of classes implementing
            // AbstractFactoryInterface, or any PHP callbacks
            'SomeModule\Service\FallbackFactory',
        ),
        'aliases' => array(
            // Aliasing a FQCN to a service name
            'SomeModule\Model\User' => 'User',
            // Aliasing a name to a known service name
            'AdminUser' => 'User',
            // Aliasing to an alias
            'SuperUser' => 'AdminUser',
        ),
        'factories' => array(
            // Keys are the service names.
            // Valid values include names of classes implementing
            // FactoryInterface, instances of classes implementing
            // FactoryInterface, or any PHP callbacks
            'User'     => 'SomeModule\Service\UserFactory',
            'UserForm' => function ($serviceManager) {
                $form = new SomeModule\Form\User();

                // Retrieve a dependency from the service manager and inject it!
                $form->setInputFilter($serviceManager->get('UserInputFilter'));
                return $form;
            },
        ),
        'invokables' => array(
            // Keys are the service names
            // Values are valid class names to instantiate.
            'UserInputFilter' => 'SomeModule\InputFilter\User',
        ),
        'services' => array(
            // Keys are the service names
            // Values are objects
            'Auth' => new SomeModule\Authentication\AuthenticationService(),
        ),
        'shared' => array(
            // Usually, you'll only indicate services that should **NOT** be
            // shared -- i.e., ones where you want a different instance
            // every time.
            'UserForm' => false,
        ),
    ),
);

The second method is defining the service configuration using the getServiceConfig() method within your module’s Module.php file.

namespace SomeModule;

// you may eventually want to implement Zend\ModuleManager\Feature\ServiceProviderInterface
class Module
{
    public function getServiceConfig()
    {
        return array(
            'abstract_factories' => array(),
            'aliases' => array(),
            'factories' => array(),
            'invokables' => array(),
            'services' => array(),
            'shared' => array(),
        );
    }
}

It is advised to use the first option as using this option, the config file can be cached.

There are several different types of services we can define:

  • Invokables
  • Factories
  • Aliases
  • Abstract Factories
  • Initializers
  • Configuration Classes
  • Shared

Invokables

You should define an invokable service where the key is the unique name of the service and the value is a fully qualified class name what will be used to instantiate the service.

<?php
// a module configuration, "module/SomeModule/config/module.config.php"
return array(
    'service_manager' => array(
        'invokables' => array(
            // Keys are the service names
            // Values are valid class names to instantiate.
            'timer' => 'Debug\Service\Timer',
        ),
    ),
);

You can then use:

$serviceManager->get('timer');

Which is the same as doing:

$name = 'Debug\Service\Timer';
$timerInstance = new $name();
return $timerInstance;

You can also use the setInvokableClass() method of the ServiceManager class.

use Zend\ServiceManager\ServiceManager;

$serviceManager = new ServiceManager();
$serviceManager->setInvokableClass('foo-service-name', 'Fully\Qualified\Classname');

var_dump($serviceManager->get('foo-service-name')); // an instance of Fully\Qualified\Classname

Essentially, an invokable is a class that can be constructed without any arguments. Typically, invokable service classes are located within the module’s src/Modulename/Service directory. Should you require arguments to be passed in, you can use a factory service.

Factories

The value part of the key/value pair for factory services can be either an object implementing Zend\ServiceManager\FactoryInterface, the name of a class implementing that interface or a PHP callback.

Here are a couple of factory definitions within an application’s configuration.

<?php
// a module configuration, "module/SomeModule/config/module.config.php"
return array(
    'service_manager' => array(
        'factories' => array(
            'timer' => 'Debug\Factory\Timer',
            'UserForm' => function ($serviceManager) {
                $form = new Debug\Form\User();

                // Retrieve a dependency from the service manager and inject it!
                $form->setInputFilter($serviceManager->get('UserInputFilter'));
                return $form;
            },
        ),
    ),
);

As well as the setInvokableClass() method used by the ServiceManager class, there is also a setFactory() method for factory service types.

// registering a factory instance
$serviceManager->setFactory('foo-service-name', new MyFactory());

// registering a factory by factory class name
$serviceManager->setFactory('bar-service-name', 'MyFactory');

// registering a callback as a factory
$serviceManager->setFactory('baz-service-name', function () { return new \stdClass(); });

Using a PHP callback should only be used for fast development and testing because of the following reasons:

  • Performance may decrease.
  • Configuration caching may break as PHP cannot serialise and deserialise closures.
  • The autoloading may not work as expected.

Abstract Factories

An abstract factory can be considered as a “fallback” factory. If the service manager was not able to find a service for the requested name, it will check the registered abstract factories.

Abstract factories must implement Zend\ServiceManager\AbstractFactoryInterface which gives us two methods, the canCreateServiceWithName that tells us if this factory is able to create certain type of service, and createServiceWithName that creates the service if the first method returned true.

In order to register an abstract factory, add the ‘abstract_factories’ key under the ‘service_manager’ key within your module’s configuration.

'service_manager' => array(
    'abstract_factories' => array(
        // add here a list of resolvable class names
        'Debug\Service\Factory\TimerAbstractFactory',
    )
);

You could also use the addAbstractFactory() method to add an abstract factory.

use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\AbstractFactoryInterface;

class MyAbstractFactory implements AbstractFactoryInterface
{
    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        // this abstract factory only knows about 'foo' and 'bar'
        return $requestedName === 'foo' || $requestedName === 'bar';
    }

    public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        $service = new \stdClass();

        $service->name = $requestedName;

        return $service;
    }
}

$serviceManager->addAbstractFactory('MyAbstractFactory');

var_dump($serviceManager->get('foo')->name); // foo
var_dump($serviceManager->get('bar')->name); // bar
var_dump($serviceManager->get('baz')->name); // exception! Zend\ServiceManager\Exception\ServiceNotFoundException

Aliasing

With ServiceManager::setAlias you can create aliases of any registered service, factory or invokable, or even other aliases.

$foo      = new \stdClass();
$foo->bar = 'baz!';

$serviceManager->setService('my-foo', $foo);
$serviceManager->setAlias('my-bar', 'my-foo');
$serviceManager->setAlias('my-baz', 'my-bar');

var_dump($serviceManager->get('my-foo')->bar); // baz!
var_dump($serviceManager->get('my-bar')->bar); // baz!
var_dump($serviceManager->get('my-baz')->bar); // baz!

And within an application’s configuration, it might look something like this:

'service_manager' => array(
    'invokables' => array(
        'timer' => 'Debug\Service\Timer'
    ),
    'aliases' => array(
        'somealias' => 'timer'
    )
),

So as well as using $serviceManager->get(‘timer’), $serviceManager->get(‘somealias’) will also work.

Shared

By default, the ServiceManager always returns the same instance of a service when you request it multiple times. It is created the first time and cached during the request. That’s what a shared service is. A non-shared service will create a new instance every time it is requested.

If you always want to have unique instances when you get a service from the service manager, then you can add configuration to the ‘shared’ key under the ‘service_manager’ key.

'service_manager' => array(
    'factories' => array(
        'crypto' => 'Crypto/Service/Factory'
    ),
    'shared' => array(
        'crypto' => false,
    ),
)

You can also pass in false as a third parameter into the setInvokableClass() and setFactory() methods.

$serviceManager->setInvokableClass('foo-service-name', 'Fully\Qualified\Classname', false);
$serviceManager->setFactory('foo-service-name', new MyFactory(), false);

Initializers

Initializers don’t really create new objects, they are called after any service is created to make some updates on the state of the object.

They, like other services, can be configured within the module’s configuration file.

'service_manager' => array(
    'initializers' => array(
        'Application\Service\Initializer\EventManagerInitializer',
    ),

You can also use the addInitializer() method passing in the class name of your initializer.

$serviceManager->addInitializer('MyInitializer');

Initializers must implement Zend\ServiceManager\InitializerInterface and contain the initialize() method.

use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\InitializerInterface;

class MyInitializer implements InitializerInterface
{
    public function initialize($instance, ServiceLocatorInterface $serviceLocator)
    {
        if ($instance instanceof \stdClass) {
            $instance->initialized = 'initialized!';
        }
    }
}

$serviceManager->addInitializer('MyInitializer');
$serviceManager->setInvokableClass('my-service', 'stdClass');

var_dump($serviceManager->get('my-service')->initialized); // initialized!

Delegator Service Factories

Delegator factories allow us to override the behaviour of certain factories without code duplication.

They are similar to initializers in that they are called upon after the service has been created, however they only affect a concrete service which is much more efficient.

This is because with initializers, and the initialize() method, the method is being called once per each instantiated service, and that can lead to hundreds of useless method calls per request.

Delegators must implement Zend\ServiceManager\DelegatorFactoryInterface and contain the createDelegatorWithName() method.

namespace Zend\ServiceManager;

interface DelegatorFactoryInterface
{
    public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback);
}

A simple example of how a delegator might be used can be seen below.

namespace Application\Service\Delegator;

use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class MyServiceDelegator implements DelegatorFactoryInterface
{
    public function createDelegatorWithName(
        ServiceLocatorInterface $serviceLocator,
        $name,
        $requestedName,
        $callback
    ) {
        $myService = $callback();

        $myService->setFoo('');
        $myService->setBar(123);
        // Do something else with the service before returning it...

        return $myService;
    }
}

As with other services, delegators can be configured within a module’s configuration file:

'invokables' => array(
    'my'                   => 'MyService',
    'my-service-delegator' => 'MyServiceDelegator', // Delegators must be registered as well
),
'delegators' => array(
    'my' => array(
        'my-service-delegator'
        // eventually add more delegators here
    ),
),

Or via the addDelegator() method.

$serviceManager = new Zend\ServiceManager\ServiceManager();

$serviceManager->setInvokableClass('my', 'MyService'); // usually not under our control

// as opposed to normal factory classes, a delegator factory is a
// service like any other, and must be registered:
$serviceManager->setInvokableClass('my-service-delegator', 'MyServiceDelegator');

// telling the service manager to use a delegator factory to handle service 'my'
$serviceManager->addDelegator('my', 'my-service-delegator');

Note: This article is based on ZF version 2.4.