Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\DependencyInjection\Loader; Chris@0: Chris@0: use Symfony\Component\Config\Util\XmlUtils; Chris@0: use Symfony\Component\DependencyInjection\Alias; Chris@14: use Symfony\Component\DependencyInjection\Argument\BoundArgument; Chris@14: use Symfony\Component\DependencyInjection\Argument\IteratorArgument; Chris@17: use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; Chris@14: use Symfony\Component\DependencyInjection\ChildDefinition; Chris@14: use Symfony\Component\DependencyInjection\ContainerBuilder; Chris@17: use Symfony\Component\DependencyInjection\ContainerInterface; Chris@17: use Symfony\Component\DependencyInjection\Definition; Chris@0: use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\DependencyInjection\Exception\RuntimeException; Chris@17: use Symfony\Component\DependencyInjection\Reference; Chris@0: use Symfony\Component\ExpressionLanguage\Expression; Chris@0: Chris@0: /** Chris@0: * XmlFileLoader loads XML files service definitions. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class XmlFileLoader extends FileLoader Chris@0: { Chris@0: const NS = 'http://symfony.com/schema/dic/services'; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function load($resource, $type = null) Chris@0: { Chris@0: $path = $this->locator->locate($resource); Chris@0: Chris@0: $xml = $this->parseFileToDOM($path); Chris@0: Chris@14: $this->container->fileExists($path); Chris@14: Chris@14: $defaults = $this->getServiceDefaults($xml, $path); Chris@0: Chris@0: // anonymous services Chris@14: $this->processAnonymousServices($xml, $path, $defaults); Chris@0: Chris@0: // imports Chris@0: $this->parseImports($xml, $path); Chris@0: Chris@0: // parameters Chris@14: $this->parseParameters($xml, $path); Chris@0: Chris@0: // extensions Chris@0: $this->loadFromExtensions($xml); Chris@0: Chris@0: // services Chris@14: try { Chris@14: $this->parseDefinitions($xml, $path, $defaults); Chris@14: } finally { Chris@17: $this->instanceof = []; Chris@14: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supports($resource, $type = null) Chris@0: { Chris@17: if (!\is_string($resource)) { Chris@14: return false; Chris@14: } Chris@14: Chris@14: if (null === $type && 'xml' === pathinfo($resource, PATHINFO_EXTENSION)) { Chris@14: return true; Chris@14: } Chris@14: Chris@14: return 'xml' === $type; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses parameters. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@14: * @param string $file Chris@0: */ Chris@14: private function parseParameters(\DOMDocument $xml, $file) Chris@0: { Chris@0: if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) { Chris@14: $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses imports. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: * @param string $file Chris@0: */ Chris@0: private function parseImports(\DOMDocument $xml, $file) Chris@0: { Chris@0: $xpath = new \DOMXPath($xml); Chris@0: $xpath->registerNamespace('container', self::NS); Chris@0: Chris@0: if (false === $imports = $xpath->query('//container:imports/container:import')) { Chris@0: return; Chris@0: } Chris@0: Chris@17: $defaultDirectory = \dirname($file); Chris@0: foreach ($imports as $import) { Chris@0: $this->setCurrentDir($defaultDirectory); Chris@14: $this->import($import->getAttribute('resource'), XmlUtils::phpize($import->getAttribute('type')) ?: null, (bool) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses multiple definitions. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: * @param string $file Chris@0: */ Chris@14: private function parseDefinitions(\DOMDocument $xml, $file, $defaults) Chris@0: { Chris@0: $xpath = new \DOMXPath($xml); Chris@0: $xpath->registerNamespace('container', self::NS); Chris@0: Chris@14: if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) { Chris@0: return; Chris@0: } Chris@17: $this->setCurrentDir(\dirname($file)); Chris@0: Chris@17: $this->instanceof = []; Chris@14: $this->isLoadingInstanceof = true; Chris@14: $instanceof = $xpath->query('//container:services/container:instanceof'); Chris@14: foreach ($instanceof as $service) { Chris@17: $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, [])); Chris@14: } Chris@14: Chris@14: $this->isLoadingInstanceof = false; Chris@0: foreach ($services as $service) { Chris@14: if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { Chris@14: if ('prototype' === $service->tagName) { Chris@14: $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), (string) $service->getAttribute('exclude')); Chris@14: } else { Chris@14: $this->setDefinition((string) $service->getAttribute('id'), $definition); Chris@14: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@14: * Get service defaults. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: private function getServiceDefaults(\DOMDocument $xml, $file) Chris@14: { Chris@14: $xpath = new \DOMXPath($xml); Chris@14: $xpath->registerNamespace('container', self::NS); Chris@14: Chris@14: if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) { Chris@17: return []; Chris@14: } Chris@17: $defaults = [ Chris@14: 'tags' => $this->getChildren($defaultsNode, 'tag'), Chris@14: 'bind' => array_map(function ($v) { return new BoundArgument($v); }, $this->getArgumentsAsPhp($defaultsNode, 'bind', $file)), Chris@17: ]; Chris@14: Chris@14: foreach ($defaults['tags'] as $tag) { Chris@14: if ('' === $tag->getAttribute('name')) { Chris@14: throw new InvalidArgumentException(sprintf('The tag name for tag "" in %s must be a non-empty string.', $file)); Chris@14: } Chris@14: } Chris@14: Chris@14: if ($defaultsNode->hasAttribute('autowire')) { Chris@14: $defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire')); Chris@14: } Chris@14: if ($defaultsNode->hasAttribute('public')) { Chris@14: $defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public')); Chris@14: } Chris@14: if ($defaultsNode->hasAttribute('autoconfigure')) { Chris@14: $defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure')); Chris@14: } Chris@14: Chris@14: return $defaults; Chris@14: } Chris@14: Chris@14: /** Chris@0: * Parses an individual Definition. Chris@0: * Chris@0: * @param \DOMElement $service Chris@0: * @param string $file Chris@14: * @param array $defaults Chris@0: * Chris@0: * @return Definition|null Chris@0: */ Chris@14: private function parseDefinition(\DOMElement $service, $file, array $defaults) Chris@0: { Chris@0: if ($alias = $service->getAttribute('alias')) { Chris@0: $this->validateAlias($service, $file); Chris@0: Chris@14: $this->container->setAlias((string) $service->getAttribute('id'), $alias = new Alias($alias)); Chris@0: if ($publicAttr = $service->getAttribute('public')) { Chris@14: $alias->setPublic(XmlUtils::phpize($publicAttr)); Chris@14: } elseif (isset($defaults['public'])) { Chris@14: $alias->setPublic($defaults['public']); Chris@0: } Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@14: if ($this->isLoadingInstanceof) { Chris@14: $definition = new ChildDefinition(''); Chris@14: } elseif ($parent = $service->getAttribute('parent')) { Chris@14: if (!empty($this->instanceof)) { Chris@14: throw new InvalidArgumentException(sprintf('The service "%s" cannot use the "parent" option in the same file where "instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file.', $service->getAttribute('id'))); Chris@14: } Chris@14: Chris@14: foreach ($defaults as $k => $v) { Chris@14: if ('tags' === $k) { Chris@14: // since tags are never inherited from parents, there is no confusion Chris@14: // thus we can safely add them as defaults to ChildDefinition Chris@14: continue; Chris@14: } Chris@14: if ('bind' === $k) { Chris@14: if ($defaults['bind']) { Chris@14: throw new InvalidArgumentException(sprintf('Bound values on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file.', $service->getAttribute('id'))); Chris@14: } Chris@14: Chris@14: continue; Chris@14: } Chris@14: if (!$service->hasAttribute($k)) { Chris@14: throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $service->getAttribute('id'))); Chris@14: } Chris@14: } Chris@14: Chris@14: $definition = new ChildDefinition($parent); Chris@0: } else { Chris@0: $definition = new Definition(); Chris@14: Chris@14: if (isset($defaults['public'])) { Chris@14: $definition->setPublic($defaults['public']); Chris@14: } Chris@14: if (isset($defaults['autowire'])) { Chris@14: $definition->setAutowired($defaults['autowire']); Chris@14: } Chris@14: if (isset($defaults['autoconfigure'])) { Chris@14: $definition->setAutoconfigured($defaults['autoconfigure']); Chris@14: } Chris@14: Chris@17: $definition->setChanges([]); Chris@0: } Chris@0: Chris@17: foreach (['class', 'public', 'shared', 'synthetic', 'lazy', 'abstract'] as $key) { Chris@0: if ($value = $service->getAttribute($key)) { Chris@0: $method = 'set'.$key; Chris@0: $definition->$method(XmlUtils::phpize($value)); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($value = $service->getAttribute('autowire')) { Chris@0: $definition->setAutowired(XmlUtils::phpize($value)); Chris@0: } Chris@0: Chris@14: if ($value = $service->getAttribute('autoconfigure')) { Chris@14: if (!$definition instanceof ChildDefinition) { Chris@14: $definition->setAutoconfigured(XmlUtils::phpize($value)); Chris@14: } elseif ($value = XmlUtils::phpize($value)) { Chris@14: throw new InvalidArgumentException(sprintf('The service "%s" cannot have a "parent" and also have "autoconfigure". Try setting autoconfigure="false" for the service.', $service->getAttribute('id'))); Chris@14: } Chris@14: } Chris@14: Chris@0: if ($files = $this->getChildren($service, 'file')) { Chris@0: $definition->setFile($files[0]->nodeValue); Chris@0: } Chris@0: Chris@0: if ($deprecated = $this->getChildren($service, 'deprecated')) { Chris@0: $definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null); Chris@0: } Chris@0: Chris@14: $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', $file, false, $definition instanceof ChildDefinition)); Chris@14: $definition->setProperties($this->getArgumentsAsPhp($service, 'property', $file)); Chris@0: Chris@0: if ($factories = $this->getChildren($service, 'factory')) { Chris@0: $factory = $factories[0]; Chris@0: if ($function = $factory->getAttribute('function')) { Chris@0: $definition->setFactory($function); Chris@0: } else { Chris@14: if ($childService = $factory->getAttribute('service')) { Chris@0: $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); Chris@0: } else { Chris@14: $class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null; Chris@0: } Chris@0: Chris@17: $definition->setFactory([$class, $factory->getAttribute('method')]); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($configurators = $this->getChildren($service, 'configurator')) { Chris@0: $configurator = $configurators[0]; Chris@0: if ($function = $configurator->getAttribute('function')) { Chris@0: $definition->setConfigurator($function); Chris@0: } else { Chris@14: if ($childService = $configurator->getAttribute('service')) { Chris@0: $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); Chris@0: } else { Chris@0: $class = $configurator->getAttribute('class'); Chris@0: } Chris@0: Chris@17: $definition->setConfigurator([$class, $configurator->getAttribute('method')]); Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($this->getChildren($service, 'call') as $call) { Chris@14: $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file)); Chris@0: } Chris@0: Chris@14: $tags = $this->getChildren($service, 'tag'); Chris@14: Chris@14: if (!empty($defaults['tags'])) { Chris@14: $tags = array_merge($tags, $defaults['tags']); Chris@14: } Chris@14: Chris@14: foreach ($tags as $tag) { Chris@17: $parameters = []; Chris@0: foreach ($tag->attributes as $name => $node) { Chris@0: if ('name' === $name) { Chris@0: continue; Chris@0: } Chris@0: Chris@18: if (false !== strpos($name, '-') && false === strpos($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { Chris@0: $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); Chris@0: } Chris@0: // keep not normalized key Chris@0: $parameters[$name] = XmlUtils::phpize($node->nodeValue); Chris@0: } Chris@0: Chris@0: if ('' === $tag->getAttribute('name')) { Chris@0: throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', (string) $service->getAttribute('id'), $file)); Chris@0: } Chris@0: Chris@0: $definition->addTag($tag->getAttribute('name'), $parameters); Chris@0: } Chris@0: Chris@0: foreach ($this->getChildren($service, 'autowiring-type') as $type) { Chris@0: $definition->addAutowiringType($type->textContent); Chris@0: } Chris@0: Chris@14: $bindings = $this->getArgumentsAsPhp($service, 'bind', $file); Chris@14: if (isset($defaults['bind'])) { Chris@14: // deep clone, to avoid multiple process of the same instance in the passes Chris@14: $bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings); Chris@14: } Chris@14: if ($bindings) { Chris@14: $definition->setBindings($bindings); Chris@14: } Chris@14: Chris@0: if ($value = $service->getAttribute('decorates')) { Chris@0: $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null; Chris@0: $priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0; Chris@0: $definition->setDecoratedService($value, $renameId, $priority); Chris@0: } Chris@0: Chris@0: return $definition; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses a XML file to a \DOMDocument. Chris@0: * Chris@0: * @param string $file Path to a file Chris@0: * Chris@0: * @return \DOMDocument Chris@0: * Chris@0: * @throws InvalidArgumentException When loading of XML file returns error Chris@0: */ Chris@0: private function parseFileToDOM($file) Chris@0: { Chris@0: try { Chris@17: $dom = XmlUtils::loadFile($file, [$this, 'validateSchema']); Chris@0: } catch (\InvalidArgumentException $e) { Chris@17: throw new InvalidArgumentException(sprintf('Unable to parse file "%s": %s', $file, $e->getMessage()), $e->getCode(), $e); Chris@0: } Chris@0: Chris@0: $this->validateExtensions($dom, $file); Chris@0: Chris@0: return $dom; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Processes anonymous services. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: * @param string $file Chris@14: * @param array $defaults Chris@0: */ Chris@14: private function processAnonymousServices(\DOMDocument $xml, $file, $defaults) Chris@0: { Chris@17: $definitions = []; Chris@0: $count = 0; Chris@17: $suffix = '~'.ContainerBuilder::hash($file); Chris@0: Chris@0: $xpath = new \DOMXPath($xml); Chris@0: $xpath->registerNamespace('container', self::NS); Chris@0: Chris@0: // anonymous services as arguments/properties Chris@14: if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:bind[not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) { Chris@0: foreach ($nodes as $node) { Chris@14: if ($services = $this->getChildren($node, 'service')) { Chris@14: // give it a unique name Chris@17: $id = sprintf('%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $services[0]->getAttribute('class')).$suffix); Chris@14: $node->setAttribute('id', $id); Chris@14: $node->setAttribute('service', $id); Chris@0: Chris@17: $definitions[$id] = [$services[0], $file, false]; Chris@0: $services[0]->setAttribute('id', $id); Chris@0: Chris@0: // anonymous services are always private Chris@0: // we could not use the constant false here, because of XML parsing Chris@0: $services[0]->setAttribute('public', 'false'); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // anonymous services "in the wild" Chris@0: if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) { Chris@0: foreach ($nodes as $node) { Chris@14: @trigger_error(sprintf('Top-level anonymous services are deprecated since Symfony 3.4, the "id" attribute will be required in version 4.0 in %s at line %d.', $file, $node->getLineNo()), E_USER_DEPRECATED); Chris@14: Chris@0: // give it a unique name Chris@14: $id = sprintf('%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $node->getAttribute('class')).$suffix); Chris@0: $node->setAttribute('id', $id); Chris@17: $definitions[$id] = [$node, $file, true]; Chris@0: } Chris@0: } Chris@0: Chris@0: // resolve definitions Chris@14: uksort($definitions, 'strnatcmp'); Chris@14: foreach (array_reverse($definitions) as $id => list($domElement, $file, $wild)) { Chris@17: if (null !== $definition = $this->parseDefinition($domElement, $file, $wild ? $defaults : [])) { Chris@14: $this->setDefinition($id, $definition); Chris@0: } Chris@0: Chris@0: if (true === $wild) { Chris@0: $tmpDomElement = new \DOMElement('_services', null, self::NS); Chris@0: $domElement->parentNode->replaceChild($tmpDomElement, $domElement); Chris@0: $tmpDomElement->setAttribute('id', $id); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns arguments as valid php types. Chris@0: * Chris@0: * @param \DOMElement $node Chris@0: * @param string $name Chris@14: * @param string $file Chris@0: * @param bool $lowercase Chris@0: * Chris@0: * @return mixed Chris@0: */ Chris@14: private function getArgumentsAsPhp(\DOMElement $node, $name, $file, $lowercase = true, $isChildDefinition = false) Chris@0: { Chris@17: $arguments = []; Chris@0: foreach ($this->getChildren($node, $name) as $arg) { Chris@0: if ($arg->hasAttribute('name')) { Chris@0: $arg->setAttribute('key', $arg->getAttribute('name')); Chris@0: } Chris@0: Chris@14: // this is used by ChildDefinition to overwrite a specific Chris@0: // argument of the parent definition Chris@0: if ($arg->hasAttribute('index')) { Chris@14: $key = ($isChildDefinition ? 'index_' : '').$arg->getAttribute('index'); Chris@0: } elseif (!$arg->hasAttribute('key')) { Chris@0: // Append an empty argument, then fetch its key to overwrite it later Chris@0: $arguments[] = null; Chris@0: $keys = array_keys($arguments); Chris@0: $key = array_pop($keys); Chris@0: } else { Chris@0: $key = $arg->getAttribute('key'); Chris@14: } Chris@0: Chris@14: $onInvalid = $arg->getAttribute('on-invalid'); Chris@14: $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; Chris@14: if ('ignore' == $onInvalid) { Chris@14: $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; Chris@14: } elseif ('ignore_uninitialized' == $onInvalid) { Chris@14: $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE; Chris@14: } elseif ('null' == $onInvalid) { Chris@14: $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; Chris@0: } Chris@0: Chris@0: switch ($arg->getAttribute('type')) { Chris@0: case 'service': Chris@17: if ('' === $arg->getAttribute('id')) { Chris@14: throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file)); Chris@14: } Chris@14: if ($arg->hasAttribute('strict')) { Chris@14: @trigger_error(sprintf('The "strict" attribute used when referencing the "%s" service is deprecated since Symfony 3.3 and will be removed in 4.0.', $arg->getAttribute('id')), E_USER_DEPRECATED); Chris@0: } Chris@0: Chris@0: $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior); Chris@0: break; Chris@0: case 'expression': Chris@14: if (!class_exists(Expression::class)) { Chris@14: throw new \LogicException(sprintf('The type="expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".')); Chris@14: } Chris@14: Chris@0: $arguments[$key] = new Expression($arg->nodeValue); Chris@0: break; Chris@0: case 'collection': Chris@14: $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, $file, false); Chris@14: break; Chris@14: case 'iterator': Chris@14: $arg = $this->getArgumentsAsPhp($arg, $name, $file, false); Chris@14: try { Chris@14: $arguments[$key] = new IteratorArgument($arg); Chris@14: } catch (InvalidArgumentException $e) { Chris@14: throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file)); Chris@14: } Chris@14: break; Chris@14: case 'tagged': Chris@14: if (!$arg->getAttribute('tag')) { Chris@14: throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged" has no or empty "tag" attribute in "%s".', $name, $file)); Chris@14: } Chris@14: $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag')); Chris@0: break; Chris@0: case 'string': Chris@0: $arguments[$key] = $arg->nodeValue; Chris@0: break; Chris@0: case 'constant': Chris@17: $arguments[$key] = \constant(trim($arg->nodeValue)); Chris@0: break; Chris@0: default: Chris@0: $arguments[$key] = XmlUtils::phpize($arg->nodeValue); Chris@0: } Chris@0: } Chris@0: Chris@0: return $arguments; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get child elements by name. Chris@0: * Chris@0: * @param \DOMNode $node Chris@0: * @param mixed $name Chris@0: * Chris@17: * @return \DOMElement[] Chris@0: */ Chris@0: private function getChildren(\DOMNode $node, $name) Chris@0: { Chris@17: $children = []; Chris@0: foreach ($node->childNodes as $child) { Chris@14: if ($child instanceof \DOMElement && $child->localName === $name && self::NS === $child->namespaceURI) { Chris@0: $children[] = $child; Chris@0: } Chris@0: } Chris@0: Chris@0: return $children; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates a documents XML schema. Chris@0: * Chris@0: * @param \DOMDocument $dom Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @throws RuntimeException When extension references a non-existent XSD file Chris@0: */ Chris@0: public function validateSchema(\DOMDocument $dom) Chris@0: { Chris@17: $schemaLocations = ['http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd')]; Chris@0: Chris@0: if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) { Chris@0: $items = preg_split('/\s+/', $element); Chris@17: for ($i = 0, $nb = \count($items); $i < $nb; $i += 2) { Chris@0: if (!$this->container->hasExtension($items[$i])) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) { Chris@18: $ns = $extension->getNamespace(); Chris@18: $path = str_replace([$ns, str_replace('http://', 'https://', $ns)], str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]); Chris@0: Chris@0: if (!is_file($path)) { Chris@17: throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', \get_class($extension), $path)); Chris@0: } Chris@0: Chris@0: $schemaLocations[$items[$i]] = $path; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@17: $tmpfiles = []; Chris@0: $imports = ''; Chris@0: foreach ($schemaLocations as $namespace => $location) { Chris@0: $parts = explode('/', $location); Chris@14: $locationstart = 'file:///'; Chris@0: if (0 === stripos($location, 'phar://')) { Chris@14: $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); Chris@0: if ($tmpfile) { Chris@0: copy($location, $tmpfile); Chris@0: $tmpfiles[] = $tmpfile; Chris@0: $parts = explode('/', str_replace('\\', '/', $tmpfile)); Chris@14: } else { Chris@14: array_shift($parts); Chris@14: $locationstart = 'phar:///'; Chris@0: } Chris@0: } Chris@17: $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; Chris@14: $location = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); Chris@0: Chris@0: $imports .= sprintf(' '."\n", $namespace, $location); Chris@0: } Chris@0: Chris@0: $source = << Chris@0: Chris@0: Chris@0: Chris@0: $imports Chris@0: Chris@0: EOF Chris@0: ; Chris@0: Chris@0: $disableEntities = libxml_disable_entity_loader(false); Chris@0: $valid = @$dom->schemaValidateSource($source); Chris@0: libxml_disable_entity_loader($disableEntities); Chris@0: Chris@0: foreach ($tmpfiles as $tmpfile) { Chris@0: @unlink($tmpfile); Chris@0: } Chris@0: Chris@0: return $valid; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates an alias. Chris@0: * Chris@0: * @param \DOMElement $alias Chris@0: * @param string $file Chris@0: */ Chris@0: private function validateAlias(\DOMElement $alias, $file) Chris@0: { Chris@0: foreach ($alias->attributes as $name => $node) { Chris@17: if (!\in_array($name, ['alias', 'id', 'public'])) { Chris@0: @trigger_error(sprintf('Using the attribute "%s" is deprecated for the service "%s" which is defined as an alias in "%s". Allowed attributes for service aliases are "alias", "id" and "public". The XmlFileLoader will raise an exception in Symfony 4.0, instead of silently ignoring unsupported attributes.', $name, $alias->getAttribute('id'), $file), E_USER_DEPRECATED); Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($alias->childNodes as $child) { Chris@14: if ($child instanceof \DOMElement && self::NS === $child->namespaceURI) { Chris@0: @trigger_error(sprintf('Using the element "%s" is deprecated for the service "%s" which is defined as an alias in "%s". The XmlFileLoader will raise an exception in Symfony 4.0, instead of silently ignoring unsupported elements.', $child->localName, $alias->getAttribute('id'), $file), E_USER_DEPRECATED); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates an extension. Chris@0: * Chris@0: * @param \DOMDocument $dom Chris@0: * @param string $file Chris@0: * Chris@0: * @throws InvalidArgumentException When no extension is found corresponding to a tag Chris@0: */ Chris@0: private function validateExtensions(\DOMDocument $dom, $file) Chris@0: { Chris@0: foreach ($dom->documentElement->childNodes as $node) { Chris@0: if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: // can it be handled by an extension? Chris@0: if (!$this->container->hasExtension($node->namespaceURI)) { Chris@0: $extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions())); Chris@17: throw new InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s', $node->tagName, $file, $node->namespaceURI, $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none')); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Loads from an extension. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: */ Chris@0: private function loadFromExtensions(\DOMDocument $xml) Chris@0: { Chris@0: foreach ($xml->documentElement->childNodes as $node) { Chris@14: if (!$node instanceof \DOMElement || self::NS === $node->namespaceURI) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $values = static::convertDomElementToArray($node); Chris@17: if (!\is_array($values)) { Chris@17: $values = []; Chris@0: } Chris@0: Chris@0: $this->container->loadFromExtension($node->namespaceURI, $values); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@14: * Converts a \DOMElement object to a PHP array. Chris@0: * Chris@0: * The following rules applies during the conversion: Chris@0: * Chris@0: * * Each tag is converted to a key value or an array Chris@0: * if there is more than one "value" Chris@0: * Chris@0: * * The content of a tag is set under a "value" key (bar) Chris@0: * if the tag also has some nested tags Chris@0: * Chris@0: * * The attributes are converted to keys () Chris@0: * Chris@0: * * The nested-tags are converted to keys (bar) Chris@0: * Chris@14: * @param \DOMElement $element A \DOMElement instance Chris@0: * Chris@0: * @return array A PHP array Chris@0: */ Chris@0: public static function convertDomElementToArray(\DOMElement $element) Chris@0: { Chris@0: return XmlUtils::convertDomElementToArray($element); Chris@0: } Chris@0: }