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 |