| Total Complexity | 211 |
| Total Lines | 864 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like XmlFileLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use XmlFileLoader, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 38 | class XmlFileLoader extends FileLoader |
||
| 39 | { |
||
| 40 | public const NS = 'http://symfony.com/schema/dic/services'; |
||
| 41 | |||
| 42 | protected bool $autoRegisterAliasesForSinglyImplementedInterfaces = false; |
||
| 43 | |||
| 44 | public function load(mixed $resource, ?string $type = null): mixed |
||
| 45 | { |
||
| 46 | $path = $this->locator->locate($resource); |
||
| 47 | |||
| 48 | $xml = $this->parseFileToDOM($path); |
||
|
|
|||
| 49 | |||
| 50 | $this->container->fileExists($path); |
||
| 51 | |||
| 52 | $this->loadXml($xml, $path); |
||
| 53 | |||
| 54 | if ($this->env) { |
||
| 55 | $xpath = new \DOMXPath($xml); |
||
| 56 | $xpath->registerNamespace('container', self::NS); |
||
| 57 | foreach ($xpath->query(\sprintf('//container:when[@env="%s"]', $this->env)) ?: [] as $root) { |
||
| 58 | $env = $this->env; |
||
| 59 | $this->env = null; |
||
| 60 | try { |
||
| 61 | $this->loadXml($xml, $path, $root); |
||
| 62 | } finally { |
||
| 63 | $this->env = $env; |
||
| 64 | } |
||
| 65 | } |
||
| 66 | } |
||
| 67 | |||
| 68 | return null; |
||
| 69 | } |
||
| 70 | |||
| 71 | private function loadXml(\DOMDocument $xml, string $path, ?\DOMNode $root = null): void |
||
| 72 | { |
||
| 73 | $defaults = $this->getServiceDefaults($xml, $path, $root); |
||
| 74 | |||
| 75 | // anonymous services |
||
| 76 | $this->processAnonymousServices($xml, $path, $root); |
||
| 77 | |||
| 78 | // imports |
||
| 79 | $this->parseImports($xml, $path, $root); |
||
| 80 | |||
| 81 | // parameters |
||
| 82 | $this->parseParameters($xml, $path, $root); |
||
| 83 | |||
| 84 | // extensions |
||
| 85 | $this->loadFromExtensions($xml, $root); |
||
| 86 | |||
| 87 | // services |
||
| 88 | try { |
||
| 89 | $this->parseDefinitions($xml, $path, $defaults, $root); |
||
| 90 | } finally { |
||
| 91 | $this->instanceof = []; |
||
| 92 | $this->registerAliasesForSinglyImplementedInterfaces(); |
||
| 93 | } |
||
| 94 | } |
||
| 95 | |||
| 96 | public function supports(mixed $resource, ?string $type = null): bool |
||
| 97 | { |
||
| 98 | if (!\is_string($resource)) { |
||
| 99 | return false; |
||
| 100 | } |
||
| 101 | |||
| 102 | if (null === $type && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION)) { |
||
| 103 | return true; |
||
| 104 | } |
||
| 105 | |||
| 106 | return 'xml' === $type; |
||
| 107 | } |
||
| 108 | |||
| 109 | private function parseParameters(\DOMDocument $xml, string $file, ?\DOMNode $root = null): void |
||
| 110 | { |
||
| 111 | if ($parameters = $this->getChildren($root ?? $xml->documentElement, 'parameters')) { |
||
| 112 | $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file)); |
||
| 113 | } |
||
| 114 | } |
||
| 115 | |||
| 116 | private function parseImports(\DOMDocument $xml, string $file, ?\DOMNode $root = null): void |
||
| 117 | { |
||
| 118 | $xpath = new \DOMXPath($xml); |
||
| 119 | $xpath->registerNamespace('container', self::NS); |
||
| 120 | |||
| 121 | if (false === $imports = $xpath->query('./container:imports/container:import', $root)) { |
||
| 122 | return; |
||
| 123 | } |
||
| 124 | |||
| 125 | $defaultDirectory = \dirname($file); |
||
| 126 | foreach ($imports as $import) { |
||
| 127 | $this->setCurrentDir($defaultDirectory); |
||
| 128 | $this->import($import->getAttribute('resource'), XmlUtils::phpize($import->getAttribute('type')) ?: null, XmlUtils::phpize($import->getAttribute('ignore-errors')) ?: false, $file); |
||
| 129 | } |
||
| 130 | } |
||
| 131 | |||
| 132 | private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults, ?\DOMNode $root = null): void |
||
| 133 | { |
||
| 134 | $xpath = new \DOMXPath($xml); |
||
| 135 | $xpath->registerNamespace('container', self::NS); |
||
| 136 | |||
| 137 | if (false === $services = $xpath->query('./container:services/container:service|./container:services/container:prototype|./container:services/container:stack', $root)) { |
||
| 138 | return; |
||
| 139 | } |
||
| 140 | $this->setCurrentDir(\dirname($file)); |
||
| 141 | |||
| 142 | $this->instanceof = []; |
||
| 143 | $this->isLoadingInstanceof = true; |
||
| 144 | $instanceof = $xpath->query('./container:services/container:instanceof', $root); |
||
| 145 | foreach ($instanceof as $service) { |
||
| 146 | $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, new Definition())); |
||
| 147 | } |
||
| 148 | |||
| 149 | $this->isLoadingInstanceof = false; |
||
| 150 | foreach ($services as $service) { |
||
| 151 | if ('stack' === $service->tagName) { |
||
| 152 | $service->setAttribute('parent', '-'); |
||
| 153 | $definition = $this->parseDefinition($service, $file, $defaults) |
||
| 154 | ->setTags(array_merge_recursive(['container.stack' => [[]]], $defaults->getTags())) |
||
| 155 | ; |
||
| 156 | $this->setDefinition($id = (string) $service->getAttribute('id'), $definition); |
||
| 157 | $stack = []; |
||
| 158 | |||
| 159 | foreach ($this->getChildren($service, 'service') as $k => $frame) { |
||
| 160 | $k = $frame->getAttribute('id') ?: $k; |
||
| 161 | $frame->setAttribute('id', $id.'" at index "'.$k); |
||
| 162 | |||
| 163 | if ($alias = $frame->getAttribute('alias')) { |
||
| 164 | $this->validateAlias($frame, $file); |
||
| 165 | $stack[$k] = new Reference($alias); |
||
| 166 | } else { |
||
| 167 | $stack[$k] = $this->parseDefinition($frame, $file, $defaults) |
||
| 168 | ->setInstanceofConditionals($this->instanceof); |
||
| 169 | } |
||
| 170 | } |
||
| 171 | |||
| 172 | $definition->setArguments($stack); |
||
| 173 | } elseif (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { |
||
| 174 | if ('prototype' === $service->tagName) { |
||
| 175 | $excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue'); |
||
| 176 | if ($service->hasAttribute('exclude')) { |
||
| 177 | if (\count($excludes) > 0) { |
||
| 178 | throw new InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.'); |
||
| 179 | } |
||
| 180 | $excludes = [$service->getAttribute('exclude')]; |
||
| 181 | } |
||
| 182 | $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes, $file); |
||
| 183 | } else { |
||
| 184 | $this->setDefinition((string) $service->getAttribute('id'), $definition); |
||
| 185 | } |
||
| 186 | } |
||
| 187 | } |
||
| 188 | } |
||
| 189 | |||
| 190 | private function getServiceDefaults(\DOMDocument $xml, string $file, ?\DOMNode $root = null): Definition |
||
| 191 | { |
||
| 192 | $xpath = new \DOMXPath($xml); |
||
| 193 | $xpath->registerNamespace('container', self::NS); |
||
| 194 | |||
| 195 | if (null === $defaultsNode = $xpath->query('./container:services/container:defaults', $root)->item(0)) { |
||
| 196 | return new Definition(); |
||
| 197 | } |
||
| 198 | |||
| 199 | $defaultsNode->setAttribute('id', '<defaults>'); |
||
| 200 | |||
| 201 | return $this->parseDefinition($defaultsNode, $file, new Definition()); |
||
| 202 | } |
||
| 203 | |||
| 204 | /** |
||
| 205 | * Parses an individual Definition. |
||
| 206 | */ |
||
| 207 | private function parseDefinition(\DOMElement $service, string $file, Definition $defaults): ?Definition |
||
| 208 | { |
||
| 209 | if ($alias = $service->getAttribute('alias')) { |
||
| 210 | $this->validateAlias($service, $file); |
||
| 211 | |||
| 212 | $this->container->setAlias($service->getAttribute('id'), $alias = new Alias($alias)); |
||
| 213 | if ($publicAttr = $service->getAttribute('public')) { |
||
| 214 | $alias->setPublic(XmlUtils::phpize($publicAttr)); |
||
| 215 | } elseif ($defaults->getChanges()['public'] ?? false) { |
||
| 216 | $alias->setPublic($defaults->isPublic()); |
||
| 217 | } |
||
| 218 | |||
| 219 | if ($deprecated = $this->getChildren($service, 'deprecated')) { |
||
| 220 | $message = $deprecated[0]->nodeValue ?: ''; |
||
| 221 | $package = $deprecated[0]->getAttribute('package') ?: ''; |
||
| 222 | $version = $deprecated[0]->getAttribute('version') ?: ''; |
||
| 223 | |||
| 224 | if (!$deprecated[0]->hasAttribute('package')) { |
||
| 225 | throw new InvalidArgumentException(\sprintf('Missing attribute "package" at node "deprecated" in "%s".', $file)); |
||
| 226 | } |
||
| 227 | |||
| 228 | if (!$deprecated[0]->hasAttribute('version')) { |
||
| 229 | throw new InvalidArgumentException(\sprintf('Missing attribute "version" at node "deprecated" in "%s".', $file)); |
||
| 230 | } |
||
| 231 | |||
| 232 | $alias->setDeprecated($package, $version, $message); |
||
| 233 | } |
||
| 234 | |||
| 235 | return null; |
||
| 236 | } |
||
| 237 | |||
| 238 | if ($this->isLoadingInstanceof) { |
||
| 239 | $definition = new ChildDefinition(''); |
||
| 240 | } elseif ($parent = $service->getAttribute('parent')) { |
||
| 241 | $definition = new ChildDefinition($parent); |
||
| 242 | } else { |
||
| 243 | $definition = new Definition(); |
||
| 244 | } |
||
| 245 | |||
| 246 | if ($defaults->getChanges()['public'] ?? false) { |
||
| 247 | $definition->setPublic($defaults->isPublic()); |
||
| 248 | } |
||
| 249 | $definition->setAutowired($defaults->isAutowired()); |
||
| 250 | $definition->setAutoconfigured($defaults->isAutoconfigured()); |
||
| 251 | $definition->setChanges([]); |
||
| 252 | |||
| 253 | foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) { |
||
| 254 | if ($value = $service->getAttribute($key)) { |
||
| 255 | $method = 'set'.$key; |
||
| 256 | $definition->$method(XmlUtils::phpize($value)); |
||
| 257 | } |
||
| 258 | } |
||
| 259 | |||
| 260 | if ($value = $service->getAttribute('lazy')) { |
||
| 261 | $definition->setLazy((bool) $value = XmlUtils::phpize($value)); |
||
| 262 | if (\is_string($value)) { |
||
| 263 | $definition->addTag('proxy', ['interface' => $value]); |
||
| 264 | } |
||
| 265 | } |
||
| 266 | |||
| 267 | if ($value = $service->getAttribute('autowire')) { |
||
| 268 | $definition->setAutowired(XmlUtils::phpize($value)); |
||
| 269 | } |
||
| 270 | |||
| 271 | if ($value = $service->getAttribute('autoconfigure')) { |
||
| 272 | $definition->setAutoconfigured(XmlUtils::phpize($value)); |
||
| 273 | } |
||
| 274 | |||
| 275 | if ($files = $this->getChildren($service, 'file')) { |
||
| 276 | $definition->setFile($files[0]->nodeValue); |
||
| 277 | } |
||
| 278 | |||
| 279 | if ($deprecated = $this->getChildren($service, 'deprecated')) { |
||
| 280 | $message = $deprecated[0]->nodeValue ?: ''; |
||
| 281 | $package = $deprecated[0]->getAttribute('package') ?: ''; |
||
| 282 | $version = $deprecated[0]->getAttribute('version') ?: ''; |
||
| 283 | |||
| 284 | if (!$deprecated[0]->hasAttribute('package')) { |
||
| 285 | throw new InvalidArgumentException(\sprintf('Missing attribute "package" at node "deprecated" in "%s".', $file)); |
||
| 286 | } |
||
| 287 | |||
| 288 | if (!$deprecated[0]->hasAttribute('version')) { |
||
| 289 | throw new InvalidArgumentException(\sprintf('Missing attribute "version" at node "deprecated" in "%s".', $file)); |
||
| 290 | } |
||
| 291 | |||
| 292 | $definition->setDeprecated($package, $version, $message); |
||
| 293 | } |
||
| 294 | |||
| 295 | $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', $file, $definition instanceof ChildDefinition)); |
||
| 296 | $definition->setProperties($this->getArgumentsAsPhp($service, 'property', $file)); |
||
| 297 | |||
| 298 | if ($factories = $this->getChildren($service, 'factory')) { |
||
| 299 | $factory = $factories[0]; |
||
| 300 | if ($function = $factory->getAttribute('function')) { |
||
| 301 | $definition->setFactory($function); |
||
| 302 | } elseif ($expression = $factory->getAttribute('expression')) { |
||
| 303 | if (!class_exists(Expression::class)) { |
||
| 304 | throw new \LogicException('The "expression" attribute cannot be used on factories without the ExpressionLanguage component. Try running "composer require symfony/expression-language".'); |
||
| 305 | } |
||
| 306 | $definition->setFactory('@='.$expression); |
||
| 307 | } else { |
||
| 308 | if ($childService = $factory->getAttribute('service')) { |
||
| 309 | $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); |
||
| 310 | } else { |
||
| 311 | $class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null; |
||
| 312 | } |
||
| 313 | |||
| 314 | $definition->setFactory([$class, $factory->getAttribute('method') ?: '__invoke']); |
||
| 315 | } |
||
| 316 | } |
||
| 317 | |||
| 318 | if ($constructor = $service->getAttribute('constructor')) { |
||
| 319 | if (null !== $definition->getFactory()) { |
||
| 320 | throw new LogicException(\sprintf('The "%s" service cannot declare a factory as well as a constructor.', $service->getAttribute('id'))); |
||
| 321 | } |
||
| 322 | |||
| 323 | $definition->setFactory([null, $constructor]); |
||
| 324 | } |
||
| 325 | |||
| 326 | if ($configurators = $this->getChildren($service, 'configurator')) { |
||
| 327 | $configurator = $configurators[0]; |
||
| 328 | if ($function = $configurator->getAttribute('function')) { |
||
| 329 | $definition->setConfigurator($function); |
||
| 330 | } else { |
||
| 331 | if ($childService = $configurator->getAttribute('service')) { |
||
| 332 | $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); |
||
| 333 | } else { |
||
| 334 | $class = $configurator->getAttribute('class'); |
||
| 335 | } |
||
| 336 | |||
| 337 | $definition->setConfigurator([$class, $configurator->getAttribute('method') ?: '__invoke']); |
||
| 338 | } |
||
| 339 | } |
||
| 340 | |||
| 341 | foreach ($this->getChildren($service, 'call') as $call) { |
||
| 342 | $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file), XmlUtils::phpize($call->getAttribute('returns-clone'))); |
||
| 343 | } |
||
| 344 | |||
| 345 | $tags = $this->getChildren($service, 'tag'); |
||
| 346 | |||
| 347 | foreach ($tags as $tag) { |
||
| 348 | $tagNameComesFromAttribute = $tag->childElementCount || '' === $tag->nodeValue; |
||
| 349 | if ('' === $tagName = $tagNameComesFromAttribute ? $tag->getAttribute('name') : $tag->nodeValue) { |
||
| 350 | throw new InvalidArgumentException(\sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', $service->getAttribute('id'), $file)); |
||
| 351 | } |
||
| 352 | |||
| 353 | $parameters = $this->getTagAttributes($tag, \sprintf('The attribute name of tag "%s" for service "%s" in %s must be a non-empty string.', $tagName, $service->getAttribute('id'), $file)); |
||
| 354 | foreach ($tag->attributes as $name => $node) { |
||
| 355 | if ($tagNameComesFromAttribute && 'name' === $name) { |
||
| 356 | continue; |
||
| 357 | } |
||
| 358 | |||
| 359 | if (str_contains($name, '-') && !str_contains($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { |
||
| 360 | $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); |
||
| 361 | } |
||
| 362 | // keep not normalized key |
||
| 363 | $parameters[$name] = XmlUtils::phpize($node->nodeValue); |
||
| 364 | } |
||
| 365 | |||
| 366 | $definition->addTag($tagName, $parameters); |
||
| 367 | } |
||
| 368 | |||
| 369 | $definition->setTags(array_merge_recursive($definition->getTags(), $defaults->getTags())); |
||
| 370 | |||
| 371 | $bindings = $this->getArgumentsAsPhp($service, 'bind', $file); |
||
| 372 | $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING; |
||
| 373 | foreach ($bindings as $argument => $value) { |
||
| 374 | $bindings[$argument] = new BoundArgument($value, true, $bindingType, $file); |
||
| 375 | } |
||
| 376 | |||
| 377 | // deep clone, to avoid multiple process of the same instance in the passes |
||
| 378 | $bindings = array_merge(unserialize(serialize($defaults->getBindings())), $bindings); |
||
| 379 | |||
| 380 | if ($bindings) { |
||
| 381 | $definition->setBindings($bindings); |
||
| 382 | } |
||
| 383 | |||
| 384 | if ($decorates = $service->getAttribute('decorates')) { |
||
| 385 | $decorationOnInvalid = $service->getAttribute('decoration-on-invalid') ?: 'exception'; |
||
| 386 | if ('exception' === $decorationOnInvalid) { |
||
| 387 | $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; |
||
| 388 | } elseif ('ignore' === $decorationOnInvalid) { |
||
| 389 | $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; |
||
| 390 | } elseif ('null' === $decorationOnInvalid) { |
||
| 391 | $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; |
||
| 392 | } else { |
||
| 393 | throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration-on-invalid" on service "%s". Did you mean "exception", "ignore" or "null" in "%s"?', $decorationOnInvalid, $service->getAttribute('id'), $file)); |
||
| 394 | } |
||
| 395 | |||
| 396 | $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null; |
||
| 397 | $priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0; |
||
| 398 | |||
| 399 | $definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior); |
||
| 400 | } |
||
| 401 | |||
| 402 | if ($callable = $this->getChildren($service, 'from-callable')) { |
||
| 403 | if ($definition instanceof ChildDefinition) { |
||
| 404 | throw new InvalidArgumentException(\sprintf('Attribute "parent" is unsupported when using "<from-callable>" on service "%s".', $service->getAttribute('id'))); |
||
| 405 | } |
||
| 406 | |||
| 407 | foreach ([ |
||
| 408 | 'Attribute "synthetic"' => 'isSynthetic', |
||
| 409 | 'Attribute "file"' => 'getFile', |
||
| 410 | 'Tag "<factory>"' => 'getFactory', |
||
| 411 | 'Tag "<argument>"' => 'getArguments', |
||
| 412 | 'Tag "<property>"' => 'getProperties', |
||
| 413 | 'Tag "<configurator>"' => 'getConfigurator', |
||
| 414 | 'Tag "<call>"' => 'getMethodCalls', |
||
| 415 | ] as $key => $method) { |
||
| 416 | if ($definition->$method()) { |
||
| 417 | throw new InvalidArgumentException($key.\sprintf(' is unsupported when using "<from-callable>" on service "%s".', $service->getAttribute('id'))); |
||
| 418 | } |
||
| 419 | } |
||
| 420 | |||
| 421 | $definition->setFactory(['Closure', 'fromCallable']); |
||
| 422 | |||
| 423 | if ('Closure' !== ($definition->getClass() ?? 'Closure')) { |
||
| 424 | $definition->setLazy(true); |
||
| 425 | } else { |
||
| 426 | $definition->setClass('Closure'); |
||
| 427 | } |
||
| 428 | |||
| 429 | $callable = $callable[0]; |
||
| 430 | if ($function = $callable->getAttribute('function')) { |
||
| 431 | $definition->setArguments([$function]); |
||
| 432 | } elseif ($expression = $callable->getAttribute('expression')) { |
||
| 433 | if (!class_exists(Expression::class)) { |
||
| 434 | throw new \LogicException('The "expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".'); |
||
| 435 | } |
||
| 436 | $definition->setArguments(['@='.$expression]); |
||
| 437 | } else { |
||
| 438 | if ($childService = $callable->getAttribute('service')) { |
||
| 439 | $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); |
||
| 440 | } else { |
||
| 441 | $class = $callable->hasAttribute('class') ? $callable->getAttribute('class') : null; |
||
| 442 | } |
||
| 443 | |||
| 444 | $definition->setArguments([[$class, $callable->getAttribute('method') ?: '__invoke']]); |
||
| 445 | } |
||
| 446 | } |
||
| 447 | |||
| 448 | return $definition; |
||
| 449 | } |
||
| 450 | |||
| 451 | /** |
||
| 452 | * Parses an XML file to a \DOMDocument. |
||
| 453 | * |
||
| 454 | * @throws InvalidArgumentException When loading of XML file returns error |
||
| 455 | */ |
||
| 456 | private function parseFileToDOM(string $file): \DOMDocument |
||
| 457 | { |
||
| 458 | try { |
||
| 459 | $dom = XmlUtils::loadFile($file, $this->validateSchema(...)); |
||
| 460 | } catch (\InvalidArgumentException $e) { |
||
| 461 | $invalidSecurityElements = []; |
||
| 462 | $errors = explode("\n", $e->getMessage()); |
||
| 463 | foreach ($errors as $i => $error) { |
||
| 464 | if (preg_match("#^\[ERROR 1871] Element '\{http://symfony\.com/schema/dic/security}([^']+)'#", $error, $matches)) { |
||
| 465 | $invalidSecurityElements[$i] = $matches[1]; |
||
| 466 | } |
||
| 467 | } |
||
| 468 | if ($invalidSecurityElements) { |
||
| 469 | $dom = XmlUtils::loadFile($file); |
||
| 470 | |||
| 471 | foreach ($invalidSecurityElements as $errorIndex => $tagName) { |
||
| 472 | foreach ($dom->getElementsByTagNameNS('http://symfony.com/schema/dic/security', $tagName) as $element) { |
||
| 473 | if (!$parent = $element->parentNode) { |
||
| 474 | continue; |
||
| 475 | } |
||
| 476 | if ('http://symfony.com/schema/dic/security' !== $parent->namespaceURI) { |
||
| 477 | continue; |
||
| 478 | } |
||
| 479 | if ('provider' === $parent->localName || 'firewall' === $parent->localName) { |
||
| 480 | unset($errors[$errorIndex]); |
||
| 481 | } |
||
| 482 | } |
||
| 483 | } |
||
| 484 | } |
||
| 485 | if ($errors) { |
||
| 486 | throw new InvalidArgumentException(\sprintf('Unable to parse file "%s": ', $file).implode("\n", $errors), $e->getCode(), $e); |
||
| 487 | } |
||
| 488 | } |
||
| 489 | |||
| 490 | $this->validateExtensions($dom, $file); |
||
| 491 | |||
| 492 | return $dom; |
||
| 493 | } |
||
| 494 | |||
| 495 | /** |
||
| 496 | * Processes anonymous services. |
||
| 497 | */ |
||
| 498 | private function processAnonymousServices(\DOMDocument $xml, string $file, ?\DOMNode $root = null): void |
||
| 499 | { |
||
| 500 | $definitions = []; |
||
| 501 | $count = 0; |
||
| 502 | $suffix = '~'.ContainerBuilder::hash($file); |
||
| 503 | |||
| 504 | $xpath = new \DOMXPath($xml); |
||
| 505 | $xpath->registerNamespace('container', self::NS); |
||
| 506 | |||
| 507 | // anonymous services as arguments/properties |
||
| 508 | 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)]', $root)) { |
||
| 509 | foreach ($nodes as $node) { |
||
| 510 | if ($services = $this->getChildren($node, 'service')) { |
||
| 511 | // give it a unique name |
||
| 512 | $id = \sprintf('.%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $services[0]->getAttribute('class')).$suffix); |
||
| 513 | $node->setAttribute('id', $id); |
||
| 514 | $node->setAttribute('service', $id); |
||
| 515 | |||
| 516 | $definitions[$id] = [$services[0], $file]; |
||
| 517 | $services[0]->setAttribute('id', $id); |
||
| 518 | |||
| 519 | // anonymous services are always private |
||
| 520 | // we could not use the constant false here, because of XML parsing |
||
| 521 | $services[0]->setAttribute('public', 'false'); |
||
| 522 | } |
||
| 523 | } |
||
| 524 | } |
||
| 525 | |||
| 526 | // anonymous services "in the wild" |
||
| 527 | if (false !== $nodes = $xpath->query('.//container:services/container:service[not(@id)]', $root)) { |
||
| 528 | foreach ($nodes as $node) { |
||
| 529 | throw new InvalidArgumentException(\sprintf('Top-level services must have "id" attribute, none found in "%s" at line %d.', $file, $node->getLineNo())); |
||
| 530 | } |
||
| 531 | } |
||
| 532 | |||
| 533 | // resolve definitions |
||
| 534 | uksort($definitions, 'strnatcmp'); |
||
| 535 | foreach (array_reverse($definitions) as $id => [$domElement, $file]) { |
||
| 536 | if (null !== $definition = $this->parseDefinition($domElement, $file, new Definition())) { |
||
| 537 | $this->setDefinition($id, $definition); |
||
| 538 | } |
||
| 539 | } |
||
| 540 | } |
||
| 541 | |||
| 542 | private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file, bool $isChildDefinition = false): array |
||
| 543 | { |
||
| 544 | $arguments = []; |
||
| 545 | foreach ($this->getChildren($node, $name) as $arg) { |
||
| 546 | if ($arg->hasAttribute('name')) { |
||
| 547 | $arg->setAttribute('key', $arg->getAttribute('name')); |
||
| 548 | } |
||
| 549 | |||
| 550 | // this is used by ChildDefinition to overwrite a specific |
||
| 551 | // argument of the parent definition |
||
| 552 | if ($arg->hasAttribute('index')) { |
||
| 553 | $key = ($isChildDefinition ? 'index_' : '').$arg->getAttribute('index'); |
||
| 554 | } elseif (!$arg->hasAttribute('key')) { |
||
| 555 | // Append an empty argument, then fetch its key to overwrite it later |
||
| 556 | $arguments[] = null; |
||
| 557 | $keys = array_keys($arguments); |
||
| 558 | $key = array_pop($keys); |
||
| 559 | } else { |
||
| 560 | $key = $arg->getAttribute('key'); |
||
| 561 | } |
||
| 562 | |||
| 563 | switch ($arg->getAttribute('key-type')) { |
||
| 564 | case 'binary': |
||
| 565 | if (false === $key = base64_decode($key, true)) { |
||
| 566 | throw new InvalidArgumentException(\sprintf('Tag "<%s>" with key-type="binary" does not have a valid base64 encoded key in "%s".', $name, $file)); |
||
| 567 | } |
||
| 568 | break; |
||
| 569 | case 'constant': |
||
| 570 | try { |
||
| 571 | $key = \constant(trim($key)); |
||
| 572 | } catch (\Error) { |
||
| 573 | throw new InvalidArgumentException(\sprintf('The key "%s" is not a valid constant in "%s".', $key, $file)); |
||
| 574 | } |
||
| 575 | break; |
||
| 576 | } |
||
| 577 | |||
| 578 | $trim = $arg->hasAttribute('trim') && XmlUtils::phpize($arg->getAttribute('trim')); |
||
| 579 | $onInvalid = $arg->getAttribute('on-invalid'); |
||
| 580 | $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; |
||
| 581 | if ('ignore' == $onInvalid) { |
||
| 582 | $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; |
||
| 583 | } elseif ('ignore_uninitialized' == $onInvalid) { |
||
| 584 | $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE; |
||
| 585 | } elseif ('null' == $onInvalid) { |
||
| 586 | $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; |
||
| 587 | } |
||
| 588 | |||
| 589 | switch ($type = $arg->getAttribute('type')) { |
||
| 590 | case 'service': |
||
| 591 | if ('' === $arg->getAttribute('id')) { |
||
| 592 | throw new InvalidArgumentException(\sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file)); |
||
| 593 | } |
||
| 594 | |||
| 595 | $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior); |
||
| 596 | break; |
||
| 597 | case 'expression': |
||
| 598 | if (!class_exists(Expression::class)) { |
||
| 599 | throw new \LogicException('The type="expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".'); |
||
| 600 | } |
||
| 601 | |||
| 602 | $arguments[$key] = new Expression($arg->nodeValue); |
||
| 603 | break; |
||
| 604 | case 'collection': |
||
| 605 | $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, $file); |
||
| 606 | break; |
||
| 607 | case 'iterator': |
||
| 608 | $arg = $this->getArgumentsAsPhp($arg, $name, $file); |
||
| 609 | $arguments[$key] = new IteratorArgument($arg); |
||
| 610 | break; |
||
| 611 | case 'closure': |
||
| 612 | case 'service_closure': |
||
| 613 | if ('' !== $arg->getAttribute('id')) { |
||
| 614 | $arg = new Reference($arg->getAttribute('id'), $invalidBehavior); |
||
| 615 | } else { |
||
| 616 | $arg = $this->getArgumentsAsPhp($arg, $name, $file); |
||
| 617 | } |
||
| 618 | $arguments[$key] = match ($type) { |
||
| 619 | 'service_closure' => new ServiceClosureArgument($arg), |
||
| 620 | 'closure' => (new Definition('Closure')) |
||
| 621 | ->setFactory(['Closure', 'fromCallable']) |
||
| 622 | ->addArgument($arg), |
||
| 623 | }; |
||
| 624 | break; |
||
| 625 | case 'service_locator': |
||
| 626 | $arg = $this->getArgumentsAsPhp($arg, $name, $file); |
||
| 627 | $arguments[$key] = new ServiceLocatorArgument($arg); |
||
| 628 | break; |
||
| 629 | case 'tagged': |
||
| 630 | trigger_deprecation('symfony/dependency-injection', '7.2', 'Type "tagged" is deprecated for tag <%s>, use "tagged_iterator" instead in "%s".', $name, $file); |
||
| 631 | // no break |
||
| 632 | case 'tagged_iterator': |
||
| 633 | case 'tagged_locator': |
||
| 634 | $forLocator = 'tagged_locator' === $type; |
||
| 635 | |||
| 636 | if (!$arg->getAttribute('tag')) { |
||
| 637 | throw new InvalidArgumentException(\sprintf('Tag "<%s>" with type="%s" has no or empty "tag" attribute in "%s".', $name, $type, $file)); |
||
| 638 | } |
||
| 639 | |||
| 640 | $excludes = array_column($this->getChildren($arg, 'exclude'), 'nodeValue'); |
||
| 641 | if ($arg->hasAttribute('exclude')) { |
||
| 642 | if (\count($excludes) > 0) { |
||
| 643 | throw new InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.'); |
||
| 644 | } |
||
| 645 | $excludes = [$arg->getAttribute('exclude')]; |
||
| 646 | } |
||
| 647 | |||
| 648 | $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator, $arg->getAttribute('default-priority-method') ?: null, $excludes, !$arg->hasAttribute('exclude-self') || XmlUtils::phpize($arg->getAttribute('exclude-self'))); |
||
| 649 | |||
| 650 | if ($forLocator) { |
||
| 651 | $arguments[$key] = new ServiceLocatorArgument($arguments[$key]); |
||
| 652 | } |
||
| 653 | break; |
||
| 654 | case 'binary': |
||
| 655 | if (false === $value = base64_decode($arg->nodeValue)) { |
||
| 656 | throw new InvalidArgumentException(\sprintf('Tag "<%s>" with type="binary" is not a valid base64 encoded string.', $name)); |
||
| 657 | } |
||
| 658 | $arguments[$key] = $value; |
||
| 659 | break; |
||
| 660 | case 'abstract': |
||
| 661 | $arguments[$key] = new AbstractArgument($arg->nodeValue); |
||
| 662 | break; |
||
| 663 | case 'string': |
||
| 664 | $arguments[$key] = $trim ? trim($arg->nodeValue) : $arg->nodeValue; |
||
| 665 | break; |
||
| 666 | case 'constant': |
||
| 667 | $arguments[$key] = \constant(trim($arg->nodeValue)); |
||
| 668 | break; |
||
| 669 | default: |
||
| 670 | $arguments[$key] = XmlUtils::phpize($trim ? trim($arg->nodeValue) : $arg->nodeValue); |
||
| 671 | } |
||
| 672 | } |
||
| 673 | |||
| 674 | return $arguments; |
||
| 675 | } |
||
| 676 | |||
| 677 | /** |
||
| 678 | * Get child elements by name. |
||
| 679 | * |
||
| 680 | * @return \DOMElement[] |
||
| 681 | */ |
||
| 682 | private function getChildren(\DOMNode $node, string $name): array |
||
| 683 | { |
||
| 684 | $children = []; |
||
| 685 | foreach ($node->childNodes as $child) { |
||
| 686 | if ($child instanceof \DOMElement && $child->localName === $name && self::NS === $child->namespaceURI) { |
||
| 687 | $children[] = $child; |
||
| 688 | } |
||
| 689 | } |
||
| 690 | |||
| 691 | return $children; |
||
| 692 | } |
||
| 693 | |||
| 694 | private function getTagAttributes(\DOMNode $node, string $missingName): array |
||
| 716 | } |
||
| 717 | |||
| 718 | /** |
||
| 719 | * Validates a documents XML schema. |
||
| 720 | * |
||
| 721 | * @throws RuntimeException When extension references a non-existent XSD file |
||
| 722 | */ |
||
| 723 | public function validateSchema(\DOMDocument $dom): bool |
||
| 724 | { |
||
| 725 | $schemaLocations = ['http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd')]; |
||
| 726 | |||
| 727 | if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) { |
||
| 728 | $items = preg_split('/\s+/', $element); |
||
| 729 | for ($i = 0, $nb = \count($items); $i < $nb; $i += 2) { |
||
| 730 | if (!$this->container->hasExtension($items[$i])) { |
||
| 731 | continue; |
||
| 732 | } |
||
| 733 | |||
| 734 | if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) { |
||
| 735 | $ns = $extension->getNamespace(); |
||
| 736 | $path = str_replace([$ns, str_replace('http://', 'https://', $ns)], str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]); |
||
| 737 | |||
| 738 | if (!is_file($path)) { |
||
| 739 | throw new RuntimeException(\sprintf('Extension "%s" references a non-existent XSD file "%s".', get_debug_type($extension), $path)); |
||
| 740 | } |
||
| 741 | |||
| 742 | $schemaLocations[$items[$i]] = $path; |
||
| 743 | } |
||
| 744 | } |
||
| 745 | } |
||
| 746 | |||
| 747 | $tmpfiles = []; |
||
| 748 | $imports = ''; |
||
| 749 | foreach ($schemaLocations as $namespace => $location) { |
||
| 750 | $parts = explode('/', $location); |
||
| 751 | $locationstart = 'file:///'; |
||
| 752 | if (0 === stripos($location, 'phar://')) { |
||
| 753 | $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); |
||
| 754 | if ($tmpfile) { |
||
| 755 | copy($location, $tmpfile); |
||
| 756 | $tmpfiles[] = $tmpfile; |
||
| 757 | $parts = explode('/', str_replace('\\', '/', $tmpfile)); |
||
| 758 | } else { |
||
| 759 | array_shift($parts); |
||
| 760 | $locationstart = 'phar:///'; |
||
| 761 | } |
||
| 762 | } elseif ('\\' === \DIRECTORY_SEPARATOR && str_starts_with($location, '\\\\')) { |
||
| 763 | $locationstart = ''; |
||
| 764 | } |
||
| 765 | $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; |
||
| 766 | $location = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); |
||
| 767 | |||
| 768 | $imports .= \sprintf(' <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location); |
||
| 769 | } |
||
| 770 | |||
| 771 | $source = <<<EOF |
||
| 772 | <?xml version="1.0" encoding="utf-8" ?> |
||
| 773 | <xsd:schema xmlns="http://symfony.com/schema" |
||
| 774 | xmlns:xsd="http://www.w3.org/2001/XMLSchema" |
||
| 775 | targetNamespace="http://symfony.com/schema" |
||
| 776 | elementFormDefault="qualified"> |
||
| 777 | |||
| 778 | <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/> |
||
| 779 | $imports |
||
| 780 | </xsd:schema> |
||
| 781 | EOF |
||
| 782 | ; |
||
| 783 | |||
| 784 | if ($this->shouldEnableEntityLoader()) { |
||
| 785 | $disableEntities = libxml_disable_entity_loader(false); |
||
| 786 | $valid = @$dom->schemaValidateSource($source); |
||
| 787 | libxml_disable_entity_loader($disableEntities); |
||
| 788 | } else { |
||
| 789 | $valid = @$dom->schemaValidateSource($source); |
||
| 790 | } |
||
| 791 | foreach ($tmpfiles as $tmpfile) { |
||
| 792 | @unlink($tmpfile); |
||
| 793 | } |
||
| 794 | |||
| 795 | return $valid; |
||
| 796 | } |
||
| 797 | |||
| 798 | private function shouldEnableEntityLoader(): bool |
||
| 821 | } |
||
| 822 | |||
| 823 | private function validateAlias(\DOMElement $alias, string $file): void |
||
| 837 | } |
||
| 838 | } |
||
| 839 | } |
||
| 840 | |||
| 841 | /** |
||
| 842 | * Validates an extension. |
||
| 843 | * |
||
| 844 | * @throws InvalidArgumentException When no extension is found corresponding to a tag |
||
| 845 | */ |
||
| 846 | private function validateExtensions(\DOMDocument $dom, string $file): void |
||
| 847 | { |
||
| 848 | foreach ($dom->documentElement->childNodes as $node) { |
||
| 849 | if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) { |
||
| 850 | continue; |
||
| 851 | } |
||
| 852 | |||
| 853 | // can it be handled by an extension? |
||
| 854 | if (!$this->prepend && !$this->container->hasExtension($node->namespaceURI)) { |
||
| 855 | $extensionNamespaces = array_filter(array_map(fn (ExtensionInterface $ext) => $ext->getNamespace(), $this->container->getExtensions())); |
||
| 856 | throw new InvalidArgumentException(UndefinedExtensionHandler::getErrorMessage($node->tagName, $file, $node->namespaceURI, $extensionNamespaces)); |
||
| 857 | } |
||
| 858 | } |
||
| 859 | } |
||
| 860 | |||
| 861 | /** |
||
| 862 | * Loads from an extension. |
||
| 863 | */ |
||
| 864 | private function loadFromExtensions(\DOMDocument $xml): void |
||
| 880 | } |
||
| 881 | |||
| 882 | /** |
||
| 883 | * Converts a \DOMElement object to a PHP array. |
||
| 884 | * |
||
| 885 | * The following rules applies during the conversion: |
||
| 886 | * |
||
| 887 | * * Each tag is converted to a key value or an array |
||
| 888 | * if there is more than one "value" |
||
| 889 | * |
||
| 890 | * * The content of a tag is set under a "value" key (<foo>bar</foo>) |
||
| 891 | * if the tag also has some nested tags |
||
| 892 | * |
||
| 893 | * * The attributes are converted to keys (<foo foo="bar"/>) |
||
| 894 | * |
||
| 895 | * * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>) |
||
| 896 | * |
||
| 897 | * @param \DOMElement $element A \DOMElement instance |
||
| 898 | */ |
||
| 899 | public static function convertDomElementToArray(\DOMElement $element): mixed |
||
| 902 | } |
||
| 903 | } |
||
| 904 |