diff --git a/benchmarks/BenchAsset/AbstractFactoryFoo.php b/benchmarks/BenchAsset/AbstractFactoryFoo.php index 68879f68..b64e1525 100644 --- a/benchmarks/BenchAsset/AbstractFactoryFoo.php +++ b/benchmarks/BenchAsset/AbstractFactoryFoo.php @@ -7,8 +7,8 @@ namespace ZendBench\ServiceManager\BenchAsset; -use Zend\ServiceManager\Factory\AbstractFactoryInterface; use Interop\Container\ContainerInterface; +use Zend\ServiceManager\Factory\AbstractFactoryInterface; class AbstractFactoryFoo implements AbstractFactoryInterface { diff --git a/benchmarks/BenchAsset/DelegatorFactoryFoo.php b/benchmarks/BenchAsset/DelegatorFactoryFoo.php new file mode 100644 index 00000000..c7335e0c --- /dev/null +++ b/benchmarks/BenchAsset/DelegatorFactoryFoo.php @@ -0,0 +1,23 @@ +options = $options; + } +} diff --git a/benchmarks/HasBench.php b/benchmarks/HasBench.php new file mode 100644 index 00000000..6e696d77 --- /dev/null +++ b/benchmarks/HasBench.php @@ -0,0 +1,113 @@ +sm = new ServiceManager([ + 'factories' => [ + 'factory1' => BenchAsset\FactoryFoo::class, + ], + 'invokables' => [ + 'invokable1' => BenchAsset\Foo::class, + ], + 'services' => [ + 'service1' => new \stdClass(), + ], + 'aliases' => [ + 'alias1' => 'service1', + 'recursiveAlias1' => 'alias1', + 'recursiveAlias2' => 'recursiveAlias1', + ], + 'abstract_factories' => [ + BenchAsset\AbstractFactoryFoo::class + ] + ]); + } + + public function benchHasFactory1() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('factory1'); + } + + public function benchHasInvokable1() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('invokable1'); + } + + public function benchHasService1() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('service1'); + } + + public function benchHasAlias1() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('alias1'); + } + + public function benchHasRecursiveAlias1() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('recursiveAlias1'); + } + + public function benchHasRecursiveAlias2() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('recursiveAlias2'); + } + + public function benchHasAbstractFactory() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('foo'); + } + + public function benchHasNot() + { + // @todo @link https://github.com/phpbench/phpbench/issues/304 + $sm = clone $this->sm; + + $sm->has('42'); + } +} diff --git a/benchmarks/SetNewServicesBench.php b/benchmarks/SetNewServicesBench.php index 9e351060..99a4924d 100644 --- a/benchmarks/SetNewServicesBench.php +++ b/benchmarks/SetNewServicesBench.php @@ -11,6 +11,7 @@ use PhpBench\Benchmark\Metadata\Annotations\Revs; use PhpBench\Benchmark\Metadata\Annotations\Warmup; use Zend\ServiceManager\ServiceManager; +use ZendBench\ServiceManager\BenchAsset\DelegatorFactoryFoo; /** * @Revs(1000) @@ -43,40 +44,106 @@ public function __construct() 'recursiveFactoryAlias1' => 'factoryAlias1', 'recursiveFactoryAlias2' => 'recursiveFactoryAlias1', ], - 'abstract_factories' => [ - BenchAsset\AbstractFactoryFoo::class - ], ]; for ($i = 0; $i <= self::NUM_SERVICES; $i++) { $config['factories']["factory_$i"] = BenchAsset\FactoryFoo::class; $config['aliases']["alias_$i"] = "service_$i"; + $config['abstract_factories'][] = BenchAsset\AbstractFactoryFoo::class; + $config['invokables']["invokable_$i"] = BenchAsset\Foo::class; + $config['delegators']["delegator_$i"] = [ DelegatorFactoryFoo::class ]; } + $this->initializer = new BenchAsset\InitializerFoo(); + $this->abstractFactory = new BenchAsset\AbstractFactoryFoo(); $this->sm = new ServiceManager($config); } - public function benchSetFactory() + + public function benchSetService() { - // @todo @link https://github.com/phpbench/phpbench/issues/304 $sm = clone $this->sm; + $sm->setService('service2', new \stdClass()); + } + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchSetFactory() + { + $sm = clone $this->sm; $sm->setFactory('factory2', BenchAsset\FactoryFoo::class); } + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ public function benchSetAlias() { - // @todo @link https://github.com/phpbench/phpbench/issues/304 $sm = clone $this->sm; - $sm->setAlias('factoryAlias2', 'factory1'); } - public function benchSetAliasOverrided() + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchOverrideAlias() + { + $sm = clone $this->sm; + $sm->setAlias('recursiveFactoryAlias1', 'factory1'); + } + + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchSetInvokableClass() { - // @todo @link https://github.com/phpbench/phpbench/issues/304 $sm = clone $this->sm; + $sm->setInvokableClass(BenchAsset\Foo::class, BenchAsset\Foo::class); + } - $sm->setAlias('recursiveFactoryAlias1', 'factory1'); + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchAddDelegator() + { + $sm = clone $this->sm; + $sm->addDelegator(BenchAsset\Foo::class, DelegatorFactoryFoo::class); + } + + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchAddInitializerByClassName() + { + $sm = clone $this->sm; + $sm->addInitializer(BenchAsset\InitializerFoo::class); + } + + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchAddInitializerByInstance() + { + $sm = clone $this->sm; + $sm->addInitializer($this->initializer); + } + + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchAddAbstractFactoryByClassName() + { + $sm = clone $this->sm; + $sm->addAbstractFactory(BenchAsset\AbstractFactoryFoo::class); + } + + /** + * @todo @link https://github.com/phpbench/phpbench/issues/304 + */ + public function benchAddAbstractFactoryByInstance() + { + $sm = clone $this->sm; + $sm->addAbstractFactory($this->abstractFactory); } } diff --git a/src/ServiceManager.php b/src/ServiceManager.php index 1f4272dd..e92228f5 100644 --- a/src/ServiceManager.php +++ b/src/ServiceManager.php @@ -20,6 +20,7 @@ use Zend\ServiceManager\Exception\InvalidArgumentException; use Zend\ServiceManager\Exception\ServiceNotCreatedException; use Zend\ServiceManager\Exception\ServiceNotFoundException; +use Zend\ServiceManager\Factory\InvokableFactory; /** * Service Manager. @@ -76,6 +77,13 @@ class ServiceManager implements ServiceLocatorInterface */ protected $factories = []; + /** + * A list of invokable classes + * + * @var string[]|callable[] + */ + protected $invokables = []; + /** * @var Initializer\InitializerInterface[]|callable[] */ @@ -149,6 +157,19 @@ class ServiceManager implements ServiceLocatorInterface public function __construct(array $config = []) { $this->creationContext = $this; + + if (! empty($this->initializers)) { + // null indicates that $this->initializers + // should be used for configuration + $this->resolveInitializers(null); + } + + if (! empty($this->abstractFactories)) { + // null indicates that $this->abstractFactories + // should be used for configuration + $this->resolveAbstractFactories(null); + } + $this->configure($config); } @@ -233,7 +254,7 @@ public function build($name, array $options = null) public function has($name) { $name = isset($this->resolvedAliases[$name]) ? $this->resolvedAliases[$name] : $name; - $found = isset($this->services[$name]) || isset($this->factories[$name]); + $found = isset($this->services[$name]) || isset($this->factories[$name]) || isset($this->invokables[$name]); if ($found) { return $found; @@ -317,19 +338,8 @@ public function configure(array $config) $this->services = $config['services'] + $this->services; } - if (isset($config['invokables']) && ! empty($config['invokables'])) { - $aliases = $this->createAliasesForInvokables($config['invokables']); - $factories = $this->createFactoriesForInvokables($config['invokables']); - - if (! empty($aliases)) { - $config['aliases'] = (isset($config['aliases'])) - ? array_merge($config['aliases'], $aliases) - : $aliases; - } - - $config['factories'] = (isset($config['factories'])) - ? array_merge($config['factories'], $factories) - : $factories; + if (! empty($config['invokables'])) { + $this->invokables = $config['invokables'] + $this->invokables; } if (isset($config['factories'])) { @@ -370,9 +380,7 @@ public function configure(array $config) if (isset($config['initializers'])) { $this->resolveInitializers($config['initializers']); } - $this->configured = true; - return $this; } @@ -425,7 +433,7 @@ public function setAlias($alias, $target) */ public function setInvokableClass($name, $class = null) { - $this->configure(['invokables' => [$name => $class ?: $name]]); + $this->configure(['invokables' => [ $name => (isset($class) ? $class : $name)]]); } /** @@ -513,8 +521,13 @@ public function setShared($name, $flag) * * @return void */ - private function resolveAbstractFactories(array $abstractFactories) + private function resolveAbstractFactories(array $abstractFactories = null) { + if ($abstractFactories === null) { + $abstractFactories = $this->abstractFactories; + $this->abstractFactories = []; + } + foreach ($abstractFactories as $abstractFactory) { if (is_string($abstractFactory) && class_exists($abstractFactory)) { //Cached string @@ -565,8 +578,12 @@ private function resolveAbstractFactories(array $abstractFactories) * * @return void */ - private function resolveInitializers(array $initializers) + private function resolveInitializers(array $initializers = null) { + if ($initializers === null) { + $initializers = $this->initializers; + $this->initializers = []; + } foreach ($initializers as $initializer) { if (is_string($initializer) && class_exists($initializer)) { $initializer = new $initializer(); @@ -648,39 +665,44 @@ private function resolveNewAliasesWithPreviouslyResolvedAliases(array $aliases) } /** - * Get a factory for the given service name + * Get a factory for the given service name and create an object using + * that factory or create invokable if service is invokable * * @param string $name - * @return callable + * @return object * @throws ServiceNotFoundException */ - private function getFactory($name) + private function createServiceThroughFactory($name, array $options = null) { $factory = isset($this->factories[$name]) ? $this->factories[$name] : null; - $lazyLoaded = false; if (is_string($factory) && class_exists($factory)) { $factory = new $factory(); - $lazyLoaded = true; + if (is_callable($factory)) { + $this->factories[$name] = $factory; + } + return $factory($this->creationContext, $name, $options); } - if (is_callable($factory)) { - if ($lazyLoaded) { - $this->factories[$name] = $factory; + if (! is_callable($factory)) { + if (isset($this->invokables[$name])) { + $invokable = $this->invokables[$name]; + return $options === null ? new $invokable() : new $invokable($options); } + } else { // PHP 5.6 fails on 'class::method' callables unless we explode them: if (PHP_MAJOR_VERSION < 7 && is_string($factory) && strpos($factory, '::') !== false ) { $factory = explode('::', $factory); } - return $factory; + return $factory($this->creationContext, $name, $options); } // Check abstract factories foreach ($this->abstractFactories as $abstractFactory) { if ($abstractFactory->canCreate($this->creationContext, $name)) { - return $abstractFactory; + return $abstractFactory($this->creationContext, $name, $options); } } @@ -699,8 +721,7 @@ private function createDelegatorFromName($name, array $options = null) { $creationCallback = function () use ($name, $options) { // Code is inlined for performance reason, instead of abstracting the creation - $factory = $this->getFactory($name); - return $factory($this->creationContext, $name, $options); + return $this->createServiceThroughFactory($name, $options); }; foreach ($this->delegators[$name] as $index => $delegatorFactory) { @@ -759,9 +780,7 @@ private function doCreate($resolvedName, array $options = null) { try { if (! isset($this->delegators[$resolvedName])) { - // Let's create the service by fetching the factory - $factory = $this->getFactory($resolvedName); - $object = $factory($this->creationContext, $resolvedName, $options); + $object = $this->createServiceThroughFactory($resolvedName, $options); } else { $object = $this->createDelegatorFromName($resolvedName, $options); } @@ -830,52 +849,6 @@ private function createLazyServiceDelegatorFactory() return $this->lazyServicesDelegator; } - /** - * Create aliases for invokable classes. - * - * If an invokable service name does not match the class it maps to, this - * creates an alias to the class (which will later be mapped as an - * invokable factory). - * - * @param array $invokables - * @return array - */ - private function createAliasesForInvokables(array $invokables) - { - $aliases = []; - foreach ($invokables as $name => $class) { - if ($name === $class) { - continue; - } - $aliases[$name] = $class; - } - return $aliases; - } - - /** - * Create invokable factories for invokable classes. - * - * If an invokable service name does not match the class it maps to, this - * creates an invokable factory entry for the class name; otherwise, it - * creates an invokable factory for the entry name. - * - * @param array $invokables - * @return array - */ - private function createFactoriesForInvokables(array $invokables) - { - $factories = []; - foreach ($invokables as $name => $class) { - if ($name === $class) { - $factories[$name] = Factory\InvokableFactory::class; - continue; - } - - $factories[$class] = Factory\InvokableFactory::class; - } - return $factories; - } - /** * Determine if one or more services already exist in the container. * diff --git a/test/CommonServiceLocatorBehaviorsTrait.php b/test/CommonServiceLocatorBehaviorsTrait.php index 2350e090..fb0ba06b 100644 --- a/test/CommonServiceLocatorBehaviorsTrait.php +++ b/test/CommonServiceLocatorBehaviorsTrait.php @@ -562,9 +562,6 @@ public function testPassingInvalidInitializerTypeViaConfigurationRaisesException ]); } - /** - * @covers \Zend\ServiceManager\ServiceManager::getFactory - */ public function testGetRaisesExceptionWhenNoFactoryIsResolved() { $serviceManager = $this->createContainer(); @@ -640,7 +637,6 @@ public function testCanInjectInvokables() $container = $this->createContainer(); $container->setInvokableClass('foo', stdClass::class); $this->assertTrue($container->has('foo')); - $this->assertTrue($container->has(stdClass::class)); $foo = $container->get('foo'); $this->assertInstanceOf(stdClass::class, $foo); } diff --git a/test/PreconfiguredServiceManager.php b/test/PreconfiguredServiceManager.php new file mode 100644 index 00000000..fb88bc94 --- /dev/null +++ b/test/PreconfiguredServiceManager.php @@ -0,0 +1,52 @@ + 'alias2', + 'alias2' => 'service', + ]; + + protected $factories = [ + 'delegator' => SampleFactory::class, + 'factory' => SampleFactory::class, + ]; + + protected $delegators = [ + 'delegator' => [ + PassthroughDelegatorFactory::class, + ], + ]; + + protected $invokables = [ + 'invokable' => stdClass::class, + ]; + + protected $initializers = [ + TaggingInitializer::class, + ]; + + protected $abstractFactories = [ + AbstractFactoryFoo::class, + ]; + + public function __construct(array $config = []) + { + $this->services = [ + 'service' => new stdClass(), + ]; + parent::__construct($config); + } +} diff --git a/test/ServiceManagerTest.php b/test/ServiceManagerTest.php index 8c559946..77d82e44 100644 --- a/test/ServiceManagerTest.php +++ b/test/ServiceManagerTest.php @@ -14,8 +14,12 @@ use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\Factory\InvokableFactory; use Zend\ServiceManager\ServiceManager; +use ZendTest\ServiceManager\TestAsset\Foo; use ZendTest\ServiceManager\TestAsset\InvokableObject; +use ZendTest\ServiceManager\TestAsset\PreconfiguredServiceManager; use ZendTest\ServiceManager\TestAsset\SimpleServiceManager; +use ZendTest\ServiceManager\TestAsset\SampleFactory; +use ZendTest\ServiceManager\TestAsset\TaggingDelegatorFactory; /** * @covers \Zend\ServiceManager\ServiceManager @@ -151,40 +155,6 @@ public function testShareability($sharedByDefault, $serviceShared, $serviceDefin $this->assertEquals($shouldBeSameInstance, $a === $b); } - public function testMapsOneToOneInvokablesAsInvokableFactoriesInternally() - { - $config = [ - 'invokables' => [ - InvokableObject::class => InvokableObject::class, - ], - ]; - - $serviceManager = new ServiceManager($config); - $this->assertAttributeSame([ - InvokableObject::class => InvokableFactory::class, - ], 'factories', $serviceManager, 'Invokable object factory not found'); - } - - public function testMapsNonSymmetricInvokablesAsAliasPlusInvokableFactory() - { - $config = [ - 'invokables' => [ - 'Invokable' => InvokableObject::class, - ], - ]; - - $serviceManager = new ServiceManager($config); - $this->assertAttributeSame([ - 'Invokable' => InvokableObject::class, - ], 'aliases', $serviceManager, 'Alias not found for non-symmetric invokable'); - $this->assertAttributeSame([ - InvokableObject::class => InvokableFactory::class, - ], 'factories', $serviceManager, 'Factory not found for non-symmetric invokable target'); - } - - /** - * @depends testMapsNonSymmetricInvokablesAsAliasPlusInvokableFactory - */ public function testSharedServicesReferencingInvokableAliasShouldBeHonored() { $config = [ @@ -283,4 +253,124 @@ public function testFactoryMayBeStaticMethodDescribedByCallableString() $serviceManager = new SimpleServiceManager($config); $this->assertEquals(stdClass::class, get_class($serviceManager->get(stdClass::class))); } + + public function testMemberBasedAliasConfugrationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + // will be true if $aliases array is properly setup and + // simple alias resolution works + $this->assertTrue($sm->has('alias2')); + $this->assertInstanceOf(stdClass::class, $sm->get('alias2')); + } + + public function testMemberBasedRecursiveAliasConfugrationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + // will be true if $aliases array is properly setup and + // recursive alias resolution works + $this->assertTrue($sm->has('alias1')); + $this->assertInstanceOf(stdClass::class, $sm->get('alias1')); + } + + public function testMemberBasedServiceConfugrationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + // will return true if $services array is properly setup + $this->assertTrue($sm->has('service')); + $this->assertInstanceOf(stdClass::class, $sm->get('service')); + } + + public function testMemberBasedDelegatorConfugrationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + // will be true if factory array is properly setup + $this->assertTrue($sm->has('delegator')); + $this->assertInstanceOf(InvokableObject::class, $sm->get('delegator')); + + // will be true if initializer is present + $this->assertObjectHasAttribute('initializerPresent', $sm->get('delegator')); + } + + public function testMemberBasedFactoryConfugrationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + // will be true if factory array is properly setup + $this->assertTrue($sm->has('factory')); + $this->assertInstanceOf(InvokableObject::class, $sm->get('factory')); + + // will be true if initializer is present + $this->assertObjectHasAttribute('initializerPresent', $sm->get('delegator')); + } + + public function testMemberBasedInvokableConfigurationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + // will succeed if invokable is properly set up + $this->assertTrue($sm->has('invokable')); + $this->assertInstanceOf(stdClass::class, $sm->get('invokable')); + + // will be true if initializer is present + $this->assertObjectHasAttribute('initializerPresent', $sm->get('delegator')); + } + + public function testMemberBasedAbstractFactoryConfigurationGetsApplied() + { + $sm = new PreconfiguredServiceManager(); + + + // will succeed if abstract factory is available + $this->assertTrue($sm->has('foo')); + $this->assertInstanceOf(Foo::class, $sm->get('foo')); + + // will be true if initializer is present + $this->assertObjectHasAttribute('initializerPresent', $sm->get('delegator')); + } + + public function testInvokablesShouldNotOverrideFactoriesAndDelegators() + { + $sm = new ServiceManager([ + 'factories' => [ + // produce InvokableObject + 'factory1' => SampleFactory::class, + 'factory2' => SampleFactory::class, + ], + 'delegators' => [ + 'factory1' => [ + // produce tagged invokable object + TaggingDelegatorFactory::class, + ] + ] + ]); + + $object1 = $sm->build('factory1'); + // assert delegated object is produced by delegator factory + $this->assertObjectHasAttribute('delegatorTag', $object1); + $this->assertInstanceOf(InvokableObject::class, $object1); + + + $object2 = $sm->build('factory2'); + // assert delegated object is produced by SampleFactory + $this->assertObjectNotHasAttribute('delegatorTag', $object2); + $this->assertInstanceOf(InvokableObject::class, $object2); + + $sm->setInvokableClass('factory1', stdClass::class); + $sm->setInvokableClass('factory2', stdClass::class); + + $object1 = $sm->build('factory1'); + // assert delegated object is still produced by delegator factory + $this->assertObjectHasAttribute('delegatorTag', $object1); + $this->assertInstanceOf(InvokableObject::class, $object1); + + $object2 = $sm->build('factory2'); + // assert delegated object is still produced by SampleFactor + // but not by delegator + $this->assertObjectNotHasAttribute('delegatorTag', $object2); + $this->assertInstanceOf(InvokableObject::class, $object2); + } } diff --git a/test/TestAsset/AbstractFactoryFoo.php b/test/TestAsset/AbstractFactoryFoo.php new file mode 100644 index 00000000..4b991515 --- /dev/null +++ b/test/TestAsset/AbstractFactoryFoo.php @@ -0,0 +1,27 @@ +options = $options; + } +} diff --git a/test/TestAsset/PassthroughDelegatorFactory.php b/test/TestAsset/PassthroughDelegatorFactory.php new file mode 100644 index 00000000..ea77170f --- /dev/null +++ b/test/TestAsset/PassthroughDelegatorFactory.php @@ -0,0 +1,28 @@ + 'alias2', + 'alias2' => 'service', + ]; + + protected $factories = [ + 'delegator' => SampleFactory::class, + 'factory' => SampleFactory::class, + ]; + + protected $delegators = [ + 'delegator' => [ + PassthroughDelegatorFactory::class, + ], + ]; + + protected $invokables = [ + 'invokable' => stdClass::class, + ]; + + protected $initializers = [ + TaggingInitializer::class, + ]; + + protected $abstractFactories = [ + AbstractFactoryFoo::class, + ]; + + public function __construct(array $config = []) + { + $this->services = [ + 'service' => new stdClass(), + ]; + parent::__construct($config); + } +} diff --git a/test/TestAsset/TaggingDelegatorFactory.php b/test/TestAsset/TaggingDelegatorFactory.php new file mode 100644 index 00000000..412e5d3e --- /dev/null +++ b/test/TestAsset/TaggingDelegatorFactory.php @@ -0,0 +1,29 @@ +delegatorTag = true; + return $object; + } +} diff --git a/test/TestAsset/TaggingInitializer.php b/test/TestAsset/TaggingInitializer.php new file mode 100644 index 00000000..68199b51 --- /dev/null +++ b/test/TestAsset/TaggingInitializer.php @@ -0,0 +1,22 @@ +initializerPresent = true; + } +}