Completed
Push — php-7.1 ( abf681...2da64d )
by SignpostMarv
10:42 queued 04:08
created

SchemaReader::maybeCallMethod()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 4
dl 0
loc 20
ccs 0
cts 0
cp 0
crap 20
rs 9.2
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
            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
     * @return Closure[]
839
     */
840
    private function schemaNode(
841
        Schema $schema,
842
        DOMElement $node,
843
        Schema $parent = null
844
    ): array {
845
        $this->setSchemaThingsFromNode($schema, $node, $parent);
846
        $functions = array();
847
848
        static::againstDOMNodeList(
849
            $node,
850
            function (
851
                DOMElement $node,
852
                DOMElement $childNode
853
            ) use (
854
                $schema,
855
                &$functions
856
            ): void {
857
                $callback = null;
858
859
                switch ($childNode->localName) {
860
                    case 'attributeGroup':
861
                        $callback = $this->loadAttributeGroup($schema, $childNode);
862
                        break;
863
                    case 'include':
864
                    case 'import':
865
                        $callback = $this->loadImport($schema, $childNode);
866
                        break;
867
                    case 'element':
868
                        $callback = $this->loadElementDef($schema, $childNode);
869
                        break;
870
                    case 'attribute':
871
                        $callback = $this->loadAttributeDef($schema, $childNode);
872
                        break;
873
                    case 'group':
874
                        $callback = $this->loadGroup($schema, $childNode);
875
                        break;
876
                    case 'complexType':
877
                        $callback = $this->loadComplexType($schema, $childNode);
878
                        break;
879
                    case 'simpleType':
880
                        $callback = $this->loadSimpleType($schema, $childNode);
881
                        break;
882
                }
883
884
                if ($callback instanceof Closure) {
885
                    $functions[] = $callback;
886
                }
887
            }
888
        );
889
890
        return $functions;
891
    }
892
893
    private static function maybeSetMax(
894
        InterfaceSetMinMax $ref,
895
        DOMElement $node
896
    ): InterfaceSetMinMax {
897
        if (
898
            $node->hasAttribute('maxOccurs')
899
        ) {
900
            $ref->setMax($node->getAttribute('maxOccurs') == 'unbounded' ? -1 : (int) $node->getAttribute('maxOccurs'));
901
        }
902
903
        return $ref;
904
    }
905
906
    private static function maybeSetMin(
907
        InterfaceSetMinMax $ref,
908
        DOMElement $node
909
    ): InterfaceSetMinMax {
910
        if ($node->hasAttribute('minOccurs')) {
911
            $ref->setMin((int) $node->getAttribute('minOccurs'));
912
        }
913
914
        return $ref;
915
    }
916
917
    private function findAndSetSomeBase(
918
        Type $type,
919
        Base $setBaseOnThis,
920
        DOMElement $node
921
    ): void {
922
        $parent = $this->findSomeTypeType($type, $node, 'base');
923
        $setBaseOnThis->setBase($parent);
924
    }
925
926
    /**
927
     * @throws TypeException
928
     *
929
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
930
     */
931
    private function findSomething(
932
        string $finder,
933
        Schema $schema,
934
        DOMElement $node,
935
        string $typeName
936
    ) {
937
        list($name, $namespace) = static::splitParts($node, $typeName);
938
939
        /**
940
         * @var string|null
941
         */
942
        $namespace = $namespace ?: $schema->getTargetNamespace();
943
944
        try {
945
            /**
946
             * @var ElementItem|Group|AttributeItem|AttributeGroup|Type
947
             */
948
            $out = $schema->$finder($name, $namespace);
949
950
            return $out;
951
        } catch (TypeNotFoundException $e) {
952
            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);
953
        }
954
    }
955
956
    private function findSomeElementDef(Schema $schema, DOMElement $node, string $typeName): ElementDef
957
    {
958
        /**
959
         * @var ElementDef $out
960
         */
961
        $out = $this->findSomething('findElement', $schema, $node, $typeName);
962
963
        return $out;
964
    }
965
966
    private function fillItem(Item $element, DOMElement $node): void
967
    {
968
        /**
969
         * @var bool
970
         */
971
        $skip = false;
972
        static::againstDOMNodeList(
973
            $node,
974
            function (
975
                DOMElement $node,
976
                DOMElement $childNode
977
            ) use (
978
                $element,
979
                &$skip
980
            ): void {
981
                if (
982
                    !$skip &&
983
                    in_array(
984
                        $childNode->localName,
985
                        [
986
                            'complexType',
987
                            'simpleType',
988
                        ]
989
                    )
990
                ) {
991
                    $this->loadTypeWithCallback(
992
                        $element->getSchema(),
993
                        $childNode,
994
                        function (Type $type) use ($element): void {
995
                            $element->setType($type);
996
                        }
997
                    );
998
                    $skip = true;
999
                }
1000
            }
1001
        );
1002
        if ($skip) {
1003
            return;
1004
        }
1005
        $this->fillItemNonLocalType($element, $node);
1006
    }
1007
1008
    /**
1009
     * @var Schema|null
1010
     */
1011
    protected $globalSchema;
1012
1013
    /**
1014
     * @return Schema[]
1015
     */
1016
    private function setupGlobalSchemas(array &$callbacks): array
1017
    {
1018
        $globalSchemas = array();
1019
        foreach (self::$globalSchemaInfo as $namespace => $uri) {
1020
            self::setLoadedFile(
1021
                $uri,
1022
                $globalSchemas[$namespace] = $schema = new Schema()
1023
            );
1024
            if ($namespace === self::XSD_NS) {
1025
                $this->globalSchema = $schema;
1026
            }
1027
            $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1028
            $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1029
        }
1030
1031
        return $globalSchemas;
1032
    }
1033
1034
    /**
1035
     * @return string[]
1036
     */
1037
    public function getGlobalSchemaInfo(): array
1038
    {
1039
        return self::$globalSchemaInfo;
1040
    }
1041
1042
    private function getGlobalSchema(): Schema
1043
    {
1044
        if (!$this->globalSchema) {
1045
            $callbacks = array();
1046
            $globalSchemas = $this->setupGlobalSchemas($callbacks);
1047
1048
            $globalSchemas[static::XSD_NS]->addType(new SimpleType($globalSchemas[static::XSD_NS], 'anySimpleType'));
1049
            $globalSchemas[static::XSD_NS]->addType(new SimpleType($globalSchemas[static::XSD_NS], 'anyType'));
1050
1051
            $globalSchemas[static::XML_NS]->addSchema(
1052
                $globalSchemas[static::XSD_NS],
1053
                (string) static::XSD_NS
1054
            );
1055
            $globalSchemas[static::XSD_NS]->addSchema(
1056
                $globalSchemas[static::XML_NS],
1057
                (string) static::XML_NS
1058
            );
1059
1060
            /**
1061
             * @var Closure
1062
             */
1063
            foreach ($callbacks as $callback) {
1064
                $callback();
1065
            }
1066
        }
1067
1068
        /**
1069
         * @var Schema
1070
         */
1071
        $out = $this->globalSchema;
1072
1073
        return $out;
1074
    }
1075
1076
    private function readNode(
1077
        DOMElement $node,
1078
        string $file = 'schema.xsd'
1079
    ): Schema {
1080
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1081
        self::setLoadedFile($fileKey, $rootSchema = new Schema());
1082
1083
        $rootSchema->addSchema($this->getGlobalSchema());
1084
        $callbacks = $this->schemaNode($rootSchema, $node);
1085
1086
        foreach ($callbacks as $callback) {
1087
            call_user_func($callback);
1088
        }
1089
1090
        return $rootSchema;
1091
    }
1092
1093
    /**
1094
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1095
     *
1096
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1097
     * file to distinguish between multiple schemas in a single file.
1098
     */
1099
    private function getNamespaceSpecificFileIndex(
1100
        string $file,
1101
        string $targetNamespace
1102
    ): string {
1103
        return $file.'#'.$targetNamespace;
1104
    }
1105
1106
    /**
1107
     * @throws IOException
1108
     */
1109
    public function readString(
1110
        string $content,
1111
        string $file = 'schema.xsd'
1112
    ): Schema {
1113
        $xml = new DOMDocument('1.0', 'UTF-8');
1114
        if (!$xml->loadXML($content)) {
1115
            throw new IOException("Can't load the schema");
1116
        }
1117
        $xml->documentURI = $file;
1118
1119
        return $this->readNode($xml->documentElement, $file);
1120
    }
1121
1122
    public function readFile(string $file): Schema
1123
    {
1124
        $xml = $this->getDOM($file);
1125
1126
        return $this->readNode($xml->documentElement, $file);
1127
    }
1128
1129
    /**
1130
     * @throws IOException
1131
     */
1132
    private function getDOM(string $file): DOMDocument
1133
    {
1134
        $xml = new DOMDocument('1.0', 'UTF-8');
1135
        if (!$xml->load($file)) {
1136
            throw new IOException("Can't load the file $file");
1137
        }
1138
1139
        return $xml;
1140
    }
1141
1142
    private static function againstDOMNodeList(
1143
        DOMElement $node,
1144
        Closure $againstNodeList
1145
    ): void {
1146
        $limit = $node->childNodes->length;
1147
        for ($i = 0; $i < $limit; $i += 1) {
1148
            /**
1149
             * @var DOMNode
1150
             */
1151
            $childNode = $node->childNodes->item($i);
1152
1153
            if ($childNode instanceof DOMElement) {
1154
                $againstNodeList(
1155
                    $node,
1156
                    $childNode
1157
                );
1158
            }
1159
        }
1160
    }
1161
1162
    /**
1163
     * @return Closure
1164
     */
1165
    private function CallbackGeneratorMaybeCallMethodAgainstDOMNodeList(
1166
        SchemaItem $type,
1167
        array $methods
1168
    ) {
1169
        return function (
1170
            DOMElement $node,
1171
            DOMElement $childNode
1172
        ) use (
1173
            $methods,
1174
            $type
1175
        ): void {
1176
            /**
1177
             * @var string[]
1178
             */
1179
            $methods = $methods;
1180
            if ($childNode instanceof DOMElement && isset($methods[$childNode->localName])) {
1181
                $method = $methods[$childNode->localName];
1182
1183
                $this->$method($type, $childNode);
1184
            }
1185
        };
1186
    }
1187
1188
    private function loadTypeWithCallbackOnChildNodes(
1189
        Schema $schema,
1190
        DOMElement $node,
1191
        Closure $callback
1192
    ): void {
1193
        self::againstDOMNodeList(
1194
            $node,
1195
            function (
1196
                DOMElement $node,
1197
                DOMElement $childNode
1198
            ) use (
1199
                $schema,
1200
                $callback
1201
            ): void {
1202
                $this->loadTypeWithCallback(
1203
                    $schema,
1204
                    $childNode,
1205
                    $callback
1206
                );
1207
            }
1208
        );
1209
    }
1210
1211
    private function loadTypeWithCallback(
1212
        Schema $schema,
1213
        DOMElement $childNode,
1214
        Closure $callback
1215
    ): void {
1216
        $methods = [
0 ignored issues
show
Unused Code introduced by
The assignment to $methods is dead and can be removed.
Loading history...
1217
            'complexType' => 'loadComplexType',
1218
            'simpleType' => 'loadSimpleType',
1219
        ];
1220
1221
        /**
1222
         * @var Closure|null $func
1223
         */
1224
        $func = null;
1225
1226
        switch ($childNode->localName) {
1227
            case 'complexType':
1228
                $func = $this->loadComplexType($schema, $childNode, $callback);
1229
                break;
1230
            case 'simpleType':
1231
                $func = $this->loadSimpleType($schema, $childNode, $callback);
1232
                break;
1233
        }
1234
1235
        if ($func instanceof Closure) {
1236
            call_user_func($func);
1237
        }
1238
    }
1239
1240
    private function loadImport(
1241
        Schema $schema,
1242
        DOMElement $node
1243
    ): Closure {
1244
        $base = urldecode($node->ownerDocument->documentURI);
1245
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute('schemaLocation'));
1246
1247
        $namespace = $node->getAttribute('namespace');
1248
1249
        $keys = $this->loadImportFreshKeys($namespace, $file);
1250
1251
        if (
1252
            self::hasLoadedFile(...$keys)
1253
        ) {
1254
            $schema->addSchema(self::getLoadedFile(...$keys));
1255
1256
            return function (): void {
1257
            };
1258
        }
1259
1260
        return $this->loadImportFresh($namespace, $schema, $file);
1261
    }
1262
1263
    private function loadImportFreshKeys(
1264
        string $namespace,
1265
        string $file
1266
    ): array {
1267
        $globalSchemaInfo = $this->getGlobalSchemaInfo();
1268
1269
        $keys = [];
1270
1271
        if (isset($globalSchemaInfo[$namespace])) {
1272
            $keys[] = $globalSchemaInfo[$namespace];
1273
        }
1274
1275
        $keys[] = $this->getNamespaceSpecificFileIndex(
1276
            $file,
1277
            $namespace
1278
        );
1279
1280
        $keys[] = $file;
1281
1282
        return $keys;
1283
    }
1284
1285
    private function loadImportFreshCallbacksNewSchema(
1286
        string $namespace,
1287
        Schema $schema,
1288
        string $file
1289
    ): Schema {
1290
        /**
1291
         * @var Schema $newSchema
1292
         */
1293
        $newSchema = self::setLoadedFile(
1294
            $file,
1295
            ($namespace ? new Schema() : $schema)
1296
        );
1297
1298
        if ($namespace) {
1299
            $newSchema->addSchema($this->getGlobalSchema());
1300
            $schema->addSchema($newSchema);
1301
        }
1302
1303
        return $newSchema;
1304
    }
1305
1306
    /**
1307
     * @return Closure[]
1308
     */
1309
    private function loadImportFreshCallbacks(
1310
        string $namespace,
1311
        Schema $schema,
1312
        string $file
1313
    ): array {
1314
        /**
1315
         * @var string
1316
         */
1317
        $file = $file;
1318
1319
        return $this->schemaNode(
1320
            $this->loadImportFreshCallbacksNewSchema(
1321
                $namespace,
1322
                $schema,
1323
                $file
1324
            ),
1325
            $this->getDOM(
1326
                $this->hasKnownSchemaLocation($file)
1327
                    ? $this->getKnownSchemaLocation($file)
1328
                    : $file
1329
            )->documentElement,
1330
            $schema
1331
        );
1332
    }
1333
1334
    private function loadImportFresh(
1335
        string $namespace,
1336
        Schema $schema,
1337
        string $file
1338
    ): Closure {
1339
        return function () use ($namespace, $schema, $file): void {
1340
            foreach (
1341
                $this->loadImportFreshCallbacks(
1342
                    $namespace,
1343
                    $schema,
1344
                    $file
1345
                ) as $callback
1346
            ) {
1347
                $callback();
1348
            }
1349
        };
1350
    }
1351
1352
    private function loadElement(
1353
        Schema $schema,
1354
        DOMElement $node
1355
    ): Element {
1356
        $element = new Element($schema, $node->getAttribute('name'));
1357
        $element->setDoc(self::getDocumentation($node));
1358
1359
        $this->fillItem($element, $node);
1360
1361
        self::maybeSetMax($element, $node);
1362
        self::maybeSetMin($element, $node);
1363
1364
        $xp = new \DOMXPath($node->ownerDocument);
1365
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
1366
1367
        if ($xp->query('ancestor::xs:choice', $node)->length) {
1368
            $element->setMin(0);
1369
        }
1370
1371
        if ($node->hasAttribute('nillable')) {
1372
            $element->setNil($node->getAttribute('nillable') == 'true');
1373
        }
1374
        if ($node->hasAttribute('form')) {
1375
            $element->setQualified($node->getAttribute('form') == 'qualified');
1376
        }
1377
1378
        return $element;
1379
    }
1380
1381
    private static function loadElementRef(
1382
        ElementDef $referenced,
1383
        DOMElement $node
1384
    ): ElementRef {
1385
        $ref = new ElementRef($referenced);
1386
        $ref->setDoc(self::getDocumentation($node));
1387
1388
        self::maybeSetMax($ref, $node);
1389
        self::maybeSetMin($ref, $node);
1390
        if ($node->hasAttribute('nillable')) {
1391
            $ref->setNil($node->getAttribute('nillable') == 'true');
1392
        }
1393
        if ($node->hasAttribute('form')) {
1394
            $ref->setQualified($node->getAttribute('form') == 'qualified');
1395
        }
1396
1397
        return $ref;
1398
    }
1399
1400
    private function loadAttributeGroup(
1401
        Schema $schema,
1402
        DOMElement $node
1403
    ): Closure {
1404
        $attGroup = new AttributeGroup($schema, $node->getAttribute('name'));
1405
        $attGroup->setDoc(self::getDocumentation($node));
1406
        $schema->addAttributeGroup($attGroup);
1407
1408
        return function () use ($schema, $node, $attGroup): void {
1409
            SchemaReader::againstDOMNodeList(
1410
                $node,
1411
                function (
1412
                    DOMElement $node,
1413
                    DOMElement $childNode
1414
                ) use (
1415
                    $schema,
1416
                    $attGroup
1417
                ): void {
1418
                    switch ($childNode->localName) {
1419
                        case 'attribute':
1420
                            $attribute = $this->getAttributeFromAttributeOrRef(
1421
                                $childNode,
1422
                                $schema,
1423
                                $node
1424
                            );
1425
                            $attGroup->addAttribute($attribute);
1426
                            break;
1427
                        case 'attributeGroup':
1428
                            $this->findSomethingLikeAttributeGroup(
1429
                                $schema,
1430
                                $node,
1431
                                $childNode,
1432
                                $attGroup
1433
                            );
1434
                            break;
1435
                    }
1436
                }
1437
            );
1438
        };
1439
    }
1440
1441
    private function getAttributeFromAttributeOrRef(
1442
        DOMElement $childNode,
1443
        Schema $schema,
1444
        DOMElement $node
1445
    ): AttributeItem {
1446
        if ($childNode->hasAttribute('ref')) {
1447
            /**
1448
             * @var AttributeItem
1449
             */
1450
            $attribute = $this->findSomething('findAttribute', $schema, $node, $childNode->getAttribute('ref'));
1451
        } else {
1452
            /**
1453
             * @var Attribute
1454
             */
1455
            $attribute = $this->loadAttribute($schema, $childNode);
1456
        }
1457
1458
        return $attribute;
1459
    }
1460
1461
    private function loadAttribute(
1462
        Schema $schema,
1463
        DOMElement $node
1464
    ): Attribute {
1465
        $attribute = new Attribute($schema, $node->getAttribute('name'));
1466
        $attribute->setDoc(self::getDocumentation($node));
1467
        $this->fillItem($attribute, $node);
1468
1469
        if ($node->hasAttribute('nillable')) {
1470
            $attribute->setNil($node->getAttribute('nillable') == 'true');
1471
        }
1472
        if ($node->hasAttribute('form')) {
1473
            $attribute->setQualified($node->getAttribute('form') == 'qualified');
1474
        }
1475
        if ($node->hasAttribute('use')) {
1476
            $attribute->setUse($node->getAttribute('use'));
1477
        }
1478
1479
        return $attribute;
1480
    }
1481
1482
    private function addAttributeFromAttributeOrRef(
1483
        BaseComplexType $type,
1484
        DOMElement $childNode,
1485
        Schema $schema,
1486
        DOMElement $node
1487
    ): void {
1488
        $attribute = $this->getAttributeFromAttributeOrRef(
1489
            $childNode,
1490
            $schema,
1491
            $node
1492
        );
1493
1494
        $type->addAttribute($attribute);
1495
    }
1496
1497
    private function findSomethingLikeAttributeGroup(
1498
        Schema $schema,
1499
        DOMElement $node,
1500
        DOMElement $childNode,
1501
        AttributeContainer $addToThis
1502
    ): void {
1503
        /**
1504
         * @var AttributeItem
1505
         */
1506
        $attribute = $this->findSomething('findAttributeGroup', $schema, $node, $childNode->getAttribute('ref'));
1507
        $addToThis->addAttribute($attribute);
1508
    }
1509
1510
    /**
1511
     * @var Schema[]
1512
     */
1513
    protected static $loadedFiles = array();
1514
1515
    private static function hasLoadedFile(string ...$keys): bool
1516
    {
1517
        foreach ($keys as $key) {
1518
            if (isset(self::$loadedFiles[$key])) {
1519
                return true;
1520
            }
1521
        }
1522
1523
        return false;
1524
    }
1525
1526
    /**
1527
     * @throws RuntimeException if loaded file not found
1528
     */
1529
    private static function getLoadedFile(string ...$keys): Schema
1530
    {
1531
        foreach ($keys as $key) {
1532
            if (isset(self::$loadedFiles[$key])) {
1533
                return self::$loadedFiles[$key];
1534
            }
1535
        }
1536
1537
        throw new RuntimeException('Loaded file was not found!');
1538
    }
1539
1540
    private static function setLoadedFile(string $key, Schema $schema): Schema
1541
    {
1542
        self::$loadedFiles[$key] = $schema;
1543
1544
        return $schema;
1545
    }
1546
1547
    private function setSchemaThingsFromNode(
1548
        Schema $schema,
1549
        DOMElement $node,
1550
        Schema $parent = null
1551
    ): void {
1552
        $schema->setDoc(self::getDocumentation($node));
1553
1554
        if ($node->hasAttribute('targetNamespace')) {
1555
            $schema->setTargetNamespace($node->getAttribute('targetNamespace'));
1556
        } elseif ($parent) {
1557
            $schema->setTargetNamespace($parent->getTargetNamespace());
1558
        }
1559
        $schema->setElementsQualification($node->getAttribute('elementFormDefault') == 'qualified');
1560
        $schema->setAttributesQualification($node->getAttribute('attributeFormDefault') == 'qualified');
1561
        $schema->setDoc(self::getDocumentation($node));
1562
    }
1563
}
1564