Passed
Push — static-analysis ( 4c464a...3fedee )
by SignpostMarv
03:12
created

SchemaReader::loadAttribute()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

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