Completed
Push — php-7.1 ( 2da64d...bfe0e1 )
by SignpostMarv
11:33 queued 04:19
created

SchemaReader::makeCallbackCallback()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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