Passed
Push — static-analysis ( 8fcfa4...41a0c2 )
by SignpostMarv
01:36
created

SchemaReader::loadImportFresh()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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