Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\Routing\Loader;
13
 
14
use Symfony\Component\Config\Loader\FileLoader;
15
use Symfony\Component\Config\Resource\FileResource;
16
use Symfony\Component\Config\Util\XmlUtils;
17
use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
18
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
19
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
20
use Symfony\Component\Routing\RouteCollection;
21
 
22
/**
23
 * XmlFileLoader loads XML routing files.
24
 *
25
 * @author Fabien Potencier <fabien@symfony.com>
26
 * @author Tobias Schultze <http://tobion.de>
27
 */
28
class XmlFileLoader extends FileLoader
29
{
30
    use HostTrait;
31
    use LocalizedRouteTrait;
32
    use PrefixTrait;
33
 
34
    public const NAMESPACE_URI = 'http://symfony.com/schema/routing';
35
    public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
36
 
37
    /**
38
     * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be
39
     *                                   parsed because it does not validate against the scheme
40
     */
41
    public function load(mixed $file, string $type = null): RouteCollection
42
    {
43
        $path = $this->locator->locate($file);
44
 
45
        $xml = $this->loadFile($path);
46
 
47
        $collection = new RouteCollection();
48
        $collection->addResource(new FileResource($path));
49
 
50
        // process routes and imports
51
        foreach ($xml->documentElement->childNodes as $node) {
52
            if (!$node instanceof \DOMElement) {
53
                continue;
54
            }
55
 
56
            $this->parseNode($collection, $node, $path, $file);
57
        }
58
 
59
        return $collection;
60
    }
61
 
62
    /**
63
     * Parses a node from a loaded XML file.
64
     *
65
     * @throws \InvalidArgumentException When the XML is invalid
66
     */
67
    protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file)
68
    {
69
        if (self::NAMESPACE_URI !== $node->namespaceURI) {
70
            return;
71
        }
72
 
73
        switch ($node->localName) {
74
            case 'route':
75
                $this->parseRoute($collection, $node, $path);
76
                break;
77
            case 'import':
78
                $this->parseImport($collection, $node, $path, $file);
79
                break;
80
            case 'when':
81
                if (!$this->env || $node->getAttribute('env') !== $this->env) {
82
                    break;
83
                }
84
                foreach ($node->childNodes as $node) {
85
                    if ($node instanceof \DOMElement) {
86
                        $this->parseNode($collection, $node, $path, $file);
87
                    }
88
                }
89
                break;
90
            default:
91
                throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
92
        }
93
    }
94
 
95
    public function supports(mixed $resource, string $type = null): bool
96
    {
97
        return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
98
    }
99
 
100
    /**
101
     * Parses a route and adds it to the RouteCollection.
102
     *
103
     * @throws \InvalidArgumentException When the XML is invalid
104
     */
105
    protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path)
106
    {
107
        if ('' === $id = $node->getAttribute('id')) {
108
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $path));
109
        }
110
 
111
        if ('' !== $alias = $node->getAttribute('alias')) {
112
            $alias = $collection->addAlias($id, $alias);
113
 
114
            if ($deprecationInfo = $this->parseDeprecation($node, $path)) {
115
                $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']);
116
            }
117
 
118
            return;
119
        }
120
 
121
        $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY);
122
        $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY);
123
 
124
        [$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path);
125
 
126
        if (!$paths && '' === $node->getAttribute('path')) {
127
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.', $path));
128
        }
129
 
130
        if ($paths && '' !== $node->getAttribute('path')) {
131
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.', $path));
132
        }
133
 
134
        $routes = $this->createLocalizedRoute($collection, $id, $paths ?: $node->getAttribute('path'));
135
        $routes->addDefaults($defaults);
136
        $routes->addRequirements($requirements);
137
        $routes->addOptions($options);
138
        $routes->setSchemes($schemes);
139
        $routes->setMethods($methods);
140
        $routes->setCondition($condition);
141
 
142
        if (null !== $hosts) {
143
            $this->addHost($routes, $hosts);
144
        }
145
    }
146
 
147
    /**
148
     * Parses an import and adds the routes in the resource to the RouteCollection.
149
     *
150
     * @throws \InvalidArgumentException When the XML is invalid
151
     */
152
    protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file)
153
    {
154
        /** @var \DOMElement $resourceElement */
155
        if (!($resource = $node->getAttribute('resource') ?: null) && $resourceElement = $node->getElementsByTagName('resource')[0] ?? null) {
156
            $resource = [];
157
            /** @var \DOMAttr $attribute */
158
            foreach ($resourceElement->attributes as $attribute) {
159
                $resource[$attribute->name] = $attribute->value;
160
            }
161
        }
162
 
163
        if (!$resource) {
164
            throw new \InvalidArgumentException(sprintf('The <import> element in file "%s" must have a "resource" attribute or element.', $path));
165
        }
166
 
167
        $type = $node->getAttribute('type');
168
        $prefix = $node->getAttribute('prefix');
169
        $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null;
170
        $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null;
171
        $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true;
172
        $namePrefix = $node->getAttribute('name-prefix') ?: null;
173
 
174
        [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path);
175
 
176
        if ('' !== $prefix && $prefixes) {
177
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.', $path));
178
        }
179
 
180
        $exclude = [];
181
        foreach ($node->childNodes as $child) {
182
            if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
183
                $exclude[] = $child->nodeValue;
184
            }
185
        }
186
 
187
        if ($node->hasAttribute('exclude')) {
188
            if ($exclude) {
189
                throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
190
            }
191
            $exclude = [$node->getAttribute('exclude')];
192
        }
193
 
194
        $this->setCurrentDir(\dirname($path));
195
 
196
        /** @var RouteCollection[] $imported */
197
        $imported = $this->import($resource, '' !== $type ? $type : null, false, $file, $exclude) ?: [];
198
 
199
        if (!\is_array($imported)) {
200
            $imported = [$imported];
201
        }
202
 
203
        foreach ($imported as $subCollection) {
204
            $this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot);
205
 
206
            if (null !== $hosts) {
207
                $this->addHost($subCollection, $hosts);
208
            }
209
 
210
            if (null !== $condition) {
211
                $subCollection->setCondition($condition);
212
            }
213
            if (null !== $schemes) {
214
                $subCollection->setSchemes($schemes);
215
            }
216
            if (null !== $methods) {
217
                $subCollection->setMethods($methods);
218
            }
219
            if (null !== $namePrefix) {
220
                $subCollection->addNamePrefix($namePrefix);
221
            }
222
            $subCollection->addDefaults($defaults);
223
            $subCollection->addRequirements($requirements);
224
            $subCollection->addOptions($options);
225
 
226
            $collection->addCollection($subCollection);
227
        }
228
    }
229
 
230
    /**
231
     * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
232
     *                                   or when the XML structure is not as expected by the scheme -
233
     *                                   see validate()
234
     */
235
    protected function loadFile(string $file): \DOMDocument
236
    {
237
        return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
238
    }
239
 
240
    /**
241
     * Parses the config elements (default, requirement, option).
242
     *
243
     * @throws \InvalidArgumentException When the XML is invalid
244
     */
245
    private function parseConfigs(\DOMElement $node, string $path): array
246
    {
247
        $defaults = [];
248
        $requirements = [];
249
        $options = [];
250
        $condition = null;
251
        $prefixes = [];
252
        $paths = [];
253
        $hosts = [];
254
 
255
        /** @var \DOMElement $n */
256
        foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
257
            if ($node !== $n->parentNode) {
258
                continue;
259
            }
260
 
261
            switch ($n->localName) {
262
                case 'path':
263
                    $paths[$n->getAttribute('locale')] = trim($n->textContent);
264
                    break;
265
                case 'host':
266
                    $hosts[$n->getAttribute('locale')] = trim($n->textContent);
267
                    break;
268
                case 'prefix':
269
                    $prefixes[$n->getAttribute('locale')] = trim($n->textContent);
270
                    break;
271
                case 'default':
272
                    if ($this->isElementValueNull($n)) {
273
                        $defaults[$n->getAttribute('key')] = null;
274
                    } else {
275
                        $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path);
276
                    }
277
 
278
                    break;
279
                case 'requirement':
280
                    $requirements[$n->getAttribute('key')] = trim($n->textContent);
281
                    break;
282
                case 'option':
283
                    $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent));
284
                    break;
285
                case 'condition':
286
                    $condition = trim($n->textContent);
287
                    break;
288
                case 'resource':
289
                    break;
290
                default:
291
                    throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path));
292
            }
293
        }
294
 
295
        if ($controller = $node->getAttribute('controller')) {
296
            if (isset($defaults['_controller'])) {
297
                $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
298
 
299
                throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ', $path).$name);
300
            }
301
 
302
            $defaults['_controller'] = $controller;
303
        }
304
        if ($node->hasAttribute('locale')) {
305
            $defaults['_locale'] = $node->getAttribute('locale');
306
        }
307
        if ($node->hasAttribute('format')) {
308
            $defaults['_format'] = $node->getAttribute('format');
309
        }
310
        if ($node->hasAttribute('utf8')) {
311
            $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8'));
312
        }
313
        if ($stateless = $node->getAttribute('stateless')) {
314
            if (isset($defaults['_stateless'])) {
315
                $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
316
 
317
                throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name);
318
            }
319
 
320
            $defaults['_stateless'] = XmlUtils::phpize($stateless);
321
        }
322
 
323
        if (!$hosts) {
324
            $hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
325
        }
326
 
327
        return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts];
328
    }
329
 
330
    /**
331
     * Parses the "default" elements.
332
     */
333
    private function parseDefaultsConfig(\DOMElement $element, string $path): array|bool|float|int|string|null
334
    {
335
        if ($this->isElementValueNull($element)) {
336
            return null;
337
        }
338
 
339
        // Check for existing element nodes in the default element. There can
340
        // only be a single element inside a default element. So this element
341
        // (if one was found) can safely be returned.
342
        foreach ($element->childNodes as $child) {
343
            if (!$child instanceof \DOMElement) {
344
                continue;
345
            }
346
 
347
            if (self::NAMESPACE_URI !== $child->namespaceURI) {
348
                continue;
349
            }
350
 
351
            return $this->parseDefaultNode($child, $path);
352
        }
353
 
354
        // If the default element doesn't contain a nested "bool", "int", "float",
355
        // "string", "list", or "map" element, the element contents will be treated
356
        // as the string value of the associated default option.
357
        return trim($element->textContent);
358
    }
359
 
360
    /**
361
     * Recursively parses the value of a "default" element.
362
     *
363
     * @throws \InvalidArgumentException when the XML is invalid
364
     */
365
    private function parseDefaultNode(\DOMElement $node, string $path): array|bool|float|int|string|null
366
    {
367
        if ($this->isElementValueNull($node)) {
368
            return null;
369
        }
370
 
371
        switch ($node->localName) {
372
            case 'bool':
373
                return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue);
374
            case 'int':
375
                return (int) trim($node->nodeValue);
376
            case 'float':
377
                return (float) trim($node->nodeValue);
378
            case 'string':
379
                return trim($node->nodeValue);
380
            case 'list':
381
                $list = [];
382
 
383
                foreach ($node->childNodes as $element) {
384
                    if (!$element instanceof \DOMElement) {
385
                        continue;
386
                    }
387
 
388
                    if (self::NAMESPACE_URI !== $element->namespaceURI) {
389
                        continue;
390
                    }
391
 
392
                    $list[] = $this->parseDefaultNode($element, $path);
393
                }
394
 
395
                return $list;
396
            case 'map':
397
                $map = [];
398
 
399
                foreach ($node->childNodes as $element) {
400
                    if (!$element instanceof \DOMElement) {
401
                        continue;
402
                    }
403
 
404
                    if (self::NAMESPACE_URI !== $element->namespaceURI) {
405
                        continue;
406
                    }
407
 
408
                    $map[$element->getAttribute('key')] = $this->parseDefaultNode($element, $path);
409
                }
410
 
411
                return $map;
412
            default:
413
                throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".', $node->localName, $path));
414
        }
415
    }
416
 
417
    private function isElementValueNull(\DOMElement $element): bool
418
    {
419
        $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
420
 
421
        if (!$element->hasAttributeNS($namespaceUri, 'nil')) {
422
            return false;
423
        }
424
 
425
        return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
426
    }
427
 
428
    /**
429
     * Parses the deprecation elements.
430
     *
431
     * @throws \InvalidArgumentException When the XML is invalid
432
     */
433
    private function parseDeprecation(\DOMElement $node, string $path): array
434
    {
435
        $deprecatedNode = null;
436
        foreach ($node->childNodes as $child) {
437
            if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) {
438
                continue;
439
            }
440
            if ('deprecated' !== $child->localName) {
441
                throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path));
442
            }
443
 
444
            $deprecatedNode = $child;
445
        }
446
 
447
        if (null === $deprecatedNode) {
448
            return [];
449
        }
450
 
451
        if (!$deprecatedNode->hasAttribute('package')) {
452
            throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "package" attribute.', $path));
453
        }
454
        if (!$deprecatedNode->hasAttribute('version')) {
455
            throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "version" attribute.', $path));
456
        }
457
 
458
        return [
459
            'package' => $deprecatedNode->getAttribute('package'),
460
            'version' => $deprecatedNode->getAttribute('version'),
461
            'message' => trim($deprecatedNode->nodeValue),
462
        ];
463
    }
464
}