diff --git a/.travis/run-tests.sh b/.travis/run-tests.sh deleted file mode 100755 index a84e0ba2..00000000 --- a/.travis/run-tests.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -travisdir=$(dirname "$0") -testdir="$travisdir/../tests" -testedcomponents=(`cat "$travisdir/tested-components"`) -result=0 - -for tested in "${testedcomponents[@]}" - do - echo "$tested:" - phpunit -c $testdir/phpunit.xml.dist $testdir/$tested - result=$(($result || $?)) -done - -exit $result diff --git a/.travis/skipped-components b/.travis/skipped-components deleted file mode 100644 index 171dfe9d..00000000 --- a/.travis/skipped-components +++ /dev/null @@ -1,7 +0,0 @@ -Zend/Amf -Zend/Date -Zend/Dojo -Zend/Queue -Zend/Service -Zend/Test -Zend/Wildfire diff --git a/.travis/tested-components b/.travis/tested-components deleted file mode 100644 index b0b94380..00000000 --- a/.travis/tested-components +++ /dev/null @@ -1,61 +0,0 @@ -Zend/Acl -Zend/Authentication -Zend/Barcode -Zend/Cache -Zend/Captcha -Zend/Cloud -Zend/Code -Zend/Config -Zend/Console -Zend/Crypt -Zend/Currency -Zend/Db -Zend/Di -Zend/DocBook -Zend/Dojo -Zend/Dom -Zend/EventManager -Zend/Feed -Zend/File -Zend/Filter -Zend/Form -Zend/GData -Zend/Http -Zend/InfoCard -Zend/InputFilter -Zend/Json -Zend/Ldap -Zend/Loader -Zend/Locale -Zend/Log -Zend/Mail -Zend/Markup -Zend/Math -Zend/Measure -Zend/Memory -Zend/Mime -Zend/ModuleManager -Zend/Mvc -Zend/Navigation -Zend/OAuth -Zend/OpenId -Zend/Paginator -Zend/Pdf -Zend/ProgressBar -Zend/RegistryTest.php -Zend/Rest -Zend/Search -Zend/Serializer -Zend/Server -Zend/Session -Zend/Soap -Zend/Stdlib -Zend/Tag -Zend/Text -Zend/TimeSync -Zend/Translator -Zend/Uri -Zend/Validator -Zend/VersionTest.php -Zend/View -Zend/XmlRpc diff --git a/src/AbstractFactoryInterface.php b/src/AbstractFactoryInterface.php index 57bc5d18..9cbb3876 100644 --- a/src/AbstractFactoryInterface.php +++ b/src/AbstractFactoryInterface.php @@ -4,6 +4,6 @@ interface AbstractFactoryInterface { - public function canCreateServiceWithName($name /*, $requestedName */); - public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name /*, $requestedName */); + public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName); + public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName); } diff --git a/src/AbstractPluginManager.php b/src/AbstractPluginManager.php new file mode 100644 index 00000000..e17dde8a --- /dev/null +++ b/src/AbstractPluginManager.php @@ -0,0 +1,217 @@ +addInitializer(function ($instance) use ($self) { + if ($instance instanceof ServiceManagerAwareInterface) { + $instance->setServiceManager($self); + } + }); + } + + /** + * Validate the plugin + * + * Checks that the filter loaded is either a valid callback or an instance + * of FilterInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + abstract public function validatePlugin($plugin); + + /** + * Retrieve a service from the manager by name + * + * Allows passing an array of options to use when creating the instance. + * createFromInvokable() will use these and pass them to the instance + * constructor if not null and a non-empty array. + * + * @param string $name + * @param array $options + * @param bool $usePeeringServiceManagers + * @return object + */ + public function get($name, $options = array(), $usePeeringServiceManagers = true) + { + // Allow specifying a class name directly; registers as an invokable class + if (!$this->has($name) && class_exists($name)) { + $this->setInvokableClass($name, $name); + } + + $this->creationOptions = $options; + $instance = parent::get($name, $usePeeringServiceManagers); + $this->creationOptions = null; + $this->validatePlugin($instance); + return $instance; + } + + /** + * Register a service with the locator. + * + * Validates that the service object via validatePlugin() prior to + * attempting to register it. + * + * @param string $name + * @param mixed $service + * @param bool $shared + * @return AbstractPluginManager + * @throws Exception\InvalidServiceNameException + */ + public function setService($name, $service, $shared = true) + { + if ($service) { + $this->validatePlugin($service); + } + parent::setService($name, $service, $shared); + return $this; + } + + /** + * Set the main service locator so factories can have access to it to pull deps + * + * @param ServiceLocatorInterface $serviceLocator + * @return AbstractPluginManager + */ + public function setServiceLocator(ServiceLocatorInterface $serviceLocator) + { + $this->serviceLocator = $serviceLocator; + return $this; + } + + /** + * Get the main plugin manager. Useful for fetching dependencies from within factories. + * + * @return mixed + */ + public function getServiceLocator() + { + return $this->serviceLocator; + } + + /** + * Attempt to create an instance via an invokable class + * + * Overrides parent implementation by passing $creationOptions to the + * constructor, if non-null. + * + * @param string $canonicalName + * @param string $requestedName + * @return null|\stdClass + * @throws Exception\ServiceNotCreatedException If resolved class does not exist + */ + protected function createFromInvokable($canonicalName, $requestedName) + { + $invokable = $this->invokableClasses[$canonicalName]; + if (!class_exists($invokable)) { + throw new Exception\ServiceNotCreatedException(sprintf( + '%s: failed retrieving "%s%s" via invokable class "%s"; class does not exist', + __METHOD__, + $canonicalName, + ($requestedName ? '(alias: ' . $requestedName . ')' : ''), + $canonicalName + )); + } + + if (null === $this->creationOptions + || (is_array($this->creationOptions) && empty($this->creationOptions)) + ) { + $instance = new $invokable(); + } else { + $instance = new $invokable($this->creationOptions); + } + + return $instance; + } + + /** + * Determine if a class implements a given interface + * + * For PHP versions >= 5.3.7, uses is_subclass_of; otherwise, uses + * reflection to determine the interfaces implemented. + * + * @param string $class + * @param string $type + * @return bool + */ + protected function isSubclassOf($class, $type) + { + if (version_compare(PHP_VERSION, '5.3.7', 'gte')) { + return is_subclass_of($class, $type); + } + + $r = new ClassReflection($class); + $interfaces = $r->getInterfaceNames(); + return (in_array($type, $interfaces)); + } +} diff --git a/src/Configuration.php b/src/Configuration.php index 7e9f8f8a..2b24faf5 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -15,7 +15,7 @@ public function getAllowOverride() { return (isset($this->configuration['allow_override'])) ? $this->configuration['allow_override'] : null; } - + public function getFactories() { return (isset($this->configuration['factories'])) ? $this->configuration['factories'] : array(); @@ -41,6 +41,11 @@ public function getAliases() return (isset($this->configuration['aliases'])) ? $this->configuration['aliases'] : array(); } + public function getInitializers() + { + return (isset($this->configuration['initializers'])) ? $this->configuration['initializers'] : array(); + } + public function getShared() { return (isset($this->configuration['shared'])) ? $this->configuration['shared'] : array(); @@ -50,7 +55,7 @@ public function configureServiceManager(ServiceManager $serviceManager) { $allowOverride = $this->getAllowOverride(); isset($allowOverride) ? $serviceManager->setAllowOverride($allowOverride) : null; - + foreach ($this->getFactories() as $name => $factory) { $serviceManager->setFactory($name, $factory); } @@ -71,6 +76,10 @@ public function configureServiceManager(ServiceManager $serviceManager) $serviceManager->setAlias($alias, $nameOrAlias); } + foreach ($this->getInitializers() as $initializer) { + $serviceManager->addInitializer($initializer); + } + foreach ($this->getShared() as $name => $isShared) { $serviceManager->setShared($name, $isShared); } diff --git a/src/Di/DiAbstractServiceFactory.php b/src/Di/DiAbstractServiceFactory.php index 81317d44..c3c82077 100644 --- a/src/Di/DiAbstractServiceFactory.php +++ b/src/Di/DiAbstractServiceFactory.php @@ -8,7 +8,6 @@ class DiAbstractServiceFactory extends DiServiceFactory implements AbstractFactoryInterface { - /** * @param \Zend\Di\Di $di * @param null|string|\Zend\Di\InstanceManager $useServiceLocator @@ -26,12 +25,9 @@ public function __construct(Di $di, $useServiceLocator = self::USE_SL_NONE) } /** - * @param ServiceLocatorInterface $serviceLocator - * @param $serviceName - * @param null $requestedName - * @return object + * {@inheritDoc} */ - public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $serviceName, $requestedName = null) + public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $serviceName, $requestedName) { $this->serviceLocator = $serviceLocator; if ($requestedName) { @@ -43,11 +39,14 @@ public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $ } /** - * @param $name - * @return null + * {@inheritDoc} */ - public function canCreateServiceWithName($name) + public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) { - return null; // not sure + return $this->instanceManager->hasSharedInstance($requestedName) + || $this->instanceManager->hasAlias($requestedName) + || $this->instanceManager->hasConfiguration($requestedName) + || $this->instanceManager->hasTypePreferences($requestedName) + || $this->definitions->hasClass($requestedName); } } diff --git a/src/ServiceManager.php b/src/ServiceManager.php index aa03feaf..ac32797b 100644 --- a/src/ServiceManager.php +++ b/src/ServiceManager.php @@ -12,6 +12,13 @@ class ServiceManager implements ServiceLocatorInterface const SCOPE_CHILD = 'child'; /**@#-*/ + /** + * Lookup for canonicalized names. + * + * @var array + */ + protected $canonicalNames = array(); + /** * @var bool */ @@ -28,10 +35,15 @@ class ServiceManager implements ServiceLocatorInterface protected $factories = array(); /** - * @var Closure|AbstractFactoryInterface[] + * @var AbstractFactoryInterface[] */ protected $abstractFactories = array(); + /** + * @var array + */ + protected $pendingAbstractFactoryRequests = array(); + /** * @var array */ @@ -59,6 +71,18 @@ class ServiceManager implements ServiceLocatorInterface */ 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 ot throw exceptions during create() */ @@ -91,6 +115,35 @@ 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; + } + /** * @param bool $throwExceptionInCreate * @return ServiceManager @@ -109,6 +162,28 @@ 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; + } + /** * @param $name * @param $invokableClass @@ -117,16 +192,17 @@ public function getThrowExceptionInCreate() */ public function setInvokableClass($name, $invokableClass, $shared = true) { - $name = $this->canonicalizeName($name); + $cName = $this->canonicalizeName($name); + $rName = $name; - if ($this->allowOverride === false && $this->has($name)) { + if ($this->allowOverride === false && $this->has(array($cName, $rName), 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', - $name + $cName )); } - $this->invokableClasses[$name] = $invokableClass; - $this->shared[$name] = $shared; + $this->invokableClasses[$cName] = $invokableClass; + $this->shared[$cName] = $shared; return $this; } @@ -137,7 +213,8 @@ public function setInvokableClass($name, $invokableClass, $shared = true) */ public function setFactory($name, $factory, $shared = true) { - $name = $this->canonicalizeName($name); + $cName = $this->canonicalizeName($name); + $rName = $name; if (!is_string($factory) && !$factory instanceof FactoryInterface && !is_callable($factory)) { throw new Exception\InvalidArgumentException( @@ -145,29 +222,43 @@ public function setFactory($name, $factory, $shared = true) ); } - if ($this->allowOverride === false && $this->has($name)) { + if ($this->allowOverride === false && $this->has(array($cName, $rName), 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', - $name + $cName )); } - $this->factories[$name] = $factory; - $this->shared[$name] = $shared; + $this->factories[$cName] = $factory; + $this->shared[$cName] = $shared; return $this; } /** - * @param $factory + * @param AbstractFactoryInterface|string $factory * @param bool $topOfStack + * @throws Exception\InvalidArgumentException if the abstract factory is invalid */ public function addAbstractFactory($factory, $topOfStack = true) { - if (!is_string($factory) && !$factory instanceof AbstractFactoryInterface && !is_callable($factory)) { + 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); @@ -207,9 +298,10 @@ public function addInitializer($initializer, $topOfStack = true) */ public function setService($name, $service, $shared = true) { - $name = $this->canonicalizeName($name); + $cName = $this->canonicalizeName($name); + $rName = $name; - if ($this->allowOverride === false && $this->has($name)) { + if ($this->allowOverride === false && $this->has($cName, 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__, @@ -221,8 +313,8 @@ public function setService($name, $service, $shared = true) * @todo If a service is being overwritten, destroy all previous aliases */ - $this->instances[$name] = $service; - $this->shared[$name] = (bool) $shared; + $this->instances[$cName] = $service; + $this->shared[$cName] = (bool) $shared; return $this; } @@ -265,7 +357,7 @@ public function get($name, $usePeeringServiceManagers = true) $cName = $this->aliases[$cName]; } while ($this->hasAlias($cName)); - if (!$this->has($cName)) { + if (!$this->has(array($cName, $rName))) { throw new Exception\ServiceNotFoundException(sprintf( 'An alias "%s" was requested but no service could be found.', $name @@ -282,26 +374,15 @@ public function get($name, $usePeeringServiceManagers = true) $selfException = null; if (!$instance && !is_array($instance)) { - try { - $instance = $this->create(array($cName, $rName)); - } catch (\Exception $selfException) { - if (!$selfException instanceof Exception\ServiceNotFoundException && - !$selfException instanceof Exception\ServiceNotCreatedException) { - throw $selfException; - } - if ($usePeeringServiceManagers) { - foreach ($this->peeringServiceManagers as $peeringServiceManager) { - try { - $instance = $peeringServiceManager->get($name); - } catch (Exception\ServiceNotFoundException $e) { - continue; - } catch (Exception\ServiceNotCreatedException $e) { - continue; - } catch (\Exception $e) { - throw $e; - } - break; - } + $retrieveFromPeeringManagerFirst = $this->retrieveFromPeeringManagerFirst(); + if ($usePeeringServiceManagers && $retrieveFromPeeringManagerFirst) { + $instance = $this->retrieveFromPeeringManager($name); + } + if (!$instance) { + if ($this->canCreate(array($cName, $rName))) { + $instance = $this->create(array($cName, $rName)); + } else if ($usePeeringServiceManagers && !$retrieveFromPeeringManagerFirst) { + $instance = $this->retrieveFromPeeringManager($name); } } } @@ -318,7 +399,10 @@ public function get($name, $usePeeringServiceManagers = true) ); } - if (isset($this->shared[$cName]) && $this->shared[$cName] === true) { + if ($this->shareByDefault() + && !isset($this->instances[$cName]) + && (!isset($this->shared[$cName]) || $this->shared[$cName] === true) + ) { $this->instances[$cName] = $instance; } @@ -326,7 +410,7 @@ public function get($name, $usePeeringServiceManagers = true) } /** - * @param $cName + * @param string|array $name * @return false|object * @throws Exception\ServiceNotCreatedException * @throws Exception\InvalidServiceNameException @@ -334,70 +418,24 @@ public function get($name, $usePeeringServiceManagers = true) public function create($name) { $instance = false; - $rName = null; if (is_array($name)) { list($cName, $rName) = $name; } else { - $cName = $name; + $rName = $name; + $cName = $this->canonicalizeName($rName); } - $cName = $this->canonicalizeName($cName); - - if (isset($this->invokableClasses[$cName])) { - $invokable = $this->invokableClasses[$cName]; - if (!class_exists($invokable)) { - throw new Exception\ServiceNotCreatedException(sprintf( - '%s: failed retrieving "%s%s" via invokable class "%s"; class does not exist', - __METHOD__, - $cName, - ($rName ? '(alias: ' . $rName . ')' : ''), - $cName - )); - } - $instance = new $invokable; + if (isset($this->factories[$cName])) { + $instance = $this->createFromFactory($cName, $rName); } - if (!$instance && isset($this->factories[$cName])) { - $factory = $this->factories[$cName]; - if (is_string($factory) && class_exists($factory, true)) { - $factory = new $factory; - $this->factories[$cName] = $factory; - } - if ($factory instanceof FactoryInterface) { - $instance = $this->createServiceViaCallback(array($factory, 'createService'), $cName, $rName); - } elseif (is_callable($factory)) { - $instance = $this->createServiceViaCallback($factory, $cName, $rName); - } else { - throw new Exception\ServiceNotCreatedException(sprintf( - 'While attempting to create %s%s an invalid factory was registered for this instance type.', - $cName, - ($rName ? '(alias: ' . $rName . ')' : '') - )); - } + if (!$instance && isset($this->invokableClasses[$cName])) { + $instance = $this->createFromInvokable($cName, $rName); } - if (!$instance && !empty($this->abstractFactories)) { - foreach ($this->abstractFactories as $index => $abstractFactory) { - // support factories as strings - if (is_string($abstractFactory) && class_exists($abstractFactory, true)) { - $this->abstractFactories[$index] = $abstractFactory = new $abstractFactory; - } - if ($abstractFactory instanceof AbstractFactoryInterface) { - $instance = $this->createServiceViaCallback(array($abstractFactory, 'createServiceWithName'), $cName, $rName); - } elseif (is_callable($abstractFactory)) { - $instance = $this->createServiceViaCallback($abstractFactory, $cName, $rName); - } else { - throw new Exception\ServiceNotCreatedException(sprintf( - 'While attempting to create %s%s an abstract factory could not produce a valid instance.', - $cName, - ($rName ? '(alias: ' . $rName . ')' : '') - )); - } - if (is_object($instance)) { - break; - } - } + if (!$instance && $this->canCreateFromAbstractFactory($cName, $rName)) { + $instance = $this->createFromAbstractFactory($cName, $rName); } if ($this->throwExceptionInCreate == true && $instance === false) { @@ -411,8 +449,10 @@ public function create($name) foreach ($this->initializers as $initializer) { if ($initializer instanceof InitializerInterface) { $initializer->initialize($instance); - } else { + } elseif (is_object($initializer) && is_callable($initializer)) { $initializer($instance); + } else { + call_user_func($initializer, $instance); } } @@ -420,16 +460,17 @@ public function create($name) } /** - * @param $nameOrAlias + * Determine if we can create an instance. + * @param $name * @return bool */ - public function has($nameOrAlias, $usePeeringServiceManagers = true) + public function canCreate($name, $checkAbstractFactories = true) { - if (is_array($nameOrAlias)) { - list($cName, $rName) = $nameOrAlias; + if (is_array($name)) { + list($cName, $rName) = $name; } else { - $cName = $this->canonicalizeName($nameOrAlias); - $rName = $cName; + $rName = $name; + $cName = $this->canonicalizeName($rName); } $has = ( @@ -443,21 +484,36 @@ public function has($nameOrAlias, $usePeeringServiceManagers = true) return true; } - // check abstract factories - foreach ($this->abstractFactories as $index => $abstractFactory) { - // Support string abstract factory class names - if (is_string($abstractFactory) && class_exists($abstractFactory, true)) { - $this->abstractFactory[$index] = $abstractFactory = new $abstractFactory(); - } + if (isset($this->factories[$cName])) { + return true; + } - // Bad abstract factory; skip - if (!$abstractFactory instanceof AbstractFactoryInterface) { - continue; - } + if (isset($this->invokableClasses[$cName])) { + return true; + } - if ($abstractFactory->canCreateServiceWithName($cName, $rName)) { - return true; - } + if ($checkAbstractFactories && $this->canCreateFromAbstractFactory($cName, $rName)) { + return true; + } + + return false; + } + + /** + * @param $name + * @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) { @@ -471,6 +527,36 @@ public function has($nameOrAlias, $usePeeringServiceManagers = 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->abstractFactory[$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 $alias * @param $nameOrAlias @@ -484,18 +570,18 @@ public function setAlias($alias, $nameOrAlias) throw new Exception\InvalidServiceNameException('Service or alias names must be strings.'); } - $alias = $this->canonicalizeName($alias); + $cAlias = $this->canonicalizeName($alias); $nameOrAlias = $this->canonicalizeName($nameOrAlias); if ($alias == '' || $nameOrAlias == '') { throw new Exception\InvalidServiceNameException('Invalid service name alias'); } - if ($this->allowOverride === false && $this->hasAlias($alias)) { + if ($this->allowOverride === false && $this->has(array($cAlias, $alias), false)) { throw new Exception\InvalidServiceNameException('An alias by this name already exists'); } - $this->aliases[$alias] = $nameOrAlias; + $this->aliases[$cAlias] = $nameOrAlias; return $this; } @@ -525,13 +611,34 @@ public function createScopedServiceManager($peering = self::SCOPE_PARENT) return $scopedServiceManager; } + /** + * Add a peering relationship + * + * @param ServiceManager $manager + * @param string $peering + * @return ServiceManager Current instance + */ + 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; + } + /** * @param $name * @return string */ protected function canonicalizeName($name) { - return strtolower(str_replace(array('-', '_', ' ', '\\', '/'), '', $name)); + if (!isset($this->canonicalNames[$name])) { + $this->canonicalNames[$name] = strtolower(str_replace(array('-', '_', ' ', '\\', '/'), '', $name)); + } + return $this->canonicalNames[$name]; } /** @@ -545,20 +652,24 @@ protected function canonicalizeName($name) protected function createServiceViaCallback($callable, $cName, $rName) { static $circularDependencyResolver = array(); + $depKey = spl_object_hash($this) . '-' . $cName; - if (isset($circularDependencyResolver[spl_object_hash($this) . '-' . $cName])) { + if (isset($circularDependencyResolver[$depKey])) { $circularDependencyResolver = array(); throw new Exception\CircularDependencyFoundException('Circular dependency for LazyServiceLoader was found for instance ' . $rName); } - $circularDependencyResolver[spl_object_hash($this) . '-' . $cName] = true; 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('Abstract factory raised an exception when creating "%s"; no instance returned', $rName), + sprintf('An exception was raised while creating "%s"; no instance returned', $rName), $e->getCode(), $e ); @@ -566,7 +677,6 @@ protected function createServiceViaCallback($callable, $cName, $rName) if ($instance === null) { throw new Exception\ServiceNotCreatedException('The factory was called but did not return an instance.'); } - unset($circularDependencyResolver[spl_object_hash($this) . '-' . $cName]); return $instance; } @@ -586,4 +696,121 @@ public function getRegisteredServices() ); } + /** + * 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); + } + } + return null; + } + + /** + * Attempt to create an instance via an invokable class + * + * @param string $canonicalName + * @param string $requestedName + * @return null|\stdClass + * @throws Exception\ServiceNotCreatedException 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 . ')' : ''), + $canonicalName + )); + } + $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 \stdClass|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; + } else if (!$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 { + $this->pendingAbstractFactoryRequests[get_class($abstractFactory)] = $requestedName; + $instance = $this->createServiceViaCallback( + array($abstractFactory, 'createServiceWithName'), + $canonicalName, + $requestedName + ); + unset($this->pendingAbstractFactoryRequests[get_class($abstractFactory)]); + } 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; + } } diff --git a/test/Di/DiAbstractServiceFactoryTest.php b/test/Di/DiAbstractServiceFactoryTest.php index dcd4726b..468033f7 100644 --- a/test/Di/DiAbstractServiceFactoryTest.php +++ b/test/Di/DiAbstractServiceFactoryTest.php @@ -3,7 +3,8 @@ namespace ZendTest\ServiceManager\Di; use Zend\ServiceManager\Di\DiAbstractServiceFactory, -Zend\ServiceManager\Di\DiInstanceManagerProxy; + Zend\ServiceManager\ServiceManager, + Zend\ServiceManager\Di\DiInstanceManagerProxy; class DiAbstractServiceFactoryTest extends \PHPUnit_Framework_TestCase { @@ -51,7 +52,50 @@ public function testConstructor() */ public function testCreateServiceWithName() { - $foo = $this->diAbstractServiceFactory->createServiceWithName($this->mockServiceLocator, 'foo'); + $foo = $this->diAbstractServiceFactory->createServiceWithName($this->mockServiceLocator, 'foo', 'foo'); $this->assertEquals($this->fooInstance, $foo); } + + /** + * @covers Zend\ServiceManager\Di\DiAbstractServiceFactory::canCreateServiceWithName + */ + public function testCanCreateServiceWithName() + { + $instance = new DiAbstractServiceFactory($this->getMock('Zend\Di\Di')); + $im = $instance->instanceManager(); + + $locator = new ServiceManager(); + + // will check shared instances + $this->assertFalse($instance->canCreateServiceWithName($locator, 'a-shared-instance-alias', 'a-shared-instance-alias')); + $im->addSharedInstance(new \stdClass(), 'a-shared-instance-alias'); + $this->assertTrue($instance->canCreateServiceWithName($locator, 'a-shared-instance-alias', 'a-shared-instance-alias')); + + // will check aliases + $this->assertFalse($instance->canCreateServiceWithName($locator, 'an-alias', 'an-alias')); + $im->addAlias('an-alias', 'stdClass'); + $this->assertTrue($instance->canCreateServiceWithName($locator, 'an-alias', 'an-alias')); + + // will check instance configurations + $this->assertFalse($instance->canCreateServiceWithName($locator, __NAMESPACE__ . '\Non\Existing', __NAMESPACE__ . '\Non\Existing')); + $im->setConfiguration(__NAMESPACE__ . '\Non\Existing', array('parameters' => array('a' => 'b'))); + $this->assertTrue($instance->canCreateServiceWithName($locator, __NAMESPACE__ . '\Non\Existing', __NAMESPACE__ . '\Non\Existing')); + + // will check preferences for abstract types + $this->assertFalse($instance->canCreateServiceWithName($locator, __NAMESPACE__ . '\AbstractClass', __NAMESPACE__ . '\AbstractClass')); + $im->setTypePreference(__NAMESPACE__ . '\AbstractClass', array(__NAMESPACE__ . '\Non\Existing')); + $this->assertTrue($instance->canCreateServiceWithName($locator, __NAMESPACE__ . '\AbstractClass', __NAMESPACE__ . '\AbstractClass')); + + // will check definitions + $def = $instance->definitions(); + $this->assertFalse($instance->canCreateServiceWithName($locator, __NAMESPACE__ . '\Other\Non\Existing', __NAMESPACE__ . '\Other\Non\Existing')); + $classDefinition = $this->getMock('Zend\Di\Definition\DefinitionInterface'); + $classDefinition + ->expects($this->any()) + ->method('hasClass') + ->with($this->equalTo(__NAMESPACE__ . '\Other\Non\Existing')) + ->will($this->returnValue(true)); + $def->addDefinition($classDefinition); + $this->assertTrue($instance->canCreateServiceWithName($locator, __NAMESPACE__ . '\Other\Non\Existing', __NAMESPACE__ . '\Other\Non\Existing')); + } } diff --git a/test/Di/DiServiceFactoryTest.php b/test/Di/DiServiceFactoryTest.php index 6808bf07..e3c40a0a 100644 --- a/test/Di/DiServiceFactoryTest.php +++ b/test/Di/DiServiceFactoryTest.php @@ -2,12 +2,18 @@ namespace ZendTest\ServiceManager\Di; -use Zend\ServiceManager\Di\DiServiceFactory, - Zend\ServiceManager\Di\DiInstanceManagerProxy; +use Zend\Di\Di; +use Zend\ServiceManager\Di\DiServiceFactory; +use Zend\ServiceManager\Di\DiInstanceManagerProxy; +use Zend\ServiceManager\Di\DiAbstractServiceFactory; +use Zend\ServiceManager\ServiceManager; class DiServiceFactoryTest extends \PHPUnit_Framework_TestCase { + /** + * @var DiServiceFactory + */ protected $diServiceFactory = null; /**@#+ diff --git a/test/ServiceManagerTest.php b/test/ServiceManagerTest.php index 63df6598..5677e369 100644 --- a/test/ServiceManagerTest.php +++ b/test/ServiceManagerTest.php @@ -199,6 +199,39 @@ public function testGetWithScopedContainer() $this->assertEquals('bar', $scopedServiceManager->get('foo')); } + public function testCanRetrieveFromParentPeeringManager() + { + $parent = new ServiceManager(); + $parent->setService('foo', 'bar'); + $child = new ServiceManager(); + $child->addPeeringServiceManager($parent, ServiceManager::SCOPE_PARENT); + $this->assertEquals('bar', $child->get('foo')); + } + + public function testCanRetrieveFromChildPeeringManager() + { + $parent = new ServiceManager(); + $child = new ServiceManager(); + $child->addPeeringServiceManager($parent, ServiceManager::SCOPE_CHILD); + $child->setService('foo', 'bar'); + $this->assertEquals('bar', $parent->get('foo')); + } + + public function testAllowsRetrievingFromPeeringContainerFirst() + { + $parent = new ServiceManager(); + $parent->setFactory('foo', function($sm) { + return 'bar'; + }); + $child = new ServiceManager(); + $child->setFactory('foo', function($sm) { + return 'baz'; + }); + $child->addPeeringServiceManager($parent, ServiceManager::SCOPE_PARENT); + $child->setRetrieveFromPeeringManagerFirst(true); + $this->assertEquals('bar', $child->get('foo')); + } + /** * @covers Zend\ServiceManager\ServiceManager::create */ @@ -235,15 +268,6 @@ public function testCreateWithAbstractFactory() $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Foo', $this->serviceManager->get('foo')); } - /** - * @covers Zend\ServiceManager\ServiceManager::create - */ - public function testCreateWithCallableAbstractFactory() - { - $this->serviceManager->addAbstractFactory(function () { return new TestAsset\Foo; }); - $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Foo', $this->serviceManager->get('foo')); - } - public function testCreateWithInitializerObject() { $this->serviceManager->addInitializer(new TestAsset\FooInitializer(array('foo' => 'bar'))); @@ -353,7 +377,7 @@ public function testCreateScopedServiceManager() $this->serviceManager->setService('foo', 'bar'); $scopedServiceManager = $this->serviceManager->createScopedServiceManager(); $this->assertNotSame($this->serviceManager, $scopedServiceManager); - $this->assertFalse($scopedServiceManager->has('foo', false)); + $this->assertFalse($scopedServiceManager->has('foo', true, false)); $this->assertContains($this->serviceManager, $this->readAttribute($scopedServiceManager, 'peeringServiceManagers')); @@ -374,19 +398,6 @@ public function testConfigureWithInvokableClass() $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Foo', $foo); } - public function testCanUseStringAbstractFactoryClassName() - { - $config = new Configuration(array( - 'abstract_factories' => array( - 'ZendTest\ServiceManager\TestAsset\FooAbstractFactory', - ), - )); - $serviceManager = new ServiceManager($config); - $serviceManager->setFactory('foo', 'ZendTest\ServiceManager\TestAsset\FooFactory'); - $foo = $serviceManager->get('unknownObject'); - $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Foo', $foo); - } - public function testPeeringService() { $di = new Di(); @@ -398,18 +409,86 @@ public function testPeeringService() $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Bar', $bar); } - public function testPeeringServiceFallbackOnCreateFailure() + public function testDiAbstractServiceFactory() + { + $di = $this->getMock('Zend\Di\Di'); + $factory = new DiAbstractServiceFactory($di); + $factory->instanceManager()->setConfiguration('ZendTest\ServiceManager\TestAsset\Bar', array('parameters' => array('foo' => array('a')))); + $this->serviceManager->addAbstractFactory($factory); + + $this->assertTrue($this->serviceManager->has('ZendTest\ServiceManager\TestAsset\Bar', true)); + + $bar = $this->serviceManager->get('ZendTest\ServiceManager\TestAsset\Bar', true); + $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Bar', $bar); + } + + public function testExceptionThrowingFactory() + { + $this->serviceManager->setFactory('foo', 'ZendTest\ServiceManager\TestAsset\ExceptionThrowingFactory'); + try { + $this->serviceManager->get('foo'); + $this->fail("No exception thrown"); + } catch (Exception\ServiceNotCreatedException $e) { + $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\FooException', $e->getPrevious()); + } + } + + /** + * @expectedException Zend\ServiceManager\Exception\ServiceNotFoundException + */ + public function testCannotUseUnknownServiceNameForAbstractFactory() + { + $config = new Configuration(array( + 'abstract_factories' => array( + 'ZendTest\ServiceManager\TestAsset\FooAbstractFactory', + ), + )); + $serviceManager = new ServiceManager($config); + $serviceManager->setFactory('foo', 'ZendTest\ServiceManager\TestAsset\FooFactory'); + $foo = $serviceManager->get('unknownObject'); + } + + /** + * @expectedException Zend\ServiceManager\Exception\ServiceNotCreatedException + */ + public function testDoNotFallbackToAbstractFactory() { $factory = function ($sm) { return new TestAsset\Bar(); }; $serviceManager = new ServiceManager(); $serviceManager->setFactory('ZendTest\ServiceManager\TestAsset\Bar', $factory); - $sm = $serviceManager->createScopedServiceManager(ServiceManager::SCOPE_CHILD); $di = new Di(); $di->instanceManager()->setParameters('ZendTest\ServiceManager\TestAsset\Bar', array('foo' => array('a'))); - $sm->addAbstractFactory(new DiAbstractServiceFactory($di)); + $serviceManager->addAbstractFactory(new DiAbstractServiceFactory($di)); $bar = $serviceManager->get('ZendTest\ServiceManager\TestAsset\Bar'); - $this->assertInstanceOf('ZendTest\ServiceManager\TestAsset\Bar', $bar); + } + + /** + * @expectedException Zend\ServiceManager\Exception\InvalidServiceNameException + */ + public function testAssignAliasWithExistingServiceName() + { + $this->serviceManager->setFactory('foo', 'ZendTest\ServiceManager\TestAsset\FooFactory'); + $this->serviceManager->setFactory('bar', function ($sm) + { + return new Bar(array('a')); + }); + $this->serviceManager->setAllowOverride(false); + // should throw an exception because 'foo' already exists in the service manager + $this->serviceManager->setAlias('foo', 'bar'); + } + + /** + * @covers Zend\ServiceManager\ServiceManager::createFromAbstractFactory + * @covers Zend\ServiceManager\ServiceManager::has + */ + public function testWillNotCreateCircularReferences() + { + $abstractFactory = new TestAsset\CircularDependencyAbstractFactory(); + $sm = new ServiceManager(); + $sm->addAbstractFactory($abstractFactory); + $foo = $sm->get('foo'); + $this->assertSame($abstractFactory->expectedInstance, $foo); } } diff --git a/test/TestAsset/CircularDependencyAbstractFactory.php b/test/TestAsset/CircularDependencyAbstractFactory.php new file mode 100644 index 00000000..a0117a71 --- /dev/null +++ b/test/TestAsset/CircularDependencyAbstractFactory.php @@ -0,0 +1,35 @@ +has($name)) { + return $serviceLocator->get($name); + } + + return $this->expectedInstance; + } +} diff --git a/test/TestAsset/ExceptionThrowingFactory.php b/test/TestAsset/ExceptionThrowingFactory.php new file mode 100644 index 00000000..17ac8350 --- /dev/null +++ b/test/TestAsset/ExceptionThrowingFactory.php @@ -0,0 +1,15 @@ +