Completed
Push — php-7.1 ( 657786...020623 )
by SignpostMarv
11:22 queued 02:43
created

SchemaReader::setSchemaThingsFromNode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 3
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GoetasWebservices\XML\XSDReader;
6
7
use Closure;
8
use DOMDocument;
9
use DOMElement;
10
use DOMNode;
11
use GoetasWebservices\XML\XSDReader\Exception\IOException;
12
use GoetasWebservices\XML\XSDReader\Exception\TypeException;
13
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Attribute;
14 45
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer;
15
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
16
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeDef;
17
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group as AttributeGroup;
18 45
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
19 45
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
20 45
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
21 45
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
22
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
23
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
24 45
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
25
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
26
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
27 45
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
28 45
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
29 45
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
30
use GoetasWebservices\XML\XSDReader\Schema\Item;
31
use GoetasWebservices\XML\XSDReader\Schema\Schema;
32
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
33
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
34
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
35
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
36
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
37
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
38
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
39
use RuntimeException;
40
41
class SchemaReader
42
{
43
    /**
44
     * @return mixed[]
45
     */
46
    private static function splitParts(
47
        DOMElement $node,
48
        string $typeName
49
    ): array {
50
        $prefix = null;
51
        $name = $typeName;
52
        if (strpos($typeName, ':') !== false) {
53
            list($prefix, $name) = explode(':', $typeName);
54
        }
55
56
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
57
58
        return array(
59
            $name,
60
            $namespace,
61
            $prefix,
62
        );
63
    }
64
65
    private function loadAttributeOrElementDef(
66
        Schema $schema,
67
        DOMElement $node,
68
        bool $attributeDef
69
    ): Closure {
70
        $name = $node->getAttribute('name');
71
        if ($attributeDef) {
72
            $attribute = new AttributeDef($schema, $name);
73
            $schema->addAttribute($attribute);
74
        } else {
75
            $attribute = new ElementDef($schema, $name);
76
            $schema->addElement($attribute);
77
        }
78
79
        return function () use ($attribute, $node): void {
80
            $this->fillItem($attribute, $node);
81
        };
82
    }
83
84
    private function loadAttributeDef(
85
        Schema $schema,
86
        DOMElement $node
87
    ): Closure {
88
        return $this->loadAttributeOrElementDef($schema, $node, true);
89
    }
90
91
    private static function loadSequenceNormaliseMax(
92
        DOMElement $node,
93
        ? int $max
94
    ): ? int {
95
        return
96
        (
97
            (is_int($max) && (bool) $max) ||
98
            $node->getAttribute('maxOccurs') == 'unbounded' ||
99
            $node->getAttribute('maxOccurs') > 1
100
        )
101
            ? 2
102
            : null;
103
    }
104
105
    private function loadSequence(
106
        ElementContainer $elementContainer,
107
        DOMElement $node,
108
        int $max = null
109
    ): void {
110
        $max = static::loadSequenceNormaliseMax($node, $max);
111
112
        static::againstDOMNodeList(
113
            $node,
114
            function (
115
                DOMElement $node,
116
                DOMElement $childNode
117
            ) use (
118
                $elementContainer,
119
                $max
120
            ): void {
121
                $this->loadSequenceChildNode(
122
                    $elementContainer,
123
                    $node,
124
                    $childNode,
125
                    $max
126
                );
127
            }
128
        );
129
    }
130
131
    private function loadSequenceChildNode(
132
        ElementContainer $elementContainer,
133
        DOMElement $node,
134
        DOMElement $childNode,
135
        ? int $max
136
    ): void {
137
        $commonMethods = [
138
            [
139
                ['sequence', 'choice', 'all'],
140
                [$this, 'loadSequenceChildNodeLoadSequence'],
141
                [
142
                    $elementContainer,
143
                    $childNode,
144
                    $max,
145
                ],
146
            ],
147
        ];
148
        $methods = [
149
            'element' => [
150
                [$this, 'loadSequenceChildNodeLoadElement'],
151
                [
152
                    $elementContainer,
153
                    $node,
154
                    $childNode,
155
                    $max,
156
                ],
157
            ],
158
            'group' => [
159
                [$this, 'loadSequenceChildNodeLoadGroup'],
160
                [
161
                    $elementContainer,
162
                    $node,
163
                    $childNode,
164
                ],
165
            ],
166
        ];
167
168
        $this->maybeCallCallableWithArgs($childNode, $commonMethods, $methods);
169
    }
170
171
    private function loadSequenceChildNodeLoadSequence(
172
        ElementContainer $elementContainer,
173
        DOMElement $childNode,
174
        ? int $max
175
    ): void {
176
        $this->loadSequence($elementContainer, $childNode, $max);
177
    }
178
179
    private function loadSequenceChildNodeLoadElement(
180
        ElementContainer $elementContainer,
181
        DOMElement $node,
182
        DOMElement $childNode,
183
        ? int $max
184
    ): void {
185
        if ($childNode->hasAttribute('ref')) {
186
            /**
187
             * @var ElementDef $referencedElement
188
             */
189
            $referencedElement = $this->findSomeElementDef(
190
                $elementContainer->getSchema(),
191
                $node,
192
                $childNode->getAttribute('ref')
193
            );
194
            $element = static::loadElementRef(
195
                $referencedElement,
196
                $childNode
197
            );
198
        } else {
199
            $element = $this->loadElement(
200
                $elementContainer->getSchema(),
201
                $childNode
202
            );
203
        }
204
        if ($max > 1) {
205
            /*
206
            * although one might think the typecast is not needed with $max being `? int $max` after passing > 1,
207
            * phpstan@a4f89fa still thinks it's possibly null.
208
            * see https://github.com/phpstan/phpstan/issues/577 for related issue
209
            */
210
            $element->setMax((int) $max);
211
        }
212
        $elementContainer->addElement($element);
213
    }
214
215
    private function loadSequenceChildNodeLoadGroup(
216
        ElementContainer $elementContainer,
217
        DOMElement $node,
218
        DOMElement $childNode
219
    ): void {
220
        $this->addGroupAsElement(
221
            $elementContainer->getSchema(),
222
            $node,
223
            $childNode,
224
            $elementContainer
225
        );
226
    }
227
228
    private function addGroupAsElement(
229
        Schema $schema,
230
        DOMElement $node,
231
        DOMElement $childNode,
232
        ElementContainer $elementContainer
233
    ): void {
234
        /**
235
         * @var Group
236
         */
237
        $referencedGroup = $this->findSomething(
238
            'findGroup',
239
            $schema,
240
            $node,
241
            $childNode->getAttribute('ref')
242
        );
243
244
        $group = $this->loadGroupRef($referencedGroup, $childNode);
245
        $elementContainer->addElement($group);
246
    }
247
248
    private function loadGroup(Schema $schema, DOMElement $node): Closure
249
    {
250
        $group = static::loadGroupBeforeCheckingChildNodes(
251
            $schema,
252
            $node
253
        );
254
        static $methods = [
255
            'sequence' => 'loadSequence',
256
            'choice' => 'loadSequence',
257
            'all' => 'loadSequence',
258
        ];
259
260
        return function () use ($group, $node, $methods): void {
261
            /**
262
             * @var string[]
263
             */
264
            $methods = $methods;
265
            $this->maybeCallMethodAgainstDOMNodeList(
266
                $node,
267
                $group,
268
                $methods
269
            );
270
        };
271
    }
272
273
    private static function loadGroupBeforeCheckingChildNodes(
274
        Schema $schema,
275
        DOMElement $node
276
    ): Group {
277
        $group = new Group($schema, $node->getAttribute('name'));
278
        $group->setDoc(self::getDocumentation($node));
279
280
        if ($node->hasAttribute('maxOccurs')) {
281
            /**
282
             * @var GroupRef
283
             */
284
            $group = self::maybeSetMax(new GroupRef($group), $node);
285
        }
286
        if ($node->hasAttribute('minOccurs')) {
287
            /**
288
             * @var GroupRef
289
             */
290
            $group = self::maybeSetMin(
291
                $group instanceof GroupRef ? $group : new GroupRef($group),
292
                $node
293
            );
294
        }
295
296
        $schema->addGroup($group);
297
298
        return $group;
299
    }
300
301
    private function loadGroupRef(
302
        Group $referenced,
303
        DOMElement $node
304
    ): GroupRef {
305
        $ref = new GroupRef($referenced);
306
        $ref->setDoc(self::getDocumentation($node));
307
308
        self::maybeSetMax($ref, $node);
309
        self::maybeSetMin($ref, $node);
310
311
        return $ref;
312
    }
313
314
    private function loadComplexTypeBeforeCallbackCallback(
315
        Schema $schema,
316
        DOMElement $node
317
    ): BaseComplexType {
318
        /**
319
         * @var bool
320
         */
321
        $isSimple = false;
322
323
        static::againstDOMNodeList(
324
            $node,
325
            function (
326
                DOMElement $node,
327
                DOMElement $childNode
328
            ) use (
329
                &$isSimple
330
            ): void {
331
                if ($isSimple) {
332
                    return;
333
                }
334
                if ($childNode->localName === 'simpleContent') {
335
                    $isSimple = true;
336
                }
337
            }
338
        );
339
340
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute('name')) : new ComplexType($schema, $node->getAttribute('name'));
341
342
        $type->setDoc(static::getDocumentation($node));
343
        if ($node->getAttribute('name')) {
344
            $schema->addType($type);
345
        }
346
347
        return $type;
348
    }
349
350
    private function loadComplexType(
351
        Schema $schema,
352
        DOMElement $node,
353
        Closure $callback = null
354
    ): Closure {
355
        $type = $this->loadComplexTypeBeforeCallbackCallback($schema, $node);
356
357
        return $this->makeCallbackCallback(
358
            $type,
359
            $node,
360
            function (
361
                DOMElement $node,
362
                DOMElement $childNode
363
            ) use (
364
                $schema,
365
                $type
366
            ): void {
367
                $this->loadComplexTypeFromChildNode(
368
                    $type,
369
                    $node,
370
                    $childNode,
371
                    $schema
372
                );
373
            },
374
            $callback
375
        );
376
    }
377
378
    private function loadComplexTypeFromChildNode(
379
        BaseComplexType $type,
380
        DOMElement $node,
381
        DOMElement $childNode,
382
        Schema $schema
383
    ): void {
384
        $commonMethods = [
385
            [
386
                ['sequence', 'choice', 'all'],
387
                [$this, 'maybeLoadSequenceFromElementContainer'],
388
                [
389
                    $type,
390
                    $childNode,
391
                ],
392
            ],
393
        ];
394
        $methods = [
395
            'attribute' => [
396
                [$this, 'addAttributeFromAttributeOrRef'],
397
                [
398
                    $type,
399
                    $childNode,
400
                    $schema,
401
                    $node,
402
                ],
403
            ],
404
            'attributeGroup' => [
405
                [$this, 'findSomethingLikeAttributeGroup'],
406
                [
407
                    $schema,
408
                    $node,
409
                    $childNode,
410
                    $type,
411
                ],
412
            ],
413
        ];
414
        if (
415
            $type instanceof ComplexType
416
        ) {
417
            $methods['group'] = [
418
                [$this, 'addGroupAsElement'],
419
                [
420
                    $schema,
421
                    $node,
422
                    $childNode,
423
                    $type,
424
                ],
425
            ];
426
        }
427
428
        $this->maybeCallCallableWithArgs($childNode, $commonMethods, $methods);
429
    }
430
431
    private function loadSimpleType(
432
        Schema $schema,
433
        DOMElement $node,
434
        Closure $callback = null
435
    ): Closure {
436
        $type = new SimpleType($schema, $node->getAttribute('name'));
437
        $type->setDoc(static::getDocumentation($node));
438
        if ($node->getAttribute('name')) {
439
            $schema->addType($type);
440
        }
441
442
        return $this->makeCallbackCallback(
443
            $type,
444
            $node,
445
            $this->CallbackGeneratorMaybeCallMethodAgainstDOMNodeList(
446
                $type,
447
                [
448
                    'union' => 'loadUnion',
449
                    'list' => 'loadList',
450
                ]
451
            ),
452
            $callback
453
        );
454
    }
455
456
    private function loadList(SimpleType $type, DOMElement $node): void
457
    {
458
        if ($node->hasAttribute('itemType')) {
459
            $listType = $this->findSomeSimpleType($type, $node);
460
            $type->setList($listType);
461
        } else {
462
            $addCallback = function (SimpleType $list) use ($type): void {
463
                $type->setList($list);
464
            };
465
466
            $this->loadTypeWithCallbackOnChildNodes(
467
                $type->getSchema(),
468
                $node,
469
                $addCallback
470
            );
471
        }
472
    }
473
474
    private function loadUnion(SimpleType $type, DOMElement $node): void
475
    {
476
        if ($node->hasAttribute('memberTypes')) {
477
            $types = preg_split('/\s+/', $node->getAttribute('memberTypes'));
478
            foreach ($types as $typeName) {
479
                $unionType = $this->findSomeSimpleTypeFromAttribute(
480
                    $type,
481
                    $node,
482
                    $typeName
483
                );
484
                $type->addUnion($unionType);
485
            }
486
        }
487
        $addCallback = function (SimpleType $unType) use ($type): void {
488
            $type->addUnion($unType);
489
        };
490
491
        $this->loadTypeWithCallbackOnChildNodes(
492
            $type->getSchema(),
493
            $node,
494
            $addCallback
495
        );
496
    }
497
498
    private function loadExtensionChildNodes(
499
        BaseComplexType $type,
500
        DOMElement $node
501
    ): void {
502
        static::againstDOMNodeList(
503
            $node,
504
            function (
505
                DOMElement $node,
506
                DOMElement $childNode
507
            ) use (
508
                $type
509
            ): void {
510
                $commonMethods = [
511
                    [
512
                        ['sequence', 'choice', 'all'],
513
                        [$this, 'maybeLoadSequenceFromElementContainer'],
514
                        [
515
                            $type,
516
                            $childNode,
517
                        ],
518
                    ],
519
                ];
520
                $methods = [
521
                    'attribute' => [
522
                        [$this, 'addAttributeFromAttributeOrRef'],
523
                        [
524
                            $type,
525
                            $childNode,
526
                            $type->getSchema(),
527
                            $node,
528
                        ],
529
                    ],
530
                    'attributeGroup' => [
531
                        [$this, 'findSomethingLikeAttributeGroup'],
532
                        [
533
                            $type->getSchema(),
534
                            $node,
535
                            $childNode,
536
                            $type,
537
                        ],
538
                    ],
539
                ];
540
541
                $this->maybeCallCallableWithArgs(
542
                    $childNode,
543
                    $commonMethods,
544
                    $methods
545
                );
546
            }
547
        );
548
    }
549
550
    private function loadExtension(
551
        BaseComplexType $type,
552
        DOMElement $node
553
    ): void {
554
        $extension = new Extension();
555
        $type->setExtension($extension);
556
557
        if ($node->hasAttribute('base')) {
558
            $this->findAndSetSomeBase(
559
                $type,
560
                $extension,
561
                $node
562
            );
563
        }
564
        $this->loadExtensionChildNodes($type, $node);
565
    }
566
567
    private function loadRestriction(Type $type, DOMElement $node): void
568
    {
569
        $restriction = new Restriction();
570
        $type->setRestriction($restriction);
571
        if ($node->hasAttribute('base')) {
572
            $this->findAndSetSomeBase($type, $restriction, $node);
573
        } else {
574
            $addCallback = function (Type $restType) use (
575
                $restriction
576
            ): void {
577
                $restriction->setBase($restType);
578
            };
579
580
            $this->loadTypeWithCallbackOnChildNodes(
581
                $type->getSchema(),
582
                $node,
583
                $addCallback
584
            );
585
        }
586
        self::againstDOMNodeList(
587
            $node,
588
            function (
589
                DOMElement $node,
590
                DOMElement $childNode
591
            ) use (
592
                $restriction
593
            ): void {
594
                static::maybeLoadRestrictionOnChildNode(
595
                    $restriction,
596
                    $childNode
597
                );
598
            }
599
        );
600
    }
601
602
    private static function maybeLoadRestrictionOnChildNode(
603
        Restriction $restriction,
604
        DOMElement $childNode
605
    ): void {
606
        if (
607
            in_array(
608
                $childNode->localName,
609
                [
610
                    'enumeration',
611
                    'pattern',
612
                    'length',
613
                    'minLength',
614
                    'maxLength',
615
                    'minInclusive',
616
                    'maxInclusive',
617
                    'minExclusive',
618
                    'maxExclusive',
619
                    'fractionDigits',
620
                    'totalDigits',
621
                    'whiteSpace',
622
                ],
623
                true
624
            )
625
        ) {
626
            static::definitelyLoadRestrictionOnChildNode(
627
                $restriction,
628
                $childNode
629
            );
630
        }
631
    }
632
633
    private static function definitelyLoadRestrictionOnChildNode(
634
        Restriction $restriction,
635
        DOMElement $childNode
636
    ): void {
637
        $restriction->addCheck(
638
            $childNode->localName,
639
            [
640
                'value' => $childNode->getAttribute('value'),
641
                'doc' => self::getDocumentation($childNode),
642
            ]
643
        );
644
    }
645
646
    private function loadElementDef(
647
        Schema $schema,
648
        DOMElement $node
649
    ): Closure {
650
        return $this->loadAttributeOrElementDef($schema, $node, false);
651
    }
652
653
    private function fillTypeNode(
654
        Type $type,
655
        DOMElement $node,
656
        bool $checkAbstract = false
657
    ): void {
658
        if ($checkAbstract) {
659
            $type->setAbstract($node->getAttribute('abstract') === 'true' || $node->getAttribute('abstract') === '1');
660
        }
661
        static $methods = [
662
            'restriction' => 'loadRestriction',
663
            'extension' => 'maybeLoadExtensionFromBaseComplexType',
664
            'simpleContent' => 'fillTypeNode',
665
            'complexContent' => 'fillTypeNode',
666
        ];
667
668
        /**
669
         * @var string[]
670
         */
671
        $methods = $methods;
672
673
        $this->maybeCallMethodAgainstDOMNodeList($node, $type, $methods);
674
    }
675
676
    private function fillItemNonLocalType(
677
        Item $element,
678
        DOMElement $node
679
    ): void {
680
        if ($node->getAttribute('type')) {
681
            $type = $this->findSomeTypeType($element, $node, 'type');
682
        } else {
683
            $type = $this->findSomeTypeTypeFromAttribute(
684
                $element,
685
                $node
686
            );
687
        }
688
689
        $element->setType($type);
690
    }
691
692
    private function findSomeType(
693
        SchemaItem $fromThis,
694
        DOMElement $node,
695
        string $attributeName
696
    ): SchemaItem {
697
        return $this->findSomeTypeFromAttribute(
698
            $fromThis,
699
            $node,
700
            $node->getAttribute($attributeName)
701
        );
702
    }
703
704
    protected function findSomeTypeType(SchemaItem $element, DOMElement $node, string $attributeName): Type
705
    {
706
        /**
707
         * @var Type $out
708
         */
709
        $out = $this->findSomeType($element, $node, $attributeName);
710
711
        return $out;
712
    }
713
714
    protected function findSomeTypeTypeFromAttribute(
715
        SchemaItem $element,
716
        DOMElement $node
717
    ): Type {
718
        /**
719
         * @var Type $out
720
         */
721
        $out = $this->findSomeTypeFromAttribute(
722
            $element,
723
            $node,
724
            ($node->lookupPrefix(self::XSD_NS).':anyType')
725
        );
726
727
        return $out;
728
    }
729
730
    protected function findSomeSimpleType(SchemaItem $type, DOMElement $node): SimpleType
731
    {
732
        /**
733
         * @var SimpleType $out
734
         */
735
        $out = $this->findSomeType($type, $node, 'itemType');
736
737
        return $out;
738
    }
739
740
    private function findSomeTypeFromAttribute(
741
        SchemaItem $fromThis,
742
        DOMElement $node,
743
        string $attributeName
744
    ): SchemaItem {
745
        /**
746
         * @var SchemaItem
747
         */
748
        $out = $this->findSomething(
749
            'findType',
750
            $fromThis->getSchema(),
751
            $node,
752
            $attributeName
753
        );
754
755
        return $out;
756
    }
757
758
    protected function findSomeSimpleTypeFromAttribute(
759
        SchemaItem $type,
760
        DOMElement $node,
761
        string $typeName
762
    ): SimpleType {
763
        /**
764
         * @var SimpleType $out
765
         */
766
        $out = $this->findSomeTypeFromAttribute(
767
            $type,
768
            $node,
769
            $typeName
770
        );
771
772
        return $out;
773
    }
774
775
    /**
776
     * @param mixed[][] $commonMethods
777
     * @param mixed[][] $methods
778
     * @param mixed[][] $commonArguments
779
     *
780
     * @return mixed
781
     */
782
    private function maybeCallCallableWithArgs(
783
        DOMElement $childNode,
784
        array $commonMethods = [],
785
        array $methods = [],
786
        array $commonArguments = []
787
    ) {
788
        foreach ($commonMethods as $commonMethodsSpec) {
789
            list($localNames, $callable, $args) = $commonMethodsSpec;
790
791
            /**
792
             * @var string[]
793
             */
794
            $localNames = $localNames;
795
796
            /**
797
             * @var callable
798
             */
799
            $callable = $callable;
800
801
            /**
802
             * @var mixed[]
803
             */
804
            $args = $args;
805
806
            if (in_array($childNode->localName, $localNames)) {
807
                return call_user_func_array($callable, $args);
808
            }
809
        }
810
        foreach ($commonArguments as $commonArgumentSpec) {
811
            /*
812
            * @var mixed[] $commonArgumentSpec
813
            */
814
            list($callables, $args) = $commonArgumentSpec;
815
816
            /**
817
             * @var callable[]
818
             */
819
            $callables = $callables;
820
821
            /**
822
             * @var mixed[]
823
             */
824
            $args = $args;
825
826
            if (isset($callables[$childNode->localName])) {
827
                return call_user_func_array(
828
                    $callables[$childNode->localName],
829
                    $args
830
                );
831
            }
832
        }
833
        if (isset($methods[$childNode->localName])) {
834
            list($callable, $args) = $methods[$childNode->localName];
835
836
            /**
837
             * @var callable
838
             */
839
            $callable = $callable;
840
841
            /**
842
             * @var mixed[]
843
             */
844
            $args = $args;
845
846
            return call_user_func_array($callable, $args);
847
        }
848
    }
849
850
    private function maybeLoadSequenceFromElementContainer(
851
        BaseComplexType $type,
852
        DOMElement $childNode
853
    ): void {
854
        $this->maybeLoadThingFromThing(
855
            $type,
856
            $childNode,
857
            ElementContainer::class,
858
            'loadSequence'
859
        );
860
    }
861
862
    /**
863
     * @param string $instanceof
864
     * @param string $passTo
865
     */
866
    private function maybeLoadThingFromThing(
867
        Type $type,
868
        DOMElement $childNode,
869
        $instanceof,
870
        $passTo
871
    ): void {
872
        if (!is_a($type, $instanceof, true)) {
873
            /**
874
             * @var string
875
             */
876
            $class = static::class;
877
            throw new RuntimeException(
878
                'Argument 1 passed to '.
879
                __METHOD__.
880
                ' needs to be an instance of '.
881
                $instanceof.
882
                ' when passed onto '.
883
                $class.
884
                '::'.
885
                $passTo.
886
                '(), '.
887
                (string) get_class($type).
888
                ' given.'
889
            );
890
        }
891
892
        $this->$passTo($type, $childNode);
893
    }
894
895
    private function makeCallbackCallback(
896
        Type $type,
897
        DOMElement $node,
898
        Closure $callbackCallback,
899
        Closure $callback = null
900
    ): Closure {
901
        return function (
902
        ) use (
903
            $type,
904
            $node,
905
            $callbackCallback,
906
            $callback
907
        ): void {
908
            $this->runCallbackAgainstDOMNodeList(
909
                $type,
910
                $node,
911
                $callbackCallback,
912
                $callback
913
            );
914
        };
915
    }
916
917
    private function runCallbackAgainstDOMNodeList(
918
        Type $type,
919
        DOMElement $node,
920
        Closure $againstNodeList,
921
        Closure $callback = null
922
    ): void {
923
        $this->fillTypeNode($type, $node, true);
924
925
        static::againstDOMNodeList($node, $againstNodeList);
926
927
        if ($callback) {
928
            call_user_func($callback, $type);
929
        }
930
    }
931
932
    private function maybeLoadExtensionFromBaseComplexType(
933
        Type $type,
934
        DOMElement $childNode
935
    ): void {
936
        $this->maybeLoadThingFromThing(
937
            $type,
938
            $childNode,
939
            BaseComplexType::class,
940
            'loadExtension'
941
        );
942
    }
943
944
    const XSD_NS = 'http://www.w3.org/2001/XMLSchema';
945
946
    const XML_NS = 'http://www.w3.org/XML/1998/namespace';
947
948
    /**
949
     * @var string[]
950
     */
951
    protected $knownLocationSchemas = [
952
        'http://www.w3.org/2001/xml.xsd' => (
953
            __DIR__.'/Resources/xml.xsd'
954
        ),
955
        'http://www.w3.org/2001/XMLSchema.xsd' => (
956
            __DIR__.'/Resources/XMLSchema.xsd'
957
        ),
958
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' => (
959
            __DIR__.'/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd'
960
        ),
961
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' => (
962
            __DIR__.'/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd'
963
        ),
964
        'https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
965
            __DIR__.'/Resources/xmldsig-core-schema.xsd'
966
        ),
967
        'http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
968
            __DIR__.'/Resources/xmldsig-core-schema.xsd'
969
        ),
970
    ];
971
972
    /**
973
     * @var string[]
974
     */
975
    protected static $globalSchemaInfo = array(
976
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
977
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd',
978
    );
979
980
    public function addKnownSchemaLocation(
981
        string $remote,
982
        string $local
983
    ): void {
984
        $this->knownLocationSchemas[$remote] = $local;
985
    }
986
987
    private function hasKnownSchemaLocation(string $remote): bool
988
    {
989
        return isset($this->knownLocationSchemas[$remote]);
990
    }
991
992
    private function getKnownSchemaLocation(string $remote): string
993
    {
994
        return $this->knownLocationSchemas[$remote];
995
    }
996
997
    private static function getDocumentation(DOMElement $node): string
998
    {
999
        $doc = '';
1000
        static::againstDOMNodeList(
1001
            $node,
1002
            function (
1003
                DOMElement $node,
1004
                DOMElement $childNode
1005
            ) use (
1006
                &$doc
1007
            ): void {
1008
                if ($childNode->localName == 'annotation') {
1009
                    $doc .= static::getDocumentation($childNode);
1010
                } elseif ($childNode->localName == 'documentation') {
1011
                    $doc .= $childNode->nodeValue;
1012
                }
1013
            }
1014
        );
1015
        $doc = preg_replace('/[\t ]+/', ' ', $doc);
1016
1017
        return trim($doc);
1018
    }
1019
1020
    /**
1021
     * @param mixed    ...$args
1022
     * @param string[] $methods
1023
     */
1024
    private function maybeCallMethod(
1025
        array $methods,
1026
        string $key,
1027
        DOMNode $childNode,
1028
        ...$args
1029
    ): ? Closure {
1030
        if ($childNode instanceof DOMElement && isset($methods[$key])) {
1031
            $method = $methods[$key];
1032
1033
            /**
1034
             * @var Closure|null
1035
             */
1036
            $append = $this->$method(...$args);
1037
1038
            if ($append instanceof Closure) {
1039
                return $append;
1040
            }
1041
        }
1042
1043
        return null;
1044
    }
1045
1046
    /**
1047
     * @return Closure[]
1048
     */
1049
    private function schemaNode(
1050
        Schema $schema,
1051
        DOMElement $node,
1052
        Schema $parent = null
1053
    ): array {
1054
        $this->setSchemaThingsFromNode($schema, $node, $parent);
1055
        $functions = array();
1056
1057
        $thisMethods = [
1058
            'attributeGroup' => [$this, 'loadAttributeGroup'],
1059
            'include' => [$this, 'loadImport'],
1060
            'import' => [$this, 'loadImport'],
1061
            'element' => [$this, 'loadElementDef'],
1062
            'attribute' => [$this, 'loadAttributeDef'],
1063
            'group' => [$this, 'loadGroup'],
1064
            'complexType' => [$this, 'loadComplexType'],
1065
            'simpleType' => [$this, 'loadSimpleType'],
1066
        ];
1067
1068
        static::againstDOMNodeList(
1069
            $node,
1070
            function (
1071
                DOMElement $node,
1072
                DOMElement $childNode
1073
            ) use (
1074
                $schema,
1075
                $thisMethods,
1076
                &$functions
1077
            ): void {
1078
                /**
1079
                 * @var Closure|null
1080
                 */
1081
                $callback = $this->maybeCallCallableWithArgs(
1082
                    $childNode,
1083
                    [],
1084
                    [],
1085
                    [
1086
                        [
1087
                            $thisMethods,
1088
                            [
1089
                                $schema,
1090
                                $childNode,
1091
                            ],
1092
                        ],
1093
                    ]
1094
                );
1095
1096
                if ($callback instanceof Closure) {
1097
                    $functions[] = $callback;
1098
                }
1099
            }
1100
        );
1101
1102
        return $functions;
1103
    }
1104
1105
    private static function maybeSetMax(
1106
        InterfaceSetMinMax $ref,
1107
        DOMElement $node
1108
    ): InterfaceSetMinMax {
1109
        if (
1110
            $node->hasAttribute('maxOccurs')
1111
        ) {
1112
            $ref->setMax($node->getAttribute('maxOccurs') == 'unbounded' ? -1 : (int) $node->getAttribute('maxOccurs'));
1113
        }
1114
1115
        return $ref;
1116
    }
1117
1118
    private static function maybeSetMin(
1119
        InterfaceSetMinMax $ref,
1120
        DOMElement $node
1121
    ): InterfaceSetMinMax {
1122
        if ($node->hasAttribute('minOccurs')) {
1123
            $ref->setMin((int) $node->getAttribute('minOccurs'));
1124
        }
1125
1126
        return $ref;
1127
    }
1128
1129
    private function findAndSetSomeBase(
1130
        Type $type,
1131
        Base $setBaseOnThis,
1132
        DOMElement $node
1133
    ): void {
1134
        $parent = $this->findSomeTypeType($type, $node, 'base');
1135
        $setBaseOnThis->setBase($parent);
1136
    }
1137
1138
    /**
1139
     * @throws TypeException
1140
     *
1141
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
1142
     */
1143
    private function findSomething(
1144
        string $finder,
1145
        Schema $schema,
1146
        DOMElement $node,
1147
        string $typeName
1148
    ) {
1149
        list($name, $namespace) = static::splitParts($node, $typeName);
1150
1151
        /**
1152
         * @var string|null
1153
         */
1154
        $namespace = $namespace ?: $schema->getTargetNamespace();
1155
1156
        try {
1157
            /**
1158
             * @var ElementItem|Group|AttributeItem|AttributeGroup|Type
1159
             */
1160
            $out = $schema->$finder($name, $namespace);
1161
1162
            return $out;
1163
        } catch (TypeNotFoundException $e) {
1164
            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);
1165
        }
1166
    }
1167
1168
    private function findSomeElementDef(Schema $schema, DOMElement $node, string $typeName): ElementDef
1169
    {
1170
        /**
1171
         * @var ElementDef $out
1172
         */
1173
        $out = $this->findSomething('findElement', $schema, $node, $typeName);
1174
1175
        return $out;
1176
    }
1177
1178
    private function fillItem(Item $element, DOMElement $node): void
1179
    {
1180
        /**
1181
         * @var bool
1182
         */
1183
        $skip = false;
1184
        static::againstDOMNodeList(
1185
            $node,
1186
            function (
1187
                DOMElement $node,
1188
                DOMElement $childNode
1189
            ) use (
1190
                $element,
1191
                &$skip
1192
            ): void {
1193
                if (
1194
                    !$skip &&
1195
                    in_array(
1196
                        $childNode->localName,
1197
                        [
1198
                            'complexType',
1199
                            'simpleType',
1200
                        ]
1201
                    )
1202
                ) {
1203
                    $this->loadTypeWithCallback(
1204
                        $element->getSchema(),
1205
                        $childNode,
1206
                        function (Type $type) use ($element): void {
1207
                            $element->setType($type);
1208
                        }
1209
                    );
1210
                    $skip = true;
1211
                }
1212
            }
1213
        );
1214
        if ($skip) {
1215
            return;
1216
        }
1217
        $this->fillItemNonLocalType($element, $node);
1218
    }
1219
1220
    /**
1221
     * @var Schema|null
1222
     */
1223
    protected $globalSchema;
1224
1225
    /**
1226
     * @return Schema[]
1227
     */
1228
    private function setupGlobalSchemas(array &$callbacks): array
1229
    {
1230
        $globalSchemas = array();
1231
        foreach (self::$globalSchemaInfo as $namespace => $uri) {
1232
            self::setLoadedFile(
1233
                $uri,
1234
                $globalSchemas[$namespace] = $schema = new Schema()
1235
            );
1236
            if ($namespace === self::XSD_NS) {
1237
                $this->globalSchema = $schema;
1238
            }
1239
            $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1240
            $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1241
        }
1242
1243
        return $globalSchemas;
1244
    }
1245
1246
    /**
1247
     * @return string[]
1248
     */
1249
    public function getGlobalSchemaInfo(): array
1250
    {
1251
        return self::$globalSchemaInfo;
1252
    }
1253
1254
    private function getGlobalSchema(): Schema
1255
    {
1256
        if (!$this->globalSchema) {
1257
            $callbacks = array();
1258
            $globalSchemas = $this->setupGlobalSchemas($callbacks);
1259
1260
            $globalSchemas[static::XSD_NS]->addType(new SimpleType($globalSchemas[static::XSD_NS], 'anySimpleType'));
1261
            $globalSchemas[static::XSD_NS]->addType(new SimpleType($globalSchemas[static::XSD_NS], 'anyType'));
1262
1263
            $globalSchemas[static::XML_NS]->addSchema(
1264
                $globalSchemas[static::XSD_NS],
1265
                (string) static::XSD_NS
1266
            );
1267
            $globalSchemas[static::XSD_NS]->addSchema(
1268
                $globalSchemas[static::XML_NS],
1269
                (string) static::XML_NS
1270
            );
1271
1272
            /**
1273
             * @var Closure
1274
             */
1275
            foreach ($callbacks as $callback) {
1276
                $callback();
1277
            }
1278
        }
1279
1280
        /**
1281
         * @var Schema
1282
         */
1283
        $out = $this->globalSchema;
1284
1285
        return $out;
1286
    }
1287
1288
    private function readNode(
1289
        DOMElement $node,
1290
        string $file = 'schema.xsd'
1291
    ): Schema {
1292
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1293
        self::setLoadedFile($fileKey, $rootSchema = new Schema());
1294
1295
        $rootSchema->addSchema($this->getGlobalSchema());
1296
        $callbacks = $this->schemaNode($rootSchema, $node);
1297
1298
        foreach ($callbacks as $callback) {
1299
            call_user_func($callback);
1300
        }
1301
1302
        return $rootSchema;
1303
    }
1304
1305
    /**
1306
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1307
     *
1308
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1309
     * file to distinguish between multiple schemas in a single file.
1310
     */
1311
    private function getNamespaceSpecificFileIndex(
1312
        string $file,
1313
        string $targetNamespace
1314
    ): string {
1315
        return $file.'#'.$targetNamespace;
1316
    }
1317
1318
    /**
1319
     * @throws IOException
1320
     */
1321
    public function readString(
1322
        string $content,
1323
        string $file = 'schema.xsd'
1324
    ): Schema {
1325
        $xml = new DOMDocument('1.0', 'UTF-8');
1326
        if (!$xml->loadXML($content)) {
1327
            throw new IOException("Can't load the schema");
1328
        }
1329
        $xml->documentURI = $file;
1330
1331
        return $this->readNode($xml->documentElement, $file);
1332
    }
1333
1334
    public function readFile(string $file): Schema
1335
    {
1336
        $xml = $this->getDOM($file);
1337
1338
        return $this->readNode($xml->documentElement, $file);
1339
    }
1340
1341
    /**
1342
     * @throws IOException
1343
     */
1344
    private function getDOM(string $file): DOMDocument
1345
    {
1346
        $xml = new DOMDocument('1.0', 'UTF-8');
1347
        if (!$xml->load($file)) {
1348
            throw new IOException("Can't load the file $file");
1349
        }
1350
1351
        return $xml;
1352
    }
1353
1354
    private static function againstDOMNodeList(
1355
        DOMElement $node,
1356
        Closure $againstNodeList
1357
    ): void {
1358
        $limit = $node->childNodes->length;
1359
        for ($i = 0; $i < $limit; $i += 1) {
1360
            /**
1361
             * @var DOMNode
1362
             */
1363
            $childNode = $node->childNodes->item($i);
1364
1365
            if ($childNode instanceof DOMElement) {
1366
                $againstNodeList(
1367
                    $node,
1368
                    $childNode
1369
                );
1370
            }
1371
        }
1372
    }
1373
1374
    private function maybeCallMethodAgainstDOMNodeList(
1375
        DOMElement $node,
1376
        SchemaItem $type,
1377
        array $methods
1378
    ): void {
1379
        static::againstDOMNodeList(
1380
            $node,
1381
            $this->CallbackGeneratorMaybeCallMethodAgainstDOMNodeList(
1382
                $type,
1383
                $methods
1384
            )
1385
        );
1386
    }
1387
1388
    /**
1389
     * @return Closure
1390
     */
1391
    private function CallbackGeneratorMaybeCallMethodAgainstDOMNodeList(
1392
        SchemaItem $type,
1393
        array $methods
1394
    ) {
1395
        return function (
1396
            DOMElement $node,
1397
            DOMElement $childNode
1398
        ) use (
1399
            $methods,
1400
            $type
1401
        ): void {
1402
            /**
1403
             * @var string[]
1404
             */
1405
            $methods = $methods;
1406
1407
            $this->maybeCallMethod(
1408
                $methods,
1409
                $childNode->localName,
1410
                $childNode,
1411
                $type,
1412
                $childNode
1413
            );
1414
        };
1415
    }
1416
1417
    private function loadTypeWithCallbackOnChildNodes(
1418
        Schema $schema,
1419
        DOMElement $node,
1420
        Closure $callback
1421
    ): void {
1422
        self::againstDOMNodeList(
1423
            $node,
1424
            function (
1425
                DOMElement $node,
1426
                DOMElement $childNode
1427
            ) use (
1428
                $schema,
1429
                $callback
1430
            ): void {
1431
                $this->loadTypeWithCallback(
1432
                    $schema,
1433
                    $childNode,
1434
                    $callback
1435
                );
1436
            }
1437
        );
1438
    }
1439
1440
    private function loadTypeWithCallback(
1441
        Schema $schema,
1442
        DOMElement $childNode,
1443
        Closure $callback
1444
    ): void {
1445
        $methods = [
1446
            'complexType' => 'loadComplexType',
1447
            'simpleType' => 'loadSimpleType',
1448
        ];
1449
1450
        /**
1451
         * @var Closure|null
1452
         */
1453
        $func = $this->maybeCallMethod(
1454
            $methods,
1455
            $childNode->localName,
1456
            $childNode,
1457
            $schema,
1458
            $childNode,
1459
            $callback
1460
        );
1461
1462
        if ($func instanceof Closure) {
1463
            call_user_func($func);
1464
        }
1465
    }
1466
1467
    private function loadImport(
1468
        Schema $schema,
1469
        DOMElement $node
1470
    ): Closure {
1471
        $base = urldecode($node->ownerDocument->documentURI);
1472
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute('schemaLocation'));
1473
1474
        $namespace = $node->getAttribute('namespace');
1475
1476
        $keys = $this->loadImportFreshKeys($namespace, $file);
1477
1478
        if (
1479
            self::hasLoadedFile(...$keys)
1480
        ) {
1481
            $schema->addSchema(self::getLoadedFile(...$keys));
1482
1483
            return function (): void {
1484
            };
1485
        }
1486
1487
        return $this->loadImportFresh($namespace, $schema, $file);
1488
    }
1489
1490
    private function loadImportFreshKeys(
1491
        string $namespace,
1492
        string $file
1493
    ): array {
1494
        $globalSchemaInfo = $this->getGlobalSchemaInfo();
1495
1496
        $keys = [];
1497
1498
        if (isset($globalSchemaInfo[$namespace])) {
1499
            $keys[] = $globalSchemaInfo[$namespace];
1500
        }
1501
1502
        $keys[] = $this->getNamespaceSpecificFileIndex(
1503
            $file,
1504
            $namespace
1505
        );
1506
1507
        $keys[] = $file;
1508
1509
        return $keys;
1510
    }
1511
1512
    private function loadImportFreshCallbacksNewSchema(
1513
        string $namespace,
1514
        Schema $schema,
1515
        string $file
1516
    ): Schema {
1517
        /**
1518
         * @var Schema $newSchema
1519
         */
1520
        $newSchema = self::setLoadedFile(
1521
            $file,
1522
            ($namespace ? new Schema() : $schema)
1523
        );
1524
1525
        if ($namespace) {
1526
            $newSchema->addSchema($this->getGlobalSchema());
1527
            $schema->addSchema($newSchema);
1528
        }
1529
1530
        return $newSchema;
1531
    }
1532
1533
    /**
1534
     * @return Closure[]
1535
     */
1536
    private function loadImportFreshCallbacks(
1537
        string $namespace,
1538
        Schema $schema,
1539
        string $file
1540
    ): array {
1541
        /**
1542
         * @var string
1543
         */
1544
        $file = $file;
1545
1546
        return $this->schemaNode(
1547
            $this->loadImportFreshCallbacksNewSchema(
1548
                $namespace,
1549
                $schema,
1550
                $file
1551
            ),
1552
            $this->getDOM(
1553
                $this->hasKnownSchemaLocation($file)
1554
                    ? $this->getKnownSchemaLocation($file)
1555
                    : $file
1556
            )->documentElement,
1557
            $schema
1558
        );
1559
    }
1560
1561
    private function loadImportFresh(
1562
        string $namespace,
1563
        Schema $schema,
1564
        string $file
1565
    ): Closure {
1566
        return function () use ($namespace, $schema, $file): void {
1567
            foreach (
1568
                $this->loadImportFreshCallbacks(
1569
                    $namespace,
1570
                    $schema,
1571
                    $file
1572
                ) as $callback
1573
            ) {
1574
                $callback();
1575
            }
1576
        };
1577
    }
1578
1579
    private function loadElement(
1580
        Schema $schema,
1581
        DOMElement $node
1582
    ): Element {
1583
        $element = new Element($schema, $node->getAttribute('name'));
1584
        $element->setDoc(self::getDocumentation($node));
1585
1586
        $this->fillItem($element, $node);
1587
1588
        self::maybeSetMax($element, $node);
1589
        self::maybeSetMin($element, $node);
1590
1591
        $xp = new \DOMXPath($node->ownerDocument);
1592
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
1593
1594
        if ($xp->query('ancestor::xs:choice', $node)->length) {
1595
            $element->setMin(0);
1596
        }
1597
1598
        if ($node->hasAttribute('nillable')) {
1599
            $element->setNil($node->getAttribute('nillable') == 'true');
1600
        }
1601
        if ($node->hasAttribute('form')) {
1602
            $element->setQualified($node->getAttribute('form') == 'qualified');
1603
        }
1604
1605
        return $element;
1606
    }
1607
1608
    private static function loadElementRef(
1609
        ElementDef $referenced,
1610
        DOMElement $node
1611
    ): ElementRef {
1612
        $ref = new ElementRef($referenced);
1613
        $ref->setDoc(self::getDocumentation($node));
1614
1615
        self::maybeSetMax($ref, $node);
1616
        self::maybeSetMin($ref, $node);
1617
        if ($node->hasAttribute('nillable')) {
1618
            $ref->setNil($node->getAttribute('nillable') == 'true');
1619
        }
1620
        if ($node->hasAttribute('form')) {
1621
            $ref->setQualified($node->getAttribute('form') == 'qualified');
1622
        }
1623
1624
        return $ref;
1625
    }
1626
1627
    private function loadAttributeGroup(
1628
        Schema $schema,
1629
        DOMElement $node
1630
    ): Closure {
1631
        $attGroup = new AttributeGroup($schema, $node->getAttribute('name'));
1632
        $attGroup->setDoc(self::getDocumentation($node));
1633
        $schema->addAttributeGroup($attGroup);
1634
1635
        return function () use ($schema, $node, $attGroup): void {
1636
            SchemaReader::againstDOMNodeList(
1637
                $node,
1638
                function (
1639
                    DOMElement $node,
1640
                    DOMElement $childNode
1641
                ) use (
1642
                    $schema,
1643
                    $attGroup
1644
                ): void {
1645
                    switch ($childNode->localName) {
1646
                        case 'attribute':
1647
                            $attribute = $this->getAttributeFromAttributeOrRef(
1648
                                $childNode,
1649
                                $schema,
1650
                                $node
1651
                            );
1652
                            $attGroup->addAttribute($attribute);
1653
                            break;
1654
                        case 'attributeGroup':
1655
                            $this->findSomethingLikeAttributeGroup(
1656
                                $schema,
1657
                                $node,
1658
                                $childNode,
1659
                                $attGroup
1660
                            );
1661
                            break;
1662
                    }
1663
                }
1664
            );
1665
        };
1666
    }
1667
1668
    private function getAttributeFromAttributeOrRef(
1669
        DOMElement $childNode,
1670
        Schema $schema,
1671
        DOMElement $node
1672
    ): AttributeItem {
1673
        if ($childNode->hasAttribute('ref')) {
1674
            /**
1675
             * @var AttributeItem
1676
             */
1677
            $attribute = $this->findSomething('findAttribute', $schema, $node, $childNode->getAttribute('ref'));
1678
        } else {
1679
            /**
1680
             * @var Attribute
1681
             */
1682
            $attribute = $this->loadAttribute($schema, $childNode);
1683
        }
1684
1685
        return $attribute;
1686
    }
1687
1688
    private function loadAttribute(
1689
        Schema $schema,
1690
        DOMElement $node
1691
    ): Attribute {
1692
        $attribute = new Attribute($schema, $node->getAttribute('name'));
1693
        $attribute->setDoc(self::getDocumentation($node));
1694
        $this->fillItem($attribute, $node);
1695
1696
        if ($node->hasAttribute('nillable')) {
1697
            $attribute->setNil($node->getAttribute('nillable') == 'true');
1698
        }
1699
        if ($node->hasAttribute('form')) {
1700
            $attribute->setQualified($node->getAttribute('form') == 'qualified');
1701
        }
1702
        if ($node->hasAttribute('use')) {
1703
            $attribute->setUse($node->getAttribute('use'));
1704
        }
1705
1706
        return $attribute;
1707
    }
1708
1709
    private function addAttributeFromAttributeOrRef(
1710
        BaseComplexType $type,
1711
        DOMElement $childNode,
1712
        Schema $schema,
1713
        DOMElement $node
1714
    ): void {
1715
        $attribute = $this->getAttributeFromAttributeOrRef(
1716
            $childNode,
1717
            $schema,
1718
            $node
1719
        );
1720
1721
        $type->addAttribute($attribute);
1722
    }
1723
1724
    private function findSomethingLikeAttributeGroup(
1725
        Schema $schema,
1726
        DOMElement $node,
1727
        DOMElement $childNode,
1728
        AttributeContainer $addToThis
1729
    ): void {
1730
        /**
1731
         * @var AttributeItem
1732
         */
1733
        $attribute = $this->findSomething('findAttributeGroup', $schema, $node, $childNode->getAttribute('ref'));
1734
        $addToThis->addAttribute($attribute);
1735
    }
1736
1737
    /**
1738
     * @var Schema[]
1739
     */
1740
    protected static $loadedFiles = array();
1741
1742
    private static function hasLoadedFile(string ...$keys): bool
1743
    {
1744
        foreach ($keys as $key) {
1745
            if (isset(self::$loadedFiles[$key])) {
1746
                return true;
1747
            }
1748
        }
1749
1750
        return false;
1751
    }
1752
1753
    /**
1754
     * @throws RuntimeException if loaded file not found
1755
     */
1756
    private static function getLoadedFile(string ...$keys): Schema
1757
    {
1758
        foreach ($keys as $key) {
1759
            if (isset(self::$loadedFiles[$key])) {
1760
                return self::$loadedFiles[$key];
1761
            }
1762
        }
1763
1764
        throw new RuntimeException('Loaded file was not found!');
1765
    }
1766
1767
    private static function setLoadedFile(string $key, Schema $schema): Schema
1768
    {
1769
        self::$loadedFiles[$key] = $schema;
1770
1771
        return $schema;
1772
    }
1773
1774
    private function setSchemaThingsFromNode(
1775
        Schema $schema,
1776
        DOMElement $node,
1777
        Schema $parent = null
1778
    ): void {
1779
        $schema->setDoc(self::getDocumentation($node));
1780
1781
        if ($node->hasAttribute('targetNamespace')) {
1782
            $schema->setTargetNamespace($node->getAttribute('targetNamespace'));
1783
        } elseif ($parent) {
1784
            $schema->setTargetNamespace($parent->getTargetNamespace());
1785
        }
1786
        $schema->setElementsQualification($node->getAttribute('elementFormDefault') == 'qualified');
1787
        $schema->setAttributesQualification($node->getAttribute('attributeFormDefault') == 'qualified');
1788
        $schema->setDoc(self::getDocumentation($node));
1789
    }
1790
}
1791