Completed
Push — php-7.1 ( c6ca60...c9fd77 )
by SignpostMarv
06:57
created

SchemaReader::hasLoadedFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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