Passed
Push — static-analysis ( 1289d7...32726d )
by SignpostMarv
01:30
created

SchemaReader::getDocumentation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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