From 8de7f22e761c90779a302f8dd14dc28821e7e498 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 7 Oct 2015 15:53:28 -0500 Subject: [PATCH] Reconfigure the service manager instead of aggregate config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The various plugin managers are built using configured services — but those services are typically not yet configured at the time the ServiceListener is invoked. As such, we need to re-configure the service manager prior to attempts to create the various plugin managers. Since the logic is the same between merging configuration for the service manager as it is for plugin managers, I have extracted methods for this that each routine can invoke. Finally, I've also added logic to *not* create a plugin manager if we did not receive any configuration for it. --- src/Listener/ServiceListener.php | 152 +++++++++++++----- src/Listener/ServiceListenerInterface.php | 9 +- test/Listener/ServiceListenerTest.php | 64 ++++---- .../TestAsset/SampleAbstractFactory.php | 3 + 4 files changed, 150 insertions(+), 78 deletions(-) diff --git a/src/Listener/ServiceListener.php b/src/Listener/ServiceListener.php index 7dd42d0..cc992ee 100644 --- a/src/Listener/ServiceListener.php +++ b/src/Listener/ServiceListener.php @@ -28,6 +28,13 @@ class ServiceListener implements ServiceListenerInterface */ protected $appServiceConfig = []; + /** + * Service manager post-configuration. + * + * @var ServiceManager + */ + protected $configuredServiceManager; + /** * @var \Zend\Stdlib\CallbackHandler[] */ @@ -113,18 +120,21 @@ public function setApplicationServiceManager($key, $moduleInterface, $method) 'config_key' => $key, 'module_class_interface' => $moduleInterface, 'module_class_method' => $method, - 'configuration' => [ $this->defaultServiceConfig ], + 'configuration' => [ + '__DEFAULT__' => $this->defaultServiceConfig, + ], ]; } /** - * Retrieve configuration aggregated for the application service manager. - * - * @param array + * @inheritDoc */ - public function getServiceManagerConfig() + public function getConfiguredServiceManager() { - return $this->appServiceConfig; + if (! $this->configuredServiceManager) { + return $this->defaultServiceManager; + } + return $this->configuredServiceManager; } /** @@ -189,11 +199,11 @@ public function onLoadModule(ModuleEvent $e) } if (! is_array($config)) { - // If we don't have an array by this point, nothing left to do. + // If we do not have an array by this point, nothing left to do. continue; } - // We're keeping track of which modules provided which configuration to which service managers. + // We are keeping track of which modules provided which configuration to which service managers. // The actual merging takes place later. Doing it this way will enable us to provide more powerful // debugging tools for showing which modules overrode what. $fullname = $e->getModuleName() . '::' . $sm['module_class_method'] . '()'; @@ -216,32 +226,19 @@ public function onLoadModulesPost(ModuleEvent $e) { $configListener = $e->getConfigListener(); $config = $configListener->getMergedConfig(false); - - $appServiceConfig = []; - $pluginManagers = []; + $pluginManagers = []; + $serviceManager = $this->configureServiceManager($config); foreach ($this->serviceManagers as $key => $sm) { - if (isset($config[$sm['config_key']]) - && is_array($config[$sm['config_key']]) - && !empty($config[$sm['config_key']]) - ) { - $this->serviceManagers[$key]['configuration']['merged_config'] = $config[$sm['config_key']]; + if ($key === self::IS_APP_MANAGER) { + // Already completed. + continue; } - // Merge all of the things! - $smConfig = []; - foreach ($this->serviceManagers[$key]['configuration'] as $name => $configs) { - if (isset($configs['configuration_classes'])) { - foreach ($configs['configuration_classes'] as $class) { - $configs = ArrayUtils::merge($configs, $this->serviceConfigToArray($class)); - } - } - $smConfig = ArrayUtils::merge($smConfig, $configs); - } + $serviceConfig = $this->mergeServiceConfiguration($key, $sm, $config); - // If this is for the application service manager, we're done. - if ($key === self::IS_APP_MANAGER) { - $appServiceConfig = $smConfig; + // Nothing to do? move to the next + if (empty($serviceConfig)) { continue; } @@ -249,15 +246,8 @@ public function onLoadModulesPost(ModuleEvent $e) // // Use the build method, so that we can pass the configuration, but // also so we can prevent caching it in the SM instance itself. - $instance = $this->defaultServiceManager->build($sm['service_manager'], $smConfig); - - if (! $instance instanceof ServiceManager) { - throw new Exception\RuntimeException(sprintf( - 'Instance returned for %s is not a valid service or plugin manager; received instance of %s', - $sm['service_manager'], - (is_object($instance) ? get_class($instance) : gettype($instance)) - )); - } + $instance = $serviceManager->build($sm['service_manager'], $serviceConfig); + $this->validateServiceManager($instance, $sm['service_manager']); // Map the configuration key (and class name, if it differs) to the instance. $pluginManagers[$key] = $instance; @@ -266,14 +256,13 @@ public function onLoadModulesPost(ModuleEvent $e) } } - // Register plugin managers as services in the application configuration. - if (! isset($appServiceConfig['services'])) { - $appServiceConfig['services'] = []; + // Register plugin managers as services in the application service manager + if (! empty($pluginManagers)) { + $serviceManager = $serviceManager->withConfig(['services' => $pluginManagers]); } - $appServiceConfig['services'] = array_merge($appServiceConfig['services'], $pluginManagers); - // Set the application service manager configuration - $this->appServiceConfig = (new ServiceConfig($appServiceConfig))->toArray(); + // Set the configured application service manager instance + $this->configuredServiceManager = $serviceManager; } /** @@ -302,4 +291,79 @@ protected function serviceConfigToArray($config) return $config->toArray(); } + + /** + * Configure the service manager. + * + * If we've indicated we want to set the application service manager config + * metadata, then we will see if any was provided in the merged + * configuration, and merge it with any default service configuration we + * have in the instance to return a new service manager instance. + * + * @param array $config + * @return ServiceManager + */ + private function configureServiceManager(array $config) + { + if (! isset($this->serviceManagers[self::IS_APP_MANAGER])) { + return $this->defaultServiceManager->withConfig(['services' => [ + 'config' => $config, + ]]); + } + + $services = $this->defaultServiceManager; + $metadata = $this->serviceManagers[self::IS_APP_MANAGER]; + + $serviceConfig = $this->mergeServiceConfiguration(self::IS_APP_MANAGER, $metadata, $config); + $serviceConfig['services']['config'] = $config; + + return $services->withConfig($serviceConfig); + } + + /** + * Merge all configuration for a given service manager to a single array. + * + * @param string $key Named service manager + * @param array $metadata Service manager metadata + * @param array $config Merged configuration + * @return array Service manager-specific configuration + */ + private function mergeServiceConfiguration($key, array $metadata, array $config) + { + if (isset($config[$metadata['config_key']]) + && is_array($config[$metadata['config_key']]) + && !empty($config[$metadata['config_key']]) + ) { + $this->serviceManagers[$key]['configuration']['merged_config'] = $config[$metadata['config_key']]; + } + + // Merge all of the things! + $serviceConfig = []; + foreach ($this->serviceManagers[$key]['configuration'] as $name => $configs) { + if (isset($configs['configuration_classes'])) { + foreach ($configs['configuration_classes'] as $class) { + $configs = ArrayUtils::merge($configs, $this->serviceConfigToArray($class)); + } + } + $serviceConfig = ArrayUtils::merge($serviceConfig, $configs); + } + + return $serviceConfig; + } + + /** + * Ensure the returned service manager is actually a service manager. + * + * @throws Exception\RuntimeException for invalid service managers. + */ + private function validateServiceManager($instance, $name) + { + if (! $instance instanceof ServiceManager) { + throw new Exception\RuntimeException(sprintf( + 'Instance returned for %s is not a valid service or plugin manager; received instance of %s', + $name, + (is_object($instance) ? get_class($instance) : gettype($instance)) + )); + } + } } diff --git a/src/Listener/ServiceListenerInterface.php b/src/Listener/ServiceListenerInterface.php index 97d492d..3fa8c35 100644 --- a/src/Listener/ServiceListenerInterface.php +++ b/src/Listener/ServiceListenerInterface.php @@ -41,14 +41,11 @@ public function addServiceManager($serviceManager, $key, $moduleInterface, $meth public function setApplicationServiceManager($key, $moduleInterface, $method); /** - * Retrieve the aggregated configuration for the application service manager. + * Retrieve the application service manager instance post-configuration. * - * The array returned must be valid for passing to the service manager's - * constructor or withConfig() method. - * - * @return array + * @return ServiceManager */ - public function getServiceManagerConfig(); + public function getConfiguredServiceManager(); /** * @param array $configuration diff --git a/test/Listener/ServiceListenerTest.php b/test/Listener/ServiceListenerTest.php index b380912..bf13769 100644 --- a/test/Listener/ServiceListenerTest.php +++ b/test/Listener/ServiceListenerTest.php @@ -20,7 +20,6 @@ use Zend\ModuleManager\ModuleEvent; use Zend\ServiceManager\Config as ServiceConfig; use Zend\ServiceManager\ServiceManager; -use Zend\Stdlib\ArrayUtils; use ZendTest\ModuleManager\EventManagerIntrospectionTrait; /** @@ -83,7 +82,7 @@ public function testPassingInvalidModuleDoesNothing() $this->event->setModule($module); $this->listener->onLoadModule($this->event); - $this->assertEquals([], $this->listener->getServiceManagerConfig()); + $this->assertSame($this->services, $this->listener->getConfiguredServiceManager()); } public function testInvalidReturnFromModuleDoesNothing() @@ -92,14 +91,16 @@ public function testInvalidReturnFromModuleDoesNothing() $this->event->setModule($module); $this->listener->onLoadModule($this->event); - $this->assertEquals([], $this->listener->getServiceManagerConfig()); + $this->assertSame($this->services, $this->listener->getConfiguredServiceManager()); } public function getServiceConfig() { // @codingStandardsIgnoreStart return [ - 'invokables' => [__CLASS__ => __CLASS__], + 'invokables' => [ + __CLASS__ => __CLASS__ + ], 'factories' => [ 'foo' => function ($sm) { }, ], @@ -119,9 +120,16 @@ public function getServiceConfig() public function assertServiceManagerConfiguration() { - $expected = ArrayUtils::merge($this->defaultServiceConfig, $this->getServiceConfig()); $this->listener->onLoadModulesPost($this->event); - $this->assertEquals($expected, $this->listener->getServiceManagerConfig()); + $services = $this->listener->getConfiguredServiceManager(); + $this->assertNotSame($this->services, $services); + $this->assertInstanceOf(ServiceManager::class, $services); + + $this->assertTrue($services->has(__CLASS__)); + $this->assertTrue($services->has('foo')); + $this->assertTrue($services->has('bar')); + $this->assertFalse($services->has('resolved-by-abstract')); + $this->assertTrue($services->has('resolved-by-abstract', true)); } public function testModuleReturningArrayConfiguresServiceManager() @@ -130,6 +138,7 @@ public function testModuleReturningArrayConfiguresServiceManager() $module = new TestAsset\ServiceProviderModule($config); $this->event->setModule($module); $this->listener->onLoadModule($this->event); + $services = $this->listener->getConfiguredServiceManager(); $this->assertServiceManagerConfiguration(); } @@ -145,7 +154,10 @@ public function testModuleReturningTraversableConfiguresServiceManager() public function testModuleServiceConfigOverridesGlobalConfig() { - $defaultConfig = ['aliases' => ['foo' => 'bar']]; + $defaultConfig = ['aliases' => ['foo' => 'bar'], 'services' => [ + 'bar' => new stdClass(), + 'baz' => new stdClass(), + ]]; $this->listener = new ServiceListener($this->services, $defaultConfig); $this->listener->setApplicationServiceManager( 'service_manager', @@ -158,13 +170,13 @@ public function testModuleServiceConfigOverridesGlobalConfig() $this->event->setModuleName(__NAMESPACE__ . '\TestAsset\ServiceProvider'); $this->listener->onLoadModule($this->event); $this->listener->onLoadModulesPost($this->event); - $expected = ArrayUtils::merge($defaultConfig, $config); - $expected = (new ServiceConfig($expected))->toArray(); - $this->assertEquals( - $expected, - $this->listener->getServiceManagerConfig(), - 'Default configuration was not overridden' - ); + + $services = $this->listener->getConfiguredServiceManager(); + $this->assertNotSame($this->services, $services); + $this->assertTrue($services->has('config')); + $this->assertTrue($services->has('foo')); + $this->assertNotSame($services->get('foo'), $services->get('bar')); + $this->assertSame($services->get('foo'), $services->get('baz')); } public function testModuleReturningServiceConfigConfiguresServiceManager() @@ -251,13 +263,11 @@ public function testCreatesPluginManagerBasedOnModuleImplementingSpecifiedProvid $listener->onLoadModulesPost($this->event); $this->assertEquals($pluginConfig, $received); - $serviceConfig = $listener->getServiceManagerConfig(); - $this->assertArrayHasKey('services', $serviceConfig); - $this->assertArrayHasKey('CustomPluginManager', $serviceConfig['services']); - $this->assertInstanceOf( - TestAsset\CustomPluginManager::class, - $serviceConfig['services']['CustomPluginManager'] - ); + $configuredServices = $listener->getConfiguredServiceManager(); + $this->assertNotSame($services, $configuredServices); + $this->assertTrue($configuredServices->has('CustomPluginManager')); + $plugins = $configuredServices->get('CustomPluginManager'); + $this->assertInstanceOf(TestAsset\CustomPluginManager::class, $plugins); } public function testCreatesPluginManagerBasedOnModuleDuckTypingSpecifiedProviderInterface() @@ -285,13 +295,11 @@ public function testCreatesPluginManagerBasedOnModuleDuckTypingSpecifiedProvider $listener->onLoadModulesPost($this->event); $this->assertEquals($pluginConfig, $received); - $serviceConfig = $listener->getServiceManagerConfig(); - $this->assertArrayHasKey('services', $serviceConfig); - $this->assertArrayHasKey('CustomPluginManager', $serviceConfig['services']); - $this->assertInstanceOf( - TestAsset\CustomPluginManager::class, - $serviceConfig['services']['CustomPluginManager'] - ); + $configuredServices = $listener->getConfiguredServiceManager(); + $this->assertNotSame($services, $configuredServices); + $this->assertTrue($configuredServices->has('CustomPluginManager')); + $plugins = $configuredServices->get('CustomPluginManager'); + $this->assertInstanceOf(TestAsset\CustomPluginManager::class, $plugins); } public function testAttachesListenersAtExpectedPriorities() diff --git a/test/Listener/TestAsset/SampleAbstractFactory.php b/test/Listener/TestAsset/SampleAbstractFactory.php index 904c613..34652fe 100644 --- a/test/Listener/TestAsset/SampleAbstractFactory.php +++ b/test/Listener/TestAsset/SampleAbstractFactory.php @@ -10,15 +10,18 @@ namespace ZendTest\ModuleManager\Listener\TestAsset; use Interop\Container\ContainerInterface; +use stdClass; use Zend\ServiceManager\Factory\AbstractFactoryInterface; class SampleAbstractFactory implements AbstractFactoryInterface { public function canCreateServiceWithName(ContainerInterface $container, $name) { + return true; } public function __invoke(ContainerInterface $container, $name, array $options = []) { + return new stdClass; } }