Passed
Branch psalm (7e07ab)
by SignpostMarv
02:23
created

SchemaReader::loadImportFreshCallbacks()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2.0017

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 12
cts 13
cp 0.9231
rs 9.2
c 0
b 0
f 0
cc 2
eloc 12
nc 1
nop 3
crap 2.0017
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GoetasWebservices\XML\XSDReader;
6
7
use Closure;
8
use DOMDocument;
9
use DOMElement;
10
use DOMNode;
11
use GoetasWebservices\XML\XSDReader\Documentation\DocumentationReader;
12
use GoetasWebservices\XML\XSDReader\Documentation\StandardDocumentationReader;
13
use GoetasWebservices\XML\XSDReader\Exception\IOException;
14
use GoetasWebservices\XML\XSDReader\Exception\TypeException;
15
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Attribute;
16
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer;
17
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeDef;
18
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
19
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group as AttributeGroup;
20
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
21
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
22
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
23
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
24
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
25
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
26
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
27
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
28
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
29
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
30
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
31
use GoetasWebservices\XML\XSDReader\Schema\Item;
32
use GoetasWebservices\XML\XSDReader\Schema\Schema;
33
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
34
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
35
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
36
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
37
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
38
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
39
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
40
41
class SchemaReader
42
{
43
    public const XSD_NS = 'http://www.w3.org/2001/XMLSchema';
44
45
    public const XML_NS = 'http://www.w3.org/XML/1998/namespace';
46
47
    /**
48
     * @var DocumentationReader
49
     */
50
    private $documentationReader;
51
52
    /**
53
     * @var Schema[]
54
     */
55
    private $loadedFiles = array();
56
57
    /**
58
     * @var string[]
59
     */
60
    protected $knownLocationSchemas = [
61
        'http://www.w3.org/2001/xml.xsd' => (
62
            __DIR__.'/Resources/xml.xsd'
63
        ),
64
        'http://www.w3.org/2001/XMLSchema.xsd' => (
65
            __DIR__.'/Resources/XMLSchema.xsd'
66
        ),
67
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' => (
68
            __DIR__.'/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd'
69
        ),
70
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' => (
71
            __DIR__.'/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd'
72
        ),
73
        'https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
74
            __DIR__.'/Resources/xmldsig-core-schema.xsd'
75
        ),
76
        'http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
77
            __DIR__.'/Resources/xmldsig-core-schema.xsd'
78
        ),
79
    ];
80
81
    /**
82
     * @var string[]
83
     */
84
    protected static $globalSchemaInfo = array(
85
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
86
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd',
87
    );
88
89 56
    public function __construct(DocumentationReader $documentationReader = null)
90
    {
91 56
        if (null === $documentationReader) {
92 56
            $documentationReader = new StandardDocumentationReader();
93
        }
94 56
        $this->documentationReader = $documentationReader;
95 56
    }
96
97
    public function addKnownSchemaLocation(string $remote, string $local): void
98
    {
99
        $this->knownLocationSchemas[$remote] = $local;
100
    }
101
102 47
    private function loadAttributeGroup(
103
        Schema $schema,
104
        DOMElement $node
105
    ): Closure {
106 47
        $attGroup = new AttributeGroup($schema, $node->getAttribute('name'));
107 47
        $attGroup->setDoc($this->getDocumentation($node));
108 47
        $schema->addAttributeGroup($attGroup);
109
110 47
        return function () use ($schema, $node, $attGroup): void {
111 47
            SchemaReader::againstDOMNodeList(
112 47
                $node,
113 47
                function (
114
                    DOMElement $node,
115
                    DOMElement $childNode
116
                ) use (
117 47
                    $schema,
118 47
                    $attGroup
119
                ): void {
120 47
                    switch ($childNode->localName) {
121 47
                        case 'attribute':
122 47
                            $attribute = $this->getAttributeFromAttributeOrRef(
123 47
                                $childNode,
124 47
                                $schema,
125 47
                                $node
126
                            );
127 47
                            $attGroup->addAttribute($attribute);
128 47
                            break;
129 47
                        case 'attributeGroup':
130 1
                            $this->findSomethingLikeAttributeGroup(
131 1
                                $schema,
132 1
                                $node,
133 1
                                $childNode,
134 1
                                $attGroup
135
                            );
136 1
                            break;
137
                    }
138 47
                }
139
            );
140 47
        };
141
    }
142
143 47
    private function getAttributeFromAttributeOrRef(
144
        DOMElement $childNode,
145
        Schema $schema,
146
        DOMElement $node
147
    ): AttributeItem {
148 47
        if ($childNode->hasAttribute('ref')) {
149 47
            $attribute = $this->findAttributeItem($schema, $node, $childNode->getAttribute('ref'));
150
        } else {
151
            /**
152
             * @var Attribute
153
             */
154 47
            $attribute = $this->loadAttribute($schema, $childNode);
155
        }
156
157 47
        return $attribute;
158
    }
159
160 47
    private function loadAttribute(
161
        Schema $schema,
162
        DOMElement $node
163
    ): Attribute {
164 47
        $attribute = new Attribute($schema, $node->getAttribute('name'));
165 47
        $attribute->setDoc($this->getDocumentation($node));
166 47
        $this->fillItem($attribute, $node);
167
168 47
        if ($node->hasAttribute('nillable')) {
169 1
            $attribute->setNil($node->getAttribute('nillable') == 'true');
170
        }
171 47
        if ($node->hasAttribute('form')) {
172 1
            $attribute->setQualified($node->getAttribute('form') == 'qualified');
173
        }
174 47
        if ($node->hasAttribute('use')) {
175 47
            $attribute->setUse($node->getAttribute('use'));
176
        }
177
178 47
        return $attribute;
179
    }
180
181 47
    private function loadAttributeOrElementDef(
182
        Schema $schema,
183
        DOMElement $node,
184
        bool $attributeDef
185
    ): Closure {
186 47
        $name = $node->getAttribute('name');
187 47
        if ($attributeDef) {
188 47
            $attribute = new AttributeDef($schema, $name);
189 47
            $schema->addAttribute($attribute);
190
        } else {
191 47
            $attribute = new ElementDef($schema, $name);
192 47
            $schema->addElement($attribute);
193
        }
194
195 47
        return function () use ($attribute, $node): void {
196 47
            $this->fillItem($attribute, $node);
197 47
        };
198
    }
199
200 47
    private function loadAttributeDef(Schema $schema, DOMElement $node): Closure
201
    {
202 47
        return $this->loadAttributeOrElementDef($schema, $node, true);
203
    }
204
205 47
    private function getDocumentation(DOMElement $node): string
206
    {
207 47
        return $this->documentationReader->get($node);
208
    }
209
210
    /**
211
     * @return Closure[]
212
     */
213 47
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null): array
214
    {
215 47
        $this->setSchemaThingsFromNode($schema, $node, $parent);
216 47
        $functions = array();
217
218 47
        self::againstDOMNodeList(
219 47
            $node,
220 47
            function (
221
                DOMElement $node,
222
                DOMElement $childNode
223
            ) use (
224 47
                $schema,
225 47
                &$functions
226
            ): void {
227 47
                $callback = null;
228
229 47
                switch ($childNode->localName) {
230 47
                    case 'attributeGroup':
231 47
                        $callback = $this->loadAttributeGroup($schema, $childNode);
232 47
                        break;
233 47
                    case 'include':
234 47
                    case 'import':
235 47
                        $callback = $this->loadImport($schema, $childNode);
236 47
                        break;
237 47
                    case 'element':
238 47
                        $callback = $this->loadElementDef($schema, $childNode);
239 47
                        break;
240 47
                    case 'attribute':
241 47
                        $callback = $this->loadAttributeDef($schema, $childNode);
242 47
                        break;
243 47
                    case 'group':
244 47
                        $callback = $this->loadGroup($schema, $childNode);
245 47
                        break;
246 47
                    case 'complexType':
247 47
                        $callback = $this->loadComplexType($schema, $childNode);
248 47
                        break;
249 47
                    case 'simpleType':
250 47
                        $callback = $this->loadSimpleType($schema, $childNode);
251 47
                        break;
252
                }
253
254 47
                if ($callback instanceof Closure) {
255 47
                    $functions[] = $callback;
256
                }
257 47
            }
258
        );
259
260 47
        return $functions;
261
    }
262
263 47
    private function loadGroupRef(Group $referenced, DOMElement $node): GroupRef
264
    {
265 47
        $ref = new GroupRef($referenced);
266 47
        $ref->setDoc($this->getDocumentation($node));
267
268 47
        self::maybeSetMax($ref, $node);
269 47
        self::maybeSetMin($ref, $node);
270
271 47
        return $ref;
272
    }
273
274 47
    private static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node): InterfaceSetMinMax
275
    {
276
        if (
277 47
            $node->hasAttribute('maxOccurs')
278
        ) {
279 47
            $ref->setMax($node->getAttribute('maxOccurs') == 'unbounded' ? -1 : (int) $node->getAttribute('maxOccurs'));
280
        }
281
282 47
        return $ref;
283
    }
284
285 47
    private static function maybeSetMin(InterfaceSetMinMax $ref, DOMElement $node): InterfaceSetMinMax
286
    {
287 47
        if ($node->hasAttribute('minOccurs')) {
288 47
            $ref->setMin((int) $node->getAttribute('minOccurs'));
289
        }
290
291 47
        return $ref;
292
    }
293
294 47
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, int $max = null): void
295
    {
296
        $max =
297
        (
298 47
            (is_int($max) && (bool) $max) ||
299 47
            $node->getAttribute('maxOccurs') == 'unbounded' ||
300 47
            $node->getAttribute('maxOccurs') > 1
301
        )
302 47
            ? 2
303 47
            : null;
304
305 47
        self::againstDOMNodeList(
306 47
            $node,
307 47
            function (
308
                DOMElement $node,
309
                DOMElement $childNode
310
            ) use (
311 47
                $elementContainer,
312 47
                $max
313
            ): void {
314 47
                $this->loadSequenceChildNode(
315 47
                    $elementContainer,
316 47
                    $node,
317 47
                    $childNode,
318 47
                    $max
319
                );
320 47
            }
321
        );
322 47
    }
323
324
    /**
325
     * @param int|null $max
326
     */
327 47
    private function loadSequenceChildNode(
328
        ElementContainer $elementContainer,
329
        DOMElement $node,
330
        DOMElement $childNode,
331
        $max
332
    ): void {
333 47
        switch ($childNode->localName) {
334 47
            case 'sequence':
335 47
            case 'choice':
336 47
            case 'all':
337 47
                $this->loadSequence(
338 47
                    $elementContainer,
339 47
                    $childNode,
340 47
                    $max
341
                );
342 47
                break;
343 47
            case 'element':
344 47
                $this->loadSequenceChildNodeLoadElement(
345 47
                    $elementContainer,
346 47
                    $node,
347 47
                    $childNode,
348 47
                    $max
349
                );
350 47
                break;
351 47
            case 'group':
352 47
                $this->addGroupAsElement(
353 47
                    $elementContainer->getSchema(),
354 47
                    $node,
355 47
                    $childNode,
356 47
                    $elementContainer
357
                );
358 47
                break;
359
        }
360 47
    }
361
362
    /**
363
     * @param int|null $max
364
     */
365 47
    private function loadSequenceChildNodeLoadElement(
366
        ElementContainer $elementContainer,
367
        DOMElement $node,
368
        DOMElement $childNode,
369
        $max
370
    ): void {
371 47
        if ($childNode->hasAttribute('ref')) {
372 47
            $element = new ElementRef(
373 47
                $this->findElement($elementContainer->getSchema(), $node, $childNode->getAttribute('ref'))
374
            );
375 47
            $element->setDoc($this->getDocumentation($childNode));
376
377 47
            self::maybeSetMax($element, $childNode);
378 47
            self::maybeSetMin($element, $childNode);
379 47
            if ($childNode->hasAttribute('nillable')) {
380
                $element->setNil($childNode->getAttribute('nillable') == 'true');
381
            }
382 47
            if ($childNode->hasAttribute('form')) {
383 47
                $element->setQualified($childNode->getAttribute('form') == 'qualified');
384
            }
385
        } else {
386 47
            $element = $this->loadElement(
387 47
                $elementContainer->getSchema(),
388 47
                $childNode
389
            );
390
        }
391 47
        if ($max > 1) {
392
            /*
393
            * although one might think the typecast is not needed with $max being `? int $max` after passing > 1,
394
            * phpstan@a4f89fa still thinks it's possibly null.
395
            * see https://github.com/phpstan/phpstan/issues/577 for related issue
396
            */
397 47
            $element->setMax((int) $max);
398
        }
399 47
        $elementContainer->addElement($element);
400 47
    }
401
402 47
    private function addGroupAsElement(
403
        Schema $schema,
404
        DOMElement $node,
405
        DOMElement $childNode,
406
        ElementContainer $elementContainer
407
    ): void {
408 47
        $referencedGroup = $this->findGroup(
409 47
            $schema,
410 47
            $node,
411 47
            $childNode->getAttribute('ref')
412
        );
413
414 47
        $group = $this->loadGroupRef($referencedGroup, $childNode);
415 47
        $elementContainer->addElement($group);
416 47
    }
417
418 47
    private function loadGroup(Schema $schema, DOMElement $node): Closure
419
    {
420 47
        $group = new Group($schema, $node->getAttribute('name'));
421 47
        $group->setDoc($this->getDocumentation($node));
422
423 47
        if ($node->hasAttribute('maxOccurs')) {
424
            /**
425
             * @var GroupRef
426
             */
427
            $group = self::maybeSetMax(new GroupRef($group), $node);
428
        }
429 47
        if ($node->hasAttribute('minOccurs')) {
430
            /**
431
             * @var GroupRef
432
             */
433
            $group = self::maybeSetMin(
434
                $group instanceof GroupRef ? $group : new GroupRef($group),
435
                $node
436
            );
437
        }
438
439 47
        $schema->addGroup($group);
440
441 47
        return function () use ($group, $node): void {
442 47
            self::againstDOMNodeList(
443 47
                $node,
444 47
                function (DOMelement $node, DOMElement $childNode) use ($group): void {
445 47
                    switch ($childNode->localName) {
446 47
                        case 'sequence':
447 47
                        case 'choice':
448 47
                        case 'all':
449 47
                            $this->loadSequence($group, $childNode);
450 47
                            break;
451
                    }
452 47
                }
453
            );
454 47
        };
455
    }
456
457 47
    private function loadComplexType(Schema $schema, DOMElement $node, Closure $callback = null): Closure
458
    {
459
        /**
460
         * @var bool
461
         */
462 47
        $isSimple = false;
463
464 47
        self::againstDOMNodeList(
465 47
            $node,
466 47
            function (
467
                DOMElement $node,
468
                DOMElement $childNode
469
            ) use (
470 47
                &$isSimple
471
            ): void {
472 47
                if ($isSimple) {
0 ignored issues
show
introduced by
The condition $isSimple can never be true.
Loading history...
473 1
                    return;
474
                }
475 47
                if ($childNode->localName === 'simpleContent') {
476 2
                    $isSimple = true;
477
                }
478 47
            }
479
        );
480
481 47
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute('name')) : new ComplexType($schema, $node->getAttribute('name'));
0 ignored issues
show
introduced by
The condition $isSimple can never be true.
Loading history...
482
483 47
        $type->setDoc($this->getDocumentation($node));
484 47
        if ($node->getAttribute('name')) {
485 47
            $schema->addType($type);
486
        }
487
488 47
        return function () use ($type, $node, $schema, $callback): void {
489 47
            $this->fillTypeNode($type, $node, true);
490
491 47
            self::againstDOMNodeList(
492 47
                $node,
493 47
                function (
494
                    DOMElement $node,
495
                    DOMElement $childNode
496
                ) use (
497 47
                    $schema,
498 47
                    $type
499
                ): void {
500 47
                    $this->loadComplexTypeFromChildNode(
501 47
                        $type,
502 47
                        $node,
503 47
                        $childNode,
504 47
                        $schema
505
                    );
506 47
                }
507
            );
508
509 47
            if ($callback instanceof Closure) {
510 47
                call_user_func($callback, $type);
511
            }
512 47
        };
513
    }
514
515 47
    private function loadComplexTypeFromChildNode(
516
        BaseComplexType $type,
517
        DOMElement $node,
518
        DOMElement $childNode,
519
        Schema $schema
520
    ): void {
521 47
        switch ($childNode->localName) {
522 47
            case 'sequence':
523 47
            case 'choice':
524 47
            case 'all':
525 47
                if ($type instanceof ElementContainer) {
526 47
                    $this->loadSequence(
527 47
                        $type,
528 47
                        $childNode
529
                    );
530
                }
531 47
                break;
532 47
            case 'attribute':
533 47
                $this->addAttributeFromAttributeOrRef(
534 47
                    $type,
535 47
                    $childNode,
536 47
                    $schema,
537 47
                    $node
538
                );
539 47
                break;
540 47
            case 'attributeGroup':
541 2
                $this->findSomethingLikeAttributeGroup(
542 2
                    $schema,
543 2
                    $node,
544 2
                    $childNode,
545 2
                    $type
546
                );
547 2
                break;
548 47
            case 'group':
549
                if (
550 1
                    $type instanceof ComplexType
551
                ) {
552 1
                    $this->addGroupAsElement(
553 1
                        $schema,
554 1
                        $node,
555 1
                        $childNode,
556 1
                        $type
557
                    );
558
                }
559 1
                break;
560
        }
561 47
    }
562
563 47
    private function loadSimpleType(Schema $schema, DOMElement $node, Closure $callback = null): Closure
564
    {
565 47
        $type = new SimpleType($schema, $node->getAttribute('name'));
566 47
        $type->setDoc($this->getDocumentation($node));
567 47
        if ($node->getAttribute('name')) {
568 47
            $schema->addType($type);
569
        }
570
571 47
        return function () use ($type, $node, $callback): void {
572 47
            $this->fillTypeNode($type, $node, true);
573
574 47
            self::againstDOMNodeList(
575 47
                $node,
576 47
                function (DOMElement $node, DOMElement $childNode) use ($type): void {
577 47
                    switch ($childNode->localName) {
578 47
                        case 'union':
579 47
                            $this->loadUnion($type, $childNode);
580 47
                            break;
581 47
                        case 'list':
582 47
                            $this->loadList($type, $childNode);
583 47
                            break;
584
                    }
585 47
                }
586
            );
587
588 47
            if ($callback instanceof Closure) {
589 47
                call_user_func($callback, $type);
590
            }
591 47
        };
592
    }
593
594 47
    private function loadList(SimpleType $type, DOMElement $node): void
595
    {
596 47
        if ($node->hasAttribute('itemType')) {
597
            /**
598
             * @var SimpleType
599
             */
600 47
            $listType = $this->findSomeType($type, $node, 'itemType');
601 47
            $type->setList($listType);
602
        } else {
603 47
            self::againstDOMNodeList(
604 47
                $node,
605 47
                function (
606
                    DOMElement $node,
607
                    DOMElement $childNode
608
                ) use (
609 47
                    $type
610
                ): void {
611 47
                    $this->loadTypeWithCallback(
612 47
                        $type->getSchema(),
613 47
                        $childNode,
614 47
                        function (SimpleType $list) use ($type): void {
615 47
                            $type->setList($list);
616 47
                        }
617
                    );
618 47
                }
619
            );
620
        }
621 47
    }
622
623 47
    private function findSomeType(
624
        SchemaItem $fromThis,
625
        DOMElement $node,
626
        string $attributeName
627
    ): SchemaItem {
628 47
        return $this->findSomeTypeFromAttribute(
629 47
            $fromThis,
630 47
            $node,
631 47
            $node->getAttribute($attributeName)
632
        );
633
    }
634
635 47
    private function findSomeTypeFromAttribute(
636
        SchemaItem $fromThis,
637
        DOMElement $node,
638
        string $attributeName
639
    ): SchemaItem {
640 47
        $out = $this->findType(
641 47
            $fromThis->getSchema(),
642 47
            $node,
643 47
            $attributeName
644
        );
645
646 47
        return $out;
647
    }
648
649 47
    private function loadUnion(SimpleType $type, DOMElement $node): void
650
    {
651 47
        if ($node->hasAttribute('memberTypes')) {
652 47
            $types = preg_split('/\s+/', $node->getAttribute('memberTypes'));
653 47
            foreach ($types as $typeName) {
654
                /**
655
                 * @var SimpleType
656
                 */
657 47
                $unionType = $this->findSomeTypeFromAttribute(
658 47
                    $type,
659 47
                    $node,
660 47
                    $typeName
661
                );
662 47
                $type->addUnion($unionType);
663
            }
664
        }
665 47
        self::againstDOMNodeList(
666 47
            $node,
667 47
            function (
668
                DOMElement $node,
669
                DOMElement $childNode
670
            ) use (
671 47
                $type
672
            ): void {
673 47
                $this->loadTypeWithCallback(
674 47
                    $type->getSchema(),
675 47
                    $childNode,
676 47
                    function (SimpleType $unType) use ($type): void {
677 47
                        $type->addUnion($unType);
678 47
                    }
679
                );
680 47
            }
681
        );
682 47
    }
683
684 47
    private function fillTypeNode(Type $type, DOMElement $node, bool $checkAbstract = false): void
685
    {
686 47
        if ($checkAbstract) {
687 47
            $type->setAbstract($node->getAttribute('abstract') === 'true' || $node->getAttribute('abstract') === '1');
688
        }
689
690 47
        self::againstDOMNodeList(
691 47
            $node,
692 47
            function (DOMElement $node, DOMElement $childNode) use ($type): void {
693 47
                switch ($childNode->localName) {
694 47
                    case 'restriction':
695 47
                        $this->loadRestriction($type, $childNode);
696 47
                        break;
697 47
                    case 'extension':
698 47
                        if ($type instanceof BaseComplexType) {
699 47
                            $this->loadExtension($type, $childNode);
700
                        }
701 47
                        break;
702 47
                    case 'simpleContent':
703 47
                    case 'complexContent':
704 47
                        $this->fillTypeNode($type, $childNode);
705 47
                        break;
706
                }
707 47
            }
708
        );
709 47
    }
710
711 47
    private function loadExtension(BaseComplexType $type, DOMElement $node): void
712
    {
713 47
        $extension = new Extension();
714 47
        $type->setExtension($extension);
715
716 47
        if ($node->hasAttribute('base')) {
717 47
            $this->findAndSetSomeBase(
718 47
                $type,
719 47
                $extension,
720 47
                $node
721
            );
722
        }
723 47
        $this->loadExtensionChildNodes($type, $node);
724 47
    }
725
726 47
    private function findAndSetSomeBase(
727
        Type $type,
728
        Base $setBaseOnThis,
729
        DOMElement $node
730
    ): void {
731
        /**
732
         * @var Type
733
         */
734 47
        $parent = $this->findSomeType($type, $node, 'base');
735 47
        $setBaseOnThis->setBase($parent);
736 47
    }
737
738 47
    private function loadExtensionChildNodes(
739
        BaseComplexType $type,
740
        DOMElement $node
741
    ): void {
742 47
        self::againstDOMNodeList(
743 47
            $node,
744 47
            function (
745
                DOMElement $node,
746
                DOMElement $childNode
747
            ) use (
748 47
                $type
749
            ): void {
750 47
                switch ($childNode->localName) {
751 47
                    case 'sequence':
752 47
                    case 'choice':
753 47
                    case 'all':
754 47
                        if ($type instanceof ElementContainer) {
755 47
                            $this->loadSequence(
756 47
                                $type,
757 47
                                $childNode
758
                            );
759
                        }
760 47
                        break;
761 47
                    case 'attribute':
762 47
                        $this->addAttributeFromAttributeOrRef(
763 47
                            $type,
764 47
                            $childNode,
765 47
                            $type->getSchema(),
766 47
                            $node
767
                        );
768 47
                        break;
769 47
                    case 'attributeGroup':
770 47
                        $this->findSomethingLikeAttributeGroup(
771 47
                            $type->getSchema(),
772 47
                            $node,
773 47
                            $childNode,
774 47
                            $type
775
                        );
776 47
                        break;
777
                }
778 47
            }
779
        );
780 47
    }
781
782 47
    private function loadRestriction(Type $type, DOMElement $node): void
783
    {
784 47
        $restriction = new Restriction();
785 47
        $type->setRestriction($restriction);
786 47
        if ($node->hasAttribute('base')) {
787 47
            $this->findAndSetSomeBase($type, $restriction, $node);
788
        } else {
789 47
            self::againstDOMNodeList(
790 47
                $node,
791 47
                function (
792
                    DOMElement $node,
793
                    DOMElement $childNode
794
                ) use (
795 47
                    $type,
796 47
                    $restriction
797
                ): void {
798 47
                    $this->loadTypeWithCallback(
799 47
                        $type->getSchema(),
800 47
                        $childNode,
801 47
                        function (Type $restType) use ($restriction): void {
802 47
                            $restriction->setBase($restType);
803 47
                        }
804
                    );
805 47
                }
806
            );
807
        }
808 47
        self::againstDOMNodeList(
809 47
            $node,
810 47
            function (
811
                DOMElement $node,
812
                DOMElement $childNode
813
            ) use (
814 47
                $restriction
815
            ): void {
816
                if (
817 47
                    in_array(
818 47
                        $childNode->localName,
819
                        [
820 47
                            'enumeration',
821
                            'pattern',
822
                            'length',
823
                            'minLength',
824
                            'maxLength',
825
                            'minInclusive',
826
                            'maxInclusive',
827
                            'minExclusive',
828
                            'maxExclusive',
829
                            'fractionDigits',
830
                            'totalDigits',
831
                            'whiteSpace',
832
                        ],
833 47
                        true
834
                    )
835
                ) {
836 47
                    $restriction->addCheck(
837 47
                        $childNode->localName,
838
                        [
839 47
                            'value' => $childNode->getAttribute('value'),
840 47
                            'doc' => $this->getDocumentation($childNode),
841
                        ]
842
                    );
843
                }
844 47
            }
845
        );
846 47
    }
847
848
    /**
849
     * @return mixed[]
850
     */
851 47
    private static function splitParts(DOMElement $node, string $typeName): array
852
    {
853 47
        $prefix = null;
854 47
        $name = $typeName;
855 47
        if (strpos($typeName, ':') !== false) {
856 47
            list($prefix, $name) = explode(':', $typeName);
857
        }
858
859 47
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
860
861
        return array(
862 47
            $name,
863 47
            $namespace,
864 47
            $prefix,
865
        );
866
    }
867
868 47
    private function findAttributeItem(Schema $schema, DOMElement $node, string $typeName): AttributeItem
869
    {
870 47
        list($name, $namespace) = static::splitParts($node, $typeName);
871
872
        /**
873
        * @var string|null $namespace
874
        */
875 47
        $namespace = $namespace ?: $schema->getTargetNamespace();
876
877
        try {
878
            /**
879
            * @var AttributeItem $out
880
            */
881 47
            $out = $schema->findAttribute((string) $name, $namespace);
882
883 47
            return $out;
884
        } catch (TypeNotFoundException $e) {
885
            throw new TypeException(
886
                sprintf(
887
                    "Can't find %s named {%s}#%s, at line %d in %s ",
888
                    'attribute',
889
                    $namespace,
890
                    $name,
891
                    $node->getLineNo(),
892
                    $node->ownerDocument->documentURI
893
                ),
894
                0,
895
                $e
896
            );
897
        }
898
    }
899
900 47
    private function findAttributeGroup(Schema $schema, DOMElement $node, string $typeName): AttributeGroup
901
    {
902 47
        list($name, $namespace) = static::splitParts($node, $typeName);
903
904
        /**
905
        * @var string|null $namespace
906
        */
907 47
        $namespace = $namespace ?: $schema->getTargetNamespace();
908
909
        try {
910
            /**
911
            * @var AttributeGroup $out
912
            */
913 47
            $out = $schema->findAttributeGroup((string) $name, $namespace);
914
915 47
            return $out;
916
        } catch (TypeNotFoundException $e) {
917
            throw new TypeException(
918
                sprintf(
919
                    "Can't find %s named {%s}#%s, at line %d in %s ",
920
                    'attributegroup',
921
                    $namespace,
922
                    $name,
923
                    $node->getLineNo(),
924
                    $node->ownerDocument->documentURI
925
                ),
926
                0,
927
                $e
928
            );
929
        }
930
    }
931
932 47
    private function findElement(Schema $schema, DOMElement $node, string $typeName): ElementDef
933
    {
934 47
        list($name, $namespace) = static::splitParts($node, $typeName);
935
936
        /**
937
        * @var string|null $namespace
938
        */
939 47
        $namespace = $namespace ?: $schema->getTargetNamespace();
940
941
        try {
942 47
            return $schema->findElement((string) $name, $namespace);
943
        } catch (TypeNotFoundException $e) {
944
            throw new TypeException(
945
                sprintf(
946
                    "Can't find %s named {%s}#%s, at line %d in %s ",
947
                    'element',
948
                    $namespace,
949
                    $name,
950
                    $node->getLineNo(),
951
                    $node->ownerDocument->documentURI
952
                ),
953
                0,
954
                $e
955
            );
956
        }
957
    }
958
959 47
    private function findGroup(Schema $schema, DOMElement $node, string $typeName): Group
960
    {
961 47
        list($name, $namespace) = static::splitParts($node, $typeName);
962
963
        /**
964
        * @var string|null $namespace
965
        */
966 47
        $namespace = $namespace ?: $schema->getTargetNamespace();
967
968
        try {
969
            /**
970
            * @var Group $out
971
            */
972 47
            $out = $schema->findGroup((string) $name, $namespace);
973
974 47
            return $out;
975
        } catch (TypeNotFoundException $e) {
976
            throw new TypeException(
977
                sprintf(
978
                    "Can't find %s named {%s}#%s, at line %d in %s ",
979
                    'group',
980
                    $namespace,
981
                    $name,
982
                    $node->getLineNo(),
983
                    $node->ownerDocument->documentURI
984
                ),
985
                0,
986
                $e
987
            );
988
        }
989
    }
990
991 47
    private function findType(Schema $schema, DOMElement $node, string $typeName): SchemaItem
992
    {
993 47
        list($name, $namespace) = static::splitParts($node, $typeName);
994
995
        /**
996
        * @var string|null $namespace
997
        */
998 47
        $namespace = $namespace ?: $schema->getTargetNamespace();
999
1000
        try {
1001
            /**
1002
            * @var SchemaItem $out
1003
            */
1004 47
            $out = $schema->findType((string) $name, $namespace);
1005
1006 47
            return $out;
1007
        } catch (TypeNotFoundException $e) {
1008
            throw new TypeException(
1009
                sprintf(
1010
                    "Can't find %s named {%s}#%s, at line %d in %s ",
1011
                    'type',
1012
                    $namespace,
1013
                    $name,
1014
                    $node->getLineNo(),
1015
                    $node->ownerDocument->documentURI
1016
                ),
1017
                0,
1018
                $e
1019
            );
1020
        }
1021
    }
1022
1023
    /**
1024
     * @return Closure
1025
     */
1026 47
    private function loadElementDef(Schema $schema, DOMElement $node): Closure
1027
    {
1028 47
        return $this->loadAttributeOrElementDef($schema, $node, false);
1029
    }
1030
1031 47
    private function fillItem(Item $element, DOMElement $node): void
1032
    {
1033
        /**
1034
         * @var bool
1035
         */
1036 47
        $skip = false;
1037 47
        self::againstDOMNodeList(
1038 47
            $node,
1039 47
            function (
1040
                DOMElement $node,
1041
                DOMElement $childNode
1042
            ) use (
1043 47
                $element,
1044 47
                &$skip
1045
            ): void {
1046
                if (
1047 47
                    !$skip &&
1048 47
                    in_array(
1049 47
                        $childNode->localName,
1050
                        [
1051 47
                            'complexType',
1052
                            'simpleType',
1053
                        ],
1054 47
                        true
1055
                    )
1056
                ) {
1057 47
                    $this->loadTypeWithCallback(
1058 47
                        $element->getSchema(),
1059 47
                        $childNode,
1060 47
                        function (Type $type) use ($element): void {
1061 47
                            $element->setType($type);
1062 47
                        }
1063
                    );
1064 47
                    $skip = true;
1065
                }
1066 47
            }
1067
        );
1068 47
        if ($skip) {
0 ignored issues
show
introduced by
The condition $skip can never be true.
Loading history...
1069 47
            return;
1070
        }
1071 47
        $this->fillItemNonLocalType($element, $node);
1072 47
    }
1073
1074 47
    private function fillItemNonLocalType(Item $element, DOMElement $node): void
1075
    {
1076 47
        if ($node->getAttribute('type')) {
1077
            /**
1078
             * @var Type
1079
             */
1080 47
            $type = $this->findSomeType($element, $node, 'type');
1081
        } else {
1082
            /**
1083
             * @var Type
1084
             */
1085 47
            $type = $this->findSomeTypeFromAttribute(
1086 47
                $element,
1087 47
                $node,
1088 47
                ($node->lookupPrefix(self::XSD_NS).':anyType')
1089
            );
1090
        }
1091
1092 47
        $element->setType($type);
1093 47
    }
1094
1095 47
    private function loadImport(
1096
        Schema $schema,
1097
        DOMElement $node
1098
    ): Closure {
1099 47
        $base = urldecode($node->ownerDocument->documentURI);
1100 47
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute('schemaLocation'));
1101
1102 47
        $namespace = $node->getAttribute('namespace');
1103
1104 47
        $keys = $this->loadImportFreshKeys($namespace, $file);
1105
1106 47
        foreach ($keys as $key) {
1107 47
            if (isset($this->loadedFiles[$key])) {
1108 47
                $schema->addSchema($this->loadedFiles[$key]);
1109
1110 47
                return function (): void {
1111 47
                };
1112
            }
1113
        }
1114
1115 1
        return $this->loadImportFresh($namespace, $schema, $file);
1116
    }
1117
1118
    /**
1119
     * @return string[]
1120
     */
1121 47
    private function loadImportFreshKeys(
1122
        string $namespace,
1123
        string $file
1124
    ): array {
1125 47
        $globalSchemaInfo = $this->getGlobalSchemaInfo();
1126
1127 47
        $keys = [];
1128
1129 47
        if (isset($globalSchemaInfo[$namespace])) {
1130 47
            $keys[] = $globalSchemaInfo[$namespace];
1131
        }
1132
1133 47
        $keys[] = $this->getNamespaceSpecificFileIndex(
1134 47
            $file,
1135 47
            $namespace
1136
        );
1137
1138 47
        $keys[] = $file;
1139
1140 47
        return $keys;
1141
    }
1142
1143 1
    private function loadImportFreshCallbacksNewSchema(
1144
        string $namespace,
1145
        Schema $schema,
1146
        string $file
1147
    ): Schema {
1148
        /**
1149
         * @var Schema $newSchema
1150
         */
1151 1
        $newSchema = $this->setLoadedFile(
1152 1
            $file,
1153 1
            (('' !== trim($namespace)) ? new Schema() : $schema)
1154
        );
1155
1156 1
        if ('' !== trim($namespace)) {
1157
            $newSchema->addSchema($this->getGlobalSchema());
1158
            $schema->addSchema($newSchema);
1159
        }
1160
1161 1
        return $newSchema;
1162
    }
1163
1164
    /**
1165
     * @return Closure[]
1166
     */
1167 1
    private function loadImportFreshCallbacks(
1168
        string $namespace,
1169
        Schema $schema,
1170
        string $file
1171
    ): array {
1172
        /**
1173
         * @var string
1174
         */
1175 1
        $file = $file;
1176
1177 1
        return $this->schemaNode(
1178 1
            $this->loadImportFreshCallbacksNewSchema(
1179 1
                $namespace,
1180 1
                $schema,
1181 1
                $file
1182
            ),
1183 1
            $this->getDOM(
1184 1
                isset($this->knownLocationSchemas[$file])
1185
                    ? $this->knownLocationSchemas[$file]
1186 1
                    : $file
1187 1
            )->documentElement,
1188 1
            $schema
1189
        );
1190
    }
1191
1192
    private function loadImportFresh(
1193
        string $namespace,
1194
        Schema $schema,
1195
        string $file
1196
    ): Closure {
1197 1
        return function () use ($namespace, $schema, $file): void {
1198
            foreach (
1199 1
                $this->loadImportFreshCallbacks(
1200 1
                    $namespace,
1201 1
                    $schema,
1202 1
                    $file
1203
                ) as $callback
1204
            ) {
1205 1
                $callback();
1206
            }
1207 1
        };
1208
    }
1209
1210
    /**
1211
     * @var Schema|null
1212
     */
1213
    protected $globalSchema;
1214
1215
    /**
1216
     * @return string[]
1217
     */
1218 47
    public function getGlobalSchemaInfo(): array
1219
    {
1220 47
        return self::$globalSchemaInfo;
1221
    }
1222
1223
    /**
1224
     * @return Schema
1225
     */
1226 47
    public function getGlobalSchema(): Schema
1227
    {
1228 47
        if (!($this->globalSchema instanceof Schema)) {
1229 47
            $callbacks = array();
1230 47
            $globalSchemas = array();
1231
            /**
1232
             * @var string $namespace
1233
             */
1234 47
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1235 47
                $this->setLoadedFile(
1236 47
                    $uri,
1237 47
                    $globalSchemas[$namespace] = $schema = new Schema()
1238
                );
1239 47
                if ($namespace === self::XSD_NS) {
1240 47
                    $this->globalSchema = $schema;
1241
                }
1242 47
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1243 47
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1244
            }
1245
1246 47
            $globalSchemas[(string) static::XSD_NS]->addType(new SimpleType($globalSchemas[(string) static::XSD_NS], 'anySimpleType'));
1247 47
            $globalSchemas[(string) static::XSD_NS]->addType(new SimpleType($globalSchemas[(string) static::XSD_NS], 'anyType'));
1248
1249 47
            $globalSchemas[(string) static::XML_NS]->addSchema(
1250 47
                $globalSchemas[(string) static::XSD_NS],
1251 47
                (string) static::XSD_NS
1252
            );
1253 47
            $globalSchemas[(string) static::XSD_NS]->addSchema(
1254 47
                $globalSchemas[(string) static::XML_NS],
1255 47
                (string) static::XML_NS
1256
            );
1257
1258
            /**
1259
             * @var Closure
1260
             */
1261 47
            foreach ($callbacks as $callback) {
1262 47
                $callback();
1263
            }
1264
        }
1265
1266
        /**
1267
         * @var Schema
1268
         */
1269 47
        $out = $this->globalSchema;
1270
1271 47
        if (!($out instanceof Schema)) {
1272
            throw new TypeException('Globa schema not discoverd');
1273
        }
1274
1275 47
        return $out;
1276
    }
1277
1278 47
    private function readNode(DOMElement $node, string $file = 'schema.xsd'): Schema
1279
    {
1280 47
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1281 47
        $this->setLoadedFile($fileKey, $rootSchema = new Schema());
1282
1283 47
        $rootSchema->addSchema($this->getGlobalSchema());
1284 47
        $callbacks = $this->schemaNode($rootSchema, $node);
1285
1286 47
        foreach ($callbacks as $callback) {
1287 41
            call_user_func($callback);
1288
        }
1289
1290 47
        return $rootSchema;
1291
    }
1292
1293
    /**
1294
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1295
     *
1296
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1297
     * file to distinguish between multiple schemas in a single file.
1298
     */
1299 47
    private function getNamespaceSpecificFileIndex(string $file, string $targetNamespace): string
1300
    {
1301 47
        return $file.'#'.$targetNamespace;
1302
    }
1303
1304
    /**
1305
     * @throws IOException
1306
     */
1307 46
    public function readString(string $content, string $file = 'schema.xsd'): Schema
1308
    {
1309 46
        $xml = new DOMDocument('1.0', 'UTF-8');
1310 46
        if (!$xml->loadXML($content)) {
1311
            throw new IOException("Can't load the schema");
1312
        }
1313 46
        $xml->documentURI = $file;
1314
1315 46
        return $this->readNode($xml->documentElement, $file);
1316
    }
1317
1318 1
    public function readFile(string $file): Schema
1319
    {
1320 1
        $xml = $this->getDOM($file);
1321
1322 1
        return $this->readNode($xml->documentElement, $file);
1323
    }
1324
1325
    /**
1326
     * @throws IOException
1327
     */
1328 47
    private function getDOM(string $file): DOMDocument
1329
    {
1330 47
        $xml = new DOMDocument('1.0', 'UTF-8');
1331 47
        if (!$xml->load($file)) {
1332
            throw new IOException("Can't load the file $file");
1333
        }
1334
1335 47
        return $xml;
1336
    }
1337
1338 47
    private static function againstDOMNodeList(
1339
        DOMElement $node,
1340
        Closure $againstNodeList
1341
    ) : void {
1342 47
        $limit = $node->childNodes->length;
1343 47
        for ($i = 0; $i < $limit; $i += 1) {
1344
            /**
1345
             * @var DOMNode
1346
             */
1347 47
            $childNode = $node->childNodes->item($i);
1348
1349 47
            if ($childNode instanceof DOMElement) {
0 ignored issues
show
introduced by
The condition $childNode instanceof DOMElement can never be false since $childNode is always a sub-type of DOMElement.
Loading history...
1350 47
                $againstNodeList(
1351 47
                    $node,
1352 47
                    $childNode
1353
                );
1354
            }
1355
        }
1356 47
    }
1357
1358 47
    private function loadTypeWithCallback(
1359
        Schema $schema,
1360
        DOMElement $childNode,
1361
        Closure $callback
1362
    ) : void {
1363
        /**
1364
         * @var Closure|null $func
1365
         */
1366 47
        $func = null;
1367
1368 47
        switch ($childNode->localName) {
1369 47
            case 'complexType':
1370 47
                $func = $this->loadComplexType($schema, $childNode, $callback);
1371 47
                break;
1372 47
            case 'simpleType':
1373 47
                $func = $this->loadSimpleType($schema, $childNode, $callback);
1374 47
                break;
1375
        }
1376
1377 47
        if ($func instanceof Closure) {
1378 47
            call_user_func($func);
1379
        }
1380 47
    }
1381
1382 47
    private function loadElement(
1383
        Schema $schema,
1384
        DOMElement $node
1385
    ): Element {
1386 47
        $element = new Element($schema, $node->getAttribute('name'));
1387 47
        $element->setDoc($this->getDocumentation($node));
1388
1389 47
        $this->fillItem($element, $node);
1390
1391 47
        self::maybeSetMax($element, $node);
1392 47
        self::maybeSetMin($element, $node);
1393
1394 47
        $xp = new \DOMXPath($node->ownerDocument);
1395 47
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
1396
1397 47
        if ($xp->query('ancestor::xs:choice', $node)->length) {
1398 47
            $element->setMin(0);
1399
        }
1400
1401 47
        if ($node->hasAttribute('nillable')) {
1402 3
            $element->setNil($node->getAttribute('nillable') == 'true');
1403
        }
1404 47
        if ($node->hasAttribute('form')) {
1405 3
            $element->setQualified($node->getAttribute('form') == 'qualified');
1406
        }
1407
1408 47
        return $element;
1409
    }
1410
1411 47
    private function addAttributeFromAttributeOrRef(
1412
        BaseComplexType $type,
1413
        DOMElement $childNode,
1414
        Schema $schema,
1415
        DOMElement $node
1416
    ): void {
1417 47
        $attribute = $this->getAttributeFromAttributeOrRef(
1418 47
            $childNode,
1419 47
            $schema,
1420 47
            $node
1421
        );
1422
1423 47
        $type->addAttribute($attribute);
1424 47
    }
1425
1426 47
    private function findSomethingLikeAttributeGroup(
1427
        Schema $schema,
1428
        DOMElement $node,
1429
        DOMElement $childNode,
1430
        AttributeContainer $addToThis
1431
    ): void {
1432 47
        $attribute = $this->findAttributeGroup($schema, $node, $childNode->getAttribute('ref'));
1433 47
        $addToThis->addAttribute($attribute);
1434 47
    }
1435
1436 47
    private function setLoadedFile(string $key, Schema $schema): Schema
1437
    {
1438 47
        $this->loadedFiles[$key] = $schema;
1439
1440 47
        return $schema;
1441
    }
1442
1443 47
    private function setSchemaThingsFromNode(
1444
        Schema $schema,
1445
        DOMElement $node,
1446
        Schema $parent = null
1447
    ): void {
1448 47
        $schema->setDoc($this->getDocumentation($node));
1449
1450 47
        if ($node->hasAttribute('targetNamespace')) {
1451 47
            $schema->setTargetNamespace($node->getAttribute('targetNamespace'));
1452
        } elseif ($parent instanceof Schema) {
1453
            $schema->setTargetNamespace($parent->getTargetNamespace());
1454
        }
1455 47
        $schema->setElementsQualification($node->getAttribute('elementFormDefault') == 'qualified');
1456 47
        $schema->setAttributesQualification($node->getAttribute('attributeFormDefault') == 'qualified');
1457 47
        $schema->setDoc($this->getDocumentation($node));
1458 47
    }
1459
}
1460