Passed
Push — static-analysis ( 0ce8af...88cd87 )
by SignpostMarv
01:28
created

SchemaReader::addElementSingleToContainer()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 4
dl 0
loc 19
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\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
    private function getClosuresFromChildNode(
229
        Schema $schema,
230
        DOMElement $childNode,
231
        array & $functions
232
    ) {
233
        static $methods = [
234
            'include' => 'loadImport',
235
            'import' => 'loadImport',
236
            'element' => 'loadElementDef',
237
            'attribute' => 'loadAttributeDef',
238
            'attributeGroup' => 'loadAttributeGroup',
239
            'group' => 'loadGroup',
240
            'complexType' => 'loadComplexType',
241
            'simpleType' => 'loadSimpleType',
242
        ];
243
244
        if (isset($methods[$childNode->localName])) {
245
            $method = $methods[$childNode->localName];
246
247
            $functions[] = $this->$method($schema, $childNode);
248
        }
249
    }
250
251
    /**
252
     *
253
     * @param Schema $schema
254
     * @param DOMElement $node
255
     * @param Schema $parent
256
     * @return array
257
     */
258
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null)
259
    {
260
        $this->setSchemaThingsFromNode($schema, $node, $parent);
261
        $functions = array();
262
263
        foreach ($node->childNodes as $childNode) {
264
            if ($childNode instanceof DOMElement) {
265
                $this->getClosuresFromChildNode(
266
                    $schema,
267
                    $childNode,
268
                    $functions
269
                );
270
            }
271
        }
272
273
        return $functions;
274
    }
275
276
    /**
277
    * @return Element
278
    */
279
    private function loadElement(Schema $schema, DOMElement $node)
280
    {
281
        $element = new Element($schema, $node->getAttribute("name"));
282
        $element->setDoc($this->getDocumentation($node));
283
284
        $this->fillItem($element, $node);
285
286
        static::maybeSetMax($element, $node);
287
        static::maybeSetMin($element, $node);
288
289
        $xp = new \DOMXPath($node->ownerDocument);
290
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
291
292
        if ($xp->query('ancestor::xs:choice', $node)->length) {
293
            $element->setMin(0);
294
        }
295
296
        if ($node->hasAttribute("nillable")) {
297
            $element->setNil($node->getAttribute("nillable") == "true");
298
        }
299
        if ($node->hasAttribute("form")) {
300
            $element->setQualified($node->getAttribute("form") == "qualified");
301
        }
302
        return $element;
303
    }
304
305
    /**
306
    * @return GroupRef
307
    */
308
    private function loadGroupRef(Group $referenced, DOMElement $node)
309
    {
310
        $ref = new GroupRef($referenced);
311
        $ref->setDoc($this->getDocumentation($node));
312
313
        static::maybeSetMax($ref, $node);
314
        static::maybeSetMin($ref, $node);
315
316
        return $ref;
317
    }
318
319
    /**
320
    * @return ElementRef
321
    */
322
    private function loadElementRef(ElementDef $referenced, DOMElement $node)
323
    {
324
        $ref = new ElementRef($referenced);
325
        $this->setDoc($ref, $node);
326
327
        static::maybeSetMax($ref, $node);
328
        static::maybeSetMin($ref, $node);
329
        if ($node->hasAttribute("nillable")) {
330
            $ref->setNil($node->getAttribute("nillable") == "true");
331
        }
332
        if ($node->hasAttribute("form")) {
333
            $ref->setQualified($node->getAttribute("form") == "qualified");
334
        }
335
336
        return $ref;
337
    }
338
339
    private function setDoc(Item $ref, DOMElement $node)
340
    {
341
        $ref->setDoc($this->getDocumentation($node));
342
    }
343
344
    /**
345
    * @return InterfaceSetMinMax
346
    */
347
    private static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node)
348
    {
349
        if (
350
            $node->hasAttribute("maxOccurs")
351
        ) {
352
            $ref->setMax($node->getAttribute("maxOccurs") == "unbounded" ? -1 : (int)$node->getAttribute("maxOccurs"));
353
        }
354
355
        return $ref;
356
    }
357
358
    /**
359
    * @return InterfaceSetMinMax
360
    */
361
    private static function maybeSetMin(InterfaceSetMinMax $ref, DOMElement $node)
362
    {
363
        if ($node->hasAttribute("minOccurs")) {
364
            $ref->setMin((int) $node->getAttribute("minOccurs"));
365
        }
366
367
        return $ref;
368
    }
369
370
    /**
371
    * @return AttributeRef
372
    */
373
    private function loadAttributeRef(AttributeDef $referencedAttribiute, DOMElement $node)
374
    {
375
        $attribute = new AttributeRef($referencedAttribiute);
376
        $this->setDoc($attribute, $node);
377
378
        if ($node->hasAttribute("nillable")) {
379
            $attribute->setNil($node->getAttribute("nillable") == "true");
380
        }
381
        if ($node->hasAttribute("form")) {
382
            $attribute->setQualified($node->getAttribute("form") == "qualified");
383
        }
384
        if ($node->hasAttribute("use")) {
385
            $attribute->setUse($node->getAttribute("use"));
386
        }
387
        return $attribute;
388
    }
389
390
    /**
391
    * @param int|null $max
392
    */
393
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, $max = null)
394
    {
395
        $max = (
396
            (is_int($max) && (bool) $max) ||
397
            $node->getAttribute("maxOccurs") == "unbounded" ||
398
            $node->getAttribute("maxOccurs") > 1
399
        )
400
            ? 2
401
            : null;
402
403
        foreach ($node->childNodes as $childNode) {
404
            if (
405
                in_array(
406
                    $childNode->localName,
407
                    [
408
                        'choice',
409
                        'sequence',
410
                        'all',
411
                    ]
412
                )
413
            ) {
414
                $this->loadSequence($elementContainer, $childNode, $max);
415
            } elseif ($childNode->localName === 'element') {
416
                $this->addElementSingleToContainer(
417
                    $elementContainer,
418
                    $node,
419
                    $childNode,
420
                    $max
421
                );
422
            } elseif ($childNode->localName === 'group') {
423
                $this->addGroupAsElement(
424
                    $elementContainer->getSchema(),
425
                    $node,
426
                    $childNode,
427
                    $elementContainer
428
                );
429
            }
430
        }
431
    }
432
433
    /**
434
    * @param int|null $max
435
    */
436
    private function addElementSingleToContainer(
437
        ElementContainer $elementContainer,
438
        DOMElement $node,
439
        DOMElement $childNode,
440
        $max = null
441
    ) {
442
        if ($childNode->hasAttribute("ref")) {
443
            /**
444
            * @var ElementDef $referencedElement
445
            */
446
            $referencedElement = $this->findSomething('findElement', $elementContainer->getSchema(), $node, $childNode->getAttribute("ref"));
447
            $element = $this->loadElementRef($referencedElement, $childNode);
448
        } else {
449
            $element = $this->loadElement($elementContainer->getSchema(), $childNode);
450
        }
451
        if (is_int($max) && (bool) $max) {
452
            $element->setMax($max);
453
        }
454
        $elementContainer->addElement($element);
455
    }
456
457
    private function addGroupAsElement(
458
        Schema $schema,
459
        DOMElement $node,
460
        DOMElement $childNode,
461
        ElementContainer $elementContainer
462
    ) {
463
        /**
464
        * @var Group $referencedGroup
465
        */
466
        $referencedGroup = $this->findSomething(
467
            'findGroup',
468
            $schema,
469
            $node,
470
            $childNode->getAttribute("ref")
471
        );
472
473
        $group = $this->loadGroupRef($referencedGroup, $childNode);
474
        $elementContainer->addElement($group);
475
    }
476
477
    private function maybeLoadSequenceFromElementContainer(
478
        BaseComplexType $type,
479
        DOMElement $childNode
480
    ) {
481
        if (! ($type instanceof ElementContainer)) {
482
            throw new RuntimeException(
483
                '$type passed to ' .
484
                __FUNCTION__ .
485
                'expected to be an instance of ' .
486
                ElementContainer::class .
487
                ' when child node localName is "group", ' .
488
                get_class($type) .
489
                ' given.'
490
            );
491
        }
492
        $this->loadSequence($type, $childNode);
493
    }
494
495
    /**
496
    * @return Closure
497
    */
498
    private function loadGroup(Schema $schema, DOMElement $node)
499
    {
500
        $group = new Group($schema, $node->getAttribute("name"));
501
        $group->setDoc($this->getDocumentation($node));
502
503
        if ($node->hasAttribute("maxOccurs")) {
504
            /**
505
            * @var GroupRef $group
506
            */
507
            $group = static::maybeSetMax(new GroupRef($group), $node);
508
        }
509
        if ($node->hasAttribute("minOccurs")) {
510
            /**
511
            * @var GroupRef $group
512
            */
513
            $group = static::maybeSetMin(
514
                $group instanceof GroupRef ? $group : new GroupRef($group),
515
                $node
516
            );
517
        }
518
519
        $schema->addGroup($group);
520
521
        return function () use ($group, $node) {
522
            foreach ($node->childNodes as $childNode) {
523
                if (
524
                    in_array(
525
                        $childNode->localName,
526
                        [
527
                            'sequence',
528
                            'choice',
529
                            'all',
530
                        ]
531
                    )
532
                ) {
533
                        $this->loadSequence($group, $childNode);
534
                }
535
            }
536
        };
537
    }
538
539
    /**
540
    * @param Closure|null $callback
541
    *
542
    * @return Closure
543
    */
544
    private function loadComplexType(Schema $schema, DOMElement $node, $callback = null)
545
    {
546
        $isSimple = false;
547
548
        foreach ($node->childNodes as $childNode) {
549
            if ($childNode->localName === "simpleContent") {
550
                $isSimple = true;
551
                break;
552
            }
553
        }
554
555
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute("name")) : new ComplexType($schema, $node->getAttribute("name"));
556
557
        $type->setDoc($this->getDocumentation($node));
558
        if ($node->getAttribute("name")) {
559
            $schema->addType($type);
560
        }
561
562
        return function () use ($type, $node, $schema, $callback) {
563
564
            $this->fillTypeNode($type, $node);
565
566
            foreach ($node->childNodes as $childNode) {
567
                if ($childNode instanceof DOMElement) {
568
                    $this->loadComplexTypeFromChildNode(
569
                        $type,
570
                        $node,
571
                        $childNode,
572
                        $schema
573
                    );
574
                }
575
            }
576
577
            if ($callback) {
578
                call_user_func($callback, $type);
579
            }
580
        };
581
    }
582
583
    private function loadComplexTypeFromChildNode(
584
        BaseComplexType $type,
585
        DOMElement $node,
586
        DOMElement $childNode,
587
        Schema $schema
588
    ) {
589
        if (
590
            in_array(
591
                $childNode->localName,
592
                [
593
                    'sequence',
594
                    'choice',
595
                    'all',
596
                ]
597
            )
598
        ) {
599
            $this->maybeLoadSequenceFromElementContainer(
600
                $type,
601
                $childNode
602
            );
603
        } elseif ($childNode->localName === 'attribute') {
604
            $attribute = $this->getAttributeFromAttributeOrRef(
605
                $childNode,
606
                $schema,
607
                $node
608
            );
609
610
            $type->addAttribute($attribute);
611
        } elseif (
612
            $childNode->localName === 'group' &&
613
            $type instanceof ComplexType
614
        ) {
615
            $this->addGroupAsElement(
616
                $schema,
617
                $node,
618
                $childNode,
619
                $type
620
            );
621
        } elseif ($childNode->localName === 'attributeGroup') {
622
            AttributeGroup::findSomethingLikeThis(
623
                $this,
624
                $schema,
625
                $node,
626
                $childNode,
627
                $type
628
            );
629
        }
630
    }
631
632
    /**
633
    * @param Closure|null $callback
634
    *
635
    * @return Closure
636
    */
637
    private function loadSimpleType(Schema $schema, DOMElement $node, $callback = null)
638
    {
639
        $type = new SimpleType($schema, $node->getAttribute("name"));
640
        $type->setDoc($this->getDocumentation($node));
641
        if ($node->getAttribute("name")) {
642
            $schema->addType($type);
643
        }
644
645
        return function () use ($type, $node, $callback) {
646
            $this->fillTypeNode($type, $node);
647
648
            foreach ($node->childNodes as $childNode) {
649
                switch ($childNode->localName) {
650
                    case 'union':
651
                        $this->loadUnion($type, $childNode);
652
                        break;
653
                    case 'list':
654
                        $this->loadList($type, $childNode);
655
                        break;
656
                }
657
            }
658
659
            if ($callback) {
660
                call_user_func($callback, $type);
661
            }
662
        };
663
    }
664
665
    private function loadList(SimpleType $type, DOMElement $node)
666
    {
667
        if ($node->hasAttribute("itemType")) {
668
            /**
669
            * @var SimpleType $listType
670
            */
671
            $listType = $this->findSomeType($type, $node, 'itemType');
672
            $type->setList($listType);
673
        } else {
674
            $addCallback = function (SimpleType $list) use ($type) {
675
                $type->setList($list);
676
            };
677
678
            $this->loadTypeWithCallbackOnChildNodes(
679
                $type->getSchema(),
680
                $node,
681
                $addCallback
682
            );
683
        }
684
    }
685
686
    private function loadTypeWithCallbackOnChildNodes(
687
        Schema $schema,
688
        DOMNode $node,
689
        Closure $callback
690
    ) {
691
        foreach ($node->childNodes as $childNode) {
692
            $this->loadTypeWithCallback($schema, $childNode, $callback);
693
        }
694
    }
695
696
    private function loadTypeWithCallback(
697
        Schema $schema,
698
        DOMNode $childNode,
699
        Closure $callback
700
    ) {
701
        if (! ($childNode instanceof DOMElement)) {
702
            return;
703
        }
704
        switch ($childNode->localName) {
705
            case 'complexType':
706
                $childNode = $childNode;
707
                call_user_func(
708
                    $this->loadComplexType(
709
                        $schema,
710
                        $childNode,
711
                        $callback
712
                    )
713
                );
714
                break;
715
            case 'simpleType':
716
                call_user_func(
717
                    $this->loadSimpleType($schema, $childNode, $callback)
718
                );
719
                break;
720
        }
721
    }
722
723
    /**
724
    * @return SchemaItem
725
    */
726
    private function findSomeType(
727
        SchemaItem $fromThis,
728
        DOMElement $node,
729
        string $attributeName
730
    ) {
731
        return $this->findSomeTypeFromAttribute(
732
            $fromThis,
733
            $node,
734
            $node->getAttribute($attributeName)
735
        );
736
    }
737
738
    /**
739
    * @return SchemaItem
740
    */
741
    private function findSomeTypeFromAttribute(
742
        SchemaItem $fromThis,
743
        DOMElement $node,
744
        string $attributeName
745
    ) {
746
        /**
747
        * @var SchemaItem $out
748
        */
749
        $out = $this->findSomething(
750
            'findType',
751
            $fromThis->getSchema(),
752
            $node,
753
            $attributeName
754
        );
755
756
        return $out;
757
    }
758
759
    private function loadUnion(SimpleType $type, DOMElement $node)
760
    {
761
        if ($node->hasAttribute("memberTypes")) {
762
            $types = preg_split('/\s+/', $node->getAttribute("memberTypes"));
763
            foreach ($types as $typeName) {
764
                /**
765
                * @var SimpleType $unionType
766
                */
767
                $unionType = $this->findSomeTypeFromAttribute(
768
                    $type,
769
                    $node,
770
                    $typeName
771
                );
772
                $type->addUnion($unionType);
773
            }
774
        }
775
        $addCallback = function (SimpleType $unType) use ($type) {
776
            $type->addUnion($unType);
777
        };
778
779
        $this->loadTypeWithCallbackOnChildNodes(
780
            $type->getSchema(),
781
            $node,
782
            $addCallback
783
        );
784
    }
785
786
    /**
787
    * @param bool $checkAbstract
788
    */
789
    private function fillTypeNode(Type $type, DOMElement $node, $checkAbstract = true)
790
    {
791
792
        if ($checkAbstract) {
793
            $type->setAbstract($node->getAttribute("abstract") === "true" || $node->getAttribute("abstract") === "1");
794
        }
795
796
        foreach ($node->childNodes as $childNode) {
797
            switch ($childNode->localName) {
798
                case 'restriction':
799
                    $this->loadRestriction($type, $childNode);
800
                    break;
801
                case 'extension':
802
                    $this->maybeLoadExtensionFromBaseComplexType(
803
                        $type,
804
                        $childNode
805
                    );
806
                    break;
807
                case 'simpleContent':
808
                case 'complexContent':
809
                    $this->fillTypeNode($type, $childNode, false);
810
                    break;
811
            }
812
        }
813
    }
814
815
    private function loadExtension(BaseComplexType $type, DOMElement $node)
816
    {
817
        $extension = new Extension();
818
        $type->setExtension($extension);
819
820
        if ($node->hasAttribute("base")) {
821
            $this->findAndSetSomeBase(
822
                $type,
823
                $extension,
824
                $node
825
            );
826
        }
827
828
        foreach ($node->childNodes as $childNode) {
829
            switch ($childNode->localName) {
830
                case 'sequence':
831
                case 'choice':
832
                case 'all':
833
                    $this->maybeLoadSequenceFromElementContainer(
834
                        $type,
835
                        $childNode
836
                    );
837
                    break;
838
                case 'attribute':
839
                    $attribute = $this->getAttributeFromAttributeOrRef(
840
                        $childNode,
841
                        $type->getSchema(),
842
                        $node
843
                    );
844
                    $type->addAttribute($attribute);
845
                    break;
846
                case 'attributeGroup':
847
                    AttributeGroup::findSomethingLikeThis(
848
                        $this,
849
                        $type->getSchema(),
850
                        $node,
851
                        $childNode,
852
                        $type
853
                    );
854
                    break;
855
            }
856
        }
857
    }
858
859
    private function findAndSetSomeBase(
860
        Type $type,
861
        Base $setBaseOnThis,
862
        DOMElement $node
863
    ) {
864
        /**
865
        * @var Type $parent
866
        */
867
        $parent = $this->findSomeType($type, $node, 'base');
868
        $setBaseOnThis->setBase($parent);
869
    }
870
871
    private function maybeLoadExtensionFromBaseComplexType(
872
        Type $type,
873
        DOMElement $childNode
874
    ) {
875
        if (! ($type instanceof BaseComplexType)) {
876
            throw new RuntimeException(
877
                'Argument 1 passed to ' .
878
                __METHOD__ .
879
                ' needs to be an instance of ' .
880
                BaseComplexType::class .
881
                ' when passed onto ' .
882
                static::class .
883
                '::loadExtension(), ' .
884
                get_class($type) .
885
                ' given.'
886
            );
887
        }
888
        $this->loadExtension($type, $childNode);
889
    }
890
891
    private function loadRestriction(Type $type, DOMElement $node)
892
    {
893
        $restriction = new Restriction();
894
        $type->setRestriction($restriction);
895
        if ($node->hasAttribute("base")) {
896
            $this->findAndSetSomeBase($type, $restriction, $node);
897
        } else {
898
            $addCallback = function (Type $restType) use ($restriction) {
899
                $restriction->setBase($restType);
900
            };
901
902
            $this->loadTypeWithCallbackOnChildNodes(
903
                $type->getSchema(),
904
                $node,
905
                $addCallback
906
            );
907
        }
908
        foreach ($node->childNodes as $childNode) {
909
            if (in_array($childNode->localName,
910
                [
911
                    'enumeration',
912
                    'pattern',
913
                    'length',
914
                    'minLength',
915
                    'maxLength',
916
                    'minInclusive',
917
                    'maxInclusive',
918
                    'minExclusive',
919
                    'maxExclusive',
920
                    'fractionDigits',
921
                    'totalDigits',
922
                    'whiteSpace'
923
                ], true)) {
924
                $restriction->addCheck($childNode->localName,
925
                    [
926
                        'value' => $childNode->getAttribute("value"),
927
                        'doc' => $this->getDocumentation($childNode)
928
                    ]);
929
            }
930
        }
931
    }
932
933
    /**
934
    * @param string $typeName
935
    *
936
    * @return mixed[]
937
    */
938
    private static function splitParts(DOMElement $node, $typeName)
939
    {
940
        $prefix = null;
941
        $name = $typeName;
942
        if (strpos($typeName, ':') !== false) {
943
            list ($prefix, $name) = explode(':', $typeName);
944
        }
945
946
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
947
        return array(
948
            $name,
949
            $namespace,
950
            $prefix
951
        );
952
    }
953
954
    /**
955
     *
956
     * @param string $finder
957
     * @param Schema $schema
958
     * @param DOMElement $node
959
     * @param string $typeName
960
     * @throws TypeException
961
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
962
     */
963
    public function findSomething($finder, Schema $schema, DOMElement $node, $typeName)
964
    {
965
        list ($name, $namespace) = self::splitParts($node, $typeName);
966
967
        $namespace = $namespace ?: $schema->getTargetNamespace();
968
969
        try {
970
            return $schema->$finder($name, $namespace);
971
        } catch (TypeNotFoundException $e) {
972
            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);
973
        }
974
    }
975
976
    /**
977
    * @return Closure
978
    */
979
    private function loadElementDef(Schema $schema, DOMElement $node)
980
    {
981
        return $this->loadAttributeOrElementDef($schema, $node, false);
982
    }
983
984
    private function fillItem(Item $element, DOMElement $node)
985
    {
986
        $localType = null;
987
        foreach ($node->childNodes as $childNode) {
988
            switch ($childNode->localName) {
989
                case 'complexType':
990
                case 'simpleType':
991
                    $localType = $childNode;
992
                    break 2;
993
            }
994
        }
995
996
        if ($localType) {
997
            $addCallback = function (Type $type) use ($element) {
998
                $element->setType($type);
999
            };
1000
            $this->loadTypeWithCallback(
1001
                $element->getSchema(),
1002
                $localType,
1003
                $addCallback
1004
            );
1005
        } else {
1006
1007
            if ($node->getAttribute("type")) {
1008
                /**
1009
                * @var Type $type
1010
                */
1011
                $type = $this->findSomeType($element, $node, 'type');
1012
            } else {
1013
                /**
1014
                * @var Type $type
1015
                */
1016
                $type = $this->findSomeTypeFromAttribute(
1017
                    $element,
1018
                    $node,
1019
                    ($node->lookupPrefix(self::XSD_NS) . ':anyType')
1020
                );
1021
            }
1022
1023
            $element->setType($type);
1024
        }
1025
    }
1026
1027
    /**
1028
    * @return Closure
1029
    */
1030
    private function loadImport(Schema $schema, DOMElement $node)
1031
    {
1032
        $base = urldecode($node->ownerDocument->documentURI);
1033
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute("schemaLocation"));
1034
        if ($node->hasAttribute("namespace")
1035
            && isset(self::$globalSchemaInfo[$node->getAttribute("namespace")])
1036
            && isset($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]])
1037
        ) {
1038
1039
            $schema->addSchema($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]]);
1040
1041
            return function () {
1042
            };
1043
        } elseif ($node->hasAttribute("namespace")
1044
            && isset($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))])) {
1045
            $schema->addSchema($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))]);
1046
            return function () {
1047
            };
1048
        } elseif (isset($this->loadedFiles[$file])) {
1049
            $schema->addSchema($this->loadedFiles[$file]);
1050
            return function () {
1051
            };
1052
        }
1053
1054
        if (!$node->getAttribute("namespace")) {
1055
            $this->loadedFiles[$file] = $newSchema = $schema;
1056
        } else {
1057
            $this->loadedFiles[$file] = $newSchema = new Schema();
1058
            $newSchema->addSchema($this->getGlobalSchema());
1059
        }
1060
1061
        $xml = $this->getDOM(isset($this->knownLocationSchemas[$file]) ? $this->knownLocationSchemas[$file] : $file);
1062
1063
        $callbacks = $this->schemaNode($newSchema, $xml->documentElement, $schema);
1064
1065
        if ($node->getAttribute("namespace")) {
1066
            $schema->addSchema($newSchema);
1067
        }
1068
1069
1070
        return function () use ($callbacks) {
1071
            foreach ($callbacks as $callback) {
1072
                call_user_func($callback);
1073
            }
1074
        };
1075
    }
1076
1077
    /**
1078
    * @var Schema|null
1079
    */
1080
    private $globalSchema;
1081
1082
    /**
1083
     *
1084
     * @return Schema
1085
     */
1086
    public function getGlobalSchema()
1087
    {
1088
        if (!$this->globalSchema) {
1089
            $callbacks = array();
1090
            $globalSchemas = array();
1091
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1092
                $this->loadedFiles[$uri] = $globalSchemas [$namespace] = $schema = new Schema();
1093
                if ($namespace === self::XSD_NS) {
1094
                    $this->globalSchema = $schema;
1095
                }
1096
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1097
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1098
            }
1099
1100
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anySimpleType"));
1101
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anyType"));
1102
1103
            $globalSchemas[self::XML_NS]->addSchema($globalSchemas[self::XSD_NS], self::XSD_NS);
1104
            $globalSchemas[self::XSD_NS]->addSchema($globalSchemas[self::XML_NS], self::XML_NS);
1105
1106
            foreach ($callbacks as $callback) {
1107
                $callback();
1108
            }
1109
        }
1110
1111
        /**
1112
        * @var Schema $out
1113
        */
1114
        $out = $this->globalSchema;
1115
1116
        return $out;
1117
    }
1118
1119
    /**
1120
     * @param DOMElement $node
1121
     * @param string  $file
1122
     *
1123
     * @return Schema
1124
     */
1125
    public function readNode(DOMElement $node, $file = 'schema.xsd')
1126
    {
1127
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1128
        $this->loadedFiles[$fileKey] = $rootSchema = new Schema();
1129
1130
        $rootSchema->addSchema($this->getGlobalSchema());
1131
        $callbacks = $this->schemaNode($rootSchema, $node);
1132
1133
        foreach ($callbacks as $callback) {
1134
            call_user_func($callback);
1135
        }
1136
1137
        return $rootSchema;
1138
    }
1139
1140
    /**
1141
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1142
     *
1143
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1144
     * file to distinguish between multiple schemas in a single file.
1145
     *
1146
     * @param string $file
1147
     * @param string $targetNamespace
1148
     *
1149
     * @return string
1150
     */
1151
    private function getNamespaceSpecificFileIndex($file, $targetNamespace)
1152
    {
1153
        return $file . '#' . $targetNamespace;
1154
    }
1155
1156
    /**
1157
     * @param string $content
1158
     * @param string $file
1159
     *
1160
     * @return Schema
1161
     *
1162
     * @throws IOException
1163
     */
1164
    public function readString($content, $file = 'schema.xsd')
1165
    {
1166
        $xml = new DOMDocument('1.0', 'UTF-8');
1167
        if (!$xml->loadXML($content)) {
1168
            throw new IOException("Can't load the schema");
1169
        }
1170
        $xml->documentURI = $file;
1171
1172
        return $this->readNode($xml->documentElement, $file);
1173
    }
1174
1175
    /**
1176
     * @param string $file
1177
     *
1178
     * @return Schema
1179
     */
1180
    public function readFile($file)
1181
    {
1182
        $xml = $this->getDOM($file);
1183
        return $this->readNode($xml->documentElement, $file);
1184
    }
1185
1186
    /**
1187
     * @param string $file
1188
     *
1189
     * @return DOMDocument
1190
     *
1191
     * @throws IOException
1192
     */
1193
    private function getDOM($file)
1194
    {
1195
        $xml = new DOMDocument('1.0', 'UTF-8');
1196
        if (!$xml->load($file)) {
1197
            throw new IOException("Can't load the file $file");
1198
        }
1199
        return $xml;
1200
    }
1201
}
1202