Completed
Push — php-7.1 ( bfe0e1...ae7913 )
by SignpostMarv
13:26 queued 03:16
created

SchemaReader::loadTypeWithCallback()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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