Passed
Push — static-analysis ( 3ae303...21e585 )
by SignpostMarv
01:36
created

SchemaReader::loadTypeWithCallbackOnChildNodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 3
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace GoetasWebservices\XML\XSDReader;
3
4
use Closure;
5
use DOMDocument;
6
use DOMElement;
7
use DOMNode;
8
use GoetasWebservices\XML\XSDReader\Exception\IOException;
9
use GoetasWebservices\XML\XSDReader\Exception\TypeException;
10
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Attribute;
11
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeDef;
12
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
13
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeRef;
14
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group as AttributeGroup;
15
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
16
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
17
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
18
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
19
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
20
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
21
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
22
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
23
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
24
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
25
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
26
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
27
use GoetasWebservices\XML\XSDReader\Schema\Item;
28
use GoetasWebservices\XML\XSDReader\Schema\Schema;
29
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
30
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
31
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
32
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
33
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
34
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
35
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
36
use RuntimeException;
37
38
class SchemaReader
39
{
40
41
    const XSD_NS = "http://www.w3.org/2001/XMLSchema";
42
43
    const XML_NS = "http://www.w3.org/XML/1998/namespace";
44
45
    /**
46
    * @var Schema[]
47
    */
48
    private $loadedFiles = array();
49
50
    /**
51
    * @var string[]
52
    */
53
    private $knownLocationSchemas = array();
54
55
    /**
56
    * @var string[]
57
    */
58
    private static $globalSchemaInfo = array(
59
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
60
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd'
61
    );
62
63
    public function __construct()
64
    {
65
        $this->addKnownSchemaLocation('http://www.w3.org/2001/xml.xsd', __DIR__ . '/Resources/xml.xsd');
66
        $this->addKnownSchemaLocation('http://www.w3.org/2001/XMLSchema.xsd', __DIR__ . '/Resources/XMLSchema.xsd');
67
        $this->addKnownSchemaLocation('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', __DIR__ . '/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd');
68
        $this->addKnownSchemaLocation('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', __DIR__ . '/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd');
69
        $this->addKnownSchemaLocation('https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', __DIR__ . '/Resources/xmldsig-core-schema.xsd');
70
        $this->addKnownSchemaLocation('http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', __DIR__ . '/Resources/xmldsig-core-schema.xsd');
71
    }
72
73
    /**
74
    * @param string $remote
75
    * @param string $local
76
    */
77
    public function addKnownSchemaLocation($remote, $local)
78
    {
79
        $this->knownLocationSchemas[$remote] = $local;
80
    }
81
82
    /**
83
    * @return Closure
84
    */
85
    private function loadAttributeGroup(Schema $schema, DOMElement $node)
86
    {
87
        $attGroup = new AttributeGroup($schema, $node->getAttribute("name"));
88
        $attGroup->setDoc($this->getDocumentation($node));
89
        $schema->addAttributeGroup($attGroup);
90
91
        return function () use ($schema, $node, $attGroup) {
92
            foreach ($node->childNodes as $childNode) {
93
                switch ($childNode->localName) {
94
                    case 'attribute':
95
                        $attribute = $this->getAttributeFromAttributeOrRef(
96
                            $childNode,
97
                            $schema,
98
                            $node
99
                        );
100
                        $attGroup->addAttribute($attribute);
101
                        break;
102
                    case 'attributeGroup':
103
                        AttributeGroup::findSomethingLikeThis(
104
                            $this,
105
                            $schema,
106
                            $node,
107
                            $childNode,
108
                            $attGroup
109
                        );
110
                        break;
111
                }
112
            }
113
        };
114
    }
115
116
    /**
117
    * @return AttributeItem
118
    */
119
    private function getAttributeFromAttributeOrRef(
120
        DOMElement $childNode,
121
        Schema $schema,
122
        DOMElement $node
123
    ) {
124
        if ($childNode->hasAttribute("ref")) {
125
            /**
126
            * @var AttributeItem $attribute
127
            */
128
            $attribute = $this->findSomething('findAttribute', $schema, $node, $childNode->getAttribute("ref"));
129
        } else {
130
            /**
131
            * @var Attribute $attribute
132
            */
133
            $attribute = $this->loadAttribute($schema, $childNode);
134
        }
135
136
        return $attribute;
137
    }
138
139
    /**
140
    * @return Attribute
141
    */
142
    private function loadAttribute(Schema $schema, DOMElement $node)
143
    {
144
        $attribute = new Attribute($schema, $node->getAttribute("name"));
145
        $attribute->setDoc($this->getDocumentation($node));
146
        $this->fillItem($attribute, $node);
147
148
        if ($node->hasAttribute("nillable")) {
149
            $attribute->setNil($node->getAttribute("nillable") == "true");
150
        }
151
        if ($node->hasAttribute("form")) {
152
            $attribute->setQualified($node->getAttribute("form") == "qualified");
153
        }
154
        if ($node->hasAttribute("use")) {
155
            $attribute->setUse($node->getAttribute("use"));
156
        }
157
        return $attribute;
158
    }
159
160
    /**
161
    * @return Closure
162
    */
163
    private function loadAttributeOrElementDef(
164
        Schema $schema,
165
        DOMElement $node,
166
        bool $attributeDef
167
    ) {
168
        $name = $node->getAttribute('name');
169
        if ($attributeDef) {
170
            $attribute = new AttributeDef($schema, $name);
171
            $schema->addAttribute($attribute);
172
        } else {
173
            $attribute = new ElementDef($schema, $name);
174
            $schema->addElement($attribute);
175
        }
176
177
178
        return function () use ($attribute, $node) {
179
            $this->fillItem($attribute, $node);
180
        };
181
    }
182
183
    /**
184
    * @return Closure
185
    */
186
    private function loadAttributeDef(Schema $schema, DOMElement $node)
187
    {
188
        return $this->loadAttributeOrElementDef($schema, $node, true);
189
    }
190
191
    /**
192
     * @param DOMElement $node
193
     * @return string
194
     */
195
    private function getDocumentation(DOMElement $node)
196
    {
197
        $doc = '';
198
        foreach ($node->childNodes as $childNode) {
199
            if ($childNode->localName == "annotation") {
200
                foreach ($childNode->childNodes as $subChildNode) {
201
                    if ($subChildNode->localName == "documentation") {
202
                        $doc .= ($subChildNode->nodeValue);
203
                    }
204
                }
205
            }
206
        }
207
        $doc = preg_replace('/[\t ]+/', ' ', $doc);
208
        return trim($doc);
209
    }
210
211
    private function setSchemaThingsFromNode(
212
        Schema $schema,
213
        DOMElement $node,
214
        Schema $parent = null
215
    ) {
216
        $schema->setDoc($this->getDocumentation($node));
217
218
        if ($node->hasAttribute("targetNamespace")) {
219
            $schema->setTargetNamespace($node->getAttribute("targetNamespace"));
220
        } elseif ($parent) {
221
            $schema->setTargetNamespace($parent->getTargetNamespace());
222
        }
223
        $schema->setElementsQualification($node->getAttribute("elementFormDefault") == "qualified");
224
        $schema->setAttributesQualification($node->getAttribute("attributeFormDefault") == "qualified");
225
        $schema->setDoc($this->getDocumentation($node));
226
    }
227
228
    /**
229
    * @param mixed $schema
230
    *
231
    * @return Closure|null
232
    */
233
    private function maybeCallMethod(
234
        array $methods,
235
        string $key,
236
        DOMNode $childNode,
237
        ...$args
238
    ) {
239
        if ($childNode instanceof DOMElement && isset($methods[$key])) {
240
            $method = $methods[$key];
241
242
            $append = $this->$method(...$args);
243
244
            if ($append instanceof Closure) {
245
                return $append;
246
            }
247
        }
248
    }
249
250
    /**
251
     *
252
     * @param Schema $schema
253
     * @param DOMElement $node
254
     * @param Schema $parent
255
     * @return Closure[]
256
     */
257
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null)
258
    {
259
        $this->setSchemaThingsFromNode($schema, $node, $parent);
260
        $functions = array();
261
262
        static $methods = [
263
            'include' => 'loadImport',
264
            'import' => 'loadImport',
265
            'element' => 'loadElementDef',
266
            'attribute' => 'loadAttributeDef',
267
            'attributeGroup' => 'loadAttributeGroup',
268
            'group' => 'loadGroup',
269
            'complexType' => 'loadComplexType',
270
            'simpleType' => 'loadSimpleType',
271
        ];
272
273
        foreach ($node->childNodes as $childNode) {
274
            $callback = $this->maybeCallMethod(
275
                $methods,
276
                (string) $childNode->localName,
277
                $childNode,
278
                $schema,
279
                $childNode
280
            );
281
282
            if ($callback instanceof Closure) {
283
                $functions[] = $callback;
284
            }
285
        }
286
287
        return $functions;
288
    }
289
290
    /**
291
    * @return Element
292
    */
293
    private function loadElement(Schema $schema, DOMElement $node)
294
    {
295
        $element = new Element($schema, $node->getAttribute("name"));
296
        $element->setDoc($this->getDocumentation($node));
297
298
        $this->fillItem($element, $node);
299
300
        static::maybeSetMax($element, $node);
301
        static::maybeSetMin($element, $node);
302
303
        $xp = new \DOMXPath($node->ownerDocument);
304
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
305
306
        if ($xp->query('ancestor::xs:choice', $node)->length) {
307
            $element->setMin(0);
308
        }
309
310
        if ($node->hasAttribute("nillable")) {
311
            $element->setNil($node->getAttribute("nillable") == "true");
312
        }
313
        if ($node->hasAttribute("form")) {
314
            $element->setQualified($node->getAttribute("form") == "qualified");
315
        }
316
        return $element;
317
    }
318
319
    /**
320
    * @return GroupRef
321
    */
322
    private function loadGroupRef(Group $referenced, DOMElement $node)
323
    {
324
        $ref = new GroupRef($referenced);
325
        $ref->setDoc($this->getDocumentation($node));
326
327
        static::maybeSetMax($ref, $node);
328
        static::maybeSetMin($ref, $node);
329
330
        return $ref;
331
    }
332
333
    /**
334
    * @return ElementRef
335
    */
336
    private function loadElementRef(ElementDef $referenced, DOMElement $node)
337
    {
338
        $ref = new ElementRef($referenced);
339
        $this->setDoc($ref, $node);
340
341
        static::maybeSetMax($ref, $node);
342
        static::maybeSetMin($ref, $node);
343
        if ($node->hasAttribute("nillable")) {
344
            $ref->setNil($node->getAttribute("nillable") == "true");
345
        }
346
        if ($node->hasAttribute("form")) {
347
            $ref->setQualified($node->getAttribute("form") == "qualified");
348
        }
349
350
        return $ref;
351
    }
352
353
    private function setDoc(Item $ref, DOMElement $node)
354
    {
355
        $ref->setDoc($this->getDocumentation($node));
356
    }
357
358
    /**
359
    * @return InterfaceSetMinMax
360
    */
361
    private static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node)
362
    {
363
        if (
364
            $node->hasAttribute("maxOccurs")
365
        ) {
366
            $ref->setMax($node->getAttribute("maxOccurs") == "unbounded" ? -1 : (int)$node->getAttribute("maxOccurs"));
367
        }
368
369
        return $ref;
370
    }
371
372
    /**
373
    * @return InterfaceSetMinMax
374
    */
375
    private static function maybeSetMin(InterfaceSetMinMax $ref, DOMElement $node)
376
    {
377
        if ($node->hasAttribute("minOccurs")) {
378
            $ref->setMin((int) $node->getAttribute("minOccurs"));
379
        }
380
381
        return $ref;
382
    }
383
384
    /**
385
    * @return AttributeRef
386
    */
387
    private function loadAttributeRef(AttributeDef $referencedAttribiute, DOMElement $node)
388
    {
389
        $attribute = new AttributeRef($referencedAttribiute);
390
        $this->setDoc($attribute, $node);
391
392
        if ($node->hasAttribute("nillable")) {
393
            $attribute->setNil($node->getAttribute("nillable") == "true");
394
        }
395
        if ($node->hasAttribute("form")) {
396
            $attribute->setQualified($node->getAttribute("form") == "qualified");
397
        }
398
        if ($node->hasAttribute("use")) {
399
            $attribute->setUse($node->getAttribute("use"));
400
        }
401
        return $attribute;
402
    }
403
404
    /**
405
    * @param int|null $max
406
    */
407
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, $max = null)
408
    {
409
        $max = (
410
            (is_int($max) && (bool) $max) ||
411
            $node->getAttribute("maxOccurs") == "unbounded" ||
412
            $node->getAttribute("maxOccurs") > 1
413
        )
414
            ? 2
415
            : null;
416
417
        foreach ($node->childNodes as $childNode) {
418
            if ($childNode instanceof DOMElement) {
419
                $this->loadSequenceChildNode(
420
                    $elementContainer,
421
                    $node,
422
                    $childNode,
423
                    $max
424
                );
425
            }
426
        }
427
    }
428
429
    /**
430
    * @param int|null $max
431
    */
432
    private function loadSequenceChildNode(
433
        ElementContainer $elementContainer,
434
        DOMElement $node,
435
        DOMElement $childNode,
436
        $max
437
    ) {
438
        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
        foreach ($node->childNodes as $childNode) {
904
            switch ($childNode->localName) {
905
                case 'sequence':
906
                case 'choice':
907
                case 'all':
908
                    $this->maybeLoadSequenceFromElementContainer(
909
                        $type,
910
                        $childNode
911
                    );
912
                    break;
913
                case 'attribute':
914
                    $attribute = $this->getAttributeFromAttributeOrRef(
915
                        $childNode,
916
                        $type->getSchema(),
917
                        $node
918
                    );
919
                    $type->addAttribute($attribute);
920
                    break;
921
                case 'attributeGroup':
922
                    AttributeGroup::findSomethingLikeThis(
923
                        $this,
924
                        $type->getSchema(),
925
                        $node,
926
                        $childNode,
927
                        $type
928
                    );
929
                    break;
930
            }
931
        }
932
    }
933
934
    private function findAndSetSomeBase(
935
        Type $type,
936
        Base $setBaseOnThis,
937
        DOMElement $node
938
    ) {
939
        /**
940
        * @var Type $parent
941
        */
942
        $parent = $this->findSomeType($type, $node, 'base');
943
        $setBaseOnThis->setBase($parent);
944
    }
945
946
    private function maybeLoadExtensionFromBaseComplexType(
947
        Type $type,
948
        DOMElement $childNode
949
    ) {
950
        if (! ($type instanceof BaseComplexType)) {
951
            throw new RuntimeException(
952
                'Argument 1 passed to ' .
953
                __METHOD__ .
954
                ' needs to be an instance of ' .
955
                BaseComplexType::class .
956
                ' when passed onto ' .
957
                static::class .
958
                '::loadExtension(), ' .
959
                get_class($type) .
960
                ' given.'
961
            );
962
        }
963
        $this->loadExtension($type, $childNode);
964
    }
965
966
    private function loadRestriction(Type $type, DOMElement $node)
967
    {
968
        $restriction = new Restriction();
969
        $type->setRestriction($restriction);
970
        if ($node->hasAttribute("base")) {
971
            $this->findAndSetSomeBase($type, $restriction, $node);
972
        } else {
973
            $addCallback = function (Type $restType) use ($restriction) {
974
                $restriction->setBase($restType);
975
            };
976
977
            $this->loadTypeWithCallbackOnChildNodes(
978
                $type->getSchema(),
979
                $node,
980
                $addCallback
981
            );
982
        }
983
        foreach ($node->childNodes as $childNode) {
984
            if (in_array($childNode->localName,
985
                [
986
                    'enumeration',
987
                    'pattern',
988
                    'length',
989
                    'minLength',
990
                    'maxLength',
991
                    'minInclusive',
992
                    'maxInclusive',
993
                    'minExclusive',
994
                    'maxExclusive',
995
                    'fractionDigits',
996
                    'totalDigits',
997
                    'whiteSpace'
998
                ], true)) {
999
                $restriction->addCheck($childNode->localName,
1000
                    [
1001
                        'value' => $childNode->getAttribute("value"),
1002
                        'doc' => $this->getDocumentation($childNode)
1003
                    ]);
1004
            }
1005
        }
1006
    }
1007
1008
    /**
1009
    * @param string $typeName
1010
    *
1011
    * @return mixed[]
1012
    */
1013
    private static function splitParts(DOMElement $node, $typeName)
1014
    {
1015
        $prefix = null;
1016
        $name = $typeName;
1017
        if (strpos($typeName, ':') !== false) {
1018
            list ($prefix, $name) = explode(':', $typeName);
1019
        }
1020
1021
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
1022
        return array(
1023
            $name,
1024
            $namespace,
1025
            $prefix
1026
        );
1027
    }
1028
1029
    /**
1030
     *
1031
     * @param string $finder
1032
     * @param Schema $schema
1033
     * @param DOMElement $node
1034
     * @param string $typeName
1035
     * @throws TypeException
1036
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
1037
     */
1038
    public function findSomething($finder, Schema $schema, DOMElement $node, $typeName)
1039
    {
1040
        list ($name, $namespace) = self::splitParts($node, $typeName);
1041
1042
        $namespace = $namespace ?: $schema->getTargetNamespace();
1043
1044
        try {
1045
            return $schema->$finder($name, $namespace);
1046
        } catch (TypeNotFoundException $e) {
1047
            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);
1048
        }
1049
    }
1050
1051
    /**
1052
    * @return Closure
1053
    */
1054
    private function loadElementDef(Schema $schema, DOMElement $node)
1055
    {
1056
        return $this->loadAttributeOrElementDef($schema, $node, false);
1057
    }
1058
1059
    private function fillItem(Item $element, DOMElement $node)
1060
    {
1061
        $localType = null;
1062
        foreach ($node->childNodes as $childNode) {
1063
            switch ($childNode->localName) {
1064
                case 'complexType':
1065
                case 'simpleType':
1066
                    $localType = $childNode;
1067
                    break 2;
1068
            }
1069
        }
1070
1071
        if ($localType) {
1072
            $addCallback = function (Type $type) use ($element) {
1073
                $element->setType($type);
1074
            };
1075
            $this->loadTypeWithCallback(
1076
                $element->getSchema(),
1077
                $localType,
1078
                $addCallback
1079
            );
1080
        } else {
1081
1082
            if ($node->getAttribute("type")) {
1083
                /**
1084
                * @var Type $type
1085
                */
1086
                $type = $this->findSomeType($element, $node, 'type');
1087
            } else {
1088
                /**
1089
                * @var Type $type
1090
                */
1091
                $type = $this->findSomeTypeFromAttribute(
1092
                    $element,
1093
                    $node,
1094
                    ($node->lookupPrefix(self::XSD_NS) . ':anyType')
1095
                );
1096
            }
1097
1098
            $element->setType($type);
1099
        }
1100
    }
1101
1102
    /**
1103
    * @return Closure
1104
    */
1105
    private function loadImport(Schema $schema, DOMElement $node)
1106
    {
1107
        $base = urldecode($node->ownerDocument->documentURI);
1108
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute("schemaLocation"));
1109
        if ($node->hasAttribute("namespace")
1110
            && isset(self::$globalSchemaInfo[$node->getAttribute("namespace")])
1111
            && isset($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]])
1112
        ) {
1113
1114
            $schema->addSchema($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]]);
1115
1116
            return function () {
1117
            };
1118
        } elseif ($node->hasAttribute("namespace")
1119
            && isset($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))])) {
1120
            $schema->addSchema($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))]);
1121
            return function () {
1122
            };
1123
        } elseif (isset($this->loadedFiles[$file])) {
1124
            $schema->addSchema($this->loadedFiles[$file]);
1125
            return function () {
1126
            };
1127
        }
1128
1129
        if (!$node->getAttribute("namespace")) {
1130
            $this->loadedFiles[$file] = $newSchema = $schema;
1131
        } else {
1132
            $this->loadedFiles[$file] = $newSchema = new Schema();
1133
            $newSchema->addSchema($this->getGlobalSchema());
1134
        }
1135
1136
        $xml = $this->getDOM(isset($this->knownLocationSchemas[$file]) ? $this->knownLocationSchemas[$file] : $file);
1137
1138
        $callbacks = $this->schemaNode($newSchema, $xml->documentElement, $schema);
1139
1140
        if ($node->getAttribute("namespace")) {
1141
            $schema->addSchema($newSchema);
1142
        }
1143
1144
1145
        return function () use ($callbacks) {
1146
            foreach ($callbacks as $callback) {
1147
                call_user_func($callback);
1148
            }
1149
        };
1150
    }
1151
1152
    /**
1153
    * @var Schema|null
1154
    */
1155
    private $globalSchema;
1156
1157
    /**
1158
     *
1159
     * @return Schema
1160
     */
1161
    public function getGlobalSchema()
1162
    {
1163
        if (!$this->globalSchema) {
1164
            $callbacks = array();
1165
            $globalSchemas = array();
1166
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1167
                $this->loadedFiles[$uri] = $globalSchemas [$namespace] = $schema = new Schema();
1168
                if ($namespace === self::XSD_NS) {
1169
                    $this->globalSchema = $schema;
1170
                }
1171
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1172
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1173
            }
1174
1175
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anySimpleType"));
1176
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anyType"));
1177
1178
            $globalSchemas[self::XML_NS]->addSchema($globalSchemas[self::XSD_NS], self::XSD_NS);
1179
            $globalSchemas[self::XSD_NS]->addSchema($globalSchemas[self::XML_NS], self::XML_NS);
1180
1181
            foreach ($callbacks as $callback) {
1182
                $callback();
1183
            }
1184
        }
1185
1186
        /**
1187
        * @var Schema $out
1188
        */
1189
        $out = $this->globalSchema;
1190
1191
        return $out;
1192
    }
1193
1194
    /**
1195
     * @param DOMElement $node
1196
     * @param string  $file
1197
     *
1198
     * @return Schema
1199
     */
1200
    public function readNode(DOMElement $node, $file = 'schema.xsd')
1201
    {
1202
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1203
        $this->loadedFiles[$fileKey] = $rootSchema = new Schema();
1204
1205
        $rootSchema->addSchema($this->getGlobalSchema());
1206
        $callbacks = $this->schemaNode($rootSchema, $node);
1207
1208
        foreach ($callbacks as $callback) {
1209
            call_user_func($callback);
1210
        }
1211
1212
        return $rootSchema;
1213
    }
1214
1215
    /**
1216
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1217
     *
1218
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1219
     * file to distinguish between multiple schemas in a single file.
1220
     *
1221
     * @param string $file
1222
     * @param string $targetNamespace
1223
     *
1224
     * @return string
1225
     */
1226
    private function getNamespaceSpecificFileIndex($file, $targetNamespace)
1227
    {
1228
        return $file . '#' . $targetNamespace;
1229
    }
1230
1231
    /**
1232
     * @param string $content
1233
     * @param string $file
1234
     *
1235
     * @return Schema
1236
     *
1237
     * @throws IOException
1238
     */
1239
    public function readString($content, $file = 'schema.xsd')
1240
    {
1241
        $xml = new DOMDocument('1.0', 'UTF-8');
1242
        if (!$xml->loadXML($content)) {
1243
            throw new IOException("Can't load the schema");
1244
        }
1245
        $xml->documentURI = $file;
1246
1247
        return $this->readNode($xml->documentElement, $file);
1248
    }
1249
1250
    /**
1251
     * @param string $file
1252
     *
1253
     * @return Schema
1254
     */
1255
    public function readFile($file)
1256
    {
1257
        $xml = $this->getDOM($file);
1258
        return $this->readNode($xml->documentElement, $file);
1259
    }
1260
1261
    /**
1262
     * @param string $file
1263
     *
1264
     * @return DOMDocument
1265
     *
1266
     * @throws IOException
1267
     */
1268
    private function getDOM($file)
1269
    {
1270
        $xml = new DOMDocument('1.0', 'UTF-8');
1271
        if (!$xml->load($file)) {
1272
            throw new IOException("Can't load the file $file");
1273
        }
1274
        return $xml;
1275
    }
1276
}
1277