Passed
Push — static-analysis ( dd25f9...536d5c )
by SignpostMarv
03:09
created

SchemaReader::getNamespaceSpecificFileIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 2
crap 1
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($this->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
    private function loadAttribute(Schema $schema, DOMElement $node)
142
    {
143 135
        $attribute = new Attribute($schema, $node->getAttribute("name"));
144 135
        $attribute->setDoc($this->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 function getDocumentation(DOMElement $node)
197
    {
198 135
        $doc = '';
199 135
        foreach ($node->childNodes as $childNode) {
200 135
            if ($childNode->localName == "annotation") {
201 135
                $doc .= $this->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($this->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($this->getDocumentation($node));
225 135
    }
226
227
    /**
228
    * @param string $key
229
    *
230
    * @return Closure|null
231
    */
232 135
    private 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($this->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($this->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($this->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($this->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($this->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($this->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
            $this->loadTypeWithCallbackOnChildNodes(
757 135
                $type->getSchema(),
758 135
                $node,
759 90
                $addCallback
760 45
            );
761
        }
762 135
    }
763
764 135
    private function loadTypeWithCallbackOnChildNodes(
765
        Schema $schema,
766
        DOMNode $node,
767
        Closure $callback
768
    ) {
769 135
        foreach ($node->childNodes as $childNode) {
770 135
            $this->loadTypeWithCallback($schema, $childNode, $callback);
771 45
        }
772 135
    }
773
774 135
    private function loadTypeWithCallback(
775
        Schema $schema,
776
        DOMNode $childNode,
777
        Closure $callback
778
    ) {
779 135
        if (! ($childNode instanceof DOMElement)) {
780 135
            return;
781
        }
782
        $methods = [
783 135
            'complexType' => 'loadComplexType',
784 45
            'simpleType' => 'loadSimpleType',
785 45
        ];
786
787 135
        $func = $this->maybeCallMethod(
788 135
            $methods,
789 135
            $childNode->localName,
790 135
            $childNode,
791 135
            $schema,
792 135
            $childNode,
793 90
            $callback
794 45
        );
795
796 135
        if ($func instanceof Closure) {
797 135
            call_user_func($func);
798 45
        }
799 135
    }
800
801
    /**
802
    * @param string $attributeName
803
    *
804
    * @return SchemaItem
805
    */
806 135
    private function findSomeType(
807
        SchemaItem $fromThis,
808
        DOMElement $node,
809
        $attributeName
810
    ) {
811 135
        return $this->findSomeTypeFromAttribute(
812 135
            $fromThis,
813 135
            $node,
814 135
            $node->getAttribute($attributeName)
815 45
        );
816
    }
817
818
    /**
819
    * @param string $attributeName
820
    *
821
    * @return SchemaItem
822
    */
823 135
    private function findSomeTypeFromAttribute(
824
        SchemaItem $fromThis,
825
        DOMElement $node,
826
        $attributeName
827
    ) {
828
        /**
829
        * @var SchemaItem $out
830
        */
831 135
        $out = $this->findSomething(
832 135
            'findType',
833 135
            $fromThis->getSchema(),
834 135
            $node,
835 90
            $attributeName
836 45
        );
837
838 135
        return $out;
839
    }
840
841 135
    private function loadUnion(SimpleType $type, DOMElement $node)
842
    {
843 135
        if ($node->hasAttribute("memberTypes")) {
844 135
            $types = preg_split('/\s+/', $node->getAttribute("memberTypes"));
845 135
            foreach ($types as $typeName) {
846
                /**
847
                * @var SimpleType $unionType
848
                */
849 135
                $unionType = $this->findSomeTypeFromAttribute(
850 135
                    $type,
851 135
                    $node,
852 90
                    $typeName
853 45
                );
854 135
                $type->addUnion($unionType);
855 45
            }
856 45
        }
857 90
        $addCallback = function (SimpleType $unType) use ($type) {
858 135
            $type->addUnion($unType);
859 135
        };
860
861 135
        $this->loadTypeWithCallbackOnChildNodes(
862 135
            $type->getSchema(),
863 135
            $node,
864 90
            $addCallback
865 45
        );
866 135
    }
867
868
    /**
869
    * @param bool $checkAbstract
870
    */
871 135
    private function fillTypeNode(Type $type, DOMElement $node, $checkAbstract = false)
872
    {
873
874 135
        if ($checkAbstract) {
875 135
            $type->setAbstract($node->getAttribute("abstract") === "true" || $node->getAttribute("abstract") === "1");
876 45
        }
877
878 90
        static $methods = [
879
            'restriction' => 'loadRestriction',
880
            'extension' => 'maybeLoadExtensionFromBaseComplexType',
881
            'simpleContent' => 'fillTypeNode',
882
            'complexContent' => 'fillTypeNode',
883 45
        ];
884
885 135
        foreach ($node->childNodes as $childNode) {
886 135
            $this->maybeCallMethod(
887 135
                $methods,
888 135
                (string) $childNode->localName,
889 135
                $childNode,
890 135
                $type,
891 90
                $childNode
892 45
            );
893 45
        }
894 135
    }
895
896 135
    private function loadExtension(BaseComplexType $type, DOMElement $node)
897
    {
898 135
        $extension = new Extension();
899 135
        $type->setExtension($extension);
900
901 135
        if ($node->hasAttribute("base")) {
902 135
            $this->findAndSetSomeBase(
903 135
                $type,
904 135
                $extension,
905 90
                $node
906 45
            );
907 45
        }
908
909 90
        $seqFromElement = function (DOMElement $childNode) use ($type) {
910 135
            $this->maybeLoadSequenceFromElementContainer(
911 135
                $type,
912 90
                $childNode
913 45
            );
914 135
        };
915
916
        $methods = [
917 135
            'sequence' => $seqFromElement,
918 135
            'choice' => $seqFromElement,
919 135
            'all' => $seqFromElement,
920 90
            'attribute' => function (
921
                DOMElement $childNode
922
            ) use (
923 135
                $node,
924 135
                $type
925
            ) {
926 135
                $attribute = $this->getAttributeFromAttributeOrRef(
927 135
                    $childNode,
928 135
                    $type->getSchema(),
929 90
                    $node
930 45
                );
931 135
                $type->addAttribute($attribute);
932 135
            },
933 90
            'attributeGroup' => function (
934
                DOMElement $childNode
935
            ) use (
936 135
                $node,
937 135
                $type
938
            ) {
939 135
                AttributeGroup::findSomethingLikeThis(
940 135
                    $this,
941 135
                    $type->getSchema(),
942 135
                    $node,
943 135
                    $childNode,
944 90
                    $type
945 45
                );
946 135
            },
947 45
        ];
948
949 135
        foreach ($node->childNodes as $childNode) {
950 135
            if (isset($methods[$childNode->localName])) {
951 135
                $method = $methods[$childNode->localName];
952 135
                $method($childNode);
953 45
            }
954 45
        }
955 135
    }
956
957 135
    private function findAndSetSomeBase(
958
        Type $type,
959
        Base $setBaseOnThis,
960
        DOMElement $node
961
    ) {
962
        /**
963
        * @var Type $parent
964
        */
965 135
        $parent = $this->findSomeType($type, $node, 'base');
966 135
        $setBaseOnThis->setBase($parent);
967 135
    }
968
969 135
    private function maybeLoadExtensionFromBaseComplexType(
970
        Type $type,
971
        DOMElement $childNode
972
    ) {
973 135
        if (! ($type instanceof BaseComplexType)) {
974
            throw new RuntimeException(
975
                'Argument 1 passed to ' .
976
                __METHOD__ .
977
                ' needs to be an instance of ' .
978
                BaseComplexType::class .
979
                ' when passed onto ' .
980
                static::class .
981
                '::loadExtension(), ' .
982
                get_class($type) .
983
                ' given.'
984
            );
985
        }
986 135
        $this->loadExtension($type, $childNode);
987 135
    }
988
989 135
    private function loadRestriction(Type $type, DOMElement $node)
990
    {
991 135
        $restriction = new Restriction();
992 135
        $type->setRestriction($restriction);
993 135
        if ($node->hasAttribute("base")) {
994 135
            $this->findAndSetSomeBase($type, $restriction, $node);
995 45
        } else {
996 90
            $addCallback = function (Type $restType) use ($restriction) {
997 135
                $restriction->setBase($restType);
998 135
            };
999
1000 135
            $this->loadTypeWithCallbackOnChildNodes(
1001 135
                $type->getSchema(),
1002 135
                $node,
1003 90
                $addCallback
1004 45
            );
1005
        }
1006 135
        foreach ($node->childNodes as $childNode) {
1007 135
            if (in_array($childNode->localName,
1008
                [
1009 135
                    'enumeration',
1010 45
                    'pattern',
1011 45
                    'length',
1012 45
                    'minLength',
1013 45
                    'maxLength',
1014 45
                    'minInclusive',
1015 45
                    'maxInclusive',
1016 45
                    'minExclusive',
1017 45
                    'maxExclusive',
1018 45
                    'fractionDigits',
1019 45
                    'totalDigits',
1020
                    'whiteSpace'
1021 135
                ], true)) {
1022 135
                $restriction->addCheck($childNode->localName,
1023
                    [
1024 135
                        'value' => $childNode->getAttribute("value"),
1025 135
                        'doc' => $this->getDocumentation($childNode)
1026 45
                    ]);
1027 45
            }
1028 45
        }
1029 135
    }
1030
1031
    /**
1032
    * @param string $typeName
1033
    *
1034
    * @return mixed[]
1035
    */
1036 135
    private static function splitParts(DOMElement $node, $typeName)
1037
    {
1038 135
        $prefix = null;
1039 135
        $name = $typeName;
1040 135
        if (strpos($typeName, ':') !== false) {
1041 135
            list ($prefix, $name) = explode(':', $typeName);
1042 45
        }
1043
1044 135
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
1045
        return array(
1046 135
            $name,
1047 135
            $namespace,
1048 90
            $prefix
1049 45
        );
1050
    }
1051
1052
    /**
1053
     *
1054
     * @param string $finder
1055
     * @param Schema $schema
1056
     * @param DOMElement $node
1057
     * @param string $typeName
1058
     * @throws TypeException
1059
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
1060
     */
1061 135
    public function findSomething($finder, Schema $schema, DOMElement $node, $typeName)
1062
    {
1063 135
        list ($name, $namespace) = self::splitParts($node, $typeName);
1064
1065 135
        $namespace = $namespace ?: $schema->getTargetNamespace();
1066
1067
        try {
1068 135
            return $schema->$finder($name, $namespace);
1069
        } catch (TypeNotFoundException $e) {
1070
            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);
1071
        }
1072
    }
1073
1074
    /**
1075
    * @return Closure
1076
    */
1077 135
    private function loadElementDef(Schema $schema, DOMElement $node)
1078
    {
1079 135
        return $this->loadAttributeOrElementDef($schema, $node, false);
1080
    }
1081
1082 135
    private function fillItem(Item $element, DOMElement $node)
1083
    {
1084 135
        $localType = null;
1085 135
        foreach ($node->childNodes as $childNode) {
1086
            if (
1087 135
                in_array(
1088 135
                    $childNode->localName,
1089
                    [
1090 135
                        'complexType',
1091 45
                        'simpleType',
1092
                    ]
1093 45
                )
1094 45
            ) {
1095 135
                $localType = $childNode;
1096 135
                break;
1097
            }
1098 45
        }
1099
1100 135
        if ($localType) {
1101 90
            $addCallback = function (Type $type) use ($element) {
1102 135
                $element->setType($type);
1103 135
            };
1104 135
            $this->loadTypeWithCallback(
1105 135
                $element->getSchema(),
1106 135
                $localType,
1107 90
                $addCallback
1108 45
            );
1109 45
        } else {
1110 135
            $this->fillItemNonLocalType($element, $node);
1111
        }
1112 135
    }
1113
1114 135
    private function fillItemNonLocalType(Item $element, DOMElement $node)
1115
    {
1116 135
        if ($node->getAttribute("type")) {
1117
            /**
1118
            * @var Type $type
1119
            */
1120 135
            $type = $this->findSomeType($element, $node, 'type');
1121 45
        } else {
1122
            /**
1123
            * @var Type $type
1124
            */
1125 135
            $type = $this->findSomeTypeFromAttribute(
1126 135
                $element,
1127 135
                $node,
1128 135
                ($node->lookupPrefix(self::XSD_NS) . ':anyType')
1129 45
            );
1130
        }
1131
1132 135
        $element->setType($type);
1133 135
    }
1134
1135
    /**
1136
    * @return Closure
1137
    */
1138 135
    private function loadImport(Schema $schema, DOMElement $node)
1139
    {
1140 135
        $base = urldecode($node->ownerDocument->documentURI);
1141 135
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute("schemaLocation"));
1142
1143 135
        $namespace = $node->getAttribute("namespace");
1144
1145
        if (
1146
            isset(
1147 135
                self::$globalSchemaInfo[$namespace],
1148
                $this->loadedFiles[
1149 135
                    $loadedFilesKey = self::$globalSchemaInfo[$namespace]
1150 45
                ]
1151 45
            ) ||
1152
            isset(
1153
                $this->loadedFiles[
1154 9
                    $loadedFilesKey = $this->getNamespaceSpecificFileIndex(
1155 9
                        $file,
1156 6
                        $namespace
1157 3
                    )
1158 3
                ]
1159 3
            ) ||
1160 47
            isset($this->loadedFiles[$loadedFilesKey = $file])
1161 45
        ) {
1162 135
            $schema->addSchema($this->loadedFiles[$loadedFilesKey]);
1163
1164 90
            return function() {
1165 135
            };
1166
        }
1167
1168 3
        return $this->loadImportFresh($schema, $node, $file, $namespace);
1169
    }
1170
1171
    /**
1172
    * @param string $file
1173
    * @param string $namespace
1174
    *
1175
    * @return Closure
1176
    */
1177 3
    private function loadImportFresh(
1178
        Schema $schema,
1179
        DOMElement $node,
1180
        $file,
1181
        $namespace
1182
    ) {
1183 3
        if (! $namespace) {
1184 3
            $this->loadedFiles[$file] = $newSchema = $schema;
1185 1
        } else {
1186
            $this->loadedFiles[$file] = $newSchema = new Schema();
1187
            $newSchema->addSchema($this->getGlobalSchema());
1188
        }
1189
1190 3
        $xml = $this->getDOM(isset($this->knownLocationSchemas[$file]) ? $this->knownLocationSchemas[$file] : $file);
1191
1192 3
        $callbacks = $this->schemaNode($newSchema, $xml->documentElement, $schema);
1193
1194 3
        if ($namespace) {
1195
            $schema->addSchema($newSchema);
1196
        }
1197
1198
1199 3
        return function () use ($callbacks) {
1200 3
            foreach ($callbacks as $callback) {
1201 3
                $callback();
1202 1
            }
1203 3
        };
1204
    }
1205
1206
    /**
1207
    * @var Schema|null
1208
    */
1209
    private $globalSchema;
1210
1211
    /**
1212
     *
1213
     * @return Schema
1214
     */
1215 135
    public function getGlobalSchema()
1216
    {
1217 135
        if (!$this->globalSchema) {
1218 135
            $callbacks = array();
1219 135
            $globalSchemas = array();
1220 135
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1221 135
                $this->loadedFiles[$uri] = $globalSchemas [$namespace] = $schema = new Schema();
1222 135
                if ($namespace === self::XSD_NS) {
1223 135
                    $this->globalSchema = $schema;
1224 45
                }
1225 135
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1226 135
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1227 45
            }
1228
1229 135
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anySimpleType"));
1230 135
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anyType"));
1231
1232 135
            $globalSchemas[self::XML_NS]->addSchema($globalSchemas[self::XSD_NS], self::XSD_NS);
1233 135
            $globalSchemas[self::XSD_NS]->addSchema($globalSchemas[self::XML_NS], self::XML_NS);
1234
1235 135
            foreach ($callbacks as $callback) {
1236 135
                $callback();
1237 45
            }
1238 45
        }
1239
1240
        /**
1241
        * @var Schema $out
1242
        */
1243 135
        $out = $this->globalSchema;
1244
1245 135
        return $out;
1246
    }
1247
1248
    /**
1249
     * @param DOMElement $node
1250
     * @param string  $file
1251
     *
1252
     * @return Schema
1253
     */
1254 135
    public function readNode(DOMElement $node, $file = 'schema.xsd')
1255
    {
1256 135
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1257 135
        $this->loadedFiles[$fileKey] = $rootSchema = new Schema();
1258
1259 135
        $rootSchema->addSchema($this->getGlobalSchema());
1260 135
        $callbacks = $this->schemaNode($rootSchema, $node);
1261
1262 135
        foreach ($callbacks as $callback) {
1263 117
            call_user_func($callback);
1264 45
        }
1265
1266 135
        return $rootSchema;
1267
    }
1268
1269
    /**
1270
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1271
     *
1272
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1273
     * file to distinguish between multiple schemas in a single file.
1274
     *
1275
     * @param string $file
1276
     * @param string $targetNamespace
1277
     *
1278
     * @return string
1279
     */
1280 135
    private function getNamespaceSpecificFileIndex($file, $targetNamespace)
1281
    {
1282 135
        return $file . '#' . $targetNamespace;
1283
    }
1284
1285
    /**
1286
     * @param string $content
1287
     * @param string $file
1288
     *
1289
     * @return Schema
1290
     *
1291
     * @throws IOException
1292
     */
1293 132
    public function readString($content, $file = 'schema.xsd')
1294
    {
1295 132
        $xml = new DOMDocument('1.0', 'UTF-8');
1296 132
        if (!$xml->loadXML($content)) {
1297
            throw new IOException("Can't load the schema");
1298
        }
1299 132
        $xml->documentURI = $file;
1300
1301 132
        return $this->readNode($xml->documentElement, $file);
1302
    }
1303
1304
    /**
1305
     * @param string $file
1306
     *
1307
     * @return Schema
1308
     */
1309 3
    public function readFile($file)
1310
    {
1311 3
        $xml = $this->getDOM($file);
1312 3
        return $this->readNode($xml->documentElement, $file);
1313
    }
1314
1315
    /**
1316
     * @param string $file
1317
     *
1318
     * @return DOMDocument
1319
     *
1320
     * @throws IOException
1321
     */
1322 135
    private function getDOM($file)
1323
    {
1324 135
        $xml = new DOMDocument('1.0', 'UTF-8');
1325 135
        if (!$xml->load($file)) {
1326
            throw new IOException("Can't load the file $file");
1327
        }
1328 135
        return $xml;
1329
    }
1330
}
1331