On this page
Caution
The documentation you are viewing is for an older version of this component.
Switch to the latest (v4) version.
Migration Guide
The Service Manager was first introduced for Laminas 2.0.0. Its API remained the same throughout that version.
Version 3 is the first new major release of the Service Manager, and contains a number of backwards compatibility breaks. These were introduced to provide better performance and stability.
Case Sensitivity and Normalization
v2 normalized service names as follows:
- It stripped non alphanumeric characters.
- It lowercased the resulting string.
This was done to help prevent typographical errors from creating configuration errors. However, it also presented a large performance hit, and led to some unexpected behaviors.
In v3, service names are case sensitive, and are not normalized in any way.
As such, you must refer to services using the same case in which they were registered.
Configuration
A number of changes have been made to configuration of service and plugin managers:
- Minor changes in configuration arrays may impact your usage.
ConfigInterface
implementations and consumers will need updating.
Configuration arrays
Configuration for v2 consisted of the following:
[
'services' => [
// service name => instance pairs
],
'aliases' => [
// alias => service name pairs
],
'invokables' => [
// service name => class name pairs
],
'factories' => [
// service name => factory pairs
],
'abstract_factories' => [
// abstract factories
],
'initializers' => [
// initializers
],
'delegators' => [
// service name => [ delegator factories ]
],
'shared' => [
// service name => boolean
],
'share_by_default' => boolean,
]
In v3, the configuration remains the same, with the following additions:
[
'lazy_services' => [
// The class_map is required if using lazy services:
'class_map' => [
// service name => class name pairs
],
// The following are optional:
'proxies_namespace' => 'Alternate namespace to use for generated proxy classes',
'proxies_target_dir' => 'path in which to write generated proxy classes',
'write_proxy_files' => true, // boolean; false by default
],
]
The main change is the addition of integrated lazy service configuration is now integrated.
ConfigInterface
The principal change to the ConfigInterface
is the addition of the
toArray()
method. This method is intended to return a configuration array in
the format listed above, for passing to either the constructor or the
configure()
method of the ServiceManager
..
Config class
Laminas\ServiceManager\Config
has been updated to follow the changes to the
ConfigInterface
and ServiceManager
. This essentially means that it removes
the various getter methods, and adds the toArray()
method.
Invokables
Invokables no longer exist, at least, not identically to how they existed in Laminas.
Internally, ServiceManager
now does the following for invokables
entries:
- If the name and value match, it creates a
factories
entry mapping the service name toLaminas\ServiceManager\Factory\InvokableFactory
. - If the name and value do not match, it creates an
aliases
entry mapping the service name to the class name, and afactories
entry mapping the class name toLaminas\ServiceManager\Factory\InvokableFactory
.
This means that you can use your existing invokables
configuration from
version 2 in version 3. However, we recommend starting to update your
configuration to remove invokables
entries in favor of factories (and aliases,
if needed).
Invokables and plugin managers
If you are creating a plugin manager and in-lining invokables into the class definition, you will need to make some changes.
$invokableClasses
will need to become$factories
entries, and you will potentially need to add$aliases
entries.As an example, consider the following, from laminas-math v2.x:
class AdapterPluginManager extends AbstractPluginManager { protected $invokableClasses = [ 'bcmath' => Adapter\Bcmath::class, 'gmp' => Adapter\Gmp::class, ]; }
Because we no longer define an
$invokableClasses
property, for v3.x, this now becomes:use Laminas\ServiceManager\Factory\InvokableFactory; class AdapterPluginManager extends AbstractPluginManager { protected $aliases = [ 'bcmath' => Adapter\Bcmath::class, 'gmp' => Adapter\Gmp::class, ]; protected $factories = [ Adapter\BcMath::class => InvokableFactory::class, Adapter\Gmp::class => InvokableFactory::class, ]; }
Lazy Services
In v2, if you wanted to create a lazy service, you needed to take the following steps:
- Ensure you have a
config
service, with alazy_services
key that contained the configuration necessary for theLazyServiceFactory
. - Assign the
LazyServiceFactoryFactory
as a factory for theLazyServiceFactory
- Assign the
LazyServiceFactory
as a delegator factory for your service.
As an example:
use Laminas\ServiceManager\Proxy\LazyServiceFactoryFactory;
$config = [
'lazy_services' => [
'class_map' => [
'MyClass' => 'MyClass',
],
'proxies_namespace' => 'TestAssetProxy',
'proxies_target_dir' => 'data/proxies/',
'write_proxy_files' => true,
],
];
return [
'services' => [
'config' => $config,
],
'invokables' => [
'MyClass' => 'MyClass',
],
'factories' => [
'LazyServiceFactory' => LazyServiceFactoryFactory::class,
],
'delegators' => [
'MyClass' => [
'LazyServiceFactory',
],
],
];
This was done in part because lazy services were introduced later in the v2 cycle, and not fully integrated in order to retain the API.
In order to reduce the number of dependencies and steps necessary to configure lazy services, the following changes were made for v3:
- Lazy service configuration can now be passed directly to the service manager;
it is no longer dependent on a
config
service. - The ServiceManager itself is now responsible for creating the
LazyServiceFactory
delegator factory, based on the configuration present.
The above example becomes the following in v3:
use Laminas\ServiceManager\Factory\InvokableFactory;
use Laminas\ServiceManager\Proxy\LazyServiceFactory;
return [
'factories' => [
'MyClass' => InvokableFactory::class,
],
'delegators' => [
'MyClass' => [
LazyServiceFactory::class,
],
],
'lazy_services' => [
'class_map' => [
'MyClass' => 'MyClass',
],
'proxies_namespace' => 'TestAssetProxy',
'proxies_target_dir' => 'data/proxies/',
'write_proxy_files' => true,
],
];
Additionally, assuming you have configured lazy services initially with the
proxy namespace, target directory, etc., you can map lazy services using the new
method mapLazyService($name, $class)
:
$container->mapLazyService('MyClass', 'MyClass');
// or, more simply:
$container->mapLazyService('MyClass');
ServiceLocatorInterface Changes
The ServiceLocatorInterface
now extends the
container-interop
interface ContainerInterface
, which defines the same get()
and has()
methods as were previously defined.
Additionally, it adds a new method:
public function build($name, array $options = null)
This method is defined to always return a new instance of the requested
service, and to allow using the provided $options
when creating the instance.
ServiceManager API Changes
Laminas\ServiceManager\ServiceManager
remains the primary interface with which
developers will interact. It has the following changes in v3:
- It adds a new method,
configure()
, which allows configuring all instance generation capabilities (aliases, factories, abstract factories, etc.) at once. - Peering capabilities were removed.
- Exceptions are always thrown when service instance creation fails or produces an error; you can no longer disable this.
- Configuration no longer requires a
Laminas\ServiceManager\Config
instance.Config
can be used, but is not needed. - It adds a new method,
build()
, for creating discrete service instances.
Methods Removed
The following methods are removed in v3:
setShareByDefault()
/shareByDefault()
; this can be passed during instantiation or viaconfigure()
.setThrowExceptionInCreate()
/getThrowExceptionInCreate()
; exceptions are always thrown when errors are encountered during service instance creation.setRetrieveFromPeeringManagerFirst()
/retrieveFromPeeringManagerFirst()
; peering is no longer supported.
Constructor
The constructor now accepts an array of service configuration, not a
Laminas\ServiceManager\Config
instance.
Use build()
for discrete instances
The new method build()
acts as a factory method for configured services, and
will always return a new instance, never a shared one.
Additionally, it provides factory capabilities; you may pass an additional,
optional argument, $options
, which should be an array of additional options a
factory may use to create a new instance. This is primarily of interest when
creating plugin managers (more on plugin managers below), which may pass that
information on in order to create discrete plugin instances with specific state.
As examples:
use Laminas\Validator\Between;
$between = $container->build(Between::class, [
'min' => 5,
'max' => 10,
'inclusive' => true,
]);
$alsoBetween = $container->build(Between::class, [
'min' => 0,
'max' => 100,
'inclusive' => false,
]);
The above two validators would be different instances, with their own configuration.
Factories
Internally, the ServiceManager
now only uses the new factory interfaces
defined in the Laminas\ServiceManager\Factory
namespace. These replace the
interfaces defined in version 2, and define completely new signatures.
For migration purposes, all original interfaces were retained, and now inherit from the new interfaces. This provides a migration path; you can add the methods defined in the new interfaces to your existing factories targeting v2, and safely upgrade. (Typically, you will then have the version 2 methods proxy to those defined in version 3.)
Interfaces and relations to version 2
Version 2 Interface | Version 3 Interface |
---|---|
Laminas\ServiceManager\AbstractFactoryInterface |
Laminas\ServiceManager\Factory\AbstractFactoryInterface |
Laminas\ServiceManager\DelegatorFactoryInterface |
Laminas\ServiceManager\Factory\DelegatorFactoryInterface |
Laminas\ServiceManager\FactoryInterface |
Laminas\ServiceManager\Factory\FactoryInterface |
The version 2 interfaces now extend those in version 3, but are marked deprecated. You can continue to use them, but will be required to update your code to use the new interfaces in the future.
AbstractFactoryInterface
The previous signature of the AbstractFactoryInterface
was:
interface AbstractFactoryInterface
{
/**
* Determine if we can create a service with name
*
* @param ServiceLocatorInterface $serviceLocator
* @param $name
* @param $requestedName
* @return bool
*/
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName);
/**
* Create service with name
*
* @param ServiceLocatorInterface $serviceLocator
* @param $name
* @param $requestedName
* @return mixed
*/
public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName);
}
The new signature is:
interface AbstractFactoryInterface extends FactoryInterface
{
/**
* Does the factory have a way to create an instance for the service?
*
* @param ContainerInterface $container
* @param string $requestedName
* @return bool
*/
public function canCreate(ContainerInterface $container, $requestedName);
}
Note that it now extends the FactoryInterface
(detailed below), and thus the
factory logic has the same signature.
In v2, the abstract factory defined the method canCreateServiceWithName()
; in
v3, this is renamed to canCreate()
, and the method also now receives only two
arguments, the container and the requested service name.
To prepare your version 2 implementation to work upon upgrade to version 3:
- Add the methods
canCreate()
and__invoke()
as defined in version 3. - Modify your existing
canCreateServiceWithName()
method to proxy tocanCreate()
- Modify your existing
createServiceWithName()
method to proxy to__invoke()
As an example, given the following implementation from version 2:
use Laminas\ServiceManager\AbstractFactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class LenientAbstractFactory implements AbstractFactoryInterface
{
public function canCreateServiceWithName(ServiceLocatorInterface $services, $name, $requestedName)
{
return class_exists($requestedName);
}
public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName)
{
return new $requestedName();
}
}
To update this for version 3 compatibility, you will add the methods
canCreate()
and __invoke()
, move the code from the existing methods into
them, and update the existing methods to proxy to the new methods:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\AbstractFactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class LenientAbstractFactory implements AbstractFactoryInterface
{
public function canCreate(ContainerInterface $container, $requestedName)
{
return class_exists($requestedName);
}
public function canCreateServiceWithName(ServiceLocatorInterface $services, $name, $requestedName)
{
return $this->canCreate($services, $requestedName);
}
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new $requestedName();
}
public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName)
{
return $this($services, $requestedName);
}
}
After you have upgraded to version 3, you can take the following steps to remove the migration artifacts:
- Update your class to implement the new interface.
- Remove the
canCreateServiceWithName()
andcreateServiceWithName()
methods from your implementation.
From our example above, we would update the class to read as follows:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\AbstractFactoryInterface; // <-- note the change!
class LenientAbstractFactory implements AbstractFactoryInterface
{
public function canCreate(ContainerInterface $container, $requestedName)
{
return class_exists($requestedName);
}
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new $requestedName();
}
}
DelegatorFactoryInterface
The previous signature of the DelegatorFactoryInterface
was:
interface DelegatorFactoryInterface
{
/**
* A factory that creates delegates of a given service
*
* @param ServiceLocatorInterface $serviceLocator the service locator which requested the service
* @param string $name the normalized service name
* @param string $requestedName the requested service name
* @param callable $callback the callback that is responsible for creating the service
*
* @return mixed
*/
public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback);
}
The new signature is:
interface DelegatorFactoryInterface
{
/**
* A factory that creates delegates of a given service
*
* @param ContainerInterface $container
* @param string $name
* @param callable $callback
* @param null|array $options
* @return object
*/
public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null);
}
Note that the $name
and $requestedName
arguments are now merged into a
single $name
argument, and that the factory now allows passing additional
options to use (typically as passed via build()
).
To prepare your existing delegator factories for version 3, take the following steps:
- Implement the
__invoke()
method in your existing factory, copying the code from your existingcreateDelegatorWithName()
method into it. - Modify the
createDelegatorWithName()
method to proxy to the new method.
Consider the following delegator factory that works for version 2:
use Laminas\ServiceManager\DelegatorFactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class ObserverAttachmentDelegator implements DelegatorFactoryInterface
{
public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
{
$subject = $callback();
$subject->attach($serviceLocator->get(Observer::class);
return $subject;
}
}
To prepare this for version 3, we'd implement the __invoke()
signature from
version 3, and modify createDelegatorWithName()
to proxy to it:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\DelegatorFactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class ObserverAttachmentDelegator implements DelegatorFactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null)
{
$subject = $callback();
$subject->attach($container->get(Observer::class);
return $subject;
}
public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
{
return $this($serviceLocator, $requestedName, $callback);
}
}
After you have upgraded to version 3, you can take the following steps to remove the migration artifacts:
- Update your class to implement the new interface.
- Remove the
createDelegatorWithName()
method from your implementation.
From our example above, we would update the class to read as follows:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; // <-- note the change!
class ObserverAttachmentDelegator implements DelegatorFactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null)
{
$subject = $callback();
$subject->attach($container->get(Observer::class);
return $subject;
}
}
FactoryInterface
The previous signature of the FactoryInterface
was:
interface FactoryInterface
{
/**
* Create service
*
* @param ServiceLocatorInterface $serviceLocator
* @return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator);
}
The new signature is:
interface FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null);
}
Note that the factory now accepts an additional required argument,
$requestedName
; v2 already passed this argument, but it was not specified in
the interface itself. Additionally, a third optional argument, $options
,
allows you to provide $options
to the ServiceManager::build()
method;
factories can then take these into account when creating an instance.
Because factories now can expect to receive the service name, they may be re-used for multiple services, largely replacing abstract factories in version 3.
To prepare your existing factories for version 3, take the following steps:
- Implement the
__invoke()
method in your existing factory, copying the code from your existingcreateService()
method into it. - Modify the
createService()
method to proxy to the new method.
Consider the following factory that works for version 2:
use Laminas\ServiceManager\FactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class FooFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $services)
{
return new Foo($services->get(Bar::class));
}
}
To prepare this for version 3, we'd implement the __invoke()
signature from
version 3, and modify createService()
to proxy to it:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\FactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class FooFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new Foo($container->get(Bar::class));
}
public function createService(ServiceLocatorInterface $services)
{
return $this($services, Foo::class);
}
}
Note that the call to $this()
adds a new argument; since your factory isn't
using the $requestedName
, this can be anything, but must be passed to prevent
a fatal exception due to a missing argument. In this case, we chose to pass the
name of the class the factory is creating.
After you have upgraded to version 3, you can take the following steps to remove the migration artifacts:
- Update your class to implement the new interface.
- Remove the
createService()
method from your implementation.
From our example above, we would update the class to read as follows:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface; // <-- note the change!
class FooFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new Foo($container->get(Bar::class));
}
}
Many factories already work with v3
Within the skeleton application, tutorial, and even in commonly shipped modules such as those in Laminas API Tools, we have typically suggested building your factories as invokable classes. If you were doing this already, your factories will already work with version 3!
Version 2 factories can accept the requested name already
Since 2.2, factories have been passed two additional parameters, the "canonical" name (a mis-nomer, as it is actually the normalized name), and the "requested" name (the actual string passed to
get()
). As such, you can already write factories that accept the requested name, and have them change behavior based on that information!
New InvokableFactory Class
Laminas\ServiceManager\Factory\InvokableFactory
is a new FactoryInterface
implementation that provides the capabilities of the "invokable classes" present
in version 2. It essentially instantiates and returns the requested class name;
if $options
is non-empty, it passes them directly to the constructor.
This class was added to the version 2 tree to allow developers to start using it when preparing their code for version 3. This is particularly of interest when creating plugin managers, as you'll typically want the internal configuration to only include factories and aliases.
Initializers
Initializers are still present in the Service Manager component, but exist primarily for backwards compatibility; we recommend using delegator factories for setter and interface injection instead of initializers, as those will be run per-service, versus for all services.
For migration purposes, the original interface was retained, and now inherits from the new interface. This provides a migration path; you can add the method defined in the new interface to your existing initializers targeting v2, and safely upgrade. (Typically, you will then have the version 2 method proxy to the one defined in version 3.)
The following changes were made to initializers:
Laminas\ServiceManager\InitializerInterface
was renamed toLaminas\ServiceManager\Initializer\InitializerInterface
.- The interface itself has a new signature.
The previous signature was:
public function initialize($instance, ServiceLocatorInterface $serviceLocator)
It is now:
public function __invoke(ContainerInterface $container, $instance)
The changes were made to ensure the signature is internally consistent with the various factories.
To prepare your existing initializers for version 3, take the following steps:
- Implement the
__invoke()
method in your existing factory, copying the code from your existinginitialize()
method into it. - Modify the
initialize()
method to proxy to the new method.
As an example, consider this initializer for version 2:
use Laminas\ServiceManager\InitializerInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class FooInitializer implements InitializerInterface
{
public function initializer($instance, ServiceLocatorInterface $services)
{
if (! $instance implements FooAwareInterface) {
return $instance;
}
$instance->setFoo($services->get(FooInterface::class);
return $instance;
}
}
To prepare this for version 3, we'd implement the __invoke()
signature from
version 3, and modify initialize()
to proxy to it:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\InitializerInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class FooInitializer implements InitializerInterface
{
public function __invoke(ContainerInterface $container, $instance)
{
if (! $instance implements FooAwareInterface) {
return $instance;
}
$container->setFoo($services->get(FooInterface::class);
return $instance;
}
public function initializer($instance, ServiceLocatorInterface $services)
{
return $this($services, $instance);
}
}
After you have upgraded to version 3, you can take the following steps to remove the migration artifacts:
- Update your class to implement the new interface.
- Remove the
initialize()
method from your implementation.
From our example above, we would update the class to read as follows:
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Initializer\InitializerInterface; // <-- note the change!
class FooInitializer implements InitializerInterface
{
public function __invoke(ContainerInterface $container, $instance)
{
if (! $instance implements FooAwareInterface) {
return $instance;
}
$container->setFoo($services->get(FooInterface::class);
return $instance;
}
}
Update your callables
Version 2 allows you to provide initializers as PHP callables. However, this means that the signature of those callables is incorrect for version 3!
To make your code forwards compatible, you have two paths:
The first is to simply provide an
InitializerInterface
implementation instead. This guarantees that the correct method is called based on the version of theServiceManager
in use.The second approach is to omit typehints on the arguments, and do typechecks internally. As an example, let's say you have the following:
$container->addInitializer(function ($instance, ContainerInterface $container) { if (! $instance implements FooAwareInterface) { return $instance; } $container->setFoo($services->get(FooInterface::class); return $instance; });
To make this future-proof, remove the typehints, and check the types within the callable:
$container->addInitializer(function ($first, $second) { if ($first instanceof ContainerInterface) { $container = $first; $instance = $second; } else { $container = $second; $instance = $first; } if (! $instance implements FooAwareInterface) { return; } $container->setFoo($services->get(FooInterface::class); });
This approach can also be done if you omitted typehints in the first place. Regardless, the important part to remember is that order of arguments is inverted between the two versions.
Plugin Managers
In version 2, plugin managers were ServiceManager
instances that implemented
both the MutableCreationOptionsInterface
and ServiceLocatorAwareInterface
,
and extended AbstractPluginManager
. Plugin managers passed themselves to
factories, abstract factories, etc., requiring pulling the parent service
manager, if composed, in order to resolve application-level dependencies.
In version 3, we define the following:
Laminas\ServiceManager\PluginManagerInterface
, which provides the public API differences from theServiceLocatorInterface
.Laminas\ServiceManager\AbstractPluginManager
, which gives the basic capabilities for plugin managers. The class now has a (semi) required dependency on the application-level service manager instance, which is passed to all factories, abstract factories, etc. (More on this below.)
PluginManagerInterface
Laminas\ServiceManager\PluginInterface
is a new interface for version 3,
extending ServiceLocatorInterface
and adding one method:
/**
* Validate an instance
*
* @param object $instance
* @return void
* @throws InvalidServiceException If created instance does not respect the
* constraint on type imposed by the plugin manager
*/
public function validate($instance);
All plugin managers must implement this interface. For backwards-compatibility
purposes, AbstractPluginManager
will check for the validatePlugin()
method
(defined as abstract in v2), and, on discovery, trigger an E_USER_DEPRECATED
notice, followed by invocation of that method.
AbstractPluginManager
As it did in version 2, AbstractPluginManager
extends ServiceManager
. That
means that all changes made to the ServiceManager
for v3 also apply to the
AbstractPluginManager
.
In addition, review the following changes.
Constructor
- The constructor now accepts the following arguments, in the following order:
- The parent container instance; this is usually the application-level
ServiceManager
instance. - Optionally, an array of configuration for the plugin manager instance; this
should have the same format as for a
ServiceManager
instance.
- The parent container instance; this is usually the application-level
validatePlugin()
was renamed tovalidate()
(now defined inPluginManagerInterface
). TheAbstractPluginManager
provides a basic implementation (detailed below).- The signature of
get()
changes (more below).
For backwards compatibility purposes, the constructor also allows the following for the initial argument:
- A
null
value. In this case, the plugin manager will use itself as the creation context, but also raise a deprecation notice indicating a container should be passed instead. You can pass the parent container to thesetServiceLocator()
method to reset the creation context, but, again, this raises a deprecation notice. - A
ConfigInterface
instance. In this case, the plugin manager will call the config instance'stoArray()
method to cast it to an array, and use the return value as the configuration to pass to the parent constructor. As with thenull
value, the plugin manager will be set as its own creation context.
Validation
The validate()
method is defined as follows:
public function validate($instance)
{
if (method_exists($this, 'validatePlugin')) {
trigger_error(sprintf(
'%s::validatePlugin() has been deprecated as of 3.0; please define validate() instead',
get_class($this)
), E_USER_DEPRECATED);
$this->validatePlugin($instance);
return;
}
if (empty($this->instanceOf) || $instance instanceof $this->instanceOf) {
return;
}
throw new InvalidServiceException(sprintf(
'Plugin manager "%s" expected an instance of type "%s", but "%s" was received',
__CLASS__,
$this->instanceOf,
is_object($instance) ? get_class($instance) : gettype($instance)
));
}
The two takeaways from this are:
- If you are upgrading from v2, your code should continue to work, but will
emit a deprecation notice. The way to remove the deprecation notice is to
rename the
validatePlugin()
method tovalidate()
, or to remove it and define the$instanceOf
property (if all you're doing is checking the plugin against a single typehint). - Most plugin manager instances can simply define the
$instanceOf
property to indicate what plugin interface is considered valid for the plugin manager, and make no further changes to the abstract plugin manager:
protected $instanceOf = ValidatorInterface::class;
get()
The get()
signature changes from:
public function get($name, $options = [], $usePeeringServiceManagers = true)
to:
public function get($name, array $options = null)
Essentially: $options
now must be an array if passed, and peering is no
longer supported.
Deprecated methods
Finally, the following methods from v2's ServiceLocatorAwareInterface
are
retained (without implementing the interface), but marked as deprecated:
setServiceLocator()
. This method exists as many tests and plugin manager factories were using it to inject the parent locator (now called the creation context). This method may still be used, and will now set the creation context for the plugin manager, but also emit a deprecation warning.getServiceLocator()
is implemented inServiceManager
(from whichAbstractPluginManager
inherits), but marked as deprecated.
Regarding this latter point, getServiceLocator()
exists to provide backwards
compatibility for existing plugin factories. These factories typically pull
dependencies from the parent/application container in order to initialize the
plugin. In v2, this would look like:
function ($plugins)
{
$services = $plugins->getServiceLocator();
// pull dependencies from $services:
$foo = $services->get('Foo');
$bar = $services->get('Bar');
return new Plugin($foo, $bar);
}
In v3, the initial argument to the factory is not the plugin manager instance,
but the creation context, which is analogous to the parent locator in v2. In
order to preserve existing behavior, we added the getServiceLocator()
method
to the ServiceManager
. As such, the above will continue to work in v3.
However, this method is marked as deprecated, and will emit an
E_USER_DEPRECATED
notice. To remove the notice, you will need to upgrade your
code. The above example thus becomes:
function ($services)
{
// pull dependencies from $services:
$foo = $services->get('Foo');
$bar = $services->get('Bar');
return new Plugin($foo, $bar);
}
If you were using the passed plugin manager and pulling other plugins, you will need to update your code to retrieve the plugin manager from the passed container. As an example, given this:
function ($plugins)
{
$anotherPlugin = $plugins->get('AnotherPlugin');
return new Plugin($anotherPlugin);
}
You will need to rewrite it to:
function ($services)
{
$plugins = $services->get('PluginManager');
$anotherPlugin = $plugins->get('AnotherPlugin');
return new Plugin($anotherPlugin);
}
Plugin Service Creation
The get()
method has new behavior:
- When non-empty
$options
are passed, it always delegates tobuild()
, and thus will always return a new instance. If you are using$options
, the assumption is that you are using the plugin manager as a factory, and thus the instance should not be cached. - Without
$options
,get()
will cache by default (the default behavior ofServiceManager
). To never cache instances, either set the$sharedByDefault
class property tofalse
, or pass a booleanfalse
value via theshared_by_default
configuration key.
Migration example
Let's consider the following plugin manager geared towards version 2:
use RuntimeException;
use Laminas\ServiceManager\AbstractPluginManager;
class ObserverPluginManager extends AbstractPluginManager
{
protected $invokables = [
'mail' => MailObserver::class,
'log' => LogObserver::class,
];
protected $shareByDefault = false;
public function validatePlugin($instance)
{
if (! $instance instanceof ObserverInterface) {
throw new RuntimeException(sprintf(
'Invalid plugin "%s" created; not an instance of %s',
get_class($instance),
ObserverInterface::class
));
}
}
}
To prepare this for version 3, we need to do the following:
- We need to change the
$invokables
configuration to a combination offactories
andaliases
. - We need to implement a
validate()
method. - We need to update the
validatePlugin()
method to proxy tovalidate()
. - We need to add a
$sharedByDefault
property (if$shareByDefault
is present).
Doing so, we get the following result:
namespace MyNamespace;
use RuntimeException;
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use Laminas\ServiceManager\Factory\InvokableFactory;
class ObserverPluginManager extends AbstractPluginManager
{
protected $instanceOf = ObserverInterface::class;
protected $aliases = [
'mail' => MailObserver::class,
'Mail' => MailObserver::class,
'log' => LogObserver::class,
'Log' => LogObserver::class,
];
protected $factories = [
MailObserver::class => InvokableFactory::class,
LogObserver::class => InvokableFactory::class,
// Legacy (v2) due to alias resolution
'mynamespacemailobserver' => InvokableFactory::class,
'mynamespacelogobserver' => InvokableFactory::class,
];
protected $shareByDefault = false;
protected $sharedByDefault = false;
public function validate($instance)
{
if (! $instance instanceof $this->instanceOf) {
throw new InvalidServiceException(sprintf(
'Invalid plugin "%s" created; not an instance of %s',
get_class($instance),
$this->instanceOf
));
}
}
public function validatePlugin($instance)
{
try {
$this->validate($instance);
} catch (InvalidServiceException $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
}
}
Things to note about the above:
- It introduces a new property,
$instanceOf
. We'll use this later, when we're ready to clean up post-migration. - It introduces four aliases. This is to allow fetching the various plugins as
any of
mail
,Mail
,log
, orLog
— all of which are valid in version 2, but, because version 3 does not normalize names, need to be explicitly aliased. - The aliases point to the fully qualified class name (FQCN) for the service
being generated, and these are mapped to
InvokableFactory
instances. This means you can also fetch your plugins by their FQCN. - There are also factory entries for the canonicalized FQCN of each factory, which will be used in v2. (Canonicalization in v2 strips non-alphanumeric characters, and casts to lowercase.)
validatePlugin()
continues to throw the old exception
The above will now work in both version 2 and version 3.
Migration testing
To test your changes, create a new MigrationTest
case that uses
Laminas\ServiceManager\Test\CommonPluginManagerTrait
. Override
getPluginManager()
to return an instance of your plugin manager, and override
getV2InvalidPluginException()
to return the classname of the exception your
validatePlugin()
method throws:
use MyNamespace\ObserverInterface;
use MyNamespace\ObserverPluginManager;
use MyNamespace\Exception\RuntimeException;
use PHPUnit_Framework_TestCase as TestCase;
use Laminas\ServiceManager\ServiceManager;
use Laminas\ServiceManager\Test\CommonPluginManagerTrait;
class MigrationTest extends TestCase
{
use CommonPluginManagerTrait;
protected function getPluginManager()
{
return new ObserverPluginManager(new ServiceManager());
}
protected function getV2InvalidPluginException()
{
return RuntimeException::class;
}
protected function getInstanceOf()
{
return ObserverInterface::class;
}
}
This will check that:
- You have set the
$instanceOf
property. $shareByDefault
and$sharedByDefault
match, if present.- That requesting an invalid plugin throws the right exception.
- That all your aliases resolve.
Post migration
After you migrate to version 3, you can clean up your plugin manager:
- Remove the
validatePlugin()
method. - If your
validate()
routine is only checking that the instance is of a single type, and has no other logic, you can remove that implementation as well, as theAbstractPluginManager
already takes care of that when$instanceOf
is defined! - Remove the canonicalized FQCN entry for each factory
Performing these steps on the above, we get:
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\Factory\InvokableFactory;
class ObserverPluginManager extends AbstractPluginManager
{
protected $instanceOf = ObserverInterface::class;
protected $aliases = [
'mail' => MailObserver::class,
'Mail' => MailObserver::class,
'log' => LogObserver::class,
'Log' => LogObserver::class,
];
protected $factories = [
MailObserver::class => InvokableFactory::class,
LogObserver::class => InvokableFactory::class,
];
}
DI Namespace
The Laminas\ServiceManager\Di
namespace has been removed.
The Laminas\Di
component is not actively maintained, and has been largely
deprecated during the Laminas lifecycle in favor of the Service Manager. Its usage
as an abstract factory is problematic and error prone when used in conjunction
with the Service Manager; as such, we've removed it for the initial v3 release.
We may re-introduce it via a separate component in the future.
Miscellaneous Interfaces, Traits, and Classes
The following interfaces, traits, and classes were removed:
Laminas\ServiceManager\MutableCreationOptionsInterface
; this was previously used by theAbstractPluginManager
, and is no longer required as we ship a separatePluginManagerInterface
, and because the functionality is encompassed by thebuild()
method.Laminas\ServiceManager\MutableCreationOptionsTrait
Laminas\ServiceManager\Proxy\LazyServiceFactoryFactory
; its capabilities were moved directly into theServiceManager
.Laminas\ServiceManager\ServiceLocatorAwareInterface
Laminas\ServiceManager\ServiceLocatorAwareTrait
Laminas\ServiceManager\ServiceManagerAwareInterface
The ServiceLocatorAware
and ServiceManagerAware
interfaces and traits were
too often abused under v2, and represent the antithesis of the purpose of the
Service Manager component; dependencies should be directly injected, and the
container should never be composed by objects.
The following classes and interfaces have changes:
Laminas\ServiceManager\Proxy\LazyServiceFactory
is now markedfinal
, and implementsLaminas\ServiceManager\Proxy\DelegatorFactoryInterface
. Its dependencies and capabilities remain the same.Laminas\ServiceManager\ConfigInterface
now is expected to return the modifiedServiceManager
instance.Laminas\ServiceManager\Config
was updated to follow the changes toConfigInterface
andServiceManager
, and now returns the updatedServiceManager
instance fromconfigureServiceManager()
.