Passed
Push — static-analysis ( 88cd87...62dcd9 )
by SignpostMarv
01:44
created

SchemaReader::loadSequence()   C

Complexity

Conditions 12
Paths 64

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 28
nc 64
nop 3
dl 0
loc 42
rs 5.1612
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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