You can imagine the web application as a set of services. For example, you can have an authentication service responsible for logging in the site users, entity manager service responsible for accessing the database, event manager service responsible for triggering events and delivering them to event listeners, etc.
In Zend Framework, the ServiceManager
class is a centralized container for all
application services. The service manager is implemented in Zend\ServiceManager
component, as the ServiceManager
class. Class inheritance diagram is shown in figure 3.5 below:
The service manager is created on application start up (inside of init()
static method of Zend\Mvc\Application
class).
The standard services available through service manager are presented in table 3.1.
This table is incomplete, because the actual number of services registered in service manager
may be much bigger.
Service Name | Description |
---|---|
Application |
Allows to retrieve the singleton of Zend\Mvc\Application class. |
ApplicationConfig |
Configuration array extracted from application.config.php file. |
Config |
Merged configuration array extracted from module.config.php files merged with autoload/global.php and autoload/local.php. |
EventManager |
Allows to retrieve a new instance of Zend\EventManager\EventManager class. The event manager allows to send (trigger) events and attach event listeners. |
SharedEventManager |
Allows to retrieve the singleton instance of Zend\EventManager\SharedEventManager class. The shared event manager allows to listen to events defined by other classes and components. |
ModuleManager |
Allows to retrieve the singleton of Zend\ModuleManager\ModuleManager class. The module manager is responsible for loading application modules. |
Request |
The singleton of Zend\Http\Request class. Represents HTTP request received from client. |
Response |
The singleton of Zend\Http\Response class. Represents HTTP response that will be sent to client. |
Router |
The singleton of Zend\Router\Http\TreeRouteStack . Performs URL routing. |
ServiceManager |
Service manager itself. |
ViewManager |
The singleton of Zend\Mvc\View\Http\ViewManager class. Responsible for preparing the view layer for page rendering. |
A service is typically an arbitrary PHP class, but not always. For example, when ZF3
loads the configuration files and merges the data into nested arrays, it saves the arrays
in the service manager as a couple of services (!): ApplicationConfig
and Config
.
The first one is the array loaded from application-level configuration file application.config.php,
and the later one is the merged array from module-level config files and auto-loaded
application-level config files. Thus, in the service manager you can store anything
you want: a PHP class, a simple variable or an array.
From table 3.1, you can see that in ZF3 almost everything can be considered as a service. The service
manager is itself registered as a service. Moreover, the Application
class is also
registered as a service.
An important thing you should note about the services is that they are typically stored in a single instance only (this is also called the singleton pattern). Obviously, you don't need the second instance of the
Application
class (in that case you would have a nightmare).
But, there is an important exception from the rule above. It may be confusing at first, but the
EventManager
is not a singleton. Each time you retrieve the event manager service from service manager, you receive a new object. This is done for performance reasons and to avoid possible event conflicts between different components. We will discuss this further in the About Event Manager section later in this chapter.
The service manager defines several methods needed for locating and retrieving a service from the service manager (see the table 3.2 below).
Method Name | Description |
---|---|
has($name) |
Checks if such a service is registered. |
get($name) |
Retrieves a registered service's instance. |
build($name, $options) |
Always returns a new instance of the requested service. |
You can test if a service is registered by passing its name to the service manager's
has()
method. It returns a boolean true
if the service is registered, or
false
if the service with such a name is not registered.
You can retrieve a service by its name later with the help of the service manager's get()
method.
This method takes a single parameter representing the service name. Look at the following
example:
<?php
// Retrieve the application config array.
$appConfig = $serviceManager->get('ApplicationConfig');
// Use it (for example, retrieve the module list).
$modules = $appConfig['modules'];
And the build()
method always creates a new instance of the service when you call it (comparing to get()
, which
typically creates the instance of the service only once and returns it on later requests).
You will typically retrieve services from service manager not in any place of your code, but inside of a factory. A factory is a code responsible for creation of an object. When creating the object, you can retrieve services it depends on from the service manager and pass those services (dependencies) to the object's constructor. This is also called dependency injection.
If you have some experience with Zend Framework 2, you may notice that the things are now a little different than before. In ZF2, there was
ServiceLocator
pattern allowing to get dependencies from service manager in any part of your app (in controllers, services, etc.) In ZF3, you have to pass dependencies explicitly. It is a little more boring, but it removes "hidden" dependencies and makes your code more clear and easier to understand.
When writing your website, you will often need to register your own service
in the service manager. One of the ways to register a service is using the setService()
method of the service manager.
For example, let's create and register the currency converter service class, which
will be used, for example, on a shopping cart page to convert EUR currency to USD:
<?php
// Define a namespace where our custom service lives.
namespace Application\Service;
// Define a currency converter service class.
class CurrencyConverter
{
// Converts euros to US dollars.
public function convertEURtoUSD($amount)
{
return $amount*1.25;
}
//...
}
Above, in lines 6-15 we define an example CurrencyConverter
class (for simplicity, we implement
only a single method convertEURtoUSD()
which is able to convert euros to US dollars).
// Create an instance of the class.
$service = new CurrencyConverter();
// Save the instance to service manager.
$serviceManager->setService(CurrencyConverter::class, $service);
In the example above, we instantiate the class with the new
operator, and register it
with the service manager using the setService()
method (we assume that the $serviceManager
variable
is of type Zend\ServiceManager\ServiceManager
class, and that it was declared somewhere else).
The setService()
method takes two parameters: the service name string, and the service instance.
The service name should be unique within all other possible services.
Once the service is stored in service manager, you can retrieve it by name at any place of your
application with the help of the service manager's get()
method. Look at the following
example:
<?php
// Retrieve the currency converter service.
$service = $serviceManager->get(CurrencyConverter::class);
// Use it (convert money amount).
$convertedAmount = $service->convertEURtoUSD(50);
Different services can use different naming styles. For example, the same currency converter service
may be registered under the different names: CurrencyConverter
, currency_converter
and so on. To introduce some uniform naming convention, it is recommended to register a service by
its fully qualified class name, as follows:
$serviceManager->setService(CurrencyConverter::class);
In the example above, we used the keyword class
. It is available since PHP 5.5 and is used for class
name resolution. CurrencyConverter::class
is expanded to the fully qualified name of the class,
like \Application\Service\CurrencyConverter
.
If you are trying to register the service name which is already present, the setService()
method will throw an exception. But sometimes
you want to override the service with the same name (to replace it by the new one). For this purpose,
you can use the setAllowOverride()
method of the service manager:
<?php
// Allow to replace services
$serviceManager->setAllowOverride(true);
// Save the instance to service manager. There will be no exception
// even if there is another service with such a name.
$serviceManager->setService(CurrencyConverter::class, $service);
Above, the setAllowOverride()
method takes the single boolean parameter defining whether
to allow you replace the service CurrencyConverter
if such a name is already present, or not.
What is bad with the setService()
method is that you have to create the service instance
before you really need it. If you never use the service, the service instantiation will only
waste the time and memory. To resolve this issue, the service manager provides you with the
setInvokableClass()
method.
<?php
// Register an invokable class
$serviceManager->setInvokableClass(CurrencyConverter::class);
In the example above, we pass to the service manager the fully qualified class name of
the service instead of passing its instance. With this technique, the service
will be instantiated by the service manager only when someone calls the get(CurrencyConverter::class)
method. This is also called lazy loading.
Services often depend on each other. For example, the currency converter service may use entity manager service to read money exchange rates from database. The disadvantage of
setInvokableClass()
method is that it doesn't allow to pass parameters (dependencies) to the service on object instantiation. To resolve this issue, you can use factories, as described below.
A factory is a class that can do only one thing - to create other objects.
You register a factory for a service with the setFactory()
method of the service manager:
The simplest factory is InvokableFactory
- it is analogous to the setInvokableClass()
method from the previous
section.
<?php
use Zend\ServiceManager\Factory\InvokableFactory;
// This is equivalent to the setInvokableClass() method from previous section.
$serviceManager->setFactory(CurrencyConverter::class, InvokableFactory::class);
After you have registered the factory you can retrieve the service from service manager as usual with the get()
method. The service
will be instantiated only when you retrieve it from service manager (lazy loading).
Sometimes, service instantiation is more complex than just creating the service instance
with new
operator (like InvokableFactory
does). You may need to pass some parameters to the service's constructor or
invoke some service methods just after construction. This complex instantiation logic
can be encapsulated inside of your own custom factory class. The factory class typically implements the FactoryInterface
:
<?php
namespace Zend\ServiceManager\Factory;
use Interop\Container\ContainerInterface;
interface FactoryInterface
{
public function __invoke(ContainerInterface $container,
$requestedName, array $options = null);
}
As we see from the definition of the FactoryInterface
, the factory class must provide
the __invoke
magic method returning the instance of a single service. The service manager is
passed to the __invoke
method as the $container
parameter; it can be used during the construction of
the service for accessing other services (to inject dependencies). The second argument ($requestedName
) is
the service name. The third argument ($options
) can be used to pass some parameters to the service, and
is used only when you request the service with the build()
method of the service manager.
As an example, let's write a factory for our currency converter service (see the code below).
We don't use complex construction logics for our CurrencyConverter
service, but for more complex
services, you may need to use one.
<?php
namespace Application\Service\Factory;
use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Service\CurrencyConverter;
// Factory class
class CurrencyConverterFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container,
$requestedName, array $options = null)
{
// Create an instance of the class.
$service = new CurrencyConverter();
return $service;
}
}
Technically, in ZF3 you can use the same factory class for instantiating several services that have similar instantiation code (for that purpose, you can use the
$requestedName
argument passed to the__invoke()
method of the factory). However, mostly you will create a different factory per each service.
Even more complex case of a factory is when you need to determine at run
time which service names should be registered. For such a situation,
you can use an abstract factory. An abstract factory class should
implement the AbstractFactoryInterface
interface:
<?php
namespace Zend\ServiceManager\Factory;
use Interop\Container\ContainerInterface;
interface AbstractFactoryInterface extends FactoryInterface
{
public function canCreate(ContainerInterface $container, $requestedName);
}
An abstract factory has two methods: canCreate()
and __invoke()
. The first one is needed to test if the factory can
create the service with the certain name, and the latter one allows to actually
create the service. The methods take two parameters: service manager ($container
) and
service name ($requestedName
).
Comparing to usual factory class, the difference is that the usual factory class typically creates only a single type of service, but an abstract factory can dynamically create as many types of services as it wants.
You register an abstract factory with the setAbstractFactory()
method of the service manager.
Abstract factories are a powerful feature, but you should use them only when really necessary, because they negatively impact the performance. It is better to use the usual (non-abstract) factories.
Sometimes, you may want to define an alias for a service. The alias
is like a symbolic link: it references the already registered service.
To create an alias, you use the service manager's setAlias()
method:
<?php
// Register an alias for the CurrencyConverter service
$serviceManager->setAlias('CurConv', CurrencyConverter::class);
Once registered, you can retrieve the service by both its name and alias using the
service manager's get()
method.
By default, services are stored in service manager in single instance only. This is also called the singleton
design pattern. For example, when you try to retrieve the CurrencyConverter
service twice, you will receive
the same object. This is also called a shared service.
But, in some (rare) situations, you will need to create a new instance of a service each time someone requests
it from service manager. An example is the EventManager
- you get a new instance of it each time you request it.
To mark a service as a non-shared, you can use service manager's setShared()
method:
$serviceManager->setShared('EventManager', false);
In your website, you typically use service manager configuration to register your services (instead of calling service manager's methods as described above).
To automatically register a service within the service manager, typically the
service_manager
key of a configuration file is used. You can put this key
either inside of an application-level configuration file or in a module-level
configuration file.
If you are putting this key in a module-level configuration file, be careful about the danger of name overwriting during the configs merge. Do not register the same service name in different modules.
This service_manager
key should look like below:
<?php
return [
//...
// Register the services under this key
'service_manager' => [
'services' => [
// Register service class instances here
//...
],
'invokables' => [
// Register invokable classes here
//...
],
'factories' => [
// Register factories here
//...
],
'abstract_factories' => [
// Register abstract factories here
//...
],
'aliases' => [
// Register service aliases here
//...
],
'shared' => [
// Specify here which services must be non-shared
]
],
//...
];
In the example above, you can see that the service_manager
key may contain several
subkeys for registering services in different ways:
services
subkey (line 7) allows to register class instances;invokables
subkey (line 11) allows to register full class name of a service;
the service will be instantiated using lazy loading;factories
subkey (line 15) allows for registering a factory, which is able
to create instances of a single service;abstract_factories
(line 19) can be used for registering abstract factories,
which are able to register several services by name;aliases
subkey (line 23) provides an ability to register an alias for a service.shared
subkey (line 27) allows to specify which services must be non-shared.As an example, let's register our CurrencyConverter
service and create an alias for it:
<?php
use Zend\ServiceManager\Factory\InvokableFactory;
use Application\Service\CurrencyConverter;
return [
//...
// Register the services under this key
'service_manager' => [
'factories' => [
// Register CurrencyConverter service.
CurrencyConverter::class => InvokableFactory::class
],
'aliases' => [
// Register an alias for the CurrencyConverter service.
'CurConv' => CurrencyConverter::class
],
],
//...
];