Completed
Pull Request — master (#18)
by SignpostMarv
02:45
created

SchemaReader::loadComplexType()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 24
nc 12
nop 3
dl 0
loc 36
ccs 25
cts 25
cp 1
crap 5
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\Group as AttributeGroup;
14
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
15
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
16
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
17
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
18
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
19
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
20
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
21
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
22
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
23
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
24
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
25
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
26
use GoetasWebservices\XML\XSDReader\Schema\Item;
27
use GoetasWebservices\XML\XSDReader\Schema\Schema;
28
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
29
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
30
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
31
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
32
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
33
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
34
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
35
use RuntimeException;
36
37
class SchemaReader
38
{
39
40
    const XSD_NS = "http://www.w3.org/2001/XMLSchema";
41
42
    const XML_NS = "http://www.w3.org/XML/1998/namespace";
43
44
    /**
45
    * @var string[]
46
    */
47
    private $knownLocationSchemas = [
48
        'http://www.w3.org/2001/xml.xsd' => (
49
            __DIR__ . '/Resources/xml.xsd'
50
        ),
51
        'http://www.w3.org/2001/XMLSchema.xsd' => (
52
            __DIR__ . '/Resources/XMLSchema.xsd'
53
        ),
54
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' => (
55
            __DIR__ . '/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd'
56
        ),
57
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' => (
58
            __DIR__ . '/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd'
59
        ),
60
        'https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
61
            __DIR__ . '/Resources/xmldsig-core-schema.xsd'
62
        ),
63
        'http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
64
            __DIR__ . '/Resources/xmldsig-core-schema.xsd'
65
        ),
66
    ];
67
68
    /**
69
    * @var string[]
70
    */
71
    private static $globalSchemaInfo = array(
72
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
73
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd'
74
    );
75
76 162
    public function __construct()
77
    {
78 162
    }
79
80
    /**
81
    * @param string $remote
82
    * @param string $local
83
    */
84
    public function addKnownSchemaLocation($remote, $local)
85
    {
86
        $this->knownLocationSchemas[$remote] = $local;
87
    }
88
89
    /**
90
    * @return Closure
91
    */
92 135
    private function loadAttributeGroup(Schema $schema, DOMElement $node)
93 1
    {
94 135
        return AttributeGroup::loadAttributeGroup($this, $schema, $node);
95
    }
96
97
    /**
98
    * @param bool $attributeDef
99
    *
100
    * @return Closure
101
    */
102 135
    private function loadAttributeOrElementDef(
103
        Schema $schema,
104
        DOMElement $node,
105
        $attributeDef
106
    ) {
107 135
        $name = $node->getAttribute('name');
108 135
        if ($attributeDef) {
109 135
            $attribute = new AttributeDef($schema, $name);
110 135
            $schema->addAttribute($attribute);
111 45
        } else {
112 135
            $attribute = new ElementDef($schema, $name);
113 135
            $schema->addElement($attribute);
114
        }
115
116
117
        return function () use ($attribute, $node) {
118 135
            $this->fillItem($attribute, $node);
119 135
        };
120
    }
121
122
    /**
123
    * @return Closure
124
    */
125 135
    private function loadAttributeDef(Schema $schema, DOMElement $node)
126
    {
127 135
        return $this->loadAttributeOrElementDef($schema, $node, true);
128
    }
129
130
    /**
131
     * @param DOMElement $node
132
     * @return string
133
     */
134 135
    public static function getDocumentation(DOMElement $node)
135
    {
136 135
        $doc = '';
137 135
        foreach ($node->childNodes as $childNode) {
138 135
            if ($childNode->localName == "annotation") {
139 135
                $doc .= static::getDocumentation($childNode);
140 135
            } elseif ($childNode->localName == 'documentation') {
141 135
                $doc .= ($childNode->nodeValue);
142 45
            }
143 45
        }
144 135
        $doc = preg_replace('/[\t ]+/', ' ', $doc);
145 135
        return trim($doc);
146
    }
147
148 135
    private function setSchemaThingsFromNode(
149
        Schema $schema,
150
        DOMElement $node,
151
        Schema $parent = null
152
    ) {
153 135
        $schema->setDoc(static::getDocumentation($node));
154
155 135
        if ($node->hasAttribute("targetNamespace")) {
156 135
            $schema->setTargetNamespace($node->getAttribute("targetNamespace"));
157 45
        } elseif ($parent) {
158
            $schema->setTargetNamespace($parent->getTargetNamespace());
159
        }
160 135
        $schema->setElementsQualification($node->getAttribute("elementFormDefault") == "qualified");
161 135
        $schema->setAttributesQualification($node->getAttribute("attributeFormDefault") == "qualified");
162 135
        $schema->setDoc(static::getDocumentation($node));
163 135
    }
164
165
    /**
166
    * @param string $key
167
    *
168
    * @return Closure|null
169
    */
170 135
    public function maybeCallMethod(
171
        array $methods,
172
        $key,
173
        DOMNode $childNode,
174
        ...$args
175
    ) {
176 135
        if ($childNode instanceof DOMElement && isset($methods[$key])) {
177 135
            $method = $methods[$key];
178
179 135
            $append = $this->$method(...$args);
180
181 135
            if ($append instanceof Closure) {
182 135
                return $append;
183
            }
184 45
        }
185 135
    }
186
187
    /**
188
     *
189
     * @param Schema $schema
190
     * @param DOMElement $node
191
     * @param Schema $parent
192
     * @return Closure[]
193
     */
194 135
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null)
195
    {
196 135
        $this->setSchemaThingsFromNode($schema, $node, $parent);
197 135
        $functions = array();
198
199 90
        static $methods = [
200
            'include' => 'loadImport',
201
            'import' => 'loadImport',
202
            'element' => 'loadElementDef',
203
            'attribute' => 'loadAttributeDef',
204
            'attributeGroup' => 'loadAttributeGroup',
205
            'group' => 'loadGroup',
206
            'complexType' => 'loadComplexType',
207
            'simpleType' => 'loadSimpleType',
208 45
        ];
209
210 135
        foreach ($node->childNodes as $childNode) {
211 135
            $callback = $this->maybeCallMethod(
212 135
                $methods,
213 135
                (string) $childNode->localName,
214 135
                $childNode,
215 135
                $schema,
216 90
                $childNode
217 45
            );
218
219 135
            if ($callback instanceof Closure) {
220 135
                $functions[] = $callback;
221 45
            }
222 45
        }
223
224 135
        return $functions;
225
    }
226
227
    /**
228
    * @return InterfaceSetMinMax
229
    */
230 135
    public static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node)
231
    {
232
        if (
233 135
            $node->hasAttribute("maxOccurs")
234 45
        ) {
235 135
            $ref->setMax($node->getAttribute("maxOccurs") == "unbounded" ? -1 : (int)$node->getAttribute("maxOccurs"));
236 45
        }
237
238 135
        return $ref;
239
    }
240
241
    /**
242
    * @return InterfaceSetMinMax
243
    */
244 135
    public static function maybeSetMin(InterfaceSetMinMax $ref, DOMElement $node)
245
    {
246 135
        if ($node->hasAttribute("minOccurs")) {
247 135
            $ref->setMin((int) $node->getAttribute("minOccurs"));
248 45
        }
249
250 135
        return $ref;
251
    }
252
253
    /**
254
    * @param int|null $max
255
    *
256
    * @return int|null
257
    */
258 135
    private static function loadSequenceNormaliseMax(DOMElement $node, $max)
259
    {
260
        return
261
        (
262 135
            (is_int($max) && (bool) $max) ||
263 135
            $node->getAttribute("maxOccurs") == "unbounded" ||
264 135
            $node->getAttribute("maxOccurs") > 1
265 45
        )
266 90
            ? 2
267 135
            : null;
268
    }
269
270
    /**
271
    * @param int|null $max
272
    */
273 135
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, $max = null)
274
    {
275 135
        $max = static::loadSequenceNormaliseMax($node, $max);
276
277 135
        foreach ($node->childNodes as $childNode) {
278 135
            if ($childNode instanceof DOMElement) {
279 135
                $this->loadSequenceChildNode(
280 135
                    $elementContainer,
281 135
                    $node,
282 135
                    $childNode,
283 90
                    $max
284 45
                );
285 45
            }
286 45
        }
287 135
    }
288
289
    /**
290
    * @param int|null $max
291
    */
292 135
    private function loadSequenceChildNode(
293
        ElementContainer $elementContainer,
294
        DOMElement $node,
295
        DOMElement $childNode,
296
        $max
297
    ) {
298 135
        $loadSeq = $this->makeLoadSequenceChildNodeLoadSequence(
299 135
            $elementContainer,
300 135
            $childNode,
301 90
            $max
302 45
        );
303
        $methods = [
304 135
            'choice' => $loadSeq,
305 135
            'sequence' => $loadSeq,
306 135
            'all' => $loadSeq,
307 135
            'element' => $this->makeLoadSequenceChildNodeLoadElement(
308 135
                $elementContainer,
309 135
                $node,
310 135
                $childNode,
311 90
                $max
312 45
            ),
313 135
            'group' => $this->makeLoadSequenceChildNodeLoadGroup(
314 135
                $elementContainer,
315 135
                $node,
316 90
                $childNode
317 45
            ),
318 45
        ];
319
320 135
        if (isset($methods[$childNode->localName])) {
321 135
            $method = $methods[$childNode->localName];
322 135
            $method();
323 45
        }
324 135
    }
325
326
    /**
327
    * @param int|null $max
328
    *
329
    * @return Closure
330
    */
331 135
    private function makeLoadSequenceChildNodeLoadSequence(
332
        ElementContainer $elementContainer,
333
        DOMElement $childNode,
334
        $max
335
    ) {
336
        return function () use ($elementContainer, $childNode, $max) {
337 135
            $this->loadSequence($elementContainer, $childNode, $max);
338 135
        };
339
    }
340
341
    /**
342
    * @param int|null $max
343
    *
344
    * @return Closure
345
    */
346 135
    private function makeLoadSequenceChildNodeLoadElement(
347
        ElementContainer $elementContainer,
348
        DOMElement $node,
349
        DOMElement $childNode,
350
        $max
351
    ) {
352
        return function () use (
353 135
                $elementContainer,
354 135
                $node,
355 135
                $childNode,
356 135
                $max
357
            ) {
358 135
                if ($childNode->hasAttribute("ref")) {
359
                    /**
360
                    * @var ElementDef $referencedElement
361
                    */
362 135
                    $referencedElement = $this->findSomething('findElement', $elementContainer->getSchema(), $node, $childNode->getAttribute("ref"));
363 135
                    $element = ElementRef::loadElementRef(
364 135
                        $referencedElement,
365 90
                        $childNode
366 45
                    );
367 45
                } else {
368 135
                    $element = Element::loadElement(
369 135
                        $this,
370 135
                        $elementContainer->getSchema(),
371 90
                        $childNode
372 45
                    );
373
                }
374 135
                if (is_int($max) && (bool) $max) {
375 135
                    $element->setMax($max);
376 45
                }
377 135
                $elementContainer->addElement($element);
378 135
        };
379
    }
380
381
    /**
382
    * @return Closure
383
    */
384 135
    private function makeLoadSequenceChildNodeLoadGroup(
385
        ElementContainer $elementContainer,
386
        DOMElement $node,
387
        DOMElement $childNode
388
    ) {
389
        return function () use (
390 135
                $elementContainer,
391 135
                $node,
392 135
                $childNode
393
            ) {
394 135
                $this->addGroupAsElement(
395 135
                    $elementContainer->getSchema(),
396 135
                    $node,
397 135
                    $childNode,
398 90
                    $elementContainer
399 45
                );
400 135
        };
401
    }
402
403 135
    private function addGroupAsElement(
404
        Schema $schema,
405
        DOMElement $node,
406
        DOMElement $childNode,
407
        ElementContainer $elementContainer
408
    ) {
409
        /**
410
        * @var Group $referencedGroup
411
        */
412 135
        $referencedGroup = $this->findSomething(
413 135
            'findGroup',
414 135
            $schema,
415 135
            $node,
416 135
            $childNode->getAttribute("ref")
417 45
        );
418
419 135
        $group = GroupRef::loadGroupRef($referencedGroup, $childNode);
420 135
        $elementContainer->addElement($group);
421 135
    }
422
423 135
    private function maybeLoadSequenceFromElementContainer(
424
        BaseComplexType $type,
425
        DOMElement $childNode
426
    ) {
427 135
        if (! ($type instanceof ElementContainer)) {
428
            throw new RuntimeException(
429
                '$type passed to ' .
430
                __FUNCTION__ .
431
                'expected to be an instance of ' .
432
                ElementContainer::class .
433
                ' when child node localName is "group", ' .
434
                get_class($type) .
435
                ' given.'
436
            );
437
        }
438 135
        $this->loadSequence($type, $childNode);
439 135
    }
440
441
    /**
442
    * @return Closure
443
    */
444 135
    private function loadGroup(Schema $schema, DOMElement $node)
445
    {
446 135
        return Group::loadGroup($this, $schema, $node);
447
    }
448
449
    /**
450
    * @param Closure|null $callback
451
    *
452
    * @return Closure
453
    */
454 135
    private function loadComplexType(Schema $schema, DOMElement $node, $callback = null)
455
    {
456 135
        $isSimple = false;
457
458 135
        foreach ($node->childNodes as $childNode) {
459 135
            if ($childNode->localName === "simpleContent") {
460 6
                $isSimple = true;
461 49
                break;
462
            }
463 45
        }
464
465 135
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute("name")) : new ComplexType($schema, $node->getAttribute("name"));
466
467 135
        $type->setDoc(static::getDocumentation($node));
468 135
        if ($node->getAttribute("name")) {
469 135
            $schema->addType($type);
470 45
        }
471
472 135
        return $this->makeCallbackCallback(
473 135
            $type,
474 135
            $node,
475
                function (
476
                    DOMElement $node,
477
                    DOMElement $childNode
478
                ) use(
479 135
                    $schema,
480 135
                    $type
481
                ) {
482 135
                    $this->loadComplexTypeFromChildNode(
483 135
                        $type,
484 135
                        $node,
485 135
                        $childNode,
486 90
                        $schema
487 45
                    );
488 135
                },
489 90
            $callback
490 45
        );
491
    }
492
493
    /**
494
    * @param Closure|null $callback
495
    *
496
    * @return Closure
497
    */
498 135
    private function makeCallbackCallback(
499
        Type $type,
500
        DOMElement $node,
501
        Closure $callbackCallback,
502
        $callback = null
503
    ) {
504
        return function (
505
        ) use (
506 135
            $type,
507 135
            $node,
508 135
            $callbackCallback,
509 135
            $callback
510
        ) {
511 135
            $this->runCallbackAgainstDOMNodeList(
512 135
                $type,
513 135
                $node,
514 135
                $callbackCallback,
515 90
                $callback
516 45
            );
517 135
        };
518
    }
519
520
    /**
521
    * @param Closure|null $callback
522
    */
523 135
    private function runCallbackAgainstDOMNodeList(
524
        Type $type,
525
        DOMElement $node,
526
        Closure $againstNodeList,
527
        $callback = null
528
    ) {
529 135
        $this->fillTypeNode($type, $node, true);
530
531 135
        foreach ($node->childNodes as $childNode) {
532 135
            if ($childNode instanceof DOMElement) {
533 135
                $againstNodeList(
534 135
                    $node,
535 90
                    $childNode
536 45
                );
537 45
            }
538 45
        }
539
540 135
        if ($callback) {
541 135
            call_user_func($callback, $type);
542 45
        }
543 135
    }
544
545 135
    private function loadComplexTypeFromChildNode(
546
        BaseComplexType $type,
547
        DOMElement $node,
548
        DOMElement $childNode,
549
        Schema $schema
550
    ) {
551
        $maybeLoadSeq = function () use ($type, $childNode) {
552 135
            $this->maybeLoadSequenceFromElementContainer(
553 135
                $type,
554 90
                $childNode
555 45
            );
556 135
        };
557
        $methods = [
558 135
            'sequence' => $maybeLoadSeq,
559 135
            'choice' => $maybeLoadSeq,
560 135
            'all' => $maybeLoadSeq,
561
            'attribute' => function () use (
562 135
                $childNode,
563 135
                $schema,
564 135
                $node,
565 135
                $type
566
            ) {
567 135
                $attribute = Attribute::getAttributeFromAttributeOrRef(
568 135
                    $this,
569 135
                    $childNode,
570 135
                    $schema,
571 90
                    $node
572 45
                );
573
574 135
                $type->addAttribute($attribute);
575 135
            },
576
            'attributeGroup' => function() use (
577 6
                $schema,
578 6
                $node,
579 6
                $childNode,
580 6
                $type
581
            ) {
582 6
                AttributeGroup::findSomethingLikeThis(
583 6
                    $this,
584 6
                    $schema,
585 6
                    $node,
586 6
                    $childNode,
587 4
                    $type
588 2
                );
589 135
            },
590 45
        ];
591
        if (
592 90
            $type instanceof ComplexType
593 45
        ) {
594
            $methods['group'] = function() use (
595 3
                $schema,
596 3
                $node,
597 3
                $childNode,
598 3
                $type
599
            ) {
600 3
                $this->addGroupAsElement(
601 3
                    $schema,
602 3
                    $node,
603 3
                    $childNode,
604 2
                    $type
605 1
                );
606 3
            };
607 45
        }
608
609 135
        if (isset($methods[$childNode->localName])) {
610 135
            $method = $methods[$childNode->localName];
611 135
            $method();
612 45
        }
613 135
    }
614
615
    /**
616
    * @param Closure|null $callback
617
    *
618
    * @return Closure
619
    */
620 135
    private function loadSimpleType(Schema $schema, DOMElement $node, $callback = null)
621
    {
622 135
        $type = new SimpleType($schema, $node->getAttribute("name"));
623 135
        $type->setDoc(static::getDocumentation($node));
624 135
        if ($node->getAttribute("name")) {
625 135
            $schema->addType($type);
626 45
        }
627
628 90
        static $methods = [
629
            'union' => 'loadUnion',
630
            'list' => 'loadList',
631 45
        ];
632
633 135
        return $this->makeCallbackCallback(
634 135
            $type,
635 135
            $node,
636
            function (
637
                DOMElement $node,
638
                DOMElement $childNode
639
            ) use (
640 135
                $methods,
641 135
                $type
642
            ) {
643 135
                $this->maybeCallMethod(
644 135
                    $methods,
645 135
                    $childNode->localName,
646 135
                    $childNode,
647 135
                    $type,
648 90
                    $childNode
649 45
                );
650 135
            },
651 90
            $callback
652 45
        );
653
    }
654
655 135
    private function loadList(SimpleType $type, DOMElement $node)
656
    {
657 135
        if ($node->hasAttribute("itemType")) {
658
            /**
659
            * @var SimpleType $listType
660
            */
661 135
            $listType = $this->findSomeType($type, $node, 'itemType');
662 135
            $type->setList($listType);
663 45
        } else {
664
            $addCallback = function (SimpleType $list) use ($type) {
665 135
                $type->setList($list);
666 135
            };
667
668 135
            Type::loadTypeWithCallbackOnChildNodes(
669 135
                $this,
670 135
                $type->getSchema(),
671 135
                $node,
672 90
                $addCallback
673 45
            );
674
        }
675 135
    }
676
677
    /**
678
    * @param string $attributeName
679
    *
680
    * @return SchemaItem
681
    */
682 135
    private function findSomeType(
683
        SchemaItem $fromThis,
684
        DOMElement $node,
685
        $attributeName
686
    ) {
687 135
        return $this->findSomeTypeFromAttribute(
688 135
            $fromThis,
689 135
            $node,
690 135
            $node->getAttribute($attributeName)
691 45
        );
692
    }
693
694
    /**
695
    * @param string $attributeName
696
    *
697
    * @return SchemaItem
698
    */
699 135
    private function findSomeTypeFromAttribute(
700
        SchemaItem $fromThis,
701
        DOMElement $node,
702
        $attributeName
703
    ) {
704
        /**
705
        * @var SchemaItem $out
706
        */
707 135
        $out = $this->findSomething(
708 135
            'findType',
709 135
            $fromThis->getSchema(),
710 135
            $node,
711 90
            $attributeName
712 45
        );
713
714 135
        return $out;
715
    }
716
717 135
    private function loadUnion(SimpleType $type, DOMElement $node)
718
    {
719 135
        if ($node->hasAttribute("memberTypes")) {
720 135
            $types = preg_split('/\s+/', $node->getAttribute("memberTypes"));
721 135
            foreach ($types as $typeName) {
722
                /**
723
                * @var SimpleType $unionType
724
                */
725 135
                $unionType = $this->findSomeTypeFromAttribute(
726 135
                    $type,
727 135
                    $node,
728 90
                    $typeName
729 45
                );
730 135
                $type->addUnion($unionType);
731 45
            }
732 45
        }
733
        $addCallback = function (SimpleType $unType) use ($type) {
734 135
            $type->addUnion($unType);
735 135
        };
736
737 135
        Type::loadTypeWithCallbackOnChildNodes(
738 135
            $this,
739 135
            $type->getSchema(),
740 135
            $node,
741 90
            $addCallback
742 45
        );
743 135
    }
744
745
    /**
746
    * @param bool $checkAbstract
747
    */
748 135
    private function fillTypeNode(Type $type, DOMElement $node, $checkAbstract = false)
749
    {
750
751 135
        if ($checkAbstract) {
752 135
            $type->setAbstract($node->getAttribute("abstract") === "true" || $node->getAttribute("abstract") === "1");
753 45
        }
754
755 90
        static $methods = [
756
            'restriction' => 'loadRestriction',
757
            'extension' => 'maybeLoadExtensionFromBaseComplexType',
758
            'simpleContent' => 'fillTypeNode',
759
            'complexContent' => 'fillTypeNode',
760 45
        ];
761
762 135
        foreach ($node->childNodes as $childNode) {
763 135
            $this->maybeCallMethod(
764 135
                $methods,
765 135
                (string) $childNode->localName,
766 135
                $childNode,
767 135
                $type,
768 90
                $childNode
769 45
            );
770 45
        }
771 135
    }
772
773 135
    private function loadExtension(BaseComplexType $type, DOMElement $node)
774
    {
775 135
        $extension = new Extension();
776 135
        $type->setExtension($extension);
777
778 135
        if ($node->hasAttribute("base")) {
779 135
            $this->findAndSetSomeBase(
780 135
                $type,
781 135
                $extension,
782 90
                $node
783 45
            );
784 45
        }
785
786
        $seqFromElement = function (DOMElement $childNode) use ($type) {
787 135
            $this->maybeLoadSequenceFromElementContainer(
788 135
                $type,
789 90
                $childNode
790 45
            );
791 135
        };
792
793
        $methods = [
794 135
            'sequence' => $seqFromElement,
795 135
            'choice' => $seqFromElement,
796 135
            'all' => $seqFromElement,
797
            'attribute' => function (
798
                DOMElement $childNode
799
            ) use (
800 135
                $node,
801 135
                $type
802
            ) {
803 135
                $attribute = Attribute::getAttributeFromAttributeOrRef(
804 135
                    $this,
805 135
                    $childNode,
806 135
                    $type->getSchema(),
807 90
                    $node
808 45
                );
809 135
                $type->addAttribute($attribute);
810 135
            },
811
            'attributeGroup' => function (
812
                DOMElement $childNode
813
            ) use (
814 135
                $node,
815 135
                $type
816
            ) {
817 135
                AttributeGroup::findSomethingLikeThis(
818 135
                    $this,
819 135
                    $type->getSchema(),
820 135
                    $node,
821 135
                    $childNode,
822 90
                    $type
823 45
                );
824 135
            },
825 45
        ];
826
827 135
        foreach ($node->childNodes as $childNode) {
828 135
            if (isset($methods[$childNode->localName])) {
829 135
                $method = $methods[$childNode->localName];
830 135
                $method($childNode);
831 45
            }
832 45
        }
833 135
    }
834
835 135
    public function findAndSetSomeBase(
836
        Type $type,
837
        Base $setBaseOnThis,
838
        DOMElement $node
839
    ) {
840
        /**
841
        * @var Type $parent
842
        */
843 135
        $parent = $this->findSomeType($type, $node, 'base');
844 135
        $setBaseOnThis->setBase($parent);
845 135
    }
846
847 135
    private function maybeLoadExtensionFromBaseComplexType(
848
        Type $type,
849
        DOMElement $childNode
850
    ) {
851 135
        if (! ($type instanceof BaseComplexType)) {
852
            throw new RuntimeException(
853
                'Argument 1 passed to ' .
854
                __METHOD__ .
855
                ' needs to be an instance of ' .
856
                BaseComplexType::class .
857
                ' when passed onto ' .
858
                static::class .
859
                '::loadExtension(), ' .
860
                get_class($type) .
861
                ' given.'
862
            );
863
        }
864 135
        $this->loadExtension($type, $childNode);
865 135
    }
866
867 135
    private function loadRestriction(Type $type, DOMElement $node)
868
    {
869 135
        Restriction::loadRestriction($this, $type, $node);
870 135
    }
871
872
    /**
873
    * @param string $typeName
874
    *
875
    * @return mixed[]
876
    */
877 135
    private static function splitParts(DOMElement $node, $typeName)
878
    {
879 135
        $prefix = null;
880 135
        $name = $typeName;
881 135
        if (strpos($typeName, ':') !== false) {
882 135
            list ($prefix, $name) = explode(':', $typeName);
883 45
        }
884
885 135
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
886
        return array(
887 135
            $name,
888 135
            $namespace,
889 90
            $prefix
890 45
        );
891
    }
892
893
    /**
894
     *
895
     * @param string $finder
896
     * @param Schema $schema
897
     * @param DOMElement $node
898
     * @param string $typeName
899
     * @throws TypeException
900
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
901
     */
902 135
    public function findSomething($finder, Schema $schema, DOMElement $node, $typeName)
903
    {
904 135
        list ($name, $namespace) = self::splitParts($node, $typeName);
905
906 135
        $namespace = $namespace ?: $schema->getTargetNamespace();
907
908
        try {
909 135
            return $schema->$finder($name, $namespace);
910
        } catch (TypeNotFoundException $e) {
911
            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);
912
        }
913
    }
914
915
    /**
916
    * @return Closure
917
    */
918 135
    private function loadElementDef(Schema $schema, DOMElement $node)
919
    {
920 135
        return $this->loadAttributeOrElementDef($schema, $node, false);
921
    }
922
923 135
    public function fillItem(Item $element, DOMElement $node)
924
    {
925 135
        foreach ($node->childNodes as $childNode) {
926
            if (
927 135
                in_array(
928 135
                    $childNode->localName,
929
                    [
930 135
                        'complexType',
931 45
                        'simpleType',
932
                    ]
933 45
                )
934 45
            ) {
935 135
                Type::loadTypeWithCallback(
936 135
                    $this,
937 135
                    $element->getSchema(),
938 135
                    $childNode,
939
                    function (Type $type) use ($element) {
940 135
                        $element->setType($type);
941 135
                    }
942 45
                );
943 135
                return;
944
            }
945 45
        }
946
947 135
        $this->fillItemNonLocalType($element, $node);
948 135
    }
949
950 135
    private function fillItemNonLocalType(Item $element, DOMElement $node)
951
    {
952 135
        if ($node->getAttribute("type")) {
953
            /**
954
            * @var Type $type
955
            */
956 135
            $type = $this->findSomeType($element, $node, 'type');
957 45
        } else {
958
            /**
959
            * @var Type $type
960
            */
961 135
            $type = $this->findSomeTypeFromAttribute(
962 135
                $element,
963 135
                $node,
964 135
                ($node->lookupPrefix(self::XSD_NS) . ':anyType')
965 45
            );
966
        }
967
968 135
        $element->setType($type);
969 135
    }
970
971
    /**
972
    * @return Closure
973
    */
974 135
    private function loadImport(Schema $schema, DOMElement $node)
975
    {
976 135
        $base = urldecode($node->ownerDocument->documentURI);
977 135
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute("schemaLocation"));
978
979 135
        $namespace = $node->getAttribute("namespace");
980
981
        if (
982
            (
983 135
                isset(self::$globalSchemaInfo[$namespace]) &&
984 135
                Schema::hasLoadedFile(
985 135
                    $loadedFilesKey = self::$globalSchemaInfo[$namespace]
986 45
                )
987 45
            ) ||
988 9
            Schema::hasLoadedFile(
989 9
                $loadedFilesKey = $this->getNamespaceSpecificFileIndex(
990 9
                    $file,
991 6
                    $namespace
992 3
                )
993 3
            ) ||
994 47
            Schema::hasLoadedFile($loadedFilesKey = $file)
995 45
        ) {
996 135
            $schema->addSchema(Schema::getLoadedFile($loadedFilesKey));
997
998
            return function() {
999 135
            };
1000
        }
1001
1002 3
        return $this->loadImportFresh($schema, $node, $file, $namespace);
1003
    }
1004
1005
    /**
1006
    * @param string $file
1007
    * @param string $namespace
1008
    *
1009
    * @return Closure
1010
    */
1011 3
    private function loadImportFresh(
1012
        Schema $schema,
1013
        DOMElement $node,
1014
        $file,
1015
        $namespace
1016
    ) {
1017 3
        if (! $namespace) {
1018 3
            $newSchema = Schema::setLoadedFile($file, $schema);
1019 1
        } else {
1020
            $newSchema = Schema::setLoadedFile($file, new Schema());
1021
            $newSchema->addSchema($this->getGlobalSchema());
1022
        }
1023
1024 3
        $xml = $this->getDOM(isset($this->knownLocationSchemas[$file]) ? $this->knownLocationSchemas[$file] : $file);
1025
1026 3
        $callbacks = $this->schemaNode($newSchema, $xml->documentElement, $schema);
1027
1028 3
        if ($namespace) {
1029
            $schema->addSchema($newSchema);
1030
        }
1031
1032
1033 3
        return function () use ($callbacks) {
1034 3
            foreach ($callbacks as $callback) {
1035 3
                $callback();
1036 1
            }
1037 3
        };
1038
    }
1039
1040
    /**
1041
    * @var Schema|null
1042
    */
1043
    private $globalSchema;
1044
1045
    /**
1046
     *
1047
     * @return Schema
1048
     */
1049 135
    public function getGlobalSchema()
1050
    {
1051 135
        if (!$this->globalSchema) {
1052 135
            $callbacks = array();
1053 135
            $globalSchemas = array();
1054 135
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1055 135
                Schema::setLoadedFile(
1056 135
                    $uri,
1057 135
                    $globalSchemas[$namespace] = $schema = new Schema()
1058 45
                );
1059 135
                if ($namespace === self::XSD_NS) {
1060 135
                    $this->globalSchema = $schema;
1061 45
                }
1062 135
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1063 135
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1064 45
            }
1065
1066 135
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anySimpleType"));
1067 135
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anyType"));
1068
1069 135
            $globalSchemas[self::XML_NS]->addSchema($globalSchemas[self::XSD_NS], self::XSD_NS);
1070 135
            $globalSchemas[self::XSD_NS]->addSchema($globalSchemas[self::XML_NS], self::XML_NS);
1071
1072 135
            foreach ($callbacks as $callback) {
1073 135
                $callback();
1074 45
            }
1075 45
        }
1076
1077
        /**
1078
        * @var Schema $out
1079
        */
1080 135
        $out = $this->globalSchema;
1081
1082 135
        return $out;
1083
    }
1084
1085
    /**
1086
     * @param DOMElement $node
1087
     * @param string  $file
1088
     *
1089
     * @return Schema
1090
     */
1091 135
    public function readNode(DOMElement $node, $file = 'schema.xsd')
1092
    {
1093 135
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1094 135
        Schema::setLoadedFile($fileKey, $rootSchema = new Schema());
1095
1096 135
        $rootSchema->addSchema($this->getGlobalSchema());
1097 135
        $callbacks = $this->schemaNode($rootSchema, $node);
1098
1099 135
        foreach ($callbacks as $callback) {
1100 117
            call_user_func($callback);
1101 45
        }
1102
1103 135
        return $rootSchema;
1104
    }
1105
1106
    /**
1107
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1108
     *
1109
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1110
     * file to distinguish between multiple schemas in a single file.
1111
     *
1112
     * @param string $file
1113
     * @param string $targetNamespace
1114
     *
1115
     * @return string
1116
     */
1117 135
    private function getNamespaceSpecificFileIndex($file, $targetNamespace)
1118
    {
1119 135
        return $file . '#' . $targetNamespace;
1120
    }
1121
1122
    /**
1123
     * @param string $content
1124
     * @param string $file
1125
     *
1126
     * @return Schema
1127
     *
1128
     * @throws IOException
1129
     */
1130 132
    public function readString($content, $file = 'schema.xsd')
1131
    {
1132 132
        $xml = new DOMDocument('1.0', 'UTF-8');
1133 132
        if (!$xml->loadXML($content)) {
1134
            throw new IOException("Can't load the schema");
1135
        }
1136 132
        $xml->documentURI = $file;
1137
1138 132
        return $this->readNode($xml->documentElement, $file);
1139
    }
1140
1141
    /**
1142
     * @param string $file
1143
     *
1144
     * @return Schema
1145
     */
1146 3
    public function readFile($file)
1147
    {
1148 3
        $xml = $this->getDOM($file);
1149 3
        return $this->readNode($xml->documentElement, $file);
1150
    }
1151
1152
    /**
1153
     * @param string $file
1154
     *
1155
     * @return DOMDocument
1156
     *
1157
     * @throws IOException
1158
     */
1159 135
    private function getDOM($file)
1160
    {
1161 135
        $xml = new DOMDocument('1.0', 'UTF-8');
1162 135
        if (!$xml->load($file)) {
1163
            throw new IOException("Can't load the file $file");
1164
        }
1165 135
        return $xml;
1166
    }
1167
}
1168