Passed
Push — static-analysis ( 03455e...667a1a )
by SignpostMarv
01:32
created

SchemaReader::maybeCallMethod()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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