Passed
Push — static-analysis ( 667a1a...e9136f )
by SignpostMarv
01:23
created

SchemaReader::schemaNode()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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