Completed
Push — php-7.1 ( ae7913...c6ca60 )
by SignpostMarv
14:10 queued 06:45
created

SchemaReader::loadTypeWithCallbackOnChildNodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 13
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 18
ccs 0
cts 0
cp 0
crap 2
rs 9.4285

1 Method

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