Passed
Push — static-analysis ( 21e585...fe941b )
by SignpostMarv
01:25
created

SchemaReader::loadExtension()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 57
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 40
nc 6
nop 2
dl 0
loc 57
rs 9.0309
c 0
b 0
f 0

How to fix   Long Method   

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