diff --git a/core/lib/Drupal/Component/DependencyInjection/Container.php b/core/lib/Drupal/Component/DependencyInjection/Container.php new file mode 100644 index 0000000000000000000000000000000000000000..df25ae8b506033fa7117be1dd494eca19754cf1c --- /dev/null +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php @@ -0,0 +1,629 @@ +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(); + $this->frozen = isset($container_definition['frozen']) ? $container_definition['frozen'] : FALSE; + + // Register the service_container with itself. + $this->services['service_container'] = $this; + } + + /** + * {@inheritdoc} + */ + public function get($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + if (isset($this->aliases[$id])) { + $id = $this->aliases[$id]; + } + + // Re-use shared service instance if it exists. + if (isset($this->services[$id]) || ($invalid_behavior === ContainerInterface::NULL_ON_INVALID_REFERENCE && array_key_exists($id, $this->services))) { + return $this->services[$id]; + } + + if (isset($this->loading[$id])) { + throw new ServiceCircularReferenceException($id, array_keys($this->loading)); + } + + $definition = isset($this->serviceDefinitions[$id]) ? $this->serviceDefinitions[$id] : NULL; + + if (!$definition && $invalid_behavior === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + if (!$id) { + throw new ServiceNotFoundException($id); + } + + throw new ServiceNotFoundException($id, NULL, NULL, $this->getServiceAlternatives($id)); + } + + // 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. + if (!$definition) { + return; + } + + // Definition is a keyed array, so [0] is only defined when it is a + // serialized string. + if (isset($definition[0])) { + $definition = unserialize($definition); + } + + // Now create the service. + $this->loading[$id] = TRUE; + + try { + $service = $this->createService($definition, $id); + } + catch (\Exception $e) { + unset($this->loading[$id]); + + // Remove a potentially shared service that was constructed incompletely. + if (array_key_exists($id, $this->services)) { + unset($this->services[$id]); + } + + if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalid_behavior) { + return; + } + + throw $e; + } + + unset($this->loading[$id]); + + 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, necessary so it can be shared if its public. + * + * @return object + * The service described by the service definition. + * + * @throws \Symfony\Component\DependencyInjection\Exception\RuntimeException + * Thrown when the service is a synthetic service. + * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * Thrown when the configurator callable in $definition['configurator'] is + * not actually a callable. + * @throws \ReflectionException + * Thrown when the service class takes more than 10 parameters to construct, + * and cannot be instantiated. + */ + protected function createService(array $definition, $id) { + if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) { + 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(); + if (isset($definition['arguments'])) { + $arguments = $definition['arguments']; + + if ($arguments instanceof \stdClass) { + $arguments = $this->resolveServicesAndParameters($arguments); + } + } + + if (isset($definition['file'])) { + $file = $this->frozen ? $definition['file'] : current($this->resolveServicesAndParameters(array($definition['file']))); + require_once $file; + } + + if (isset($definition['factory'])) { + $factory = $definition['factory']; + if (is_array($factory)) { + $factory = $this->resolveServicesAndParameters(array($factory[0], $factory[1])); + } + elseif (!is_string($factory)) { + throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory', $id)); + } + + $service = call_user_func_array($factory, $arguments); + } + else { + $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 noticeably slow. + switch ($length) { + case 0: + $service = new $class(); + break; + + case 1: + $service = new $class($arguments[0]); + break; + + case 2: + $service = new $class($arguments[0], $arguments[1]); + break; + + case 3: + $service = new $class($arguments[0], $arguments[1], $arguments[2]); + break; + + case 4: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3]); + break; + + case 5: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]); + break; + + case 6: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]); + break; + + case 7: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]); + break; + + case 8: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7]); + break; + + case 9: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8]); + break; + + case 10: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8], $arguments[9]); + break; + + default: + $r = new \ReflectionClass($class); + $service = $r->newInstanceArgs($arguments); + break; + } + } + + // Share the service if it is public. + if (!isset($definition['public']) || $definition['public'] !== FALSE) { + // Forward compatibility fix for Symfony 2.8 update. + if (!isset($definition['shared']) || $definition['shared'] !== FALSE) { + $this->services[$id] = $service; + } + } + + if (isset($definition['calls'])) { + foreach ($definition['calls'] as $call) { + $method = $call[0]; + $arguments = array(); + if (!empty($call[1])) { + $arguments = $call[1]; + if ($arguments instanceof \stdClass) { + $arguments = $this->resolveServicesAndParameters($arguments); + } + } + call_user_func_array(array($service, $method), $arguments); + } + } + + if (isset($definition['properties'])) { + if ($definition['properties'] instanceof \stdClass) { + $definition['properties'] = $this->resolveServicesAndParameters($definition['properties']); + } + foreach ($definition['properties'] as $key => $value) { + $service->{$key} = $value; + } + } + + if (isset($definition['configurator'])) { + $callable = $definition['configurator']; + if (is_array($callable)) { + $callable = $this->resolveServicesAndParameters($callable); + } + + if (!is_callable($callable)) { + throw new InvalidArgumentException(sprintf('The configurator for class "%s" is not a callable.', get_class($service))); + } + + call_user_func($callable, $service); + } + + return $service; + } + + /** + * {@inheritdoc} + */ + public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) { + $this->services[$id] = $service; + } + + /** + * {@inheritdoc} + */ + public function has($id) { + return isset($this->services[$id]) || isset($this->serviceDefinitions[$id]); + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { + if (!$name) { + throw new ParameterNotFoundException($name); + } + + throw new ParameterNotFoundException($name, NULL, NULL, NULL, $this->getParameterAlternatives($name)); + } + + return $this->parameters[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) { + return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) { + if ($this->frozen) { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + $this->parameters[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function initialized($id) { + if (isset($this->aliases[$id])) { + $id = $this->aliases[$id]; + } + + return isset($this->services[$id]) || array_key_exists($id, $this->services); + } + + /** + * Resolves arguments that represent services or variables to the real values. + * + * @param array|\stdClass $arguments + * The arguments to resolve. + * + * @return array + * The resolved arguments. + * + * @throws \Symfony\Component\DependencyInjection\Exception\RuntimeException + * If a parameter/service could not be resolved. + * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * If an unknown type is met while resolving parameters and services. + */ + protected function resolveServicesAndParameters($arguments) { + // Check if this collection needs to be resolved. + if ($arguments instanceof \stdClass) { + if ($arguments->type !== 'collection') { + throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $arguments->type)); + } + // In case there is nothing to resolve, we are done here. + if (!$arguments->resolve) { + return $arguments->value; + } + $arguments = $arguments->value; + } + + // Process the arguments. + foreach ($arguments as $key => $argument) { + // 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; + + // Check for parameter. + if ($type == 'parameter') { + $name = $argument->name; + if (!isset($this->parameters[$name])) { + $arguments[$key] = $this->getParameter($name); + // This can never be reached as getParameter() throws an Exception, + // because we already checked that the parameter is not set above. + } + + // Update argument. + $argument = $arguments[$key] = $this->parameters[$name]; + + // In case there is not a machine readable value (e.g. a service) + // behind this resolved parameter, continue. + if (!($argument instanceof \stdClass)) { + continue; + } + + // Fall through. + $type = $argument->type; + } + + // Create a service. + if ($type == 'service') { + $id = $argument->id; + + // Does the service already exist? + if (isset($this->aliases[$id])) { + $id = $this->aliases[$id]; + } + + if (isset($this->services[$id])) { + $arguments[$key] = $this->services[$id]; + continue; + } + + // Return the service. + $arguments[$key] = $this->get($id, $argument->invalidBehavior); + + continue; + } + // Create private service. + elseif ($type == 'private_service') { + $id = $argument->id; + + // Does the private service already exist. + if (isset($this->privateServices[$id])) { + $arguments[$key] = $this->privateServices[$id]; + continue; + } + + // Create the private service. + $arguments[$key] = $this->createService($argument->value, $id); + if ($argument->shared) { + $this->privateServices[$id] = $arguments[$key]; + } + + continue; + } + // Check for collection. + elseif ($type == 'collection') { + $value = $argument->value; + + // Does this collection need resolving? + if ($argument->resolve) { + $arguments[$key] = $this->resolveServicesAndParameters($value); + } + else { + $arguments[$key] = $value; + } + + continue; + } + + if ($type !== NULL) { + throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $type)); + } + } + } + + return $arguments; + } + + /** + * Provides alternatives for a given array and key. + * + * @param string $search_key + * The search key to get alternatives for. + * @param array $keys + * The search space to search for alternatives in. + * + * @return string[] + * An array of strings with suitable alternatives. + */ + protected function getAlternatives($search_key, array $keys) { + $alternatives = array(); + foreach ($keys as $key) { + $lev = levenshtein($search_key, $key); + if ($lev <= strlen($search_key) / 3 || strpos($key, $search_key) !== FALSE) { + $alternatives[] = $key; + } + } + + return $alternatives; + } + + /** + * Provides alternatives in case a service was not found. + * + * @param string $id + * The service to get alternatives for. + * + * @return string[] + * An array of strings with suitable alternatives. + */ + protected function getServiceAlternatives($id) { + $all_service_keys = array_unique(array_merge(array_keys($this->services), array_keys($this->serviceDefinitions))); + return $this->getAlternatives($id, $all_service_keys); + } + + /** + * Provides alternatives in case a parameter was not found. + * + * @param string $name + * The parameter to get alternatives for. + * + * @return string[] + * An array of strings with suitable alternatives. + */ + protected function getParameterAlternatives($name) { + return $this->getAlternatives($name, array_keys($this->parameters)); + } + + + /** + * {@inheritdoc} + */ + public function enterScope($name) { + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); + } + + /** + * {@inheritdoc} + */ + public function leaveScope($name) { + 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 supported by Drupal 8.", __FUNCTION__)); + } + + /** + * {@inheritdoc} + */ + public function hasScope($name) { + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); + } + + /** + * {@inheritdoc} + */ + public function isScopeActive($name) { + throw new \BadMethodCallException(sprintf("'%s' is not supported by Drupal 8.", __FUNCTION__)); + } + + /** + * Gets all defined service IDs. + * + * @return array + * An array of all defined service IDs. + */ + public function getServiceIds() { + return array_keys($this->serviceDefinitions + $this->services); + } + +} diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php new file mode 100644 index 0000000000000000000000000000000000000000..23290b6b71b80d0e2a71375879ad8a1fcb8fde1f --- /dev/null +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php @@ -0,0 +1,513 @@ +getArray()); + } + + /** + * Gets the service container definition as a PHP array. + * + * @return array + * A PHP array representation of the service container. + */ + public function getArray() { + $definition = array(); + $definition['aliases'] = $this->getAliases(); + $definition['parameters'] = $this->getParameters(); + $definition['services'] = $this->getServiceDefinitions(); + $definition['frozen'] = $this->container->isFrozen(); + $definition['machine_format'] = $this->supportsMachineFormat(); + return $definition; + } + + /** + * Gets the aliases as a PHP array. + * + * @return array + * The aliases. + */ + protected function getAliases() { + $alias_definitions = array(); + + $aliases = $this->container->getAliases(); + foreach ($aliases as $alias => $id) { + $id = (string) $id; + while (isset($aliases[$id])) { + $id = (string) $aliases[$id]; + } + $alias_definitions[$alias] = $id; + } + + return $alias_definitions; + } + + /** + * Gets parameters of the container as a PHP array. + * + * @return array + * The escaped and prepared parameters of the container. + */ + protected function getParameters() { + if (!$this->container->getParameterBag()->all()) { + return array(); + } + + $parameters = $this->container->getParameterBag()->all(); + $is_frozen = $this->container->isFrozen(); + return $this->prepareParameters($parameters, $is_frozen); + } + + /** + * Gets services of the container as a PHP array. + * + * @return array + * The service definitions. + */ + protected function getServiceDefinitions() { + if (!$this->container->getDefinitions()) { + return array(); + } + + $services = array(); + foreach ($this->container->getDefinitions() as $id => $definition) { + // Only store public service definitions, references to shared private + // services are handled in ::getReferenceCall(). + if ($definition->isPublic()) { + $service_definition = $this->getServiceDefinition($definition); + $services[$id] = $this->serialize ? serialize($service_definition) : $service_definition; + } + } + + return $services; + } + + /** + * Prepares parameters for the PHP array dumping. + * + * @param array $parameters + * An array of parameters. + * @param bool $escape + * Whether keys with '%' should be escaped or not. + * + * @return array + * An array of prepared parameters. + */ + protected function prepareParameters(array $parameters, $escape = TRUE) { + $filtered = array(); + foreach ($parameters as $key => $value) { + if (is_array($value)) { + $value = $this->prepareParameters($value, $escape); + } + elseif ($value instanceof Reference) { + $value = $this->dumpValue($value); + } + + $filtered[$key] = $value; + } + + return $escape ? $this->escape($filtered) : $filtered; + } + + /** + * Escapes parameters. + * + * @param array $parameters + * The parameters to escape for '%' characters. + * + * @return array + * The escaped parameters. + */ + protected function escape(array $parameters) { + $args = array(); + + foreach ($parameters as $key => $value) { + if (is_array($value)) { + $args[$key] = $this->escape($value); + } + elseif (is_string($value)) { + $args[$key] = str_replace('%', '%%', $value); + } + else { + $args[$key] = $value; + } + } + + return $args; + } + + /** + * Gets a service definition as PHP array. + * + * @param \Symfony\Component\DependencyInjection\Definition $definition + * The definition to process. + * + * @return array + * The service definition as PHP array. + * + * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * Thrown when the definition is marked as decorated, or with an explicit + * scope different from SCOPE_CONTAINER and SCOPE_PROTOTYPE. + */ + protected function getServiceDefinition(Definition $definition) { + $service = array(); + if ($definition->getClass()) { + $service['class'] = $definition->getClass(); + } + + if (!$definition->isPublic()) { + $service['public'] = FALSE; + } + + if ($definition->getFile()) { + $service['file'] = $definition->getFile(); + } + + if ($definition->isSynthetic()) { + $service['synthetic'] = TRUE; + } + + if ($definition->isLazy()) { + $service['lazy'] = TRUE; + } + + if ($definition->getArguments()) { + $arguments = $definition->getArguments(); + $service['arguments'] = $this->dumpCollection($arguments); + $service['arguments_count'] = count($arguments); + } + else { + $service['arguments_count'] = 0; + } + + if ($definition->getProperties()) { + $service['properties'] = $this->dumpCollection($definition->getProperties()); + } + + if ($definition->getMethodCalls()) { + $service['calls'] = $this->dumpMethodCalls($definition->getMethodCalls()); + } + + if (($scope = $definition->getScope()) !== ContainerInterface::SCOPE_CONTAINER) { + if ($scope === ContainerInterface::SCOPE_PROTOTYPE) { + // Scope prototype has been replaced with 'shared' => FALSE. + // This is a Symfony 2.8 forward compatibility fix. + // Reference: https://github.com/symfony/symfony/blob/2.8/UPGRADE-2.8.md#dependencyinjection + $service['shared'] = FALSE; + } + else { + 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."); + } + + if ($callable = $definition->getFactory()) { + $service['factory'] = $this->dumpCallable($callable); + } + + if ($callable = $definition->getConfigurator()) { + $service['configurator'] = $this->dumpCallable($callable); + } + + return $service; + } + + /** + * Dumps method calls to a PHP array. + * + * @param array $calls + * An array of method calls. + * + * @return array + * The PHP array representation of the method calls. + */ + protected function dumpMethodCalls(array $calls) { + $code = array(); + + foreach ($calls as $key => $call) { + $method = $call[0]; + $arguments = array(); + if (!empty($call[1])) { + $arguments = $this->dumpCollection($call[1]); + } + + $code[$key] = [$method, $arguments]; + } + + return $code; + } + + + /** + * Dumps a collection to a PHP array. + * + * @param mixed $collection + * A collection to process. + * @param bool &$resolve + * Used for passing the information to the caller whether the given + * collection needed to be resolved or not. This is used for optimizing + * deep arrays that don't need to be traversed. + * + * @return \stdClass|array + * The collection in a suitable format. + */ + protected function dumpCollection($collection, &$resolve = FALSE) { + $code = array(); + + foreach ($collection as $key => $value) { + if (is_array($value)) { + $resolve_collection = FALSE; + $code[$key] = $this->dumpCollection($value, $resolve_collection); + + if ($resolve_collection) { + $resolve = TRUE; + } + } + else { + if (is_object($value)) { + $resolve = TRUE; + } + $code[$key] = $this->dumpValue($value); + } + } + + if (!$resolve) { + return $collection; + } + + return (object) array( + 'type' => 'collection', + 'value' => $code, + 'resolve' => $resolve, + ); + } + + /** + * Dumps callable to a PHP array. + * + * @param array|callable $callable + * The callable to process. + * + * @return callable + * The processed callable. + */ + protected function dumpCallable($callable) { + if (is_array($callable)) { + $callable[0] = $this->dumpValue($callable[0]); + $callable = array($callable[0], $callable[1]); + } + + return $callable; + } + + /** + * Gets a private service definition in a suitable format. + * + * @param string $id + * The ID of the service to get a private definition for. + * @param \Symfony\Component\DependencyInjection\Definition $definition + * The definition to process. + * @param bool $shared + * (optional) Whether the service will be shared with others. + * By default this parameter is FALSE. + * + * @return \stdClass + * A very lightweight private service value object. + */ + protected function getPrivateServiceCall($id, Definition $definition, $shared = FALSE) { + $service_definition = $this->getServiceDefinition($definition); + if (!$id) { + $hash = hash('sha1', serialize($service_definition)); + $id = 'private__' . $hash; + } + return (object) array( + 'type' => 'private_service', + 'id' => $id, + 'value' => $service_definition, + 'shared' => $shared, + ); + } + + /** + * Dumps the value to PHP array format. + * + * @param mixed $value + * The value to dump. + * + * @return mixed + * The dumped value in a suitable format. + * + * @throws RuntimeException + * When trying to dump object or resource. + */ + protected function dumpValue($value) { + if (is_array($value)) { + $code = array(); + foreach ($value as $k => $v) { + $code[$k] = $this->dumpValue($v); + } + + return $code; + } + elseif ($value instanceof Reference) { + return $this->getReferenceCall((string) $value, $value); + } + elseif ($value instanceof Definition) { + return $this->getPrivateServiceCall(NULL, $value); + } + elseif ($value instanceof Parameter) { + return $this->getParameterCall((string) $value); + } + elseif ($value instanceof Expression) { + throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + elseif (is_object($value)) { + // Drupal specific: Instantiated objects have a _serviceId parameter. + if (isset($value->_serviceId)) { + return $this->getReferenceCall($value->_serviceId); + } + throw new RuntimeException('Unable to dump a service container if a parameter is an object without _serviceId.'); + } + elseif (is_resource($value)) { + throw new RuntimeException('Unable to dump a service container if a parameter is a resource.'); + } + + return $value; + } + + /** + * Gets a service reference for a reference in a suitable PHP array format. + * + * The main difference is that this function treats references to private + * services differently and returns a private service reference instead of + * a normal reference. + * + * @param string $id + * The ID of the service to get a reference for. + * @param \Symfony\Component\DependencyInjection\Reference|NULL $reference + * (optional) The reference object to process; needed to get the invalid + * behavior value. + * + * @return string|\stdClass + * A suitable representation of the service reference. + */ + protected function getReferenceCall($id, Reference $reference = NULL) { + $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + + if ($reference !== NULL) { + $invalid_behavior = $reference->getInvalidBehavior(); + } + + // Private shared service. + $definition = $this->container->getDefinition($id); + if (!$definition->isPublic()) { + // The ContainerBuilder does not share a private service, but this means a + // new service is instantiated every time. Use a private shared service to + // circumvent the problem. + return $this->getPrivateServiceCall($id, $definition, TRUE); + } + + return $this->getServiceCall($id, $invalid_behavior); + } + + /** + * Gets a service reference for an ID in a suitable PHP array format. + * + * @param string $id + * The ID of the service to get a reference for. + * @param int $invalid_behavior + * (optional) The invalid behavior of the service. + * + * @return string|\stdClass + * A suitable representation of the service reference. + */ + protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + return (object) array( + 'type' => 'service', + 'id' => $id, + 'invalidBehavior' => $invalid_behavior, + ); + } + + /** + * Gets a parameter reference in a suitable PHP array format. + * + * @param string $name + * The name of the parameter to get a reference for. + * + * @return string|\stdClass + * A suitable representation of the parameter reference. + */ + protected function getParameterCall($name) { + return (object) array( + 'type' => 'parameter', + 'name' => $name, + ); + } + + /** + * Whether this supports the machine-optimized format or not. + * + * @return bool + * TRUE if this supports machine-optimized format, FALSE otherwise. + */ + protected function supportsMachineFormat() { + return TRUE; + } + +} diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php new file mode 100644 index 0000000000000000000000000000000000000000..4932dd70ead9afdf19579cf310f193f084ccf472 --- /dev/null +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php @@ -0,0 +1,77 @@ +serialize = FALSE; + return parent::getArray(); + } + + /** + * {@inheritdoc} + */ + protected function dumpCollection($collection, &$resolve = FALSE) { + $code = array(); + + foreach ($collection as $key => $value) { + if (is_array($value)) { + $code[$key] = $this->dumpCollection($value); + } + else { + $code[$key] = $this->dumpValue($value); + } + } + + return $code; + } + + /** + * {@inheritdoc} + */ + protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + return '@?' . $id; + } + + return '@' . $id; + } + + /** + * {@inheritdoc} + */ + protected function getParameterCall($name) { + return '%' . $name . '%'; + } + + /** + * {@inheritdoc} + */ + protected function supportsMachineFormat() { + return FALSE; + } + +} diff --git a/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php new file mode 100644 index 0000000000000000000000000000000000000000..53c33d903f1665a2106d13414687e8bf65e3fb34 --- /dev/null +++ b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php @@ -0,0 +1,276 @@ +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(); + $this->frozen = isset($container_definition['frozen']) ? $container_definition['frozen'] : FALSE; + + // Register the service_container with itself. + $this->services['service_container'] = $this; + } + + /** + * {@inheritdoc} + */ + protected function createService(array $definition, $id) { + // This method is a verbatim copy of + // \Drupal\Component\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 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(); + if (isset($definition['arguments'])) { + $arguments = $this->resolveServicesAndParameters($definition['arguments']); + } + + if (isset($definition['file'])) { + $file = $this->frozen ? $definition['file'] : current($this->resolveServicesAndParameters(array($definition['file']))); + require_once $file; + } + + if (isset($definition['factory'])) { + $factory = $definition['factory']; + if (is_array($factory)) { + $factory = $this->resolveServicesAndParameters(array($factory[0], $factory[1])); + } + elseif (!is_string($factory)) { + throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory', $id)); + } + + $service = call_user_func_array($factory, $arguments); + } + else { + $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 + // reflection is noticeably slow. + switch ($length) { + case 0: + $service = new $class(); + break; + + case 1: + $service = new $class($arguments[0]); + break; + + case 2: + $service = new $class($arguments[0], $arguments[1]); + break; + + case 3: + $service = new $class($arguments[0], $arguments[1], $arguments[2]); + break; + + case 4: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3]); + break; + + case 5: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]); + break; + + case 6: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]); + break; + + case 7: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]); + break; + + case 8: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7]); + break; + + case 9: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8]); + break; + + case 10: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8], $arguments[9]); + break; + + default: + $r = new \ReflectionClass($class); + $service = $r->newInstanceArgs($arguments); + break; + } + } + + // Share the service if it is public. + if (!isset($definition['public']) || $definition['public'] !== FALSE) { + // Forward compatibility fix for Symfony 2.8 update. + if (!isset($definition['shared']) || $definition['shared'] !== FALSE) { + $this->services[$id] = $service; + } + } + + if (isset($definition['calls'])) { + foreach ($definition['calls'] as $call) { + $method = $call[0]; + $arguments = array(); + if (!empty($call[1])) { + $arguments = $call[1]; + $arguments = $this->resolveServicesAndParameters($arguments); + } + call_user_func_array(array($service, $method), $arguments); + } + } + + if (isset($definition['properties'])) { + $definition['properties'] = $this->resolveServicesAndParameters($definition['properties']); + foreach ($definition['properties'] as $key => $value) { + $service->{$key} = $value; + } + } + + if (isset($definition['configurator'])) { + $callable = $definition['configurator']; + if (is_array($callable)) { + $callable = $this->resolveServicesAndParameters($callable); + } + + if (!is_callable($callable)) { + throw new InvalidArgumentException(sprintf('The configurator for class "%s" is not a callable.', get_class($service))); + } + + call_user_func($callable, $service); + } + + return $service; + } + + /** + * {@inheritdoc} + */ + protected function resolveServicesAndParameters($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. + // - The depth of the tree representing the arguments is not known in + // advance, so it needs to be fully traversed recursively. + foreach ($arguments as $key => $argument) { + if ($argument instanceof \stdClass) { + $type = $argument->type; + + // 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 an argument, which does not have an 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\Component\DependecyInjection\Dumper\OptimizedPhpArrayDumper::getPrivateServiceCall + if ($type == 'private_service') { + $id = $argument->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 a private service from a service definition. + $arguments[$key] = $this->createService($argument->value, $id); + if (!empty($argument->shared)) { + $this->privateServices[$id] = $arguments[$key]; + } + + continue; + } + + if ($type !== NULL) { + throw new InvalidArgumentException("Undefined type '$type' while resolving parameters and services."); + } + } + + if (is_array($argument)) { + $arguments[$key] = $this->resolveServicesAndParameters($argument); + continue; + } + + if (!is_string($argument)) { + continue; + } + + // Resolve parameters. + if ($argument[0] === '%') { + $name = substr($argument, 1, -1); + if (!isset($this->parameters[$name])) { + $arguments[$key] = $this->getParameter($name); + // This can never be reached as getParameter() throws an Exception, + // because we already checked that the parameter is not set above. + } + $argument = $this->parameters[$name]; + $arguments[$key] = $argument; + } + + // Resolve services. + if ($argument[0] === '@') { + $id = substr($argument, 1); + $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + if ($id[0] === '?') { + $id = substr($id, 1); + $invalid_behavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; + } + if (isset($this->services[$id])) { + $arguments[$key] = $this->services[$id]; + } + else { + $arguments[$key] = $this->get($id, $invalid_behavior); + } + } + } + + return $arguments; + } + +} diff --git a/core/lib/Drupal/Component/DependencyInjection/composer.json b/core/lib/Drupal/Component/DependencyInjection/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..7e5e66617ca66c57527d048d96576f177cd4b7b9 --- /dev/null +++ b/core/lib/Drupal/Component/DependencyInjection/composer.json @@ -0,0 +1,18 @@ +{ + "name": "drupal/core-dependency-injection", + "description": "Dependency Injection container optimized for Drupal's needs.", + "keywords": ["drupal", "dependency injection"], + "type": "library", + "homepage": "https://www.drupal.org/project/drupal", + "license": "GPL-2.0+", + "support": { + "issues": "https://www.drupal.org/project/issues/drupal", + "irc": "irc://irc.freenode.net/drupal-contribute", + "source": "https://www.drupal.org/project/drupal/git-instructions" + }, + "autoload": { + "psr-4": { + "Drupal\\Component\\DependencyInjection\\": "" + } + } +} diff --git a/core/lib/Drupal/Core/DependencyInjection/Container.php b/core/lib/Drupal/Core/DependencyInjection/Container.php index 4e28d5490e7131f8cad7c408ceff7557de484dd2..b4cac53c1c329c981e940a46be1b90770a73377e 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Container.php +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php @@ -7,17 +7,18 @@ namespace Drupal\Core\DependencyInjection; -use Symfony\Component\DependencyInjection\Container as SymfonyContainer; +use Drupal\Component\DependencyInjection\Container as DrupalContainer; +use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Extends the symfony container to set the service ID on the created object. + * Extends the Drupal container to set the service ID on the created object. */ -class Container extends SymfonyContainer { +class Container extends DrupalContainer { /** * {@inheritdoc} */ - public function set($id, $service, $scope = SymfonyContainer::SCOPE_CONTAINER) { + public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) { parent::set($id, $service, $scope); // Ensure that the _serviceId property is set on synthetic services as well. diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 89a77e1de8fbdb2c6bbdaeec1f7911f122604e62..8ae5924ce27c0db7ee0302324e918b98c4560794 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -23,12 +23,10 @@ use Drupal\Core\Http\TrustedHostsRequestFactory; use Drupal\Core\Language\Language; use Drupal\Core\PageCache\RequestPolicyInterface; -use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\Site\Settings; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; -use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -53,6 +51,55 @@ */ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { + /** + * Holds the class used for dumping the container to a PHP array. + * + * In combination with swapping the container class this is useful to e.g. + * dump to the human-readable PHP array format to debug the container + * definition in an easier way. + * + * @var string + */ + protected $phpArrayDumperClass = '\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper'; + + /** + * Holds the default bootstrap container definition. + * + * @var array + */ + protected $defaultBootstrapContainerDefinition = [ + 'parameters' => [], + 'services' => [ + 'database' => [ + 'class' => 'Drupal\Core\Database\Connection', + 'factory' => 'Drupal\Core\Database\Database::getConnection', + 'arguments' => ['default'], + ], + 'cache.container' => [ + 'class' => 'Drupal\Core\Cache\DatabaseBackend', + 'arguments' => ['@database', '@cache_tags_provider.container', 'container'], + ], + 'cache_tags_provider.container' => [ + 'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum', + 'arguments' => ['@database'], + ], + ], + ]; + + /** + * Holds the class used for instantiating the bootstrap container. + * + * @var string + */ + protected $bootstrapContainerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer'; + + /** + * Holds the bootstrap container. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $bootstrapContainer; + /** * Holds the container instance. * @@ -97,13 +144,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { */ protected $moduleData = array(); - /** - * PHP code storage object to use for the compiled container. - * - * @var \Drupal\Component\PhpStorage\PhpStorageInterface - */ - protected $storage; - /** * The class loader object. * @@ -397,6 +437,8 @@ public function boot() { FileCacheFactory::setConfiguration($configuration); FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root)); + $this->bootstrapContainer = new $this->bootstrapContainerClass(Settings::get('bootstrap_container_definition', $this->defaultBootstrapContainerDefinition)); + // Initialize the container. $this->initializeContainer(); @@ -446,6 +488,19 @@ public function setContainer(ContainerInterface $container = NULL) { return $this; } + /** + * {@inheritdoc} + */ + public function getCachedContainerDefinition() { + $cache = $this->bootstrapContainer->get('cache.container')->get($this->getContainerCacheKey()); + + if ($cache) { + return $cache->data; + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -706,24 +761,14 @@ public function updateModules(array $module_list, array $module_filenames = arra } /** - * Returns the classname based on environment. + * Returns the container cache key based on the environment. * * @return string - * The class name. + * The cache key used for the service container. */ - protected function getClassName() { - $parts = array('service_container', $this->environment, hash('crc32b', \Drupal::VERSION . Settings::get('deployment_identifier'))); - return implode('_', $parts); - } - - /** - * Returns the container class namespace based on the environment. - * - * @return string - * The class name. - */ - protected function getClassNamespace() { - return 'Drupal\\Core\\DependencyInjection\\Container\\' . $this->environment; + protected function getContainerCacheKey() { + $parts = array('service_container', $this->environment, \Drupal::VERSION, Settings::get('deployment_identifier')); + return implode(':', $parts); } /** @@ -766,32 +811,38 @@ protected function initializeContainer() { // boot the container injected via setContainer(). // @see \Drupal\KernelTests\KernelTestBase::setUp() if (isset($this->container) && !$this->booted) { - $container = $this->container; + $container = $this->container; } // If the module list hasn't already been set in updateModules and we are - // not forcing a rebuild, then try and load the container from the disk. + // not forcing a rebuild, then try and load the container from the cache. if (empty($this->moduleList) && !$this->containerNeedsRebuild) { - $fully_qualified_class_name = '\\' . $this->getClassNamespace() . '\\' . $this->getClassName(); - - // First, try to load from storage. - if (!class_exists($fully_qualified_class_name, FALSE)) { - $this->storage()->load($this->getClassName() . '.php'); - } - // If the load succeeded or the class already existed, use it. - if (class_exists($fully_qualified_class_name, FALSE)) { - $container = new $fully_qualified_class_name; - } + $container_definition = $this->getCachedContainerDefinition(); } - // If there is still no container, build a new one from scratch. - if (!isset($container)) { + // If there is no container and no cached container definition, build a new + // one from scratch. + if (!isset($container) && !isset($container_definition)) { $container = $this->compileContainer(); + + // Only dump the container if dumping is allowed. This is useful for + // KernelTestBase, which never wants to use the real container, but always + // the container builder. + if ($this->allowDumping) { + $dumper = new $this->phpArrayDumperClass($container); + $container_definition = $dumper->getArray(); + } } // The container was rebuilt successfully. $this->containerNeedsRebuild = FALSE; + // Only create a new class if we have a container definition. + if (isset($container_definition)) { + $class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container'); + $container = new $class($container_definition); + } + $this->attachSynthetic($container); $this->container = $container; @@ -816,9 +867,8 @@ protected function initializeContainer() { \Drupal::setContainer($this->container); // If needs dumping flag was set, dump the container. - $base_class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container'); - if ($this->containerNeedsDumping && !$this->dumpDrupalContainer($this->container, $base_class)) { - $this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be written to disk'); + if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) { + $this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.'); } return $this->container; @@ -1034,9 +1084,8 @@ public function invalidateContainer() { return; } - // Also wipe the PHP Storage caches, so that the container is rebuilt - // for the next request. - $this->storage()->deleteAll(); + // Also remove the container definition from the cache backend. + $this->bootstrapContainer->get('cache.container')->deleteAll(); } /** @@ -1194,35 +1243,28 @@ protected function getContainerBuilder() { } /** - * Dumps the service container to PHP code in the config directory. - * - * This method is based on the dumpContainer method in the parent class, but - * that method is reliant on the Config component which we do not use here. + * Stores the container definition in a cache. * - * @param ContainerBuilder $container - * The service container. - * @param string $baseClass - * The name of the container's base class + * @param array $container_definition + * The container definition to cache. * * @return bool - * TRUE if the container was successfully dumped to disk. + * TRUE if the container was successfully cached. */ - protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass) { - if (!$this->storage()->writeable()) { - return FALSE; + protected function cacheDrupalContainer(array $container_definition) { + $saved = TRUE; + try { + $this->bootstrapContainer->get('cache.container')->set($this->getContainerCacheKey(), $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; } - // Cache the container. - $dumper = new PhpDumper($container); - $class = $this->getClassName(); - $namespace = $this->getClassNamespace(); - $content = $dumper->dump([ - 'class' => $class, - 'base_class' => $baseClass, - 'namespace' => $namespace, - ]); - return $this->storage()->save($class . '.php', $content); - } + return $saved; + } /** * Gets a http kernel from the container @@ -1233,18 +1275,6 @@ protected function getHttpKernel() { return $this->container->get('http_kernel'); } - /** - * Gets the PHP code storage object to use for the compiled container. - * - * @return \Drupal\Component\PhpStorage\PhpStorageInterface - */ - protected function storage() { - if (!isset($this->storage)) { - $this->storage = PhpStorageFactory::get('service_container'); - } - return $this->storage; - } - /** * Returns the active configuration storage to use during building the container. * diff --git a/core/lib/Drupal/Core/DrupalKernelInterface.php b/core/lib/Drupal/Core/DrupalKernelInterface.php index 12c32f63f803a0b94d4cb222f8f5b5583a8d6d75..31496946749d0d5e0ff210d794c68b1400fd7169 100644 --- a/core/lib/Drupal/Core/DrupalKernelInterface.php +++ b/core/lib/Drupal/Core/DrupalKernelInterface.php @@ -58,6 +58,16 @@ public function getServiceProviders($origin); */ public function getContainer(); + /** + * Returns the cached container definition - if any. + * + * This also allows inspecting a built container for debugging purposes. + * + * @return array|NULL + * The cached container definition or NULL if not found in cache. + */ + public function getCachedContainerDefinition(); + /** * Set the current site path. * diff --git a/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php index a4424eab8744d1820b45343dbf241fd7d684b7b9..e8e35a6d3a0a683d9fcb6e96cbb0c9bcb93b55b2 100644 --- a/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php +++ b/core/modules/system/src/Tests/DrupalKernel/DrupalKernelTest.php @@ -55,13 +55,11 @@ protected function prepareConfigDirectories() { * A request object to use in booting the kernel. * @param array $modules_enabled * A list of modules to enable on the kernel. - * @param bool $read_only - * Build the kernel in a read only state. * * @return \Drupal\Core\DrupalKernel * New kernel for testing. */ - protected function getTestKernel(Request $request, array $modules_enabled = NULL, $read_only = FALSE) { + protected function getTestKernel(Request $request, array $modules_enabled = NULL) { // Manually create kernel to avoid replacing settings. $class_loader = require DRUPAL_ROOT . '/autoload.php'; $kernel = DrupalKernel::createFromRequest($request, $class_loader, 'testing'); @@ -72,11 +70,6 @@ protected function getTestKernel(Request $request, array $modules_enabled = NULL } $kernel->boot(); - if ($read_only) { - $php_storage = Settings::get('php_storage'); - $php_storage['service_container']['class'] = 'Drupal\Component\PhpStorage\FileReadOnlyStorage'; - $this->settingsSet('php_storage', $php_storage); - } return $kernel; } @@ -98,24 +91,19 @@ public function testCompileDIC() { $kernel = $this->getTestKernel($request); $container = $kernel->getContainer(); $refClass = new \ReflectionClass($container); - $is_compiled_container = - $refClass->getParentClass()->getName() == 'Drupal\Core\DependencyInjection\Container' && - !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); + $is_compiled_container = !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); $this->assertTrue($is_compiled_container); // Verify that the list of modules is the same for the initial and the // compiled container. $module_list = array_keys($container->get('module_handler')->getModuleList()); $this->assertEqual(array_values($modules_enabled), $module_list); - // Now use the read-only storage implementation, simulating a "production" - // environment. - $container = $this->getTestKernel($request, NULL, TRUE) + // Get the container another time, simulating a "production" environment. + $container = $this->getTestKernel($request, NULL) ->getContainer(); $refClass = new \ReflectionClass($container); - $is_compiled_container = - $refClass->getParentClass()->getName() == 'Drupal\Core\DependencyInjection\Container' && - !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); + $is_compiled_container = !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); $this->assertTrue($is_compiled_container); // Verify that the list of modules is the same for the initial and the @@ -137,16 +125,16 @@ public function testCompileDIC() { // Add another module so that we can test that the new module's bundle is // registered to the new container. $modules_enabled['service_provider_test'] = 'service_provider_test'; - $this->getTestKernel($request, $modules_enabled, TRUE); + $this->getTestKernel($request, $modules_enabled); - // Instantiate it a second time and we should still get a ContainerBuilder - // class because we are using the read-only PHP storage. - $kernel = $this->getTestKernel($request, $modules_enabled, TRUE); + // Instantiate it a second time and we should not get a ContainerBuilder + // class because we are loading the container definition from cache. + $kernel = $this->getTestKernel($request, $modules_enabled); $container = $kernel->getContainer(); $refClass = new \ReflectionClass($container); $is_container_builder = $refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); - $this->assertTrue($is_container_builder, 'Container is a builder'); + $this->assertFalse($is_container_builder, 'Container is not a builder'); // Assert that the new module's bundle was registered to the new container. $this->assertTrue($container->has('service_provider_test_class'), 'Container has test service'); diff --git a/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php b/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php index 9b6769262c72334399d2d6d4320d929f2a16f3bd..ef2fb794949a264398fd893117a17368eb996f62 100644 --- a/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php +++ b/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php @@ -7,14 +7,14 @@ namespace Drupal\system\Tests\ServiceProvider; -use Drupal\simpletest\WebTestBase; +use Drupal\simpletest\KernelTestBase; /** * Tests service provider registration to the DIC. * * @group ServiceProvider */ -class ServiceProviderTest extends WebTestBase { +class ServiceProviderTest extends KernelTestBase { /** * Modules to enable. @@ -27,13 +27,9 @@ class ServiceProviderTest extends WebTestBase { * Tests that services provided by module service providers get registered to the DIC. */ function testServiceProviderRegistration() { - $this->assertTrue(\Drupal::getContainer()->getDefinition('file.usage')->getClass() == 'Drupal\\service_provider_test\\TestFileUsage', 'Class has been changed'); + $definition = $this->container->getDefinition('file.usage'); + $this->assertTrue($definition->getClass() == 'Drupal\\service_provider_test\\TestFileUsage', 'Class has been changed'); $this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service has been registered to the DIC'); - // The event subscriber method in the test class calls drupal_set_message with - // a message saying it has fired. This will fire on every page request so it - // should show up on the front page. - $this->drupalGet(''); - $this->assertText(t('The service_provider_test event subscriber fired!'), 'The service_provider_test event subscriber fired'); } /** diff --git a/core/modules/system/src/Tests/ServiceProvider/ServiceProviderWebTest.php b/core/modules/system/src/Tests/ServiceProvider/ServiceProviderWebTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8a6d3bc6b56c925d53928b0f7a4e86eef4bdfe73 --- /dev/null +++ b/core/modules/system/src/Tests/ServiceProvider/ServiceProviderWebTest.php @@ -0,0 +1,41 @@ +assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service has been registered to the DIC'); + // The event subscriber method in the test class calls drupal_set_message() + // with a message saying it has fired. This will fire on every page request + // so it should show up on the front page. + $this->drupalGet(''); + $this->assertText(t('The service_provider_test event subscriber fired!'), 'The service_provider_test event subscriber fired'); + } + +} diff --git a/core/modules/system/src/Tests/System/UncaughtExceptionTest.php b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php index 81cb76a777394a6519e4e1fdab8710b8127e8cbd..3460125aeb15367897468f05e1e151cc26a70029 100644 --- a/core/modules/system/src/Tests/System/UncaughtExceptionTest.php +++ b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php @@ -170,14 +170,7 @@ public function testErrorContainer() { 'required' => TRUE, ]; $this->writeSettings($settings); - - // Need to rebuild the container, so the dumped container can be tested - // and not the container builder. - \Drupal::service('kernel')->rebuildContainer(); - - // Ensure that we don't use the now broken generated container on the test - // process. - \Drupal::setContainer($this->container); + \Drupal::service('kernel')->invalidateContainer(); $this->expectedExceptionMessage = 'Argument 1 passed to Drupal\system\Tests\Bootstrap\ErrorContainer::Drupal\system\Tests\Bootstrap\{closur'; $this->drupalGet(''); @@ -196,14 +189,7 @@ public function testExceptionContainer() { 'required' => TRUE, ]; $this->writeSettings($settings); - - // Need to rebuild the container, so the dumped container can be tested - // and not the container builder. - \Drupal::service('kernel')->rebuildContainer(); - - // Ensure that we don't use the now broken generated container on the test - // process. - \Drupal::setContainer($this->container); + \Drupal::service('kernel')->invalidateContainer(); $this->expectedExceptionMessage = 'Thrown exception during Container::get'; $this->drupalGet(''); diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..21f6f5f170721b34c6c4511e8e96c92f6ac0b333 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php @@ -0,0 +1,1188 @@ +machineFormat = TRUE; + $this->containerClass = '\Drupal\Component\DependencyInjection\Container'; + $this->containerDefinition = $this->getMockContainerDefinition(); + $this->container = new $this->containerClass($this->containerDefinition); + } + + /** + * Tests that passing a non-supported format throws an InvalidArgumentException. + * + * @covers ::__construct + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testConstruct() { + $container_definition = $this->getMockContainerDefinition(); + $container_definition['machine_format'] = !$this->machineFormat; + $container = new $this->containerClass($container_definition); + } + + /** + * Tests that Container::getParameter() works properly. + * + * @covers ::getParameter + */ + public function testGetParameter() { + $this->assertEquals($this->containerDefinition['parameters']['some_config'], $this->container->getParameter('some_config'), 'Container parameter matches for %some_config%.'); + $this->assertEquals($this->containerDefinition['parameters']['some_other_config'], $this->container->getParameter('some_other_config'), 'Container parameter matches for %some_other_config%.'); + } + + /** + * Tests that Container::getParameter() works properly for non-existing + * parameters. + * + * @covers ::getParameter + * @covers ::getParameterAlternatives + * @covers ::getAlternatives + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException + */ + public function testGetParameterIfNotFound() { + $this->container->getParameter('parameter_that_does_not_exist'); + } + + /** + * Tests that Container::getParameter() works properly for NULL parameters. + * + * @covers ::getParameter + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException + */ + public function testGetParameterIfNotFoundBecauseNull() { + $this->container->getParameter(NULL); + } + + /** + * Tests that Container::hasParameter() works properly. + * + * @covers ::hasParameter + */ + public function testHasParameter() { + $this->assertTrue($this->container->hasParameter('some_config'), 'Container parameters include %some_config%.'); + $this->assertFalse($this->container->hasParameter('some_config_not_exists'), 'Container parameters do not include %some_config_not_exists%.'); + } + + /** + * Tests that Container::setParameter() in an unfrozen case works properly. + * + * @covers ::setParameter + */ + public function testSetParameterWithUnfrozenContainer() { + $container_definition = $this->containerDefinition; + $container_definition['frozen'] = FALSE; + $this->container = new $this->containerClass($container_definition); + $this->container->setParameter('some_config', 'new_value'); + $this->assertEquals('new_value', $this->container->getParameter('some_config'), 'Container parameters can be set.'); + } + + /** + * Tests that Container::setParameter() in a frozen case works properly. + * + * @covers ::setParameter + * + * @expectedException LogicException + */ + public function testSetParameterWithFrozenContainer() { + $this->container = new $this->containerClass($this->containerDefinition); + $this->container->setParameter('some_config', 'new_value'); + } + + /** + * Tests that Container::get() works properly. + * + * @covers ::get + * @covers ::createService + */ + public function testGet() { + $container = $this->container->get('service_container'); + $this->assertSame($this->container, $container, 'Container can be retrieved from itself.'); + + // Retrieve services of the container. + $other_service_class = $this->containerDefinition['services']['other.service']['class']; + $other_service = $this->container->get('other.service'); + $this->assertInstanceOf($other_service_class, $other_service, 'other.service has the right class.'); + + $some_parameter = $this->containerDefinition['parameters']['some_config']; + $some_other_parameter = $this->containerDefinition['parameters']['some_other_config']; + + $service = $this->container->get('service.provider'); + + $this->assertEquals($other_service, $service->getSomeOtherService(), '@other.service was injected via constructor.'); + $this->assertEquals($some_parameter, $service->getSomeParameter(), '%some_config% was injected via constructor.'); + $this->assertEquals($this->container, $service->getContainer(), 'Container was injected via setter injection.'); + $this->assertEquals($some_other_parameter, $service->getSomeOtherParameter(), '%some_other_config% was injected via setter injection.'); + $this->assertEquals($service->_someProperty, 'foo', 'Service has added properties.'); + } + + /** + * Tests that Container::get() for non-shared services works properly. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForNonSharedService() { + $service = $this->container->get('non_shared_service'); + $service2 = $this->container->get('non_shared_service'); + + $this->assertNotSame($service, $service2, 'Non shared services are always re-instantiated.'); + } + + /** + * Tests that Container::get() works properly for class from parameters. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForClassFromParameter() { + $container_definition = $this->containerDefinition; + $container_definition['frozen'] = FALSE; + $container = new $this->containerClass($container_definition); + + $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. + * + * @covers ::set + */ + public function testSet() { + $this->assertNull($this->container->get('new_id', ContainerInterface::NULL_ON_INVALID_REFERENCE)); + $mock_service = new MockService(); + $this->container->set('new_id', $mock_service); + + $this->assertSame($mock_service, $this->container->get('new_id'), 'A manual set service works as expected.'); + } + + /** + * Tests that Container::has() works properly. + * + * @covers ::has + */ + public function testHas() { + $this->assertTrue($this->container->has('other.service')); + $this->assertFalse($this->container->has('another.service')); + + // Set the service manually, ensure that its also respected. + $mock_service = new MockService(); + $this->container->set('another.service', $mock_service); + $this->assertTrue($this->container->has('another.service')); + } + + /** + * Tests that Container::get() for circular dependencies works properly. + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException + * @covers ::get + * @covers ::createService + */ + public function testGetForCircularServices() { + $this->container->get('circular_dependency'); + } + + /** + * Tests that Container::get() for non-existent services works properly. + * + * @covers ::get + * @covers ::createService + * @covers ::getAlternatives + * @covers ::getServiceAlternatives + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + */ + public function testGetForNonExistantService() { + $this->container->get('service_not_exists'); + } + + /** + * Tests that Container::get() for a serialized definition works properly. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForSerializedServiceDefinition() { + $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-existent parameters works properly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function testGetForNonExistantParameterDependency() { + $service = $this->container->get('service_parameter_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $this->assertNull($service, 'Service is NULL.'); + } + + /** + * Tests Container::get() with an exception due to missing parameter on the second call. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testGetForParameterDependencyWithExceptionOnSecondCall() { + $service = $this->container->get('service_parameter_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $this->assertNull($service, 'Service is NULL.'); + + // Reset the service. + $this->container->set('service_parameter_not_exists', NULL); + $this->container->get('service_parameter_not_exists'); + } + + /** + * Tests that Container::get() for non-existent parameters works properly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testGetForNonExistantParameterDependencyWithException() { + $this->container->get('service_parameter_not_exists'); + } + + /** + * Tests that Container::get() for non-existent dependencies works properly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function testGetForNonExistantServiceDependency() { + $service = $this->container->get('service_dependency_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $this->assertNull($service, 'Service is NULL.'); + } + + /** + * Tests that Container::get() for non-existent dependencies works properly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + * @covers ::getAlternatives + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + */ + public function testGetForNonExistantServiceDependencyWithException() { + $this->container->get('service_dependency_not_exists'); + } + + /** + * Tests that Container::get() for non-existent services works properly. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForNonExistantServiceWhenUsingNull() { + $this->assertNull($this->container->get('service_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE), 'Not found service does not throw exception.'); + } + + /** + * Tests that Container::get() for NULL service works properly. + * @covers ::get + * @covers ::createService + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + */ + public function testGetForNonExistantNULLService() { + $this->container->get(NULL); + } + + /** + * Tests multiple Container::get() calls for non-existing dependencies work. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForNonExistantServiceMultipleTimes() { + $container = new $this->containerClass(); + + $this->assertNull($container->get('service_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE), 'Not found service does not throw exception.'); + $this->assertNull($container->get('service_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE), 'Not found service does not throw exception on second call.'); + } + + /** + * Tests multiple Container::get() calls with exception on the second time. + * + * @covers ::get + * @covers ::createService + * @covers ::getAlternatives + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + */ + public function testGetForNonExistantServiceWithExceptionOnSecondCall() { + $this->assertNull($this->container->get('service_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE), 'Not found service does nto throw exception.'); + $this->container->get('service_not_exists'); + } + + /** + * Tests that Container::get() for aliased services works properly. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForAliasedService() { + $service = $this->container->get('service.provider'); + $aliased_service = $this->container->get('service.provider_alias'); + $this->assertSame($service, $aliased_service); + } + + /** + * Tests that Container::get() for synthetic services works - if defined. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForSyntheticService() { + $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 testGetForSyntheticServiceWithException() { + $this->container->get('synthetic'); + } + + /** + * Tests that Container::get() for services with file includes works. + * + * @covers ::get + * @covers ::createService + */ + public function testGetWithFileInclude() { + $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 various arguments lengths works. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function testGetForInstantiationWithVariousArgumentLengths() { + $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 wrong factories works correctly. + * + * @covers ::get + * @covers ::createService + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testGetForWrongFactory() { + $this->container->get('wrong_factory'); + } + + /** + * Tests Container::get() for factories via services (Symfony 2.7.0). + * + * @covers ::get + * @covers ::createService + */ + public function testGetForFactoryService() { + $factory_service = $this->container->get('factory_service'); + $factory_service_class = $this->container->getParameter('factory_service_class'); + $this->assertInstanceOf($factory_service_class, $factory_service); + } + + /** + * Tests that Container::get() for factories via class works (Symfony 2.7.0). + * + * @covers ::get + * @covers ::createService + */ + public function testGetForFactoryClass() { + $service = $this->container->get('service.provider'); + $factory_service= $this->container->get('factory_class'); + + $this->assertInstanceOf(get_class($service), $factory_service); + $this->assertEquals('bar', $factory_service->getSomeParameter(), 'Correct parameter was passed via the factory class instantiation.'); + $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 testGetForConfiguratorWithException() { + $this->container->get('configurable_service_exception'); + } + + /** + * Tests that Container::get() for configurable services works. + * + * @covers ::get + * @covers ::createService + */ + public function testGetForConfigurator() { + $container = $this->container; + + // Setup a configurator. + $configurator = $this->prophesize('\Drupal\Tests\Component\DependencyInjection\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. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function testResolveServicesAndParametersForPrivateService() { + $service = $this->container->get('service_using_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_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 testResolveServicesAndParametersForSharedPrivateService() { + $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.'); + } + + /** + * Tests that services with an array of arguments work correctly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function testResolveServicesAndParametersForArgumentsUsingDeepArray() { + $service = $this->container->get('service_using_array'); + $other_service = $this->container->get('other.service'); + $this->assertEquals($other_service, $service->getSomeOtherService(), '@other.service was injected via constructor.'); + } + + /** + * Tests that services that are optional work correctly. + * + * @covers ::get + * @covers ::createService + * @covers ::resolveServicesAndParameters + */ + public function testResolveServicesAndParametersForOptionalServiceDependencies() { + $service = $this->container->get('service_with_optional_dependency'); + $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 testResolveServicesAndParametersForInvalidArgument() { + $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 testResolveServicesAndParametersForInvalidArguments() { + // In case the machine-optimized format is not used, we need to simulate the + // test failure. + if (!$this->machineFormat) { + throw new 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 testResolveServicesAndParametersForServiceInstantiatedFromParameter() { + $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. + * + * @covers ::initialized + */ + public function testInitialized() { + $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 retrieved once.'); + } + + /** + * Tests that Container::initialized works correctly for aliases. + * + * @covers ::initialized + */ + public function testInitializedForAliases() { + $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 testScopeFunctionsWithException($method, $argument) { + $callable = array( + $this->container, + $method, + ); + + $callable($argument); + } + + /** + * Data provider for scopeExceptionTestProvider(). + * + * @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 Container::getServiceIds() works properly. + * + * @covers ::getServiceIds + */ + public function testGetServiceIds() { + $service_definition_keys = array_keys($this->containerDefinition['services']); + $this->assertEquals($service_definition_keys, $this->container->getServiceIds(), 'Retrieved service IDs match definition.'); + + $mock_service = new MockService(); + $this->container->set('bar', $mock_service); + $this->container->set('service.provider', $mock_service); + $service_definition_keys[] = 'bar'; + + $this->assertEquals($service_definition_keys, $this->container->getServiceIds(), 'Retrieved service IDs match definition after setting new services.'); + } + + /** + * Gets a mock container definition. + * + * @return array + * Associated array with parameters and services. + */ + 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( + 'class' => get_class($fake_service), + ); + + $services['non_shared_service'] = array( + 'class' => get_class($fake_service), + 'shared' => FALSE, + ); + + $services['other.service_class_from_parameter'] = array( + 'class' => $this->getParameterCall('some_parameter_class'), + ); + $services['late.service'] = array( + 'class' => get_class($fake_service), + ); + $services['service.provider'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getServiceCall('other.service'), + $this->getParameterCall('some_config'), + )), + 'properties' => $this->getCollection(array('_someProperty' => 'foo')), + 'calls' => array( + array('setContainer', $this->getCollection(array( + $this->getServiceCall('service_container'), + ))), + array('setOtherConfigParameter', $this->getCollection(array( + $this->getParameterCall('some_other_config'), + ))), + ), + 'priority' => 0, + ); + + // Test private services. + $private_service = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getServiceCall('other.service'), + $this->getParameterCall('some_private_config'), + )), + 'public' => FALSE, + ); + + $services['service_using_private'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getPrivateServiceCall(NULL, $private_service), + $this->getParameterCall('some_config'), + )), + ); + $services['another_service_using_private'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\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\Component\DependencyInjection\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\Component\DependencyInjection\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\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + 1, // Test passing non-strings, too. + (object) array( + 'type' => 'invalid', + ), + )), + ); + + $services['invalid_arguments_service'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => (object) array( + 'type' => 'invalid', + ), + ); + + // Test service that needs deep-traversal. + $services['service_using_array'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getCollection(array( + $this->getServiceCall('other.service'), + )), + $this->getParameterCall('some_private_config'), + )), + ); + + $services['service_with_optional_dependency'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getServiceCall('service.does_not_exist', ContainerInterface::NULL_ON_INVALID_REFERENCE), + $this->getParameterCall('some_private_config'), + )), + + ); + + $services['factory_service'] = array( + 'class' => '\Drupal\service_container\ServiceContainer\ControllerInterface', + 'factory' => array( + $this->getServiceCall('service.provider'), + 'getFactoryMethod', + ), + 'arguments' => $this->getCollection(array( + $this->getParameterCall('factory_service_class'), + )), + ); + $services['factory_class'] = array( + 'class' => '\Drupal\service_container\ServiceContainer\ControllerInterface', + 'factory' => '\Drupal\Tests\Component\DependencyInjection\MockService::getFactoryMethod', + 'arguments' => array( + '\Drupal\Tests\Component\DependencyInjection\MockService', + array(NULL, 'bar'), + ), + 'calls' => array( + array('setContainer', $this->getCollection(array( + $this->getServiceCall('service_container'), + ))), + ), + ); + + $services['wrong_factory'] = array( + 'class' => '\Drupal\service_container\ServiceContainer\ControllerInterface', + 'factory' => (object) array('I am not a factory, but I pretend to be.'), + ); + + $services['circular_dependency'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getServiceCall('circular_dependency'), + )), + ); + $services['synthetic'] = array( + 'synthetic' => TRUE, + ); + // The file could have been named as a .php file. The reason it is a .data + // file is that SimpleTest tries to load it. SimpleTest does not like such + // fixtures and hence we use a neutral name like .data. + $services['container_test_file_service_test'] = array( + 'class' => '\stdClass', + 'file' => __DIR__ . '/Fixture/container_test_file_service_test_service_function.data', + ); + + // Test multiple arguments. + $args = array(); + for ($i=0; $i < 12; $i++) { + $services['service_test_instantiation_' . $i] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\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\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getServiceCall('service.provider'), + $this->getParameterCall('not_exists'), + )), + ); + $services['service_dependency_not_exists'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockService', + 'arguments' => $this->getCollection(array( + $this->getServiceCall('service_not_exists'), + $this->getParameterCall('some_config'), + )), + ); + + $services['service_with_parameter_service'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\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\Component\DependencyInjection\MockService', + 'arguments' => array(), + 'configurator' => array( + $this->getServiceCall('configurator'), + 'configureService' + ), + ); + $services['configurable_service_exception'] = array( + 'class' => '\Drupal\Tests\Component\DependencyInjection\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, + 'parameters' => $parameters, + 'services' => $services, + 'frozen' => TRUE, + 'machine_format' => $this->machineFormat, + ); + } + + /** + * 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, + ); + } + + /** + * Helper function to return a service definition. + */ + protected function getParameterCall($name) { + return (object) array( + 'type' => 'parameter', + 'name' => $name, + ); + } + + /** + * 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-optimized collection. + */ + protected function getCollection($collection, $resolve = TRUE) { + return (object) array( + 'type' => 'collection', + 'value' => $collection, + 'resolve' => $resolve, + ); + } + +} + +/** + * Helper interface to test Container::get() with configurator. + * + * @group DependencyInjection + */ +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 DependencyInjection + */ +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 DependencyInjection + */ +class MockService { + + /** + * @var ContainerInterface + */ + protected $container; + + /** + * @var object + */ + protected $someOtherService; + + /** + * @var string + */ + protected $someParameter; + + /** + * @var string + */ + protected $someOtherParameter; + + /** + * Constructs a MockService object. + * + * @param object $some_other_service + * (optional) Another injected service. + * @param string $some_parameter + * (optional) An injected parameter. + */ + public function __construct($some_other_service = NULL, $some_parameter = NULL) { + if (is_array($some_other_service)) { + $some_other_service = $some_other_service[0]; + } + $this->someOtherService = $some_other_service; + $this->someParameter = $some_parameter; + } + + /** + * Sets the container object. + * + * @param ContainerInterface $container + * The container to inject via setter injection. + */ + public function setContainer(ContainerInterface $container) { + $this->container = $container; + } + + /** + * Gets the container object. + * + * @return ContainerInterface + * The internally set container. + */ + public function getContainer() { + return $this->container; + } + + /** + * Gets the someOtherService object. + * + * @return object + * The injected service. + */ + public function getSomeOtherService() { + return $this->someOtherService; + } + + /** + * Gets the someParameter property. + * + * @return string + * The injected parameter. + */ + public function getSomeParameter() { + return $this->someParameter; + } + + /** + * Sets the someOtherParameter property. + * + * @param string $some_other_parameter + * The setter injected parameter. + */ + public function setOtherConfigParameter($some_other_parameter) { + $this->someOtherParameter = $some_other_parameter; + } + + /** + * Gets the someOtherParameter property. + * + * @return string + * The injected parameter. + */ + public function getSomeOtherParameter() { + return $this->someOtherParameter; + } + + /** + * Provides a factory method to get a service. + * + * @param string $class + * The class name of the class to instantiate + * @param array $arguments + * (optional) Arguments to pass to the new class. + * + * @return object + * The instantiated service object. + */ + public static function getFactoryMethod($class, $arguments = array()) { + $r = new \ReflectionClass($class); + $service = ($r->getConstructor() === NULL) ? $r->newInstance() : $r->newInstanceArgs($arguments); + + return $service; + } + +} diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e12754700704e449566d362828fc1f9620592bf9 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php @@ -0,0 +1,621 @@ +containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder'); + $this->containerBuilder->getAliases()->willReturn(array()); + $this->containerBuilder->getParameterBag()->willReturn(new ParameterBag()); + $this->containerBuilder->getDefinitions()->willReturn(NULL); + $this->containerBuilder->isFrozen()->willReturn(TRUE); + + $definition = array(); + $definition['aliases'] = array(); + $definition['parameters'] = array(); + $definition['services'] = array(); + $definition['frozen'] = TRUE; + $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 ::supportsMachineFormat + */ + public function testDumpForEmptyContainer() { + $serialized_definition = $this->dumper->dump(); + $this->assertEquals(serialize($this->containerDefinition), $serialized_definition); + } + + /** + * Tests that alias processing works properly. + * + * @covers ::getAliases + * + * @dataProvider getAliasesDataProvider + */ + public function testGetAliases($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 testGetAliases(). + * + * @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 testGetParameters($parameters, $definition_parameters, $is_frozen) { + $this->containerDefinition['parameters'] = $definition_parameters; + $this->containerDefinition['frozen'] = $is_frozen; + + $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 testGetParameters(). + * + * @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 testGetServiceDefinitions($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 testGetServiceDefinitions(). + * + * @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(), + 'scope' => ContainerInterface::SCOPE_CONTAINER, + 'shared' => TRUE, + '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 service, 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; + + $service_definitions[] = array( + 'scope' => ContainerInterface::SCOPE_PROTOTYPE, + 'shared' => FALSE, + ) + $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($service_definition['scope']); + $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]); + } + } + + // Remove any remaining scope. + unset($filtered_service_definition['scope']); + + 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 testGetServiceDefinitionWithInvalidScope() { + $bar_definition = new Definition('\stdClass'); + $bar_definition->setScope('foo_scope'); + $services['bar'] = $bar_definition; + + $this->containerBuilder->getDefinitions()->willReturn($services); + $this->dumper->getArray(); + } + + /** + * Tests that getDecoratedService() is unsupported. + * + * Tests that the correct InvalidArgumentException is thrown for + * getDecoratedService(). + * + * @covers ::getServiceDefinition + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testGetServiceDefinitionForDecoratedService() { + $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 + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testGetServiceDefinitionForExpression() { + $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 testGetServiceDefinitionForObject() { + $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 testGetServiceDefinitionForResource() { + $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-optimized collection. + */ + protected function getCollection($collection, $resolve = TRUE) { + return (object) array( + 'type' => 'collection', + 'value' => $collection, + 'resolve' => $resolve, + ); + } + + /** + * Helper function to return a parameter 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')) { + /** + * Dummy class to ensure non-existent Symfony component can be tested. + */ + class Expression { + + /** + * Gets the string representation of the expression. + */ + public function __toString() { + return 'dummy_expression'; + } + + } + } +} diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/PhpArrayDumperTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/PhpArrayDumperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..55e9a5f23837a6fb0b49c18799435ac21c3e22ca --- /dev/null +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/PhpArrayDumperTest.php @@ -0,0 +1,59 @@ +machineFormat = FALSE; + $this->dumperClass = '\Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper'; + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function serializeDefinition(array $service_definition) { + return $service_definition; + } + + /** + * {@inheritdoc} + */ + 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); + } + + /** + * {@inheritdoc} + */ + protected function getParameterCall($name) { + return '%' . $name . '%'; + } + + /** + * {@inheritdoc} + */ + protected function getCollection($collection, $resolve = TRUE) { + return $collection; + } + +} diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Fixture/container_test_file_service_test_service_function.data b/core/tests/Drupal/Tests/Component/DependencyInjection/Fixture/container_test_file_service_test_service_function.data new file mode 100644 index 0000000000000000000000000000000000000000..09960fe614f021b7727b82afefbfc759bd48a4f7 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Fixture/container_test_file_service_test_service_function.data @@ -0,0 +1,16 @@ +machineFormat = FALSE; + $this->containerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer'; + $this->containerDefinition = $this->getMockContainerDefinition(); + $this->container = new $this->containerClass($this->containerDefinition); + } + + /** + * 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-optimized '@notation'. + */ + protected function getCollection($collection, $resolve = TRUE) { + return $collection; + } + +}