diff --git a/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php index ee31e7f..1e9ff04 100644 --- a/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php @@ -12,11 +12,11 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; /** - * Provides an interface compatible alternative to the default Symfony - * dependency injection container specially optimized for Drupal's needs. + * Provides an interface-compatible alternative to the default Symfony + * dependency injection container optimized for Drupal's needs. * * This container is based on a human-readable PHP array container definition - * with a structure very similar to the YAML file format. + * with a structure very similar to the YAML container definition. * * @see \Drupal\Core\DependencyInjection\Container * @see \Drupal\Core\DependencyInjection\Dumper\DebugPhpArrayDumper @@ -31,10 +31,11 @@ class BootstrapContainer extends Container { */ public function __construct(array $container_definition = array(), $frozen = FALSE) { if (isset($container_definition['machine_format']) && $container_definition['machine_format'] === TRUE) { - throw new InvalidArgumentException('The machine format is not supported by this class. Use a human-readable format instead.'); + throw new InvalidArgumentException('The machine format is not supported by this class. Use a human-readable format instead, e.g. as produced by \Drupal\Core\DependencyInjection\Dumper\PhpArrayDumper.'); } - // Do not call the parents constructor as it would bail on the machine format. + // Do not call the parents constructor as it would bail on the machine + // format. $this->aliases = isset($container_definition['aliases']) ? $container_definition['aliases'] : array(); $this->parameters = isset($container_definition['parameters']) ? $container_definition['parameters'] : array(); $this->serviceDefinitions = isset($container_definition['services']) ? $container_definition['services'] : array(); @@ -48,8 +49,14 @@ public function __construct(array $container_definition = array(), $frozen = FAL * {@inheritdoc} */ protected function createService($definition, $id) { + // This method is a verbatim copy of + // \Drupal\Core\DependencyInjection\Container::createService + // except for the following difference: + // - There are no instanceof checks on \stdClass, which are used in the + // parent class to avoid resolving services and parameters when it is + // known from dumping that there is nothing to resolve. if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) { - throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id)); + throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The service container does not know how to construct this service. The service will need to be set before it is first used.', $id)); } $arguments = array(); @@ -90,6 +97,8 @@ protected function createService($definition, $id) { $length = count($arguments); + // Optimize class instantiation for services with up to 10 parameters as + // ReflectionClass is noticeable slow. switch ($length) { case 0: $service = new $class(); @@ -162,10 +171,10 @@ protected function createService($definition, $id) { } if (!is_callable($callable)) { - throw new InvalidArgumentException(sprintf('The configure callable for class "%s" is not a callable.', get_class($service))); + throw new InvalidArgumentException(sprintf('The configurator for class "%s" is not a callable.', get_class($service))); } - call_user_func_array($callable, $service); + call_user_func($callable, $service); } return $service; @@ -175,25 +184,40 @@ protected function createService($definition, $id) { * {@inheritdoc} */ protected function resolveServicesAndParameters($arguments) { - // Process the arguments. + // This method is different from the parent method only for the following cases: + // - A service is denoted by '@service' and not by a \stdClass object. + // - A parameter is denoted by '%parameter%' and not by a \stdClass object. + // - Arguments are traversed recursively, there is no information how deep + // to traverse. + foreach ($arguments as $key => $argument) { - // Optimized code path. if ($argument instanceof \stdClass) { $type = $argument->type; - // Create private service. + // Private services are a special flavor: In case a private service is + // only used by one other service, the ContainerBuilder uses a + // Definition object as argument, which does not have a id set. + // Therefore the format uses a \stdClass object to store the definition + // and to be able to create the service on the fly. + // + // Note: When constructing a private service by hand, 'id' must be set. + // The PhpArrayDumper just uses the hash of the private service + // definition to generate a unique id. + // + // @see \Drupal\Core\DependecyInjection\Dumper\OptimizedPhpArrayDumper::getPrivateServiceCall + // if ($type == 'private_service') { $id = $argument->id; - // Does the private service already exist. - if (isset($this->privateServices[$id])) { + // Check if the private service already exists - in case it is shared. + if (!empty($argument->shared) && isset($this->privateServices[$id])) { $arguments[$key] = $this->privateServices[$id]; continue; } - // Create the private service. + // Create a private service from a service definition. $arguments[$key] = $this->createService($argument->value, $id); - if ($argument->shared) { + if (!empty($argument->shared)) { $this->privateServices[$id] = $arguments[$key]; } @@ -219,7 +243,7 @@ protected function resolveServicesAndParameters($arguments) { $name = substr($argument, 1, -1); if (!isset($this->parameters[$name])) { $arguments[$key] = $this->getParameter($name); - continue; + // This can never be reached as getParameter() throws an Exception. } $argument = $this->parameters[$name]; $arguments[$key] = $argument; diff --git a/core/lib/Drupal/Core/DependencyInjection/Container.php b/core/lib/Drupal/Core/DependencyInjection/Container.php index 8fae55a..350d6e1 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Container.php +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php @@ -20,15 +20,15 @@ /** * Provides an interface compatible alternative to the default Symfony - * dependency injection container specially optimized for Drupal's needs. + * dependency injection container optimized for Drupal's needs. * - * This container is based on a PHP array container definition with a structure - * very similar to the YAML file format. + * This container is based on a PHP array container definition dumped as + * a performance-optimized machine-readable format. * * The best way to initialize this container is to use a * \Drupal\Core\DependencyInjection\ContainerBuilder, compile it and then * retrieve the definition via - * \Drupal\Core\DependencyInjection\Dumper/PhpArray::getArray(). + * \Drupal\Core\DependencyInjection\Dumper\OptimizedPhpArrayDumper::getArray(). * * The retrieved array can be cached safely and then passed to this container * via the constructor. @@ -46,7 +46,7 @@ * getAServiceWithAnIdByCamelCase(). * - Scopes are explicitly not allowed, because Symfony 3.0 has deprecated * them. - * - Synchronized services are explicitly not allowed, because Symfony 3.0 has + * - Synchronized services are explicitly not supported, because Symfony 3.0 has * deprecated them. * * Drupal specifically does not allow serializing the container and any service @@ -121,7 +121,7 @@ class Container implements IntrospectableContainerInterface { */ public function __construct(array $container_definition = array(), $frozen = FALSE) { if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) { - throw new InvalidArgumentException('The human readable format is not supported by this class. Use a machine-readable format instead.'); + throw new InvalidArgumentException('The non-optimized format is not supported by this class. Use an optimized machine-readable format instead, e.g. as produced by \Drupal\Core\DependencyInjection\Dumper\OptimizedPhpArrayDumper.'); } $this->aliases = isset($container_definition['aliases']) ? $container_definition['aliases'] : array(); @@ -161,7 +161,8 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV } // In case something else than ContainerInterface::NULL_ON_INVALID_REFERENCE - // is used, the actual wanted behavior is to re-try getting the service at a later point. + // is used, the actual wanted behavior is to re-try getting the service at + // a later point. if (!$definition) { return; } @@ -198,9 +199,23 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV return $service; } + /** + * Creates a service from a service definition. + * + * @param array $definition + * The service definition to create a service from. + * @param string $id + * The service identifier, neccessary so it can be shared if its public. + * + * @return object + * The service described by the service definition. + * + * @throws Symfony\Component\DependencyInjection\Exception\RuntimeException\RuntimeException When the service is a synthetic service. + * @throws Symfony\Component\DependencyInjection\Exception\InvalidArgumentException When the configurator callable is not callable. + */ protected function createService($definition, $id) { if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) { - throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id)); + throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The service container does not know how to construct this service. The service will need to be set before it is first used.', $id)); } $arguments = array(); @@ -243,6 +258,8 @@ protected function createService($definition, $id) { $class = $this->frozen ? $definition['class'] : current($this->resolveServicesAndParameters(array($definition['class']))); $length = isset($definition['arguments_count']) ? $definition['arguments_count'] : count($arguments); + // Optimize class instantiation for services with up to 10 parameters as + // ReflectionClass is noticeable slow. switch ($length) { case 0: $service = new $class(); @@ -319,10 +336,10 @@ protected function createService($definition, $id) { } if (!is_callable($callable)) { - throw new InvalidArgumentException(sprintf('The configure callable for class "%s" is not a callable.', get_class($service))); + throw new InvalidArgumentException(sprintf('The configurator for class "%s" is not a callable.', get_class($service))); } - call_user_func_array($callable, $service); + call_user_func($callable, $service); } return $service; @@ -386,9 +403,7 @@ public function initialized($id) { } /** - * Resolves arguments from %parameter and @service to the real values. - * - * For performance reasons also a \stdClass based approach is supported. + * Resolves arguments from \stdClass objects to the real values. * * @param array|\stdClass $arguments * The arguments to resolve. @@ -414,7 +429,8 @@ protected function resolveServicesAndParameters($arguments) { // Process the arguments. foreach ($arguments as $key => $argument) { - // Optimized code path. + // For this machine-optimized format, only \stdClass arguments are + // processed and resolved. All other values are kept as is. if ($argument instanceof \stdClass) { $type = $argument->type; @@ -423,7 +439,7 @@ protected function resolveServicesAndParameters($arguments) { $name = $argument->name; if (!isset($this->parameters[$name])) { $arguments[$key] = $this->getParameter($name); - continue; + // This can never be reached as getParameter() throws an Exception. } // Update argument @@ -526,35 +542,35 @@ protected function getAlternatives($id) { * {@inheritdoc} */ public function enterScope($name) { - throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__)); + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); } /** * {@inheritdoc} */ public function leaveScope($name) { - throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__)); + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); } /** * {@inheritdoc} */ public function addScope(ScopeInterface $scope) { - throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__)); + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); } /** * {@inheritdoc} */ public function hasScope($name) { - throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__)); + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); } /** * {@inheritdoc} */ public function isScopeActive($name) { - throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__)); + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php b/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php index 0f6326c..9bef32c 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php @@ -27,8 +27,7 @@ * It is human readable, for a machine optimized version based on this one see * \Drupal\Core\DependencyInjection\Dumper\PhpArrayDumper. */ -class DebugPhpArrayDumper extends PhpArrayDumper -{ +class DebugPhpArrayDumper extends PhpArrayDumper { /** * {@inheritdoc} @@ -53,17 +52,17 @@ protected function dumpCollection($collection, &$resolve = FALSE) { */ protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { - return sprintf('@?%s', $id); + return '@?' . $id; } - return sprintf('@%s', $id); + return '@' . $id; } /** * {@inheritdoc} */ protected function getParameterCall($name) { - return sprintf('%%%s%%', $name); + return '%' . $name . '%'; } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php index 71a0937..9b2a637 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php @@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\Dumper; @@ -27,8 +28,7 @@ * It is machine readable, for a human readable version based on this one see * \Drupal\Core\DependencyInjection\Dumper\DebugPhpArrayDumper. */ -class PhpArrayDumper extends Dumper -{ +class PhpArrayDumper extends Dumper { /** * {@inheritdoc} */ @@ -52,7 +52,7 @@ public function getArray() { } /** - * Returns the aliases as a PHP Array. + * Returns the aliases as a PHP array. * * @return array * The aliases. @@ -212,11 +212,11 @@ protected function getServiceDefinition($definition) { } if (($scope = $definition->getScope()) !== ContainerInterface::SCOPE_CONTAINER) { - throw new \InvalidArgumentException("The 'scope' definition is deprecated in Symfony 3.0 and not supported by Drupal 8."); + throw new InvalidArgumentException("The 'scope' definition is deprecated in Symfony 3.0 and not supported by Drupal 8."); } if (($decorated = $definition->getDecoratedService()) !== NULL) { - throw new \InvalidArgumentException("The 'decorated' definition is not supported by the Drupal 8 run-time container. The Container Builder should have resolved that during the DecoratorServicePass compiler pass."); + throw new InvalidArgumentException("The 'decorated' definition is not supported by the Drupal 8 run-time container. The Container Builder should have resolved that during the DecoratorServicePass compiler pass."); } if ($callable = $definition->getFactory()) { diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index c00adc1..258e49e 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -79,7 +79,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { ]; /** - * Holds the boostrap container instance. + * Holds the bootstrap container. * * @var \Symfony\Component\DependencyInjection\ContainerInterface */ @@ -792,8 +792,11 @@ protected function initializeContainer() { try { $cache = $this->bootstrapContainer->get('cache.container')->get($this->getCacheKey()); } catch (\Exception $e) { - // Ignore exceptions thrown by the database. In case it is really not - // available, it will fail during container compilation. + // In the InstallerKernel the database is not available at this point. + // Not being able to load from cache is not a fatal condition, hence + // the error is ignored here. + // If the database is really not available, then it will fatal during + // service construction. $cache = FALSE; } if ($cache) { @@ -1229,6 +1232,9 @@ protected function cacheDrupalContainer($container_definition) { $this->bootstrapContainer->get('cache.container')->set($this->getCacheKey(), $container_definition); } catch (\Exception $e) { + // There is no way to get from the Cache API if the cache set was + // successful or not, hence an Exception is caught and the caller + // informed about the error condition. $saved = FALSE; } diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/DebugPhpArrayDumperTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/DebugPhpArrayDumperTest.php new file mode 100644 index 0000000..91e9491 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/DebugPhpArrayDumperTest.php @@ -0,0 +1,52 @@ +machineFormat = FALSE; + $this->dumperClass = '\Drupal\Core\DependencyInjection\Dumper\DebugPhpArrayDumper'; + parent::setUp(); + } + + /** + * Helper function to return a service definition. + */ + protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + return sprintf('@?%s', $id); + } + + return sprintf('@%s', $id); + } + + /** + * Helper function to return a service definition. + */ + protected function getParameterCall($name) { + return '%' . $name . '%'; + } + + /** + * Helper function to return a machine readable '@notation'. + */ + protected function getCollection($collection, $resolve = TRUE) { + return $collection; + } + +} diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/PhpArrayDumperTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/PhpArrayDumperTest.php new file mode 100644 index 0000000..81d26cb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/PhpArrayDumperTest.php @@ -0,0 +1,579 @@ +containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder'); + $this->containerBuilder->getAliases()->willReturn(array()); + $this->containerBuilder->getParameterBag()->willReturn(new ParameterBag()); + $this->containerBuilder->getDefinitions()->willReturn(NULL); + + $definition = array(); + $definition['aliases'] = array(); + $definition['parameters'] = array(); + $definition['services'] = array(); + $definition['machine_format'] = $this->machineFormat; + + $this->containerDefinition = $definition; + + // Create the dumper. + $this->dumper = new $this->dumperClass($this->containerBuilder->reveal()); + } + + /** + * Tests that an empty container works properly. + * + * @covers ::dump + * @covers ::getArray + * @covers ::getMachineFormat + */ + public function test_dump() { + $serialized_definition = $this->dumper->dump(); + $this->assertEquals(serialize($this->containerDefinition), $serialized_definition); + } + + /** + * Tests that alias processing works properly. + * + * @covers ::getAliases + * + * @dataProvider getAliasesDataProvider + */ + public function test_getAliases($aliases, $definition_aliases) { + $this->containerDefinition['aliases'] = $definition_aliases; + $this->containerBuilder->getAliases()->willReturn($aliases); + $this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.'); + } + + /** + * Data provider for test_getAliases(). + * + * @return array[] + * Returns data-set elements with: + * - aliases as returned by ContainerBuilder. + * - aliases as expected in the container definition. + */ + public function getAliasesDataProvider() { + return array( + array(array(), array()), + array( + array('foo' => 'foo.alias'), + array('foo' => 'foo.alias'), + ), + array( + array('foo' => 'foo.alias', 'foo.alias' => 'foo.alias.alias'), + array('foo' => 'foo.alias.alias', 'foo.alias' => 'foo.alias.alias'), + ), + ); + } + + /** + * Tests that parameter processing works properly. + * + * @covers ::getParameters + * @covers ::prepareParameters + * @covers ::escape + * @covers ::dumpValue + * @covers ::getReferenceCall + * + * @dataProvider getParametersDataProvider + */ + public function test_getParameters($parameters, $definition_parameters, $is_frozen) { + $this->containerDefinition['parameters'] = $definition_parameters; + + $parameter_bag = new ParameterBag($parameters); + $this->containerBuilder->getParameterBag()->willReturn($parameter_bag); + $this->containerBuilder->isFrozen()->willReturn($is_frozen); + + if (isset($parameters['reference'])) { + $definition = new Definition('\stdClass'); + $this->containerBuilder->getDefinition('referenced_service')->willReturn($definition); + } + + $this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.'); + } + + /** + * Data provider for test_getParameters(). + * + * @return array[] + * Returns data-set elements with: + * - parameters as returned by ContainerBuilder. + * - parameters as expected in the container definition. + * - frozen value + */ + public function getParametersDataProvider() { + return array( + array(array(), array(), TRUE), + array( + array('foo' => 'value_foo'), + array('foo' => 'value_foo'), + TRUE, + ), + array( + array('foo' => array('llama' => 'yes')), + array('foo' => array('llama' => 'yes')), + TRUE, + ), + array( + array('foo' => '%llama%', 'llama' => 'yes'), + array('foo' => '%%llama%%', 'llama' => 'yes'), + TRUE, + ), + array( + array('foo' => '%llama%', 'llama' => 'yes'), + array('foo' => '%llama%', 'llama' => 'yes'), + FALSE, + ), + array( + array('reference' => new Reference('referenced_service')), + array('reference' => $this->getServiceCall('referenced_service')), + TRUE, + ), + ); + } + + /** + * Tests that service processing works properly. + * + * @covers ::getServiceDefinitions + * @covers ::getServiceDefinition + * @covers ::dumpMethodCalls + * @covers ::dumpCollection + * @covers ::dumpCallable + * @covers ::dumpValue + * @covers ::getPrivateServiceCall + * @covers ::getReferenceCall + * @covers ::getServiceCall + * @covers ::getParameterCall + * + * @dataProvider getDefinitionsDataProvider + */ + public function test_getServiceDefinitions($services, $definition_services) { + $this->containerDefinition['services'] = $definition_services; + + $this->containerBuilder->getDefinitions()->willReturn($services); + + $bar_definition = new Definition('\stdClass'); + $this->containerBuilder->getDefinition('bar')->willReturn($bar_definition); + + $private_definition = new Definition('\stdClass'); + $private_definition->setPublic(FALSE); + + $this->containerBuilder->getDefinition('private_definition')->willReturn($private_definition); + + $this->assertEquals($this->containerDefinition, $this->dumper->getArray(), 'Expected definition matches dump.'); + } + + /** + * Data provider for test_getServiceDefinitions(). + * + * @return array[] + * Returns data-set elements with: + * - parameters as returned by ContainerBuilder. + * - parameters as expected in the container definition. + * - frozen value + */ + public function getDefinitionsDataProvider() { + $base_service_definition = array( + 'class' => '\stdClass', + 'public' => TRUE, + 'file' => FALSE, + 'synthetic' => FALSE, + 'lazy' => FALSE, + 'arguments' => array(), + 'arguments_count' => 0, + 'properties' => array(), + 'calls' => array(), + 'factory' => FALSE, + 'configurator' => FALSE, + ); + + // Test basic flags. + $service_definitions[] = array( + ) + $base_service_definition; + + $service_definitions[] = array( + 'public' => FALSE, + ) + $base_service_definition; + + $service_definitions[] = array( + 'file' => 'test_include.php', + ) + $base_service_definition; + + $service_definitions[] = array( + 'synthetic' => TRUE, + ) + $base_service_definition; + + $service_definitions[] = array( + 'lazy' => TRUE, + ) + $base_service_definition; + + // Test a basic public Reference. + $service_definitions[] = array( + 'arguments' => array( 'foo', new Reference('bar')), + 'arguments_count' => 2, + 'arguments_expected' => $this->getCollection(array('foo', $this->getServiceCall('bar'))), + ) + $base_service_definition; + + // Test a public reference that should not throw an Exception. + $reference = new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $service_definitions[] = array( + 'arguments' => array($reference), + 'arguments_count' => 1, + 'arguments_expected' => $this->getCollection(array($this->getServiceCall('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE))), + ) + $base_service_definition; + + + // Test a private shared servied, denoted by having a Reference. + $private_definition = array( + 'class' => '\stdClass', + 'public' => FALSE, + 'arguments_count' => 0, + ); + + $service_definitions[] = array( + 'arguments' => array( 'foo', new Reference('private_definition')), + 'arguments_count' => 2, + 'arguments_expected' => $this->getCollection(array('foo', $this->getPrivateServiceCall('private_definition', $private_definition, TRUE ))), + ) + $base_service_definition; + + // Test a private non-shared service, denoted by having a Definition. + $private_definition_object = new Definition('\stdClass'); + $private_definition_object->setPublic(FALSE); + + $service_definitions[] = array( + 'arguments' => array( 'foo', $private_definition_object), + 'arguments_count' => 2, + 'arguments_expected' => $this->getCollection(array('foo', $this->getPrivateServiceCall(NULL, $private_definition))), + ) + $base_service_definition; + + // Test a deep collection without a reference. + $service_definitions[] = array( + 'arguments' => array(array(array('foo'))), + 'arguments_count' => 1, + ) + $base_service_definition; + + // Test a deep collection with a reference to resolve. + $service_definitions[] = array( + 'arguments' => array(array(new Reference('bar'))), + 'arguments_count' => 1, + 'arguments_expected' => $this->getCollection(array($this->getCollection(array($this->getServiceCall('bar'))))), + ) + $base_service_definition; + + // Test a collection with a variable to resolve. + $service_definitions[] = array( + 'arguments' => array(new Parameter('llama_parameter')), + 'arguments_count' => 1, + 'arguments_expected' => $this->getCollection(array($this->getParameterCall('llama_parameter'))), + ) + $base_service_definition; + + // Test objects that have _serviceId property. + $drupal_service = new \stdClass(); + $drupal_service->_serviceId = 'bar'; + + $service_definitions[] = array( + 'arguments' => array($drupal_service), + 'arguments_count' => 1, + 'arguments_expected' => $this->getCollection(array($this->getServiceCall('bar'))), + ) + $base_service_definition; + + // Test getMethodCalls. + $calls = array( + array('method', $this->getCollection(array())), + array('method2', $this->getCollection(array())), + ); + $service_definitions[] = array( + 'calls' => $calls, + ) + $base_service_definition; + + // Test factory. + $service_definitions[] = array( + 'factory' => array(new Reference('bar'), 'factoryMethod'), + 'factory_expected' => array($this->getServiceCall('bar'), 'factoryMethod'), + ) + $base_service_definition; + + // Test invalid factory - needed to test deep dumpValue(). + $service_definitions[] = array( + 'factory' => array(array('foo', 'llama'), 'factoryMethod'), + ) + $base_service_definition; + + // Test properties. + $service_definitions[] = array( + 'properties' => array( '_value' => 'llama' ), + ) + $base_service_definition; + + // Test configurator. + $service_definitions[] = array( + 'configurator' => array(new Reference('bar'), 'configureService'), + 'configurator_expected' => array($this->getServiceCall('bar'), 'configureService'), + ) + $base_service_definition; + + $services_provided = array(); + $services_provided[] = array( + array(), + array(), + ); + + foreach ($service_definitions as $service_definition) { + $definition = $this->prophesize('\Symfony\Component\DependencyInjection\Definition'); + $definition->getClass()->willReturn($service_definition['class']); + $definition->isPublic()->willReturn($service_definition['public']); + $definition->getFile()->willReturn($service_definition['file']); + $definition->isSynthetic()->willReturn($service_definition['synthetic']); + $definition->isLazy()->willReturn($service_definition['lazy']); + $definition->getArguments()->willReturn($service_definition['arguments']); + $definition->getProperties()->willReturn($service_definition['properties']); + $definition->getMethodCalls()->willReturn($service_definition['calls']); + $definition->getScope()->willReturn(ContainerInterface::SCOPE_CONTAINER); + $definition->getDecoratedService()->willReturn(NULL); + $definition->getFactory()->willReturn($service_definition['factory']); + $definition->getConfigurator()->willReturn($service_definition['configurator']); + + // Preserve order. + $filtered_service_definition = array(); + foreach ($base_service_definition as $key => $value) { + $filtered_service_definition[$key] = $service_definition[$key]; + unset($service_definition[$key]); + + if ($key == 'class' || $key == 'arguments_count') { + continue; + } + + if ($filtered_service_definition[$key] === $base_service_definition[$key]) { + unset($filtered_service_definition[$key]); + } + } + + // Add remaining properties. + $filtered_service_definition += $service_definition; + + // Allow to set _expected values. + foreach (array('arguments', 'factory', 'configurator') as $key) { + $expected = $key . '_expected'; + if (isset($filtered_service_definition[$expected])) { + $filtered_service_definition[$key] = $filtered_service_definition[$expected]; + unset($filtered_service_definition[$expected]); + } + } + + if (isset($filtered_service_definition['public']) && $filtered_service_definition['public'] === FALSE) { + $services_provided[] = array( + array('foo_service' => $definition->reveal()), + array(), + ); + continue; + } + + $services_provided[] = array( + array('foo_service' => $definition->reveal()), + array('foo_service' => $this->serializeDefinition($filtered_service_definition)), + ); + } + + return $services_provided; + } + + /** + * Helper function to serialize a definition. + * + * Used to override serialization. + */ + protected function serializeDefinition(array $service_definition) { + return serialize($service_definition); + } + + /** + * Helper function to return a service definition. + */ + protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + return (object) array( + 'type' => 'service', + 'id' => $id, + 'invalidBehavior' => $invalid_behavior, + ); + } + + /** + * Tests that the correct InvalidArgumentException is thrown for getScope(). + * + * @covers ::getServiceDefinition + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_getServiceDefinition_getScope() { + $bar_definition = new Definition('\stdClass'); + $bar_definition->setScope('foo_scope'); + $services['bar'] = $bar_definition; + + $this->containerBuilder->getDefinitions()->willReturn($services); + $this->dumper->getArray(); + } + + /** + * Tests that the correct InvalidArgumentException is thrown for getDecoratedService(). + * + * @covers ::getServiceDefinition + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_getServiceDefinition_getDecoratedService() { + $bar_definition = new Definition('\stdClass'); + $bar_definition->setDecoratedService(new Reference('foo')); + $services['bar'] = $bar_definition; + + $this->containerBuilder->getDefinitions()->willReturn($services); + $this->dumper->getArray(); + } + + /** + * Tests that the correct RuntimeException is thrown for expressions. + * + * @covers ::dumpValue + * @covers ::getExpressionCall + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function test_getServiceDefinition_getExpressionCall() { + $expression = new Expression(); + + $bar_definition = new Definition('\stdClass'); + $bar_definition->addArgument($expression); + $services['bar'] = $bar_definition; + + $this->containerBuilder->getDefinitions()->willReturn($services); + $this->dumper->getArray(); + } + + /** + * Tests that the correct RuntimeException is thrown for dumping an object. + * + * @covers ::dumpValue + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function test_getServiceDefinition_dumpObject() { + $service = new \stdClass(); + + $bar_definition = new Definition('\stdClass'); + $bar_definition->addArgument($service); + $services['bar'] = $bar_definition; + + $this->containerBuilder->getDefinitions()->willReturn($services); + $this->dumper->getArray(); + } + + /** + * Tests that the correct RuntimeException is thrown for dumping a resource. + * + * @covers ::dumpValue + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function test_getServiceDefinition_dumpResource() { + $resource = fopen('php://memory', 'r'); + + $bar_definition = new Definition('\stdClass'); + $bar_definition->addArgument($resource); + $services['bar'] = $bar_definition; + + $this->containerBuilder->getDefinitions()->willReturn($services); + $this->dumper->getArray(); + } + + /** + * Helper function to return a private service definition. + */ + protected function getPrivateServiceCall($id, $service_definition, $shared = FALSE) { + if (!$id) { + $hash = sha1(serialize($service_definition)); + $id = 'private__' . $hash; + } + return (object) array( + 'type' => 'private_service', + 'id' => $id, + 'value' => $service_definition, + 'shared' => $shared, + ); + } + + /** + * Helper function to return a machine readable collection. + */ + protected function getCollection($collection, $resolve = TRUE) { + return (object) array( + 'type' => 'collection', + 'value' => $collection, + 'resolve' => $resolve, + ); + } + + /** + * Helper function to return a service definition. + */ + protected function getParameterCall($name) { + return (object) array( + 'type' => 'parameter', + 'name' => $name, + ); + } + +} + +} + +/* + * As Drupal Core does not ship with ExpressionLanguage component we need to + * define a dummy, else it cannot be tested. + */ +namespace Symfony\Component\ExpressionLanguage { + if (!class_exists('\Symfony\Component\ExpressionLanguage\Expression')) { + class Expression { + + /** + * Returns the expression as a String. + */ + public function __toString() { + return 'dummy_expression'; + } + } + } +} diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/BootstrapContainerTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/BootstrapContainerTest.php index f6a7152..ae5448c 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/BootstrapContainerTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/BootstrapContainerTest.php @@ -44,7 +44,7 @@ protected function getParameterCall($name) { /** * Helper function to return a machine readable '@notation'. */ - protected function getCollection($collection) { + protected function getCollection($collection, $resolve = TRUE) { return $collection; } diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php index adea81a..4686190 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php @@ -9,6 +9,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\LogicException; +use Prophecy\Argument; /** * @coversDefaultClass \Drupal\Core\DependencyInjection\Container @@ -52,6 +53,19 @@ public function setUp() { } /** + * Tests that passing a non-supported format throws an InvalidArgumentException. + * @covers ::__construct + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_construct() { + $container_definition = $this->getMockContainerDefinition(); + $container_definition['machine_format'] = !$this->machineFormat; + $container = new $this->containerClass($container_definition, TRUE); + } + + + /** * Tests that Container::getParameter() works properly. * @covers ::getParameter */ @@ -61,6 +75,17 @@ public function test_getParameter() { } /** + * Tests that Container::getParameter() works properly for non-existing + * parameters. + * + * @covers ::getParameter + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_getParameter_notFound() { + $this->container->getParameter('parameter_that_does_not_exist'); + } + + /** * Tests that Container::hasParameter() works properly. * @covers ::hasParameter */ @@ -95,6 +120,7 @@ public function test_setParameter_frozenContainer() { /** * Tests that Container::get() works properly. * @covers ::get + * @covers ::createService */ public function test_get() { $container = $this->container->get('service_container'); @@ -117,6 +143,19 @@ public function test_get() { $this->assertEquals($service->_someProperty, 'foo', 'Service has added properties.'); } + /** + * Tests that Container::get() works properly for class from parameters. + * @covers ::get + * @covers ::createService + */ + public function test_get_classFromParameter() { + $container = new $this->containerClass($this->containerDefinition, FALSE); + + $other_service_class = $this->containerDefinition['parameters']['some_parameter_class']; + $other_service = $container->get('other.service_class_from_parameter'); + $this->assertInstanceOf($other_service_class, $other_service, 'other.service_class_from_parameter has the right class.'); + } + /** * Tests that Container::set() works properly. * @@ -149,6 +188,7 @@ public function test_has() { * Tests that Container::get() for circular dependencies works properly. * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException * @covers ::get + * @covers ::createService */ public function test_get_circular() { $this->container->get('circular_dependency'); @@ -156,15 +196,38 @@ public function test_get_circular() { /** * Tests that Container::get() for non-existant dependencies works properly. - * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * * @covers ::get * @covers ::createService + * @covers ::getAlternatives + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException */ public function test_get_exception() { $this->container->get('service_not_exists'); } /** + * Tests that Container::get() for serialized definition works properly. + * @covers ::get + * @covers ::createService + */ + public function test_get_serializedDefinition() { + $container_definition = $this->containerDefinition; + $container_definition['services']['other.service'] = serialize($container_definition['services']['other.service']); + $container = new $this->containerClass($container_definition); + + // Retrieve services of the container. + $other_service_class = $this->containerDefinition['services']['other.service']['class']; + $other_service = $container->get('other.service'); + $this->assertInstanceOf($other_service_class, $other_service, 'other.service has the right class.'); + + $service = $container->get('service.provider'); + $this->assertEquals($other_service, $service->getSomeOtherService(), '@other.service was injected via constructor.'); + } + + + /** * Tests that Container::get() for non-existant parameters works properly. * @covers ::get * @covers ::createService @@ -221,6 +284,7 @@ public function test_get_notFound_dependency() { * @covers ::get * @covers ::createService * @covers ::resolveServicesAndParameters + * @covers ::getAlternatives */ public function test_get_notFound_dependency_exception() { $this->container->get('service_dependency_not_exists'); @@ -237,6 +301,18 @@ public function test_get_notFound() { } /** + * Tests that Container::get() for NULL service works properly. + * @covers ::get + * @covers ::createService + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + */ + public function test_get_notFound_NULL() { + $this->container->get(NULL); + } + + + /** * Tests multiple Container::get() calls for non-existing dependencies work. * * @covers ::get @@ -254,6 +330,7 @@ public function test_get_notFoundMultiple() { * * @covers ::get * @covers ::createService + * @covers ::getAlternatives * * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException */ @@ -274,6 +351,60 @@ public function test_get_alias() { } /** + * Tests that Container::get() for synthetic services works - if defined. + * + * @covers ::get + * @covers ::createService + */ + public function test_get_synthetic() { + $synthetic_service = new \stdClass(); + $this->container->set('synthetic', $synthetic_service); + $test_service = $this->container->get('synthetic'); + $this->assertSame($synthetic_service, $test_service); + } + + /** + * Tests that Container::get() for synthetic services throws an Exception if not defined. + * + * @covers ::get + * @covers ::createService + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function test_get_synthetic_exception() { + $this->container->get('synthetic'); + } + + /** + * Tests that Container::get() for services with file includes works. + * + * @covers ::get + * @covers ::createService + */ + public function test_get_file() { + $file_service = $this->container->get('container_test_file_service_test'); + $this->assertTrue(function_exists('container_test_file_service_test_service_function')); + $this->assertEquals('Hello Container', container_test_file_service_test_service_function()); + } + + /** + * Tests that Container::get() for services with file includes works. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function test_get_instantiation() { + $args = array(); + for ($i=0; $i < 12; $i++) { + $instantiation_service = $this->container->get('service_test_instantiation_'. $i); + $this->assertEquals($args, $instantiation_service->getArguments()); + $args[] = 'arg_' . $i; + } + } + + + /** * Tests that Container::get() for factories via services works properly. * @covers ::get * @covers ::createService @@ -322,6 +453,7 @@ public function test_get_factoryServiceNew() { /** * Tests that Container::get() for factories via class works (Symfony 2.7.0). * @covers ::get + * @covers ::createService */ public function test_get_factoryClassNew() { $service = $this->container->get('service.provider'); @@ -332,7 +464,39 @@ public function test_get_factoryClassNew() { $this->assertEquals($this->container, $factory_service->getContainer(), 'Container was injected via setter injection.'); } + /** + * Tests that Container::get() for configurable services throws an Exception. + * + * @covers ::get + * @covers ::createService + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_get_configurator_exception() { + $this->container->get('configurable_service_exception'); + } + /** + * Tests that Container::get() for configurable services works. + * @covers ::get + * @covers ::createService + */ + public function test_get_configurator() { + $container = $this->container; + + // Setup a configurator. + $configurator = $this->prophesize('\Drupal\Tests\Core\DependencyInjection\PhpArray\MockConfiguratorInterface'); + $configurator->configureService(Argument::type('object')) + ->shouldBeCalled(1) + ->will(function($args) use ($container) { + $args[0]->setContainer($container); + }); + $container->set('configurator', $configurator->reveal()); + + // Test that the configurator worked. + $service = $container->get('configurable_service'); + $this->assertSame($container, $service->getContainer(), 'Container was injected via configurator.'); + } /** * Tests that private services work correctly. @@ -343,7 +507,32 @@ public function test_get_factoryClassNew() { public function test_resolveServicesAndParameters_privateService() { $service = $this->container->get('service_using_private'); $private_service = $service->getSomeOtherService(); - $this->assertEquals($private_service->getSomeParameter(), 'really_private_lama', 'Private was found successfully'); + $this->assertEquals($private_service->getSomeParameter(), 'really_private_lama', 'Private was found successfully.'); + + // Test that sharing the same private services works. + $service = $this->container->get('another_service_using_private'); + $another_private_service = $service->getSomeOtherService(); + $this->assertNotSame($private_service, $another_private_service, 'Private service is not shared.'); + $this->assertEquals($private_service->getSomeParameter(), 'really_private_lama', 'Private was found successfully.'); + } + + /** + * Tests that private service sharing works correctly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function test_resolveServicesAndParameters_privateServiceShared() { + $service = $this->container->get('service_using_shared_private'); + $private_service = $service->getSomeOtherService(); + $this->assertEquals($private_service->getSomeParameter(), 'really_private_lama', 'Private was found successfully.'); + + // Test that sharing the same private services works. + $service = $this->container->get('another_service_using_shared_private'); + $same_private_service = $service->getSomeOtherService(); + $this->assertSame($private_service, $same_private_service, 'Private service is shared.'); + $this->assertEquals($private_service->getSomeParameter(), 'really_private_lama', 'Private was found successfully.'); } /** @@ -369,6 +558,49 @@ public function test_resolveServicesAndParameters_optional() { $this->assertNull($service->getSomeOtherService(), 'other service was NULL was expected.'); } + /** + * Tests that an invalid argument throw an Exception. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_resolveServicesAndParameters_invalidArgument() { + $this->container->get('invalid_argument_service'); + } + + /** + * Tests that invalid arguments throw an Exception. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function test_resolveServicesAndParameters_invalidArguments() { + // In case the machine format is not used, we need to simulate the test failure. + if (!$this->machineFormat) { + throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Simulating the test failure.'); + } + $this->container->get('invalid_arguments_service'); + } + + /** + * Tests that a parameter that points to a service works correctly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function test_resolveServicesAndParameters_fromParameter() { + $service = $this->container->get('service.provider'); + $test_service = $this->container->get('service_with_parameter_service'); + $this->assertSame($service, $test_service->getSomeOtherService(), 'Service was passed via parameter.'); + } + /** * Tests that Container::initialized works correctly. @@ -377,7 +609,89 @@ public function test_resolveServicesAndParameters_optional() { public function test_initialized() { $this->assertFalse($this->container->initialized('late.service'), 'Late service is not initialized.'); $this->container->get('late.service'); - $this->assertTrue($this->container->initialized('late.service'), 'Late service is initialized after it was gotten.'); + $this->assertTrue($this->container->initialized('late.service'), 'Late service is initialized after it was retrieved once.'); + } + + /** + * Tests that Container::initialized works correctly for aliases. + * @covers ::initialized + */ + public function test_initialized_aliases() { + $this->assertFalse($this->container->initialized('late.service_alias'), 'Late service is not initialized.'); + $this->container->get('late.service'); + $this->assertTrue($this->container->initialized('late.service_alias'), 'Late service is initialized after it was retrieved once.'); + } + + /** + * Tests that unsupported methods throw an Exception. + * + * @covers ::enterScope + * @covers ::leaveScope + * @covers ::addScope + * @covers ::hasScope + * @covers ::isScopeActive + * + * @expectedException \BadMethodCallException + * + * @dataProvider scopeExceptionTestProvider + */ + public function test_scope_exception($method, $argument) { + $callable = array( + $this->container, + $method, + ); + + $callable($argument); + } + + /** + * Data provider for test_scope_exception(). + * + * @return array[] + * Returns per data set an array with: + * - method name to call + * - argument to pass + */ + public function scopeExceptionTestProvider() { + $scope = $this->prophesize('\Symfony\Component\DependencyInjection\ScopeInterface')->reveal(); + return array( + array('enterScope', 'test_scope'), + array('leaveScope', 'test_scope'), + array('hasScope', 'test_scope'), + array('isScopeActive', 'test_scope'), + array('addScope', $scope), + ); + } + + /** + * Tests that __sleep throws an Exception. + * + * @covers ::__sleep + * + * @expectedException \PHPUnit_Framework_Error + */ + public function test_sleep() { + $serialized = serialize($this->container); + } + + /** + * Tests that __sleep works when error handler is set to non-fatal. + * + * @covers ::__sleep + */ + public function test_sleep_working() { + $container = new $this->containerClass(); + + // Ignore any errors. + set_error_handler(array($this, 'ignoreErrorHandler')); + $serialized = serialize($container); + restore_error_handler(); + } + + /** + * An error handler that ignores all errors. + */ + public function ignoreErrorHandler() { } /** @@ -389,19 +703,24 @@ public function test_initialized() { protected function getMockContainerDefinition() { $fake_service = new \stdClass(); $parameters = array(); + $parameters['some_parameter_class'] = get_class($fake_service); $parameters['some_private_config'] = 'really_private_lama'; $parameters['some_config'] = 'foo'; $parameters['some_other_config'] = 'lama'; $parameters['factory_service_class'] = get_class($fake_service); + // Also test alias resolving. + $parameters['service_from_parameter'] = $this->getServiceCall('service.provider_alias'); $services = array(); $services['service_container'] = array( 'class' => '\Drupal\service_container\DependencyInjection\Container', ); $services['other.service'] = array( - // @todo Support parameter expansion for classes. 'class' => get_class($fake_service), ); + $services['other.service_class_from_parameter'] = array( + 'class' => $this->getParameterCall('some_parameter_class'), + ); $services['late.service'] = array( 'class' => get_class($fake_service), ); @@ -411,7 +730,7 @@ protected function getMockContainerDefinition() { $this->getServiceCall('other.service'), $this->getParameterCall('some_config'), )), - 'properties' => array('_someProperty' => 'foo'), + 'properties' => $this->getCollection(array('_someProperty' => 'foo')), 'calls' => array( array('setContainer', $this->getCollection(array( $this->getServiceCall('service_container'), @@ -422,6 +741,9 @@ protected function getMockContainerDefinition() { ), 'priority' => 0, ); + + // Test private services. + $private_service = array( 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockService', 'arguments' => $this->getCollection(array( @@ -438,7 +760,52 @@ protected function getMockContainerDefinition() { $this->getParameterCall('some_config'), )), ); + $services['another_service_using_private'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\\MockService', + 'arguments' => $this->getCollection(array( + $this->getPrivateServiceCall(NULL, $private_service), + $this->getParameterCall('some_config'), + )), + ); + + // Test shared private services. + + $id = 'private_service_shared_1'; + $services['service_using_shared_private'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\\MockService', + 'arguments' => $this->getCollection(array( + $this->getPrivateServiceCall($id, $private_service, TRUE), + $this->getParameterCall('some_config'), + )), + ); + $services['another_service_using_shared_private'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\\MockService', + 'arguments' => $this->getCollection(array( + $this->getPrivateServiceCall($id, $private_service, TRUE), + $this->getParameterCall('some_config'), + )), + ); + + // Tests service with invalid argument. + $services['invalid_argument_service'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\\MockService', + 'arguments' => $this->getCollection(array( + 1, // Test passing non-strings, too. + (object) array( + 'type' => 'invalid', + ), + )), + ); + + $services['invalid_arguments_service'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\\MockService', + 'arguments' => (object) array( + 'type' => 'invalid', + ), + ); + + // Test service that needs deep-traversal. $services['service_using_array'] = array( 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockService', 'arguments' => $this->getCollection(array( @@ -514,6 +881,25 @@ protected function getMockContainerDefinition() { $this->getServiceCall('circular_dependency'), )), ); + $services['synthetic'] = array( + 'synthetic' => TRUE, + ); + $services['container_test_file_service_test'] = array( + 'class' => '\stdClass', + 'file' => __DIR__ . '/Fixtures/container_test_file_service_test_service_function.php', + ); + + // Test multiple arguments. + $args = array(); + for ($i=0; $i < 12; $i++) { + $services['service_test_instantiation_' . $i] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockInstantiationService', + // Also test a collection that does not need resolving. + 'arguments' => $this->getCollection($args, FALSE), + ); + $args[] = 'arg_' . $i; + } + $services['service_parameter_not_exists'] = array( 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockService', 'arguments' => $this->getCollection(array( @@ -529,8 +915,43 @@ protected function getMockContainerDefinition() { )), ); + $services['service_with_parameter_service'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockService', + 'arguments' => $this->getCollection(array( + $this->getParameterCall('service_from_parameter'), + // Also test deep collections that don't need resolving. + $this->getCollection(array( + 1, + ), FALSE), + )), + ); + + // To ensure getAlternatives() finds something. + $services['service_not_exists_similar'] = array( + 'synthetic' => TRUE, + ); + + // Test configurator. + $services['configurator'] = array( + 'synthetic' => TRUE, + ); + $services['configurable_service'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockService', + 'arguments' => array(), + 'configurator' => array( + $this->getServiceCall('configurator'), + 'configureService' + ), + ); + $services['configurable_service_exception'] = array( + 'class' => '\Drupal\Tests\Core\DependencyInjection\PhpArray\MockService', + 'arguments' => array(), + 'configurator' => 'configurator_service_test_does_not_exist', + ); + $aliases = array(); $aliases['service.provider_alias'] = 'service.provider'; + $aliases['late.service_alias'] = 'late.service'; return array( 'aliases' => $aliases, @@ -564,7 +985,7 @@ protected function getParameterCall($name) { /** * Helper function to return a private service definition. */ - protected function getPrivateServiceCall($id, $service_definition) { + protected function getPrivateServiceCall($id, $service_definition, $shared = FALSE) { if (!$id) { $hash = sha1(serialize($service_definition)); $id = 'private__' . $hash; @@ -573,27 +994,77 @@ protected function getPrivateServiceCall($id, $service_definition) { 'type' => 'private_service', 'id' => $id, 'value' => $service_definition, + 'shared' => $shared, ); } /** - * Helper function to return a machine readable '@notation'. + * Helper function to return a machine readable collection. */ - protected function getCollection($collection) { + protected function getCollection($collection, $resolve = TRUE) { return (object) array( 'type' => 'collection', 'value' => $collection, - 'resolve' => TRUE, + 'resolve' => $resolve, ); } } +/** + * Helper interface to test Container::get() with configurator. + * + * @group container + */ +interface MockConfiguratorInterface { + + /** + * Configures a service. + * + * @param object $service + * The service to configure. + */ + public function configureService($service); + +} + + +/** + * Helper class to test Container::get() method for varying number of parameters. + * + * @group container + */ +class MockInstantiationService { + + /** + * @var mixed[] + */ + protected $arguments; + + /** + * Construct a mock instantiation service. + */ + public function __construct() { + $this->arguments = func_get_args(); + } + + /** + * Return arguments injected into the service. + * + * @return mixed[] + * Return the passed arguments. + */ + public function getArguments() { + return $this->arguments; + } + +} + /** * Helper class to test Container::get() method. * - * @group dic + * @group container */ class MockService { diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/Fixtures/container_test_file_service_test_service_function.php b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/Fixtures/container_test_file_service_test_service_function.php new file mode 100644 index 0000000..164eaaf --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/Fixtures/container_test_file_service_test_service_function.php @@ -0,0 +1,11 @@ +