Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Commit

Permalink
Reconfigure the service manager instead of aggregate config
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
weierophinney committed Oct 7, 2015
1 parent 407c96d commit 8de7f22
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 78 deletions.
152 changes: 108 additions & 44 deletions src/Listener/ServiceListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class ServiceListener implements ServiceListenerInterface
*/
protected $appServiceConfig = [];

/**
* Service manager post-configuration.
*
* @var ServiceManager
*/
protected $configuredServiceManager;

/**
* @var \Zend\Stdlib\CallbackHandler[]
*/
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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'] . '()';
Expand All @@ -216,48 +226,28 @@ 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;
}

// Create the plugin manager instance.
//
// 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;
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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))
));
}
}
}
9 changes: 3 additions & 6 deletions src/Listener/ServiceListenerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 36 additions & 28 deletions test/Listener/ServiceListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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()
Expand All @@ -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) { },
],
Expand All @@ -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()
Expand All @@ -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();
}

Expand All @@ -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',
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions test/Listener/TestAsset/SampleAbstractFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

0 comments on commit 8de7f22

Please sign in to comment.