<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\ServiceManager
;
use ReflectionClass
;
class ServiceManager implements ServiceLocatorInterface
{
/**@#+
* Constants
*/
const SCOPE_PARENT
= 'parent';
const SCOPE_CHILD
= 'child';
/**@#-*/
/**
* Lookup for canonicalized names.
*
* @var array
*/
protected $canonicalNames = array();
/**
* @var bool
*/
protected $allowOverride = false;
/**
* @var array
*/
protected $invokableClasses = array();
/**
* @var string|callable|\Closure|FactoryInterface[]
*/
protected $factories = array();
/**
* @var AbstractFactoryInterface[]
*/
protected $abstractFactories = array();
/**
* @var array
*/
protected $pendingAbstractFactoryRequests = array();
/**
* @var array
*/
protected $shared = array();
/**
* Registered services and cached values
*
* @var array
*/
protected $instances = array();
/**
* @var array
*/
protected $aliases = array();
/**
* @var array
*/
protected $initializers = array();
/**
* @var ServiceManager[]
*/
protected $peeringServiceManagers = array();
/**
* Whether or not to share by default
*
* @var bool
*/
protected $shareByDefault = true;
/**
* @var bool
*/
protected $retrieveFromPeeringManagerFirst = false;
/**
* @var bool Track whether not to throw exceptions during create()
*/
protected $throwExceptionInCreate = true;
/**
* @var array map of characters to be replaced through strtr
*/
protected $canonicalNamesReplacements = array('-' => '', '_' => '', ' ' => '', '\\' => '', '/' => '');
/**
* Constructor
*
* @param ConfigInterface $config
*/
public function __construct
(ConfigInterface
$config = null)
{
if ($config) {
$config->configureServiceManager($this);
}
}
/**
* Set allow override
*
* @param $allowOverride
* @return ServiceManager
*/
public function setAllowOverride
($allowOverride)
{
$this->allowOverride = (bool
) $allowOverride;
return $this;
}
/**
* Get allow override
*
* @return bool
*/
public function getAllowOverride
()
{
return $this->allowOverride;
}
/**
* Set flag indicating whether services are shared by default
*
* @param bool $shareByDefault
* @return ServiceManager
* @throws Exception\RuntimeException if allowOverride is false
*/
public function setShareByDefault
($shareByDefault)
{
if ($this->allowOverride === false) {
throw new Exception\RuntimeException
(sprintf(
'%s: cannot alter default shared service setting; container is marked immutable (allow_override is false)',
__METHOD__
));
}
$this->shareByDefault = (bool
) $shareByDefault;
return $this;
}
/**
* Are services shared by default?
*
* @return bool
*/
public function shareByDefault
()
{
return $this->shareByDefault;
}
/**
* Set throw exceptions in create
*
* @param bool $throwExceptionInCreate
* @return ServiceManager
*/
public function setThrowExceptionInCreate
($throwExceptionInCreate)
{
$this->throwExceptionInCreate = $throwExceptionInCreate;
return $this;
}
/**
* Get throw exceptions in create
*
* @return bool
*/
public function getThrowExceptionInCreate
()
{
return $this->throwExceptionInCreate;
}
/**
* Set flag indicating whether to pull from peering manager before attempting creation
*
* @param bool $retrieveFromPeeringManagerFirst
* @return ServiceManager
*/
public function setRetrieveFromPeeringManagerFirst
($retrieveFromPeeringManagerFirst = true)
{
$this->retrieveFromPeeringManagerFirst = (bool
) $retrieveFromPeeringManagerFirst;
return $this;
}
/**
* Should we retrieve from the peering manager prior to attempting to create a service?
*
* @return bool
*/
public function retrieveFromPeeringManagerFirst
()
{
return $this->retrieveFromPeeringManagerFirst;
}
/**
* Set invokable class
*
* @param string $name
* @param string $invokableClass
* @param bool $shared
* @return ServiceManager
* @throws Exception\InvalidServiceNameException
*/
public function setInvokableClass
($name, $invokableClass, $shared = null)
{
$cName = $this->canonicalizeName($name);
if ($this->has(array($cName, $name), false)) {
if ($this->allowOverride === false) {
throw new Exception\InvalidServiceNameException
(sprintf(
'A service by the name or alias "%s" already exists and cannot be overridden; please use an alternate name',
$cName
));
}
$this->unregisterService($cName);
}
if ($shared === null) {
$shared = $this->shareByDefault();
}
$this->invokableClasses[$cName] = $invokableClass;
$this->shared[$cName] = (bool
) $shared;
return $this;
}
/**
* Set factory
*
* @param string $name
* @param string|FactoryInterface|callable $factory
* @param bool $shared
* @return ServiceManager
* @throws Exception\InvalidArgumentException
* @throws Exception\InvalidServiceNameException
*/
public function setFactory
($name, $factory, $shared = null)
{
$cName = $this->canonicalizeName($name);
if (!is_string($factory) && !$factory instanceof FactoryInterface
&& !is_callable($factory)) {
throw new Exception\InvalidArgumentException
(
'Provided abstract factory must be the class name of an abstract factory or an instance of an AbstractFactoryInterface.'
);
}
if ($this->has(array($cName, $name), false)) {
if ($this->allowOverride === false) {
throw new Exception\InvalidServiceNameException
(sprintf(
'A service by the name or alias "%s" already exists and cannot be overridden, please use an alternate name',
$cName
));
}
$this->unregisterService($cName);
}
if ($shared === null) {
$shared = $this->shareByDefault();
}
$this->factories[$cName] = $factory;
$this->shared[$cName] = (bool
) $shared;
return $this;
}
/**
* Add abstract factory
*
* @param AbstractFactoryInterface|string $factory
* @param bool $topOfStack
* @return ServiceManager
* @throws Exception\InvalidArgumentException if the abstract factory is invalid
*/
public function addAbstractFactory
($factory, $topOfStack = true)
{
if (!is_string($factory) && !$factory instanceof AbstractFactoryInterface
) {
throw new Exception\InvalidArgumentException
(
'Provided abstract factory must be the class name of an abstract factory or an instance of an AbstractFactoryInterface.'
);
}
if (is_string($factory)) {
if (!class_exists($factory, true)) {
throw new Exception\InvalidArgumentException
(
'Provided abstract factory must be the class name of an abstract factory or an instance of an AbstractFactoryInterface.'
);
}
$refl = new ReflectionClass
($factory);
if (!$refl->implementsInterface(__NAMESPACE__
. '\\AbstractFactoryInterface')) {
throw new Exception\InvalidArgumentException
(
'Provided abstract factory must be the class name of an abstract factory or an instance of an AbstractFactoryInterface.'
);
}
}
if ($topOfStack) {
array_unshift($this->abstractFactories, $factory);
} else {
array_push($this->abstractFactories, $factory);
}
return $this;
}
/**
* Add initializer
*
* @param callable|InitializerInterface $initializer
* @param bool $topOfStack
* @return ServiceManager
* @throws Exception\InvalidArgumentException
*/
public function addInitializer
($initializer, $topOfStack = true)
{
if (!is_callable($initializer) && !$initializer instanceof InitializerInterface
) {
if (!is_string($initializer)
|| !$this->isSubclassOf($initializer, __NAMESPACE__
. '\InitializerInterface')
) {
throw new Exception\InvalidArgumentException
('$initializer should be callable.');
}
$initializer = new $initializer;
}
if ($topOfStack) {
array_unshift($this->initializers, $initializer);
} else {
array_push($this->initializers, $initializer);
}
return $this;
}
/**
* Register a service with the locator
*
* @param string $name
* @param mixed $service
* @return ServiceManager
* @throws Exception\InvalidServiceNameException
*/
public function setService
($name, $service)
{
$cName = $this->canonicalizeName($name);
if ($this->has($cName, false)) {
if ($this->allowOverride === false) {
throw new Exception\InvalidServiceNameException
(sprintf(
'%s: A service by the name "%s" or alias already exists and cannot be overridden, please use an alternate name.',
__METHOD__,
$name
));
}
$this->unregisterService($cName);
}
$this->instances[$cName] = $service;
return $this;
}
/**
* @param string $name
* @param bool $isShared
* @return ServiceManager
* @throws Exception\ServiceNotFoundException
*/
public function setShared
($name, $isShared)
{
$cName = $this->canonicalizeName($name);
if (
!isset($this->invokableClasses[$cName])
&& !isset($this->factories[$cName])
&& !$this->canCreateFromAbstractFactory($cName, $name)
) {
throw new Exception\ServiceNotFoundException
(sprintf(
'%s: A service by the name "%s" was not found and could not be marked as shared',
__METHOD__,
$name
));
}
$this->shared[$cName] = (bool
) $isShared;
return $this;
}
/**
* Retrieve a registered instance
*
* @param string $name
* @param bool $usePeeringServiceManagers
* @throws Exception\ServiceNotFoundException
* @return object|array
*/
public function get
($name, $usePeeringServiceManagers = true)
{
$cName = $this->canonicalizeName($name);
$isAlias = false;
if ($this->hasAlias($cName)) {
$isAlias = true;
do {
$cName = $this->aliases[$cName];
} while ($this->hasAlias($cName));
}
$instance = null;
$retrieveFromPeeringManagerFirst = $this->retrieveFromPeeringManagerFirst();
if ($usePeeringServiceManagers && $retrieveFromPeeringManagerFirst) {
$instance = $this->retrieveFromPeeringManager($name);
if(null !== $instance) {
return $instance;
}
}
if (isset($this->instances[$cName])) {
return $this->instances[$cName];
}
if (!$instance) {
if ($this->canCreate(array($cName, $name))) {
$instance = $this->create(array($cName, $name));
} elseif ($usePeeringServiceManagers && !$retrieveFromPeeringManagerFirst) {
$instance = $this->retrieveFromPeeringManager($name);
}
}
// Still no instance? raise an exception
if ($instance === null && !is_array($instance)) {
if ($isAlias) {
throw new Exception\ServiceNotFoundException
(sprintf(
'An alias "%s" was requested but no service could be found.',
$name
));
}
throw new Exception\ServiceNotFoundException
(sprintf(
'%s was unable to fetch or create an instance for %s',
__METHOD__,
$name
));
}
if (
($this->shareByDefault() && !isset($this->shared[$cName]))
|| (isset($this->shared[$cName]) && $this->shared[$cName] === true)
) {
$this->instances[$cName] = $instance;
}
return $instance;
}
/**
* Create an instance
*
* @param string|array $name
* @return bool|object
* @throws Exception\ServiceNotFoundException
* @throws Exception\ServiceNotCreatedException
*/
public function create
($name)
{
$instance = false;
if (is_array($name)) {
list($cName, $rName) = $name;
} else {
$rName = $name;
$cName = $this->canonicalizeName($rName);
}
if (isset($this->factories[$cName])) {
$instance = $this->createFromFactory($cName, $rName);
}
if ($instance === false && isset($this->invokableClasses[$cName])) {
$instance = $this->createFromInvokable($cName, $rName);
}
if ($instance === false && $this->canCreateFromAbstractFactory($cName, $rName)) {
$instance = $this->createFromAbstractFactory($cName, $rName);
}
if ($this->throwExceptionInCreate == true && $instance === false) {
throw new Exception\ServiceNotFoundException
(sprintf(
'No valid instance was found for %s%s',
$cName,
($rName ?
'(alias: ' . $rName . ')' : '')
));
}
foreach ($this->initializers as $initializer) {
if ($initializer instanceof InitializerInterface
) {
$initializer->initialize($instance, $this);
} elseif (is_object($initializer) && is_callable($initializer)) {
$initializer($instance, $this);
} else {
call_user_func($initializer, $instance, $this);
}
}
return $instance;
}
/**
* Determine if we can create an instance.
*
* @param string|array $name
* @param bool $checkAbstractFactories
* @return bool
*/
public function canCreate
($name, $checkAbstractFactories = true)
{
if (is_array($name)) {
list($cName, $rName) = $name;
} else {
$rName = $name;
$cName = $this->canonicalizeName($rName);
}
if (
isset($this->invokableClasses[$cName])
|| isset($this->factories[$cName])
|| isset($this->aliases[$cName])
|| isset($this->instances[$cName])
) {
return true;
}
if ($checkAbstractFactories && $this->canCreateFromAbstractFactory($cName, $rName)) {
return true;
}
return false;
}
/**
* @param string|array $name
* @param bool $checkAbstractFactories
* @param bool $usePeeringServiceManagers
* @return bool
*/
public function has
($name, $checkAbstractFactories = true, $usePeeringServiceManagers = true)
{
if (is_array($name)) {
list($cName, $rName) = $name;
} else {
$rName = $name;
$cName = $this->canonicalizeName($rName);
}
if ($this->canCreate(array($cName, $rName), $checkAbstractFactories)) {
return true;
}
if ($usePeeringServiceManagers) {
foreach ($this->peeringServiceManagers as $peeringServiceManager) {
if ($peeringServiceManager->has($rName)) {
return true;
}
}
}
return false;
}
/**
* Determine if we can create an instance from an abstract factory.
*
* @param string $cName
* @param string $rName
* @return bool
*/
public function canCreateFromAbstractFactory
($cName, $rName)
{
// check abstract factories
foreach ($this->abstractFactories as $index => $abstractFactory) {
// Support string abstract factory class names
if (is_string($abstractFactory) && class_exists($abstractFactory, true)) {
$this->abstractFactories[$index] = $abstractFactory = new $abstractFactory();
}
if (
isset($this->pendingAbstractFactoryRequests[get_class($abstractFactory)])
&& $this->pendingAbstractFactoryRequests[get_class($abstractFactory)] == $rName
) {
return false;
}
if ($abstractFactory->canCreateServiceWithName($this, $cName, $rName)) {
return true;
}
}
return false;
}
/**
* @param string $alias
* @param string $nameOrAlias
* @return ServiceManager
* @throws Exception\ServiceNotFoundException
* @throws Exception\InvalidServiceNameException
*/
public function setAlias
($alias, $nameOrAlias)
{
if (!is_string($alias) || !is_string($nameOrAlias)) {
throw new Exception\InvalidServiceNameException
('Service or alias names must be strings.');
}
$cAlias = $this->canonicalizeName($alias);
$nameOrAlias = $this->canonicalizeName($nameOrAlias);
if ($alias == '' || $nameOrAlias == '') {
throw new Exception\InvalidServiceNameException
('Invalid service name alias');
}
if ($this->allowOverride === false && $this->has(array($cAlias, $alias), false)) {
throw new Exception\InvalidServiceNameException
(sprintf(
'An alias by the name "%s" or "%s" already exists',
$cAlias,
$alias
));
}
$this->aliases[$cAlias] = $nameOrAlias;
return $this;
}
/**
* Determine if we have an alias
*
* @param string $alias
* @return bool
*/
public function hasAlias
($alias)
{
$alias = $this->canonicalizeName($alias);
return (isset($this->aliases[$alias]));
}
/**
* Create scoped service manager
*
* @param string $peering
* @return ServiceManager
*/
public function createScopedServiceManager
($peering = self::SCOPE_PARENT)
{
$scopedServiceManager = new ServiceManager
();
if ($peering == self::SCOPE_PARENT) {
$scopedServiceManager->peeringServiceManagers[] = $this;
}
if ($peering == self::SCOPE_CHILD) {
$this->peeringServiceManagers[] = $scopedServiceManager;
}
return $scopedServiceManager;
}
/**
* Add a peering relationship
*
* @param ServiceManager $manager
* @param string $peering
* @return ServiceManager
*/
public function addPeeringServiceManager
(ServiceManager
$manager, $peering = self::SCOPE_PARENT)
{
if ($peering == self::SCOPE_PARENT) {
$this->peeringServiceManagers[] = $manager;
}
if ($peering == self::SCOPE_CHILD) {
$manager->peeringServiceManagers[] = $this;
}
return $this;
}
/**
* Canonicalize name
*
* @param string $name
* @return string
*/
protected function canonicalizeName
($name)
{
if (isset($this->canonicalNames[$name])) {
return $this->canonicalNames[$name];
}
// this is just for performance instead of using str_replace
return $this->canonicalNames[$name] = strtolower(strtr($name, $this->canonicalNamesReplacements));
}
/**
* Create service via callback
*
* @param callable $callable
* @param string $cName
* @param string $rName
* @throws Exception\ServiceNotCreatedException
* @throws Exception\ServiceNotFoundException
* @throws Exception\CircularDependencyFoundException
* @return object
*/
protected function createServiceViaCallback
($callable, $cName, $rName)
{
static
$circularDependencyResolver = array();
$depKey = spl_object_hash($this) . '-' . $cName;
if (isset($circularDependencyResolver[$depKey])) {
$circularDependencyResolver = array();
throw new Exception\CircularDependencyFoundException
('Circular dependency for LazyServiceLoader was found for instance ' . $rName);
}
try
{
$circularDependencyResolver[$depKey] = true;
$instance = call_user_func($callable, $this, $cName, $rName);
unset($circularDependencyResolver[$depKey]);
} catch
(Exception\ServiceNotFoundException
$e) {
unset($circularDependencyResolver[$depKey]);
throw $e;
} catch
(\Exception
$e) {
unset($circularDependencyResolver[$depKey]);
throw new Exception\ServiceNotCreatedException
(
sprintf('An exception was raised while creating "%s"; no instance returned', $rName),
$e->getCode(),
$e
);
}
if ($instance === null) {
throw new Exception\ServiceNotCreatedException
('The factory was called but did not return an instance.');
}
return $instance;
}
/**
* Retrieve a keyed list of all registered services. Handy for debugging!
*
* @return array
*/
public function getRegisteredServices
()
{
return array(
'invokableClasses' => array_keys($this->invokableClasses),
'factories' => array_keys($this->factories),
'aliases' => array_keys($this->aliases),
'instances' => array_keys($this->instances),
);
}
/**
* Retrieve a keyed list of all canonical names. Handy for debugging!
*
* @return array
*/
public function getCanonicalNames
()
{
return $this->canonicalNames;
}
/**
* Allows to override the canonical names lookup map with predefined
* values.
*
* @param array $canonicalNames
* @return ServiceManager
*/
public function setCanonicalNames
($canonicalNames)
{
$this->canonicalNames = $canonicalNames;
return $this;
}
/**
* Attempt to retrieve an instance via a peering manager
*
* @param string $name
* @return mixed
*/
protected function retrieveFromPeeringManager
($name)
{
foreach ($this->peeringServiceManagers as $peeringServiceManager) {
if ($peeringServiceManager->has($name)) {
return $peeringServiceManager->get($name);
}
}
$name = $this->canonicalizeName($name);
if ($this->hasAlias($name)) {
do {
$name = $this->aliases[$name];
} while ($this->hasAlias($name));
}
foreach ($this->peeringServiceManagers as $peeringServiceManager) {
if ($peeringServiceManager->has($name)) {
return $peeringServiceManager->get($name);
}
}
return null;
}
/**
* Attempt to create an instance via an invokable class
*
* @param string $canonicalName
* @param string $requestedName
* @return null|\stdClass
* @throws Exception\ServiceNotFoundException If resolved class does not exist
*/
protected function createFromInvokable
($canonicalName, $requestedName)
{
$invokable = $this->invokableClasses[$canonicalName];
if (!class_exists($invokable)) {
throw new Exception\ServiceNotFoundException
(sprintf(
'%s: failed retrieving "%s%s" via invokable class "%s"; class does not exist',
__METHOD__,
$canonicalName,
($requestedName ?
'(alias: ' . $requestedName . ')' : ''),
$invokable
));
}
$instance = new $invokable;
return $instance;
}
/**
* Attempt to create an instance via a factory
*
* @param string $canonicalName
* @param string $requestedName
* @return mixed
* @throws Exception\ServiceNotCreatedException If factory is not callable
*/
protected function createFromFactory
($canonicalName, $requestedName)
{
$factory = $this->factories[$canonicalName];
if (is_string($factory) && class_exists($factory, true)) {
$factory = new $factory;
$this->factories[$canonicalName] = $factory;
}
if ($factory instanceof FactoryInterface
) {
$instance = $this->createServiceViaCallback(array($factory, 'createService'), $canonicalName, $requestedName);
} elseif (is_callable($factory)) {
$instance = $this->createServiceViaCallback($factory, $canonicalName, $requestedName);
} else {
throw new Exception\ServiceNotCreatedException
(sprintf(
'While attempting to create %s%s an invalid factory was registered for this instance type.',
$canonicalName,
($requestedName ?
'(alias: ' . $requestedName . ')' : '')
));
}
return $instance;
}
/**
* Attempt to create an instance via an abstract factory
*
* @param string $canonicalName
* @param string $requestedName
* @return object|null
* @throws Exception\ServiceNotCreatedException If abstract factory is not callable
*/
protected function createFromAbstractFactory
($canonicalName, $requestedName)
{
foreach ($this->abstractFactories as $index => $abstractFactory) {
// support factories as strings
if (is_string($abstractFactory) && class_exists($abstractFactory, true)) {
$this->abstractFactories[$index] = $abstractFactory = new $abstractFactory;
} elseif (!$abstractFactory instanceof AbstractFactoryInterface
) {
throw new Exception\ServiceNotCreatedException
(sprintf(
'While attempting to create %s%s an abstract factory could not produce a valid instance.',
$canonicalName,
($requestedName ?
'(alias: ' . $requestedName . ')' : '')
));
}
try
{
if ($abstractFactory->canCreateServiceWithName($this, $canonicalName, $requestedName)) {
$this->pendingAbstractFactoryRequests[get_class($abstractFactory)] = $requestedName;
$instance = $this->createServiceViaCallback(
array($abstractFactory, 'createServiceWithName'),
$canonicalName,
$requestedName
);
unset($this->pendingAbstractFactoryRequests[get_class($abstractFactory)]);
} else {
$instance = false;
}
} catch
(\Exception
$e) {
unset($this->pendingAbstractFactoryRequests[get_class($abstractFactory)]);
throw new Exception\ServiceNotCreatedException
(
sprintf(
'An abstract factory could not create an instance of %s%s.',
$canonicalName,
($requestedName ?
'(alias: ' . $requestedName . ')' : '')
),
$e->getCode(),
$e
);
}
if (is_object($instance)) {
break;
}
}
return $instance;
}
/**
* Checks if the object has this class as one of its parents
*
* @see https://bugs.php.net/bug.php?id=53727
* @see https://github.com/zendframework/zf2/pull/1807
*
* @param string $className
* @param string $type
* @return bool
*/
protected static
function isSubclassOf
($className, $type)
{
if (is_subclass_of($className, $type)) {
return true;
}
if (version_compare(PHP_VERSION, '5.3.7', '>=')) {
return false;
}
if (!interface_exists
($type)) {
return false;
}
$r = new ReflectionClass
($className);
return $r->implementsInterface($type);
}
/**
* Unregister a service
*
* Called when $allowOverride is true and we detect that a service being
* added to the instance already exists. This will remove the duplicate
* entry, and also any shared flags previously registered.
*
* @param string $canonical
* @return void
*/
protected function unregisterService
($canonical)
{
$types = array('invokableClasses', 'factories', 'aliases');
foreach ($types as $type) {
if (isset($this->{$type}[$canonical])) {
unset($this->{$type}[$canonical]);
break;
}
}
if (isset($this->instances[$canonical])) {
unset($this->instances[$canonical]);
}
if (isset($this->shared[$canonical])) {
unset($this->shared[$canonical]);
}
}
}