Passed
Push — static-analysis ( 3fedee...5a0c71 )
by SignpostMarv
03:09
created

SchemaReader   F

Complexity

Total Complexity 130

Size/Duplication

Total Lines 1185
Duplicated Lines 0 %

Test Coverage

Coverage 93.73%

Importance

Changes 0
Metric Value
dl 0
loc 1185
ccs 613
cts 654
cp 0.9373
rs 0.6314
c 0
b 0
f 0
wmc 130

48 Methods

Rating   Name   Duplication   Size   Complexity  
A addKnownSchemaLocation() 0 3 1
A __construct() 0 8 1
A readFile() 0 4 1
A runCallbackAgainstDOMNodeList() 0 19 4
A addGroupAsElement() 0 18 1
A maybeCallMethod() 0 13 4
B getGlobalSchema() 0 31 5
B loadGroup() 0 37 5
A loadAttributeDef() 0 3 1
B loadElement() 0 24 4
B loadUnion() 0 25 3
A getDocumentation() 0 12 4
A makeCallbackCallback() 0 18 1
B loadImport() 0 31 4
B loadComplexType() 0 36 5
B loadSequenceNormaliseMax() 0 10 5
A fillTypeNode() 0 21 4
B loadImportFresh() 0 25 5
A findSomeType() 0 9 1
B loadSimpleType() 0 32 2
A maybeSetMin() 0 7 2
B loadRestriction() 0 38 4
B loadComplexTypeFromChildNode() 0 46 6
B loadSequenceChildNode() 0 50 5
A readNode() 0 13 3
A getNamespaceSpecificFileIndex() 0 3 1
A loadElementRef() 0 15 3
A loadGroupRef() 0 9 1
A findAndSetSomeBase() 0 10 1
B fillItem() 0 25 3
A maybeSetMax() 0 9 3
A loadSequence() 0 11 3
A splitParts() 0 13 3
A loadList() 0 18 2
A findSomeTypeFromAttribute() 0 16 1
A loadElementDef() 0 3 1
A findSomething() 0 10 3
A readString() 0 9 2
A fillItemNonLocalType() 0 19 2
A loadAttributeGroup() 0 3 1
A setSchemaThingsFromNode() 0 15 3
A loadExtension() 0 58 4
A setDoc() 0 3 1
A loadAttributeOrElementDef() 0 17 2
A maybeLoadSequenceFromElementContainer() 0 16 2
B schemaNode() 0 31 3
A getDOM() 0 7 2
A maybeLoadExtensionFromBaseComplexType() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like SchemaReader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaReader, and based on these observations, apply Extract Interface, too.

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