Completed
Push — php-7.1 ( f0f10f...d920d5 )
by SignpostMarv
12:17 queued 03:03
created

SchemaReader::maybeLoadRestrictionOnChildNode()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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