Test Setup Failed
Push — static-analysis ( af1336...1f5c7e )
by SignpostMarv
06:23
created

SchemaReader   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1273
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 145
dl 0
loc 1273
rs 0.6314
c 0
b 0
f 0

51 Methods

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

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