Passed
Push — static-analysis ( 32726d...696a29 )
by SignpostMarv
01:30
created

SchemaReader::loadAttributeRef()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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