Completed
Push — php-7.1 ( d920d5...abf681 )
by SignpostMarv
10:28 queued 02:20
created

SchemaReader::fillTypeNode()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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