Passed
Push — static-analysis ( cd6f00...4c464a )
by SignpostMarv
03:09
created

SchemaReader::loadTypeWithCallback()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3

Importance

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