Passed
Push — static-analysis ( dbaa60...3ae303 )
by SignpostMarv
01:32
created

SchemaReader::loadComplexType()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 37
Code Lines 25

Duplication

Lines 19
Ratio 51.35 %

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 12
nop 3
dl 19
loc 37
rs 8.439
c 0
b 0
f 0
1
<?php
2
namespace GoetasWebservices\XML\XSDReader;
3
4
use Closure;
5
use DOMDocument;
6
use DOMElement;
7
use DOMNode;
8
use GoetasWebservices\XML\XSDReader\Exception\IOException;
9
use GoetasWebservices\XML\XSDReader\Exception\TypeException;
10
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Attribute;
11
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeDef;
12
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
13
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeRef;
14
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group as AttributeGroup;
15
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
16
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
17
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
18
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
19
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
20
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
21
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
22
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
23
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
24
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
25
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
26
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
27
use GoetasWebservices\XML\XSDReader\Schema\Item;
28
use GoetasWebservices\XML\XSDReader\Schema\Schema;
29
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
30
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
31
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
32
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
33
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
34
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
35
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
36
use RuntimeException;
37
38
class SchemaReader
39
{
40
41
    const XSD_NS = "http://www.w3.org/2001/XMLSchema";
42
43
    const XML_NS = "http://www.w3.org/XML/1998/namespace";
44
45
    /**
46
    * @var Schema[]
47
    */
48
    private $loadedFiles = array();
49
50
    /**
51
    * @var string[]
52
    */
53
    private $knownLocationSchemas = array();
54
55
    /**
56
    * @var string[]
57
    */
58
    private static $globalSchemaInfo = array(
59
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
60
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd'
61
    );
62
63
    public function __construct()
64
    {
65
        $this->addKnownSchemaLocation('http://www.w3.org/2001/xml.xsd', __DIR__ . '/Resources/xml.xsd');
66
        $this->addKnownSchemaLocation('http://www.w3.org/2001/XMLSchema.xsd', __DIR__ . '/Resources/XMLSchema.xsd');
67
        $this->addKnownSchemaLocation('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', __DIR__ . '/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd');
68
        $this->addKnownSchemaLocation('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', __DIR__ . '/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd');
69
        $this->addKnownSchemaLocation('https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', __DIR__ . '/Resources/xmldsig-core-schema.xsd');
70
        $this->addKnownSchemaLocation('http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', __DIR__ . '/Resources/xmldsig-core-schema.xsd');
71
    }
72
73
    /**
74
    * @param string $remote
75
    * @param string $local
76
    */
77
    public function addKnownSchemaLocation($remote, $local)
78
    {
79
        $this->knownLocationSchemas[$remote] = $local;
80
    }
81
82
    /**
83
    * @return Closure
84
    */
85
    private function loadAttributeGroup(Schema $schema, DOMElement $node)
86
    {
87
        $attGroup = new AttributeGroup($schema, $node->getAttribute("name"));
88
        $attGroup->setDoc($this->getDocumentation($node));
89
        $schema->addAttributeGroup($attGroup);
90
91
        return function () use ($schema, $node, $attGroup) {
92
            foreach ($node->childNodes as $childNode) {
93
                switch ($childNode->localName) {
94
                    case 'attribute':
95
                        $attribute = $this->getAttributeFromAttributeOrRef(
96
                            $childNode,
97
                            $schema,
98
                            $node
99
                        );
100
                        $attGroup->addAttribute($attribute);
101
                        break;
102
                    case 'attributeGroup':
103
                        AttributeGroup::findSomethingLikeThis(
104
                            $this,
105
                            $schema,
106
                            $node,
107
                            $childNode,
108
                            $attGroup
109
                        );
110
                        break;
111
                }
112
            }
113
        };
114
    }
115
116
    /**
117
    * @return AttributeItem
118
    */
119
    private function getAttributeFromAttributeOrRef(
120
        DOMElement $childNode,
121
        Schema $schema,
122
        DOMElement $node
123
    ) {
124
        if ($childNode->hasAttribute("ref")) {
125
            /**
126
            * @var AttributeItem $attribute
127
            */
128
            $attribute = $this->findSomething('findAttribute', $schema, $node, $childNode->getAttribute("ref"));
129
        } else {
130
            /**
131
            * @var Attribute $attribute
132
            */
133
            $attribute = $this->loadAttribute($schema, $childNode);
134
        }
135
136
        return $attribute;
137
    }
138
139
    /**
140
    * @return Attribute
141
    */
142
    private function loadAttribute(Schema $schema, DOMElement $node)
143
    {
144
        $attribute = new Attribute($schema, $node->getAttribute("name"));
145
        $attribute->setDoc($this->getDocumentation($node));
146
        $this->fillItem($attribute, $node);
147
148
        if ($node->hasAttribute("nillable")) {
149
            $attribute->setNil($node->getAttribute("nillable") == "true");
150
        }
151
        if ($node->hasAttribute("form")) {
152
            $attribute->setQualified($node->getAttribute("form") == "qualified");
153
        }
154
        if ($node->hasAttribute("use")) {
155
            $attribute->setUse($node->getAttribute("use"));
156
        }
157
        return $attribute;
158
    }
159
160
    /**
161
    * @return Closure
162
    */
163
    private function loadAttributeOrElementDef(
164
        Schema $schema,
165
        DOMElement $node,
166
        bool $attributeDef
167
    ) {
168
        $name = $node->getAttribute('name');
169
        if ($attributeDef) {
170
            $attribute = new AttributeDef($schema, $name);
171
            $schema->addAttribute($attribute);
172
        } else {
173
            $attribute = new ElementDef($schema, $name);
174
            $schema->addElement($attribute);
175
        }
176
177
178
        return function () use ($attribute, $node) {
179
            $this->fillItem($attribute, $node);
180
        };
181
    }
182
183
    /**
184
    * @return Closure
185
    */
186
    private function loadAttributeDef(Schema $schema, DOMElement $node)
187
    {
188
        return $this->loadAttributeOrElementDef($schema, $node, true);
189
    }
190
191
    /**
192
     * @param DOMElement $node
193
     * @return string
194
     */
195
    private function getDocumentation(DOMElement $node)
196
    {
197
        $doc = '';
198
        foreach ($node->childNodes as $childNode) {
199
            if ($childNode->localName == "annotation") {
200
                foreach ($childNode->childNodes as $subChildNode) {
201
                    if ($subChildNode->localName == "documentation") {
202
                        $doc .= ($subChildNode->nodeValue);
203
                    }
204
                }
205
            }
206
        }
207
        $doc = preg_replace('/[\t ]+/', ' ', $doc);
208
        return trim($doc);
209
    }
210
211
    private function setSchemaThingsFromNode(
212
        Schema $schema,
213
        DOMElement $node,
214
        Schema $parent = null
215
    ) {
216
        $schema->setDoc($this->getDocumentation($node));
217
218
        if ($node->hasAttribute("targetNamespace")) {
219
            $schema->setTargetNamespace($node->getAttribute("targetNamespace"));
220
        } elseif ($parent) {
221
            $schema->setTargetNamespace($parent->getTargetNamespace());
222
        }
223
        $schema->setElementsQualification($node->getAttribute("elementFormDefault") == "qualified");
224
        $schema->setAttributesQualification($node->getAttribute("attributeFormDefault") == "qualified");
225
        $schema->setDoc($this->getDocumentation($node));
226
    }
227
228
    /**
229
    * @param mixed $schema
230
    *
231
    * @return Closure|null
232
    */
233
    private function maybeCallMethod(
234
        array $methods,
235
        string $key,
236
        DOMNode $childNode,
237
        ...$args
238
    ) {
239
        if ($childNode instanceof DOMElement && isset($methods[$key])) {
240
            $method = $methods[$key];
241
242
            $append = $this->$method(...$args);
243
244
            if ($append instanceof Closure) {
245
                return $append;
246
            }
247
        }
248
    }
249
250
    /**
251
     *
252
     * @param Schema $schema
253
     * @param DOMElement $node
254
     * @param Schema $parent
255
     * @return Closure[]
256
     */
257
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null)
258
    {
259
        $this->setSchemaThingsFromNode($schema, $node, $parent);
260
        $functions = array();
261
262
        static $methods = [
263
            'include' => 'loadImport',
264
            'import' => 'loadImport',
265
            'element' => 'loadElementDef',
266
            'attribute' => 'loadAttributeDef',
267
            'attributeGroup' => 'loadAttributeGroup',
268
            'group' => 'loadGroup',
269
            'complexType' => 'loadComplexType',
270
            'simpleType' => 'loadSimpleType',
271
        ];
272
273
        foreach ($node->childNodes as $childNode) {
274
            $callback = $this->maybeCallMethod(
275
                $methods,
276
                (string) $childNode->localName,
277
                $childNode,
278
                $schema,
279
                $childNode
280
            );
281
282
            if ($callback instanceof Closure) {
283
                $functions[] = $callback;
284
            }
285
        }
286
287
        return $functions;
288
    }
289
290
    /**
291
    * @return Element
292
    */
293
    private function loadElement(Schema $schema, DOMElement $node)
294
    {
295
        $element = new Element($schema, $node->getAttribute("name"));
296
        $element->setDoc($this->getDocumentation($node));
297
298
        $this->fillItem($element, $node);
299
300
        static::maybeSetMax($element, $node);
301
        static::maybeSetMin($element, $node);
302
303
        $xp = new \DOMXPath($node->ownerDocument);
304
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
305
306
        if ($xp->query('ancestor::xs:choice', $node)->length) {
307
            $element->setMin(0);
308
        }
309
310
        if ($node->hasAttribute("nillable")) {
311
            $element->setNil($node->getAttribute("nillable") == "true");
312
        }
313
        if ($node->hasAttribute("form")) {
314
            $element->setQualified($node->getAttribute("form") == "qualified");
315
        }
316
        return $element;
317
    }
318
319
    /**
320
    * @return GroupRef
321
    */
322
    private function loadGroupRef(Group $referenced, DOMElement $node)
323
    {
324
        $ref = new GroupRef($referenced);
325
        $ref->setDoc($this->getDocumentation($node));
326
327
        static::maybeSetMax($ref, $node);
328
        static::maybeSetMin($ref, $node);
329
330
        return $ref;
331
    }
332
333
    /**
334
    * @return ElementRef
335
    */
336
    private function loadElementRef(ElementDef $referenced, DOMElement $node)
337
    {
338
        $ref = new ElementRef($referenced);
339
        $this->setDoc($ref, $node);
340
341
        static::maybeSetMax($ref, $node);
342
        static::maybeSetMin($ref, $node);
343
        if ($node->hasAttribute("nillable")) {
344
            $ref->setNil($node->getAttribute("nillable") == "true");
345
        }
346
        if ($node->hasAttribute("form")) {
347
            $ref->setQualified($node->getAttribute("form") == "qualified");
348
        }
349
350
        return $ref;
351
    }
352
353
    private function setDoc(Item $ref, DOMElement $node)
354
    {
355
        $ref->setDoc($this->getDocumentation($node));
356
    }
357
358
    /**
359
    * @return InterfaceSetMinMax
360
    */
361
    private static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node)
362
    {
363
        if (
364
            $node->hasAttribute("maxOccurs")
365
        ) {
366
            $ref->setMax($node->getAttribute("maxOccurs") == "unbounded" ? -1 : (int)$node->getAttribute("maxOccurs"));
367
        }
368
369
        return $ref;
370
    }
371
372
    /**
373
    * @return InterfaceSetMinMax
374
    */
375
    private static function maybeSetMin(InterfaceSetMinMax $ref, DOMElement $node)
376
    {
377
        if ($node->hasAttribute("minOccurs")) {
378
            $ref->setMin((int) $node->getAttribute("minOccurs"));
379
        }
380
381
        return $ref;
382
    }
383
384
    /**
385
    * @return AttributeRef
386
    */
387
    private function loadAttributeRef(AttributeDef $referencedAttribiute, DOMElement $node)
388
    {
389
        $attribute = new AttributeRef($referencedAttribiute);
390
        $this->setDoc($attribute, $node);
391
392
        if ($node->hasAttribute("nillable")) {
393
            $attribute->setNil($node->getAttribute("nillable") == "true");
394
        }
395
        if ($node->hasAttribute("form")) {
396
            $attribute->setQualified($node->getAttribute("form") == "qualified");
397
        }
398
        if ($node->hasAttribute("use")) {
399
            $attribute->setUse($node->getAttribute("use"));
400
        }
401
        return $attribute;
402
    }
403
404
    /**
405
    * @param int|null $max
406
    */
407
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, $max = null)
408
    {
409
        $max = (
410
            (is_int($max) && (bool) $max) ||
411
            $node->getAttribute("maxOccurs") == "unbounded" ||
412
            $node->getAttribute("maxOccurs") > 1
413
        )
414
            ? 2
415
            : null;
416
417
        foreach ($node->childNodes as $childNode) {
418
            if ($childNode instanceof DOMElement) {
419
                $this->loadSequenceChildNode(
420
                    $elementContainer,
421
                    $node,
422
                    $childNode,
423
                    $max
424
                );
425
            }
426
        }
427
    }
428
429
    /**
430
    * @param int|null $max
431
    */
432
    private function loadSequenceChildNode(
433
        ElementContainer $elementContainer,
434
        DOMElement $node,
435
        DOMElement $childNode,
436
        $max
437
    ) {
438
        if (
439
            in_array(
440
                $childNode->localName,
441
                [
442
                    'choice',
443
                    'sequence',
444
                    'all',
445
                ]
446
            )
447
        ) {
448
            $this->loadSequence($elementContainer, $childNode, $max);
449
        } elseif ($childNode->localName === 'element') {
450
            if ($childNode->hasAttribute("ref")) {
451
                /**
452
                * @var ElementDef $referencedElement
453
                */
454
                $referencedElement = $this->findSomething('findElement', $elementContainer->getSchema(), $node, $childNode->getAttribute("ref"));
455
                $element = $this->loadElementRef($referencedElement, $childNode);
456
            } else {
457
                $element = $this->loadElement($elementContainer->getSchema(), $childNode);
458
            }
459
            if (is_int($max) && (bool) $max) {
460
                $element->setMax($max);
461
            }
462
            $elementContainer->addElement($element);
463
        } elseif ($childNode->localName === 'group') {
464
            $this->addGroupAsElement(
465
                $elementContainer->getSchema(),
466
                $node,
467
                $childNode,
468
                $elementContainer
469
            );
470
        }
471
    }
472
473
    private function addGroupAsElement(
474
        Schema $schema,
475
        DOMElement $node,
476
        DOMElement $childNode,
477
        ElementContainer $elementContainer
478
    ) {
479
        /**
480
        * @var Group $referencedGroup
481
        */
482
        $referencedGroup = $this->findSomething(
483
            'findGroup',
484
            $schema,
485
            $node,
486
            $childNode->getAttribute("ref")
487
        );
488
489
        $group = $this->loadGroupRef($referencedGroup, $childNode);
490
        $elementContainer->addElement($group);
491
    }
492
493
    private function maybeLoadSequenceFromElementContainer(
494
        BaseComplexType $type,
495
        DOMElement $childNode
496
    ) {
497
        if (! ($type instanceof ElementContainer)) {
498
            throw new RuntimeException(
499
                '$type passed to ' .
500
                __FUNCTION__ .
501
                'expected to be an instance of ' .
502
                ElementContainer::class .
503
                ' when child node localName is "group", ' .
504
                get_class($type) .
505
                ' given.'
506
            );
507
        }
508
        $this->loadSequence($type, $childNode);
509
    }
510
511
    /**
512
    * @return Closure
513
    */
514
    private function loadGroup(Schema $schema, DOMElement $node)
515
    {
516
        $group = new Group($schema, $node->getAttribute("name"));
517
        $group->setDoc($this->getDocumentation($node));
518
519
        if ($node->hasAttribute("maxOccurs")) {
520
            /**
521
            * @var GroupRef $group
522
            */
523
            $group = static::maybeSetMax(new GroupRef($group), $node);
524
        }
525
        if ($node->hasAttribute("minOccurs")) {
526
            /**
527
            * @var GroupRef $group
528
            */
529
            $group = static::maybeSetMin(
530
                $group instanceof GroupRef ? $group : new GroupRef($group),
531
                $node
532
            );
533
        }
534
535
        $schema->addGroup($group);
536
537
        static $methods = [
538
            'sequence' => 'loadSequence',
539
            'choice' => 'loadSequence',
540
            'all' => 'loadSequence',
541
        ];
542
543
        return function () use ($group, $node, $methods) {
544
            foreach ($node->childNodes as $childNode) {
545
                $this->maybeCallMethod(
546
                    $methods,
547
                    (string) $childNode->localName,
548
                    $childNode,
549
                    $group,
550
                    $childNode
551
                );
552
            }
553
        };
554
    }
555
556
    /**
557
    * @param Closure|null $callback
558
    *
559
    * @return Closure
560
    */
561
    private function loadComplexType(Schema $schema, DOMElement $node, $callback = null)
562
    {
563
        $isSimple = false;
564
565
        foreach ($node->childNodes as $childNode) {
566
            if ($childNode->localName === "simpleContent") {
567
                $isSimple = true;
568
                break;
569
            }
570
        }
571
572
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute("name")) : new ComplexType($schema, $node->getAttribute("name"));
573
574
        $type->setDoc($this->getDocumentation($node));
575
        if ($node->getAttribute("name")) {
576
            $schema->addType($type);
577
        }
578
579 View Code Duplication
        return function () use ($type, $node, $schema, $callback) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
580
            $this->runCallbackAgainstDOMNodeList(
581
                $type,
582
                $node,
583
                function (
584
                    DOMElement $node,
585
                    DOMElement $childNode
586
                ) use(
587
                    $schema,
588
                    $type
589
                ) {
590
                    $this->loadComplexTypeFromChildNode(
591
                        $type,
592
                        $node,
593
                        $childNode,
594
                        $schema
595
                    );
596
                },
597
                $callback
598
            );
599
        };
600
    }
601
602
    /**
603
    * @param Closure|null $callback
604
    */
605
    private function runCallbackAgainstDOMNodeList(
606
        Type $type,
607
        DOMElement $node,
608
        Closure $againstNodeList,
609
        $callback = null
610
    ) {
611
            $this->fillTypeNode($type, $node, true);
612
613
            foreach ($node->childNodes as $childNode) {
614
                if ($childNode instanceof DOMElement) {
615
                    $againstNodeList(
616
                        $node,
617
                        $childNode
618
                    );
619
                }
620
            }
621
622
            if ($callback) {
623
                call_user_func($callback, $type);
624
            }
625
    }
626
627
    private function loadComplexTypeFromChildNode(
628
        BaseComplexType $type,
629
        DOMElement $node,
630
        DOMElement $childNode,
631
        Schema $schema
632
    ) {
633
        if (
634
            in_array(
635
                $childNode->localName,
636
                [
637
                    'sequence',
638
                    'choice',
639
                    'all',
640
                ]
641
            )
642
        ) {
643
            $this->maybeLoadSequenceFromElementContainer(
644
                $type,
645
                $childNode
646
            );
647
        } elseif ($childNode->localName === 'attribute') {
648
            $attribute = $this->getAttributeFromAttributeOrRef(
649
                $childNode,
650
                $schema,
651
                $node
652
            );
653
654
            $type->addAttribute($attribute);
655
        } elseif (
656
            $childNode->localName === 'group' &&
657
            $type instanceof ComplexType
658
        ) {
659
            $this->addGroupAsElement(
660
                $schema,
661
                $node,
662
                $childNode,
663
                $type
664
            );
665
        } elseif ($childNode->localName === 'attributeGroup') {
666
            AttributeGroup::findSomethingLikeThis(
667
                $this,
668
                $schema,
669
                $node,
670
                $childNode,
671
                $type
672
            );
673
        }
674
    }
675
676
    /**
677
    * @param Closure|null $callback
678
    *
679
    * @return Closure
680
    */
681
    private function loadSimpleType(Schema $schema, DOMElement $node, $callback = null)
682
    {
683
        $type = new SimpleType($schema, $node->getAttribute("name"));
684
        $type->setDoc($this->getDocumentation($node));
685
        if ($node->getAttribute("name")) {
686
            $schema->addType($type);
687
        }
688
689
        static $methods = [
690
            'union' => 'loadUnion',
691
            'list' => 'loadList',
692
        ];
693
694 View Code Duplication
        return function () use ($type, $node, $callback, $methods) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
695
            $this->runCallbackAgainstDOMNodeList(
696
                $type,
697
                $node,
698
                function (
699
                    DOMElement $node,
700
                    DOMElement $childNode
701
                ) use (
702
                    $methods,
703
                    $type
704
                ) {
705
                $this->maybeCallMethod(
706
                    $methods,
707
                    $childNode->localName,
708
                    $childNode,
709
                    $type,
710
                    $childNode
711
                );
712
                },
713
                $callback
714
            );
715
        };
716
    }
717
718
    private function loadList(SimpleType $type, DOMElement $node)
719
    {
720
        if ($node->hasAttribute("itemType")) {
721
            /**
722
            * @var SimpleType $listType
723
            */
724
            $listType = $this->findSomeType($type, $node, 'itemType');
725
            $type->setList($listType);
726
        } else {
727
            $addCallback = function (SimpleType $list) use ($type) {
728
                $type->setList($list);
729
            };
730
731
            $this->loadTypeWithCallbackOnChildNodes(
732
                $type->getSchema(),
733
                $node,
734
                $addCallback
735
            );
736
        }
737
    }
738
739
    private function loadTypeWithCallbackOnChildNodes(
740
        Schema $schema,
741
        DOMNode $node,
742
        Closure $callback
743
    ) {
744
        foreach ($node->childNodes as $childNode) {
745
            $this->loadTypeWithCallback($schema, $childNode, $callback);
746
        }
747
    }
748
749
    private function loadTypeWithCallback(
750
        Schema $schema,
751
        DOMNode $childNode,
752
        Closure $callback
753
    ) {
754
        if (! ($childNode instanceof DOMElement)) {
755
            return;
756
        }
757
        $methods = [
758
            'complexType' => 'loadComplexType',
759
            'simpleType' => 'loadSimpleType',
760
        ];
761
762
        $func = $this->maybeCallMethod(
763
            $methods,
764
            $childNode->localName,
765
            $childNode,
766
            $schema,
767
            $childNode,
768
            $callback
769
        );
770
771
        if ($func instanceof Closure) {
772
            call_user_func($func);
773
        }
774
    }
775
776
    /**
777
    * @return SchemaItem
778
    */
779
    private function findSomeType(
780
        SchemaItem $fromThis,
781
        DOMElement $node,
782
        string $attributeName
783
    ) {
784
        return $this->findSomeTypeFromAttribute(
785
            $fromThis,
786
            $node,
787
            $node->getAttribute($attributeName)
788
        );
789
    }
790
791
    /**
792
    * @return SchemaItem
793
    */
794
    private function findSomeTypeFromAttribute(
795
        SchemaItem $fromThis,
796
        DOMElement $node,
797
        string $attributeName
798
    ) {
799
        /**
800
        * @var SchemaItem $out
801
        */
802
        $out = $this->findSomething(
803
            'findType',
804
            $fromThis->getSchema(),
805
            $node,
806
            $attributeName
807
        );
808
809
        return $out;
810
    }
811
812
    private function loadUnion(SimpleType $type, DOMElement $node)
813
    {
814
        if ($node->hasAttribute("memberTypes")) {
815
            $types = preg_split('/\s+/', $node->getAttribute("memberTypes"));
816
            foreach ($types as $typeName) {
817
                /**
818
                * @var SimpleType $unionType
819
                */
820
                $unionType = $this->findSomeTypeFromAttribute(
821
                    $type,
822
                    $node,
823
                    $typeName
824
                );
825
                $type->addUnion($unionType);
826
            }
827
        }
828
        $addCallback = function (SimpleType $unType) use ($type) {
829
            $type->addUnion($unType);
830
        };
831
832
        $this->loadTypeWithCallbackOnChildNodes(
833
            $type->getSchema(),
834
            $node,
835
            $addCallback
836
        );
837
    }
838
839
    /**
840
    * @param bool $checkAbstract
841
    */
842
    private function fillTypeNode(Type $type, DOMElement $node, $checkAbstract = false)
843
    {
844
845
        if ($checkAbstract) {
846
            $type->setAbstract($node->getAttribute("abstract") === "true" || $node->getAttribute("abstract") === "1");
847
        }
848
849
        static $methods = [
850
            'restriction' => 'loadRestriction',
851
            'extension' => 'maybeLoadExtensionFromBaseComplexType',
852
            'simpleContent' => 'fillTypeNode',
853
            'complexContent' => 'fillTypeNode',
854
        ];
855
856
        foreach ($node->childNodes as $childNode) {
857
            $this->maybeCallMethod(
858
                $methods,
859
                (string) $childNode->localName,
860
                $childNode,
861
                $type,
862
                $childNode
863
            );
864
        }
865
    }
866
867
    private function loadExtension(BaseComplexType $type, DOMElement $node)
868
    {
869
        $extension = new Extension();
870
        $type->setExtension($extension);
871
872
        if ($node->hasAttribute("base")) {
873
            $this->findAndSetSomeBase(
874
                $type,
875
                $extension,
876
                $node
877
            );
878
        }
879
880
        foreach ($node->childNodes as $childNode) {
881
            switch ($childNode->localName) {
882
                case 'sequence':
883
                case 'choice':
884
                case 'all':
885
                    $this->maybeLoadSequenceFromElementContainer(
886
                        $type,
887
                        $childNode
888
                    );
889
                    break;
890
                case 'attribute':
891
                    $attribute = $this->getAttributeFromAttributeOrRef(
892
                        $childNode,
893
                        $type->getSchema(),
894
                        $node
895
                    );
896
                    $type->addAttribute($attribute);
897
                    break;
898
                case 'attributeGroup':
899
                    AttributeGroup::findSomethingLikeThis(
900
                        $this,
901
                        $type->getSchema(),
902
                        $node,
903
                        $childNode,
904
                        $type
905
                    );
906
                    break;
907
            }
908
        }
909
    }
910
911
    private function findAndSetSomeBase(
912
        Type $type,
913
        Base $setBaseOnThis,
914
        DOMElement $node
915
    ) {
916
        /**
917
        * @var Type $parent
918
        */
919
        $parent = $this->findSomeType($type, $node, 'base');
920
        $setBaseOnThis->setBase($parent);
921
    }
922
923
    private function maybeLoadExtensionFromBaseComplexType(
924
        Type $type,
925
        DOMElement $childNode
926
    ) {
927
        if (! ($type instanceof BaseComplexType)) {
928
            throw new RuntimeException(
929
                'Argument 1 passed to ' .
930
                __METHOD__ .
931
                ' needs to be an instance of ' .
932
                BaseComplexType::class .
933
                ' when passed onto ' .
934
                static::class .
935
                '::loadExtension(), ' .
936
                get_class($type) .
937
                ' given.'
938
            );
939
        }
940
        $this->loadExtension($type, $childNode);
941
    }
942
943
    private function loadRestriction(Type $type, DOMElement $node)
944
    {
945
        $restriction = new Restriction();
946
        $type->setRestriction($restriction);
947
        if ($node->hasAttribute("base")) {
948
            $this->findAndSetSomeBase($type, $restriction, $node);
949
        } else {
950
            $addCallback = function (Type $restType) use ($restriction) {
951
                $restriction->setBase($restType);
952
            };
953
954
            $this->loadTypeWithCallbackOnChildNodes(
955
                $type->getSchema(),
956
                $node,
957
                $addCallback
958
            );
959
        }
960
        foreach ($node->childNodes as $childNode) {
961
            if (in_array($childNode->localName,
962
                [
963
                    'enumeration',
964
                    'pattern',
965
                    'length',
966
                    'minLength',
967
                    'maxLength',
968
                    'minInclusive',
969
                    'maxInclusive',
970
                    'minExclusive',
971
                    'maxExclusive',
972
                    'fractionDigits',
973
                    'totalDigits',
974
                    'whiteSpace'
975
                ], true)) {
976
                $restriction->addCheck($childNode->localName,
977
                    [
978
                        'value' => $childNode->getAttribute("value"),
979
                        'doc' => $this->getDocumentation($childNode)
980
                    ]);
981
            }
982
        }
983
    }
984
985
    /**
986
    * @param string $typeName
987
    *
988
    * @return mixed[]
989
    */
990
    private static function splitParts(DOMElement $node, $typeName)
991
    {
992
        $prefix = null;
993
        $name = $typeName;
994
        if (strpos($typeName, ':') !== false) {
995
            list ($prefix, $name) = explode(':', $typeName);
996
        }
997
998
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
999
        return array(
1000
            $name,
1001
            $namespace,
1002
            $prefix
1003
        );
1004
    }
1005
1006
    /**
1007
     *
1008
     * @param string $finder
1009
     * @param Schema $schema
1010
     * @param DOMElement $node
1011
     * @param string $typeName
1012
     * @throws TypeException
1013
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
1014
     */
1015
    public function findSomething($finder, Schema $schema, DOMElement $node, $typeName)
1016
    {
1017
        list ($name, $namespace) = self::splitParts($node, $typeName);
1018
1019
        $namespace = $namespace ?: $schema->getTargetNamespace();
1020
1021
        try {
1022
            return $schema->$finder($name, $namespace);
1023
        } catch (TypeNotFoundException $e) {
1024
            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);
1025
        }
1026
    }
1027
1028
    /**
1029
    * @return Closure
1030
    */
1031
    private function loadElementDef(Schema $schema, DOMElement $node)
1032
    {
1033
        return $this->loadAttributeOrElementDef($schema, $node, false);
1034
    }
1035
1036
    private function fillItem(Item $element, DOMElement $node)
1037
    {
1038
        $localType = null;
1039
        foreach ($node->childNodes as $childNode) {
1040
            switch ($childNode->localName) {
1041
                case 'complexType':
1042
                case 'simpleType':
1043
                    $localType = $childNode;
1044
                    break 2;
1045
            }
1046
        }
1047
1048
        if ($localType) {
1049
            $addCallback = function (Type $type) use ($element) {
1050
                $element->setType($type);
1051
            };
1052
            $this->loadTypeWithCallback(
1053
                $element->getSchema(),
1054
                $localType,
1055
                $addCallback
1056
            );
1057
        } else {
1058
1059
            if ($node->getAttribute("type")) {
1060
                /**
1061
                * @var Type $type
1062
                */
1063
                $type = $this->findSomeType($element, $node, 'type');
1064
            } else {
1065
                /**
1066
                * @var Type $type
1067
                */
1068
                $type = $this->findSomeTypeFromAttribute(
1069
                    $element,
1070
                    $node,
1071
                    ($node->lookupPrefix(self::XSD_NS) . ':anyType')
1072
                );
1073
            }
1074
1075
            $element->setType($type);
1076
        }
1077
    }
1078
1079
    /**
1080
    * @return Closure
1081
    */
1082
    private function loadImport(Schema $schema, DOMElement $node)
1083
    {
1084
        $base = urldecode($node->ownerDocument->documentURI);
1085
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute("schemaLocation"));
1086
        if ($node->hasAttribute("namespace")
1087
            && isset(self::$globalSchemaInfo[$node->getAttribute("namespace")])
1088
            && isset($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]])
1089
        ) {
1090
1091
            $schema->addSchema($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]]);
1092
1093
            return function () {
1094
            };
1095
        } elseif ($node->hasAttribute("namespace")
1096
            && isset($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))])) {
1097
            $schema->addSchema($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))]);
1098
            return function () {
1099
            };
1100
        } elseif (isset($this->loadedFiles[$file])) {
1101
            $schema->addSchema($this->loadedFiles[$file]);
1102
            return function () {
1103
            };
1104
        }
1105
1106
        if (!$node->getAttribute("namespace")) {
1107
            $this->loadedFiles[$file] = $newSchema = $schema;
1108
        } else {
1109
            $this->loadedFiles[$file] = $newSchema = new Schema();
1110
            $newSchema->addSchema($this->getGlobalSchema());
1111
        }
1112
1113
        $xml = $this->getDOM(isset($this->knownLocationSchemas[$file]) ? $this->knownLocationSchemas[$file] : $file);
1114
1115
        $callbacks = $this->schemaNode($newSchema, $xml->documentElement, $schema);
1116
1117
        if ($node->getAttribute("namespace")) {
1118
            $schema->addSchema($newSchema);
1119
        }
1120
1121
1122
        return function () use ($callbacks) {
1123
            foreach ($callbacks as $callback) {
1124
                call_user_func($callback);
1125
            }
1126
        };
1127
    }
1128
1129
    /**
1130
    * @var Schema|null
1131
    */
1132
    private $globalSchema;
1133
1134
    /**
1135
     *
1136
     * @return Schema
1137
     */
1138
    public function getGlobalSchema()
1139
    {
1140
        if (!$this->globalSchema) {
1141
            $callbacks = array();
1142
            $globalSchemas = array();
1143
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1144
                $this->loadedFiles[$uri] = $globalSchemas [$namespace] = $schema = new Schema();
1145
                if ($namespace === self::XSD_NS) {
1146
                    $this->globalSchema = $schema;
1147
                }
1148
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1149
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1150
            }
1151
1152
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anySimpleType"));
1153
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anyType"));
1154
1155
            $globalSchemas[self::XML_NS]->addSchema($globalSchemas[self::XSD_NS], self::XSD_NS);
1156
            $globalSchemas[self::XSD_NS]->addSchema($globalSchemas[self::XML_NS], self::XML_NS);
1157
1158
            foreach ($callbacks as $callback) {
1159
                $callback();
1160
            }
1161
        }
1162
1163
        /**
1164
        * @var Schema $out
1165
        */
1166
        $out = $this->globalSchema;
1167
1168
        return $out;
1169
    }
1170
1171
    /**
1172
     * @param DOMElement $node
1173
     * @param string  $file
1174
     *
1175
     * @return Schema
1176
     */
1177
    public function readNode(DOMElement $node, $file = 'schema.xsd')
1178
    {
1179
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1180
        $this->loadedFiles[$fileKey] = $rootSchema = new Schema();
1181
1182
        $rootSchema->addSchema($this->getGlobalSchema());
1183
        $callbacks = $this->schemaNode($rootSchema, $node);
1184
1185
        foreach ($callbacks as $callback) {
1186
            call_user_func($callback);
1187
        }
1188
1189
        return $rootSchema;
1190
    }
1191
1192
    /**
1193
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1194
     *
1195
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1196
     * file to distinguish between multiple schemas in a single file.
1197
     *
1198
     * @param string $file
1199
     * @param string $targetNamespace
1200
     *
1201
     * @return string
1202
     */
1203
    private function getNamespaceSpecificFileIndex($file, $targetNamespace)
1204
    {
1205
        return $file . '#' . $targetNamespace;
1206
    }
1207
1208
    /**
1209
     * @param string $content
1210
     * @param string $file
1211
     *
1212
     * @return Schema
1213
     *
1214
     * @throws IOException
1215
     */
1216
    public function readString($content, $file = 'schema.xsd')
1217
    {
1218
        $xml = new DOMDocument('1.0', 'UTF-8');
1219
        if (!$xml->loadXML($content)) {
1220
            throw new IOException("Can't load the schema");
1221
        }
1222
        $xml->documentURI = $file;
1223
1224
        return $this->readNode($xml->documentElement, $file);
1225
    }
1226
1227
    /**
1228
     * @param string $file
1229
     *
1230
     * @return Schema
1231
     */
1232
    public function readFile($file)
1233
    {
1234
        $xml = $this->getDOM($file);
1235
        return $this->readNode($xml->documentElement, $file);
1236
    }
1237
1238
    /**
1239
     * @param string $file
1240
     *
1241
     * @return DOMDocument
1242
     *
1243
     * @throws IOException
1244
     */
1245
    private function getDOM($file)
1246
    {
1247
        $xml = new DOMDocument('1.0', 'UTF-8');
1248
        if (!$xml->load($file)) {
1249
            throw new IOException("Can't load the file $file");
1250
        }
1251
        return $xml;
1252
    }
1253
}
1254