Passed
Branch php-7.0 (c42fed)
by SignpostMarv
02:19
created

SchemaReader::findAttributeGroup()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20
Code Lines 14

Duplication

Lines 20
Ratio 100 %

Importance

Changes 0
Metric Value
dl 20
loc 20
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 14
nc 2
nop 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\Documentation\DocumentationReader;
12
use GoetasWebservices\XML\XSDReader\Documentation\StandardDocumentationReader;
13
use GoetasWebservices\XML\XSDReader\Exception\IOException;
14
use GoetasWebservices\XML\XSDReader\Exception\TypeException;
15
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Attribute;
16
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer;
17
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeDef;
18
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
19
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group as AttributeGroup;
20
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
21
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
22
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
23
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
24
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
25
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
26
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
27
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
28
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
29
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
30
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
31
use GoetasWebservices\XML\XSDReader\Schema\Item;
32
use GoetasWebservices\XML\XSDReader\Schema\Schema;
33
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
34
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
35
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
36
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
37
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
38
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
39
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
40
41
class SchemaReader
42
{
43
    const XSD_NS = 'http://www.w3.org/2001/XMLSchema';
44
45
    const XML_NS = 'http://www.w3.org/XML/1998/namespace';
46
47
    /**
48
     * @var DocumentationReader
49
     */
50
    private $documentationReader;
51
52
    /**
53
     * @var Schema[]
54
     */
55
    private $loadedFiles = array();
56
57
    /**
58
     * @var string[]
59
     */
60
    protected $knownLocationSchemas = [
61
        'http://www.w3.org/2001/xml.xsd' => (
62
            __DIR__.'/Resources/xml.xsd'
63
        ),
64
        'http://www.w3.org/2001/XMLSchema.xsd' => (
65
            __DIR__.'/Resources/XMLSchema.xsd'
66
        ),
67
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' => (
68
            __DIR__.'/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd'
69
        ),
70
        'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' => (
71
            __DIR__.'/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd'
72
        ),
73
        'https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
74
            __DIR__.'/Resources/xmldsig-core-schema.xsd'
75
        ),
76
        'http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd' => (
77
            __DIR__.'/Resources/xmldsig-core-schema.xsd'
78
        ),
79
    ];
80
81
    /**
82
     * @var string[]
83
     */
84
    protected static $globalSchemaInfo = array(
85
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
86
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd',
87
    );
88
89
    public function __construct(DocumentationReader $documentationReader = null)
90
    {
91
        if (null === $documentationReader) {
92
            $documentationReader = new StandardDocumentationReader();
93
        }
94
        $this->documentationReader = $documentationReader;
95
    }
96
97
    public function addKnownSchemaLocation(string $remote, string $local)
98
    {
99
        $this->knownLocationSchemas[$remote] = $local;
100
    }
101
102
    private function loadAttributeGroup(
103
        Schema $schema,
104
        DOMElement $node
105
    ): Closure {
106
        $attGroup = new AttributeGroup($schema, $node->getAttribute('name'));
107
        $attGroup->setDoc($this->getDocumentation($node));
108
        $schema->addAttributeGroup($attGroup);
109
110
        return function () use ($schema, $node, $attGroup) {
111
            SchemaReader::againstDOMNodeList(
112
                $node,
113
                function (
114
                    DOMElement $node,
115
                    DOMElement $childNode
116
                ) use (
117
                    $schema,
118
                    $attGroup
119
                ) {
120
                    switch ($childNode->localName) {
121
                        case 'attribute':
122
                            $attribute = $this->getAttributeFromAttributeOrRef(
123
                                $childNode,
124
                                $schema,
125
                                $node
126
                            );
127
                            $attGroup->addAttribute($attribute);
128
                            break;
129
                        case 'attributeGroup':
130
                            $this->findSomethingLikeAttributeGroup(
131
                                $schema,
132
                                $node,
133
                                $childNode,
134
                                $attGroup
135
                            );
136
                            break;
137
                    }
138
                }
139
            );
140
        };
141
    }
142
143
    private function getAttributeFromAttributeOrRef(
144
        DOMElement $childNode,
145
        Schema $schema,
146
        DOMElement $node
147
    ): AttributeItem {
148
        if ($childNode->hasAttribute('ref')) {
149
            $attribute = $this->findAttributeItem($schema, $node, $childNode->getAttribute('ref'));
150
        } else {
151
            /**
152
             * @var Attribute
153
             */
154
            $attribute = $this->loadAttribute($schema, $childNode);
155
        }
156
157
        return $attribute;
158
    }
159
160
    private function loadAttribute(
161
        Schema $schema,
162
        DOMElement $node
163
    ): Attribute {
164
        $attribute = new Attribute($schema, $node->getAttribute('name'));
165
        $attribute->setDoc($this->getDocumentation($node));
166
        $this->fillItem($attribute, $node);
167
168
        if ($node->hasAttribute('nillable')) {
169
            $attribute->setNil($node->getAttribute('nillable') == 'true');
170
        }
171
        if ($node->hasAttribute('form')) {
172
            $attribute->setQualified($node->getAttribute('form') == 'qualified');
173
        }
174
        if ($node->hasAttribute('use')) {
175
            $attribute->setUse($node->getAttribute('use'));
176
        }
177
178
        return $attribute;
179
    }
180
181
    private function loadAttributeOrElementDef(
182
        Schema $schema,
183
        DOMElement $node,
184
        bool $attributeDef
185
    ): Closure {
186
        $name = $node->getAttribute('name');
187
        if ($attributeDef) {
188
            $attribute = new AttributeDef($schema, $name);
189
            $schema->addAttribute($attribute);
190
        } else {
191
            $attribute = new ElementDef($schema, $name);
192
            $schema->addElement($attribute);
193
        }
194
195
        return function () use ($attribute, $node) {
196
            $this->fillItem($attribute, $node);
197
        };
198
    }
199
200
    private function loadAttributeDef(Schema $schema, DOMElement $node): Closure
201
    {
202
        return $this->loadAttributeOrElementDef($schema, $node, true);
203
    }
204
205
    private function getDocumentation(DOMElement $node): string
206
    {
207
        return $this->documentationReader->get($node);
208
    }
209
210
    /**
211
     * @return Closure[]
212
     */
213
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null): array
214
    {
215
        $this->setSchemaThingsFromNode($schema, $node, $parent);
216
        $functions = array();
217
218
        static::againstDOMNodeList(
219
            $node,
220
            function (
221
                DOMElement $node,
222
                DOMElement $childNode
223
            ) use (
224
                $schema,
225
                &$functions
226
            ) {
227
                $callback = null;
228
229
                switch ($childNode->localName) {
230
                    case 'attributeGroup':
231
                        $callback = $this->loadAttributeGroup($schema, $childNode);
232
                        break;
233
                    case 'include':
234
                    case 'import':
235
                        $callback = $this->loadImport($schema, $childNode);
236
                        break;
237
                    case 'element':
238
                        $callback = $this->loadElementDef($schema, $childNode);
239
                        break;
240
                    case 'attribute':
241
                        $callback = $this->loadAttributeDef($schema, $childNode);
242
                        break;
243
                    case 'group':
244
                        $callback = $this->loadGroup($schema, $childNode);
245
                        break;
246
                    case 'complexType':
247
                        $callback = $this->loadComplexType($schema, $childNode);
248
                        break;
249
                    case 'simpleType':
250
                        $callback = $this->loadSimpleType($schema, $childNode);
251
                        break;
252
                }
253
254
                if ($callback instanceof Closure) {
255
                    $functions[] = $callback;
256
                }
257
            }
258
        );
259
260
        return $functions;
261
    }
262
263
    private function loadGroupRef(Group $referenced, DOMElement $node): GroupRef
264
    {
265
        $ref = new GroupRef($referenced);
266
        $ref->setDoc($this->getDocumentation($node));
267
268
        self::maybeSetMax($ref, $node);
269
        self::maybeSetMin($ref, $node);
270
271
        return $ref;
272
    }
273
274
    private static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node): InterfaceSetMinMax
275
    {
276
        if (
277
            $node->hasAttribute('maxOccurs')
278
        ) {
279
            $ref->setMax($node->getAttribute('maxOccurs') == 'unbounded' ? -1 : (int) $node->getAttribute('maxOccurs'));
280
        }
281
282
        return $ref;
283
    }
284
285
    private static function maybeSetMin(InterfaceSetMinMax $ref, DOMElement $node): InterfaceSetMinMax
286
    {
287
        if ($node->hasAttribute('minOccurs')) {
288
            $ref->setMin((int) $node->getAttribute('minOccurs'));
289
        }
290
291
        return $ref;
292
    }
293
294
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, int $max = null)
295
    {
296
        $max =
297
        (
298
            (is_int($max) && (bool) $max) ||
299
            $node->getAttribute('maxOccurs') == 'unbounded' ||
300
            $node->getAttribute('maxOccurs') > 1
301
        )
302
            ? 2
303
            : null;
304
305
        static::againstDOMNodeList(
306
            $node,
307
            function (
308
                DOMElement $node,
309
                DOMElement $childNode
310
            ) use (
311
                $elementContainer,
312
                $max
313
            ) {
314
                $this->loadSequenceChildNode(
315
                    $elementContainer,
316
                    $node,
317
                    $childNode,
318
                    $max
319
                );
320
            }
321
        );
322
    }
323
324
    /**
325
     * @param int|null $max
326
     */
327 View Code Duplication
    private function loadSequenceChildNode(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
328
        ElementContainer $elementContainer,
329
        DOMElement $node,
330
        DOMElement $childNode,
331
        $max
332
    ) {
333
        switch ($childNode->localName) {
334
            case 'sequence':
335
            case 'choice':
336
            case 'all':
337
                $this->loadSequence(
338
                    $elementContainer,
339
                    $childNode,
340
                    $max
341
                );
342
                break;
343
            case 'element':
344
                $this->loadSequenceChildNodeLoadElement(
345
                    $elementContainer,
346
                    $node,
347
                    $childNode,
348
                    $max
349
                );
350
                break;
351
            case 'group':
352
                $this->addGroupAsElement(
353
                    $elementContainer->getSchema(),
354
                    $node,
355
                    $childNode,
356
                    $elementContainer
357
                );
358
                break;
359
        }
360
    }
361
362
    /**
363
     * @param int|null $max
364
     */
365
    private function loadSequenceChildNodeLoadElement(
366
        ElementContainer $elementContainer,
367
        DOMElement $node,
368
        DOMElement $childNode,
369
        $max
370
    ) {
371
        if ($childNode->hasAttribute('ref')) {
372
            $element = new ElementRef(
373
                $this->findElement($elementContainer->getSchema(), $node, $childNode->getAttribute('ref'))
374
            );
375
            $element->setDoc($this->getDocumentation($childNode));
376
377
            self::maybeSetMax($element, $childNode);
378
            self::maybeSetMin($element, $childNode);
379
            if ($childNode->hasAttribute('nillable')) {
380
                $element->setNil($childNode->getAttribute('nillable') == 'true');
381
            }
382
            if ($childNode->hasAttribute('form')) {
383
                $element->setQualified($childNode->getAttribute('form') == 'qualified');
384
            }
385
        } else {
386
            $element = $this->loadElement(
387
                $elementContainer->getSchema(),
388
                $childNode
389
            );
390
        }
391
        if ($max > 1) {
392
            /*
393
            * although one might think the typecast is not needed with $max being `? int $max` after passing > 1,
394
            * phpstan@a4f89fa still thinks it's possibly null.
395
            * see https://github.com/phpstan/phpstan/issues/577 for related issue
396
            */
397
            $element->setMax((int) $max);
398
        }
399
        $elementContainer->addElement($element);
400
    }
401
402
    private function addGroupAsElement(
403
        Schema $schema,
404
        DOMElement $node,
405
        DOMElement $childNode,
406
        ElementContainer $elementContainer
407
    ) {
408
        $referencedGroup = $this->findGroup(
409
            $schema,
410
            $node,
411
            $childNode->getAttribute('ref')
412
        );
413
414
        $group = $this->loadGroupRef($referencedGroup, $childNode);
415
        $elementContainer->addElement($group);
416
    }
417
418
    private function loadGroup(Schema $schema, DOMElement $node): Closure
419
    {
420
        $group = new Group($schema, $node->getAttribute('name'));
421
        $group->setDoc($this->getDocumentation($node));
422
423
        if ($node->hasAttribute('maxOccurs')) {
424
            /**
425
             * @var GroupRef
426
             */
427
            $group = self::maybeSetMax(new GroupRef($group), $node);
428
        }
429
        if ($node->hasAttribute('minOccurs')) {
430
            /**
431
             * @var GroupRef
432
             */
433
            $group = self::maybeSetMin(
434
                $group instanceof GroupRef ? $group : new GroupRef($group),
435
                $node
436
            );
437
        }
438
439
        $schema->addGroup($group);
440
441
        return function () use ($group, $node) {
442
            static::againstDOMNodeList(
443
                $node,
444
                function (DOMelement $node, DOMElement $childNode) use ($group) {
445
                    switch ($childNode->localName) {
446
                        case 'sequence':
447
                        case 'choice':
448
                        case 'all':
449
                            $this->loadSequence($group, $childNode);
450
                            break;
451
                    }
452
                }
453
            );
454
        };
455
    }
456
457
    private function loadComplexType(Schema $schema, DOMElement $node, Closure $callback = null): Closure
458
    {
459
        /**
460
         * @var bool
461
         */
462
        $isSimple = false;
463
464
        static::againstDOMNodeList(
465
            $node,
466
            function (
467
                DOMElement $node,
468
                DOMElement $childNode
469
            ) use (
470
                &$isSimple
471
            ) {
472
                if ($isSimple) {
473
                    return;
474
                }
475
                if ($childNode->localName === 'simpleContent') {
476
                    $isSimple = true;
477
                }
478
            }
479
        );
480
481
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute('name')) : new ComplexType($schema, $node->getAttribute('name'));
482
483
        $type->setDoc($this->getDocumentation($node));
484
        if ($node->getAttribute('name')) {
485
            $schema->addType($type);
486
        }
487
488
        return function () use ($type, $node, $schema, $callback) {
489
            $this->fillTypeNode($type, $node, true);
490
491
            static::againstDOMNodeList(
492
                $node,
493
                function (
494
                    DOMElement $node,
495
                    DOMElement $childNode
496
                ) use (
497
                    $schema,
498
                    $type
499
                ) {
500
                    $this->loadComplexTypeFromChildNode(
501
                        $type,
502
                        $node,
503
                        $childNode,
504
                        $schema
505
                    );
506
                }
507
            );
508
509
            if ($callback instanceof Closure) {
510
                call_user_func($callback, $type);
511
            }
512
        };
513
    }
514
515
    private function loadComplexTypeFromChildNode(
516
        BaseComplexType $type,
517
        DOMElement $node,
518
        DOMElement $childNode,
519
        Schema $schema
520
    ) {
521
        switch ($childNode->localName) {
522
            case 'sequence':
523
            case 'choice':
524
            case 'all':
525
                if ($type instanceof ElementContainer) {
526
                    $this->loadSequence(
527
                        $type,
528
                        $childNode
529
                    );
530
                }
531
                break;
532
            case 'attribute':
533
                $this->addAttributeFromAttributeOrRef(
534
                    $type,
535
                    $childNode,
536
                    $schema,
537
                    $node
538
                );
539
                break;
540
            case 'attributeGroup':
541
                $this->findSomethingLikeAttributeGroup(
542
                    $schema,
543
                    $node,
544
                    $childNode,
545
                    $type
546
                );
547
                break;
548
            case 'group':
549
                if (
550
                    $type instanceof ComplexType
551
                ) {
552
                    $this->addGroupAsElement(
553
                        $schema,
554
                        $node,
555
                        $childNode,
556
                        $type
557
                    );
558
                }
559
                break;
560
        }
561
    }
562
563
    private function loadSimpleType(Schema $schema, DOMElement $node, Closure $callback = null): Closure
564
    {
565
        $type = new SimpleType($schema, $node->getAttribute('name'));
566
        $type->setDoc($this->getDocumentation($node));
567
        if ($node->getAttribute('name')) {
568
            $schema->addType($type);
569
        }
570
571
        return function () use ($type, $node, $callback) {
572
            $this->fillTypeNode($type, $node, true);
573
574
            static::againstDOMNodeList(
575
                $node,
576
                function (DOMElement $node, DOMElement $childNode) use ($type) {
577
                    switch ($childNode->localName) {
578
                        case 'union':
579
                            $this->loadUnion($type, $childNode);
580
                            break;
581
                        case 'list':
582
                            $this->loadList($type, $childNode);
583
                            break;
584
                    }
585
                }
586
            );
587
588
            if ($callback instanceof Closure) {
589
                call_user_func($callback, $type);
590
            }
591
        };
592
    }
593
594
    private function loadList(SimpleType $type, DOMElement $node)
595
    {
596
        if ($node->hasAttribute('itemType')) {
597
            /**
598
             * @var SimpleType
599
             */
600
            $listType = $this->findSomeType($type, $node, 'itemType');
601
            $type->setList($listType);
602
        } else {
603
            self::againstDOMNodeList(
604
                $node,
605
                function (
606
                    DOMElement $node,
607
                    DOMElement $childNode
608
                ) use (
609
                    $type
610
                ) {
611
                    $this->loadTypeWithCallback(
612
                        $type->getSchema(),
613
                        $childNode,
614
                        function (SimpleType $list) use ($type) {
615
                            $type->setList($list);
616
                        }
617
                    );
618
                }
619
            );
620
        }
621
    }
622
623
    private function findSomeType(
624
        SchemaItem $fromThis,
625
        DOMElement $node,
626
        string $attributeName
627
    ): SchemaItem {
628
        return $this->findSomeTypeFromAttribute(
629
            $fromThis,
630
            $node,
631
            $node->getAttribute($attributeName)
632
        );
633
    }
634
635
    private function findSomeTypeFromAttribute(
636
        SchemaItem $fromThis,
637
        DOMElement $node,
638
        string $attributeName
639
    ): SchemaItem {
640
        $out = $this->findType(
641
            $fromThis->getSchema(),
642
            $node,
643
            $attributeName
644
        );
645
646
        return $out;
647
    }
648
649
    private function loadUnion(SimpleType $type, DOMElement $node)
650
    {
651
        if ($node->hasAttribute('memberTypes')) {
652
            $types = preg_split('/\s+/', $node->getAttribute('memberTypes'));
653
            foreach ($types as $typeName) {
654
                /**
655
                 * @var SimpleType
656
                 */
657
                $unionType = $this->findSomeTypeFromAttribute(
658
                    $type,
659
                    $node,
660
                    $typeName
661
                );
662
                $type->addUnion($unionType);
663
            }
664
        }
665
        self::againstDOMNodeList(
666
            $node,
667
            function (
668
                DOMElement $node,
669
                DOMElement $childNode
670
            ) use (
671
                $type
672
            ) {
673
                $this->loadTypeWithCallback(
674
                    $type->getSchema(),
675
                    $childNode,
676
                    function (SimpleType $unType) use ($type) {
677
                        $type->addUnion($unType);
678
                    }
679
                );
680
            }
681
        );
682
    }
683
684
    private function fillTypeNode(Type $type, DOMElement $node, bool $checkAbstract = false)
685
    {
686
        if ($checkAbstract) {
687
            $type->setAbstract($node->getAttribute('abstract') === 'true' || $node->getAttribute('abstract') === '1');
688
        }
689
690
        static::againstDOMNodeList(
691
            $node,
692
            function (DOMElement $node, DOMElement $childNode) use ($type) {
693
                switch ($childNode->localName) {
694
                    case 'restriction':
695
                        $this->loadRestriction($type, $childNode);
696
                        break;
697
                    case 'extension':
698
                        if ($type instanceof BaseComplexType) {
699
                            $this->loadExtension($type, $childNode);
700
                        }
701
                        break;
702
                    case 'simpleContent':
703
                    case 'complexContent':
704
                        $this->fillTypeNode($type, $childNode);
705
                        break;
706
                }
707
            }
708
        );
709
    }
710
711
    private function loadExtension(BaseComplexType $type, DOMElement $node)
712
    {
713
        $extension = new Extension();
714
        $type->setExtension($extension);
715
716
        if ($node->hasAttribute('base')) {
717
            $this->findAndSetSomeBase(
718
                $type,
719
                $extension,
720
                $node
721
            );
722
        }
723
        $this->loadExtensionChildNodes($type, $node);
724
    }
725
726
    private function findAndSetSomeBase(
727
        Type $type,
728
        Base $setBaseOnThis,
729
        DOMElement $node
730
    ) {
731
        /**
732
         * @var Type
733
         */
734
        $parent = $this->findSomeType($type, $node, 'base');
735
        $setBaseOnThis->setBase($parent);
736
    }
737
738
    private function loadExtensionChildNodes(
739
        BaseComplexType $type,
740
        DOMElement $node
741
    ) {
742
        static::againstDOMNodeList(
743
            $node,
744 View Code Duplication
            function (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
745
                DOMElement $node,
746
                DOMElement $childNode
747
            ) use (
748
                $type
749
            ) {
750
                switch ($childNode->localName) {
751
                    case 'sequence':
752
                    case 'choice':
753
                    case 'all':
754
                        if ($type instanceof ElementContainer) {
755
                            $this->loadSequence(
756
                                $type,
757
                                $childNode
758
                            );
759
                        }
760
                        break;
761
                    case 'attribute':
762
                        $this->addAttributeFromAttributeOrRef(
763
                            $type,
764
                            $childNode,
765
                            $type->getSchema(),
766
                            $node
767
                        );
768
                        break;
769
                    case 'attributeGroup':
770
                        $this->findSomethingLikeAttributeGroup(
771
                            $type->getSchema(),
772
                            $node,
773
                            $childNode,
774
                            $type
775
                        );
776
                        break;
777
                }
778
            }
779
        );
780
    }
781
782
    private function loadRestriction(Type $type, DOMElement $node)
783
    {
784
        $restriction = new Restriction();
785
        $type->setRestriction($restriction);
786
        if ($node->hasAttribute('base')) {
787
            $this->findAndSetSomeBase($type, $restriction, $node);
788
        } else {
789
            self::againstDOMNodeList(
790
                $node,
791
                function (
792
                    DOMElement $node,
793
                    DOMElement $childNode
794
                ) use (
795
                    $type,
796
                    $restriction
797
                ) {
798
                    $this->loadTypeWithCallback(
799
                        $type->getSchema(),
800
                        $childNode,
801
                        function (Type $restType) use ($restriction) {
802
                            $restriction->setBase($restType);
803
                        }
804
                    );
805
                }
806
            );
807
        }
808
        self::againstDOMNodeList(
809
            $node,
810
            function (
811
                DOMElement $node,
812
                DOMElement $childNode
813
            ) use (
814
                $restriction
815
            ) {
816
                if (
817
                    in_array(
818
                        $childNode->localName,
819
                        [
820
                            'enumeration',
821
                            'pattern',
822
                            'length',
823
                            'minLength',
824
                            'maxLength',
825
                            'minInclusive',
826
                            'maxInclusive',
827
                            'minExclusive',
828
                            'maxExclusive',
829
                            'fractionDigits',
830
                            'totalDigits',
831
                            'whiteSpace',
832
                        ],
833
                        true
834
                    )
835
                ) {
836
                    $restriction->addCheck(
837
                        $childNode->localName,
838
                        [
839
                            'value' => $childNode->getAttribute('value'),
840
                            'doc' => $this->getDocumentation($childNode),
841
                        ]
842
                    );
843
                }
844
            }
845
        );
846
    }
847
848
    /**
849
     * @return mixed[]
850
     */
851
    private static function splitParts(DOMElement $node, string $typeName): array
852
    {
853
        $prefix = null;
854
        $name = $typeName;
855
        if (strpos($typeName, ':') !== false) {
856
            list($prefix, $name) = explode(':', $typeName);
857
        }
858
859
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
860
861
        return array(
862
            $name,
863
            $namespace,
864
            $prefix,
865
        );
866
    }
867
868 View Code Duplication
    private function findAttributeItem(Schema $schema, DOMElement $node, string $typeName): AttributeItem
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
869
    {
870
        list($name, $namespace) = static::splitParts($node, $typeName);
871
872
        $namespace = $namespace ?: $schema->getTargetNamespace();
873
874
        try {
875
            return $schema->findAttribute($name, $namespace);
876
        } catch (TypeNotFoundException $e) {
877
            throw new TypeException(
878
                sprintf(
879
                    "Can't find %s named {%s}#%s, at line %d in %s ",
880
                    'attribute',
881
                    $namespace,
882
                    $name,
883
                    $node->getLineNo(),
884
                    $node->ownerDocument->documentURI
885
                ),
886
                0,
887
                $e
888
            );
889
        }
890
    }
891
892 View Code Duplication
    private function findAttributeGroup(Schema $schema, DOMElement $node, string $typeName): AttributeGroup
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
893
    {
894
        list($name, $namespace) = static::splitParts($node, $typeName);
895
896
        $namespace = $namespace ?: $schema->getTargetNamespace();
897
898
        try {
899
            return $schema->findAttributeGroup($name, $namespace);
900
        } catch (TypeNotFoundException $e) {
901
            throw new TypeException(
902
                sprintf(
903
                    "Can't find %s named {%s}#%s, at line %d in %s ",
904
                    'attributegroup',
905
                    $namespace,
906
                    $name,
907
                    $node->getLineNo(),
908
                    $node->ownerDocument->documentURI
909
                ),
910
                0,
911
                $e
912
            );
913
        }
914
    }
915
916 View Code Duplication
    private function findElement(Schema $schema, DOMElement $node, string $typeName): ElementDef
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
917
    {
918
        list($name, $namespace) = static::splitParts($node, $typeName);
919
920
        $namespace = $namespace ?: $schema->getTargetNamespace();
921
922
        try {
923
            return $schema->findElement($name, $namespace);
924
        } catch (TypeNotFoundException $e) {
925
            throw new TypeException(
926
                sprintf(
927
                    "Can't find %s named {%s}#%s, at line %d in %s ",
928
                    'element',
929
                    $namespace,
930
                    $name,
931
                    $node->getLineNo(),
932
                    $node->ownerDocument->documentURI
933
                ),
934
                0,
935
                $e
936
            );
937
        }
938
    }
939
940 View Code Duplication
    private function findGroup(Schema $schema, DOMElement $node, string $typeName): Group
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
941
    {
942
        list($name, $namespace) = static::splitParts($node, $typeName);
943
944
        $namespace = $namespace ?: $schema->getTargetNamespace();
945
946
        try {
947
            return $schema->findGroup($name, $namespace);
948
        } catch (TypeNotFoundException $e) {
949
            throw new TypeException(
950
                sprintf(
951
                    "Can't find %s named {%s}#%s, at line %d in %s ",
952
                    'group',
953
                    $namespace,
954
                    $name,
955
                    $node->getLineNo(),
956
                    $node->ownerDocument->documentURI
957
                ),
958
                0,
959
                $e
960
            );
961
        }
962
    }
963
964 View Code Duplication
    private function findType(Schema $schema, DOMElement $node, string $typeName): SchemaItem
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
965
    {
966
        list($name, $namespace) = static::splitParts($node, $typeName);
967
968
        $namespace = $namespace ?: $schema->getTargetNamespace();
969
970
        try {
971
            return $schema->findType($name, $namespace);
972
        } catch (TypeNotFoundException $e) {
973
            throw new TypeException(
974
                sprintf(
975
                    "Can't find %s named {%s}#%s, at line %d in %s ",
976
                    'type',
977
                    $namespace,
978
                    $name,
979
                    $node->getLineNo(),
980
                    $node->ownerDocument->documentURI
981
                ),
982
                0,
983
                $e
984
            );
985
        }
986
    }
987
988
    /**
989
     * @return Closure
990
     */
991
    private function loadElementDef(Schema $schema, DOMElement $node)
992
    {
993
        return $this->loadAttributeOrElementDef($schema, $node, false);
994
    }
995
996
    private function fillItem(Item $element, DOMElement $node)
997
    {
998
        /**
999
         * @var bool
1000
         */
1001
        $skip = false;
1002
        static::againstDOMNodeList(
1003
            $node,
1004
            function (
1005
                DOMElement $node,
1006
                DOMElement $childNode
1007
            ) use (
1008
                $element,
1009
                &$skip
1010
            ) {
1011
                if (
1012
                    !$skip &&
1013
                    in_array(
1014
                        $childNode->localName,
1015
                        [
1016
                            'complexType',
1017
                            'simpleType',
1018
                        ],
1019
                        true
1020
                    )
1021
                ) {
1022
                    $this->loadTypeWithCallback(
1023
                        $element->getSchema(),
1024
                        $childNode,
1025
                        function (Type $type) use ($element) {
1026
                            $element->setType($type);
1027
                        }
1028
                    );
1029
                    $skip = true;
1030
                }
1031
            }
1032
        );
1033
        if ($skip) {
1034
            return;
1035
        }
1036
        $this->fillItemNonLocalType($element, $node);
1037
    }
1038
1039
    private function fillItemNonLocalType(Item $element, DOMElement $node)
1040
    {
1041
        if ($node->getAttribute('type')) {
1042
            /**
1043
             * @var Type
1044
             */
1045
            $type = $this->findSomeType($element, $node, 'type');
1046
        } else {
1047
            /**
1048
             * @var Type
1049
             */
1050
            $type = $this->findSomeTypeFromAttribute(
1051
                $element,
1052
                $node,
1053
                ($node->lookupPrefix(self::XSD_NS).':anyType')
1054
            );
1055
        }
1056
1057
        $element->setType($type);
1058
    }
1059
1060
    private function loadImport(
1061
        Schema $schema,
1062
        DOMElement $node
1063
    ): Closure {
1064
        $base = urldecode($node->ownerDocument->documentURI);
1065
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute('schemaLocation'));
1066
1067
        $namespace = $node->getAttribute('namespace');
1068
1069
        $keys = $this->loadImportFreshKeys($namespace, $file);
1070
1071
        foreach ($keys as $key) {
1072
            if (isset($this->loadedFiles[$key])) {
1073
                $schema->addSchema($this->loadedFiles[$key]);
1074
1075
                return function () {
1076
                };
1077
            }
1078
        }
1079
1080
        return $this->loadImportFresh($namespace, $schema, $file);
1081
    }
1082
1083
    /**
1084
     * @return string[]
1085
     */
1086
    private function loadImportFreshKeys(
1087
        string $namespace,
1088
        string $file
1089
    ): array {
1090
        $globalSchemaInfo = $this->getGlobalSchemaInfo();
1091
1092
        $keys = [];
1093
1094
        if (isset($globalSchemaInfo[$namespace])) {
1095
            $keys[] = $globalSchemaInfo[$namespace];
1096
        }
1097
1098
        $keys[] = $this->getNamespaceSpecificFileIndex(
1099
            $file,
1100
            $namespace
1101
        );
1102
1103
        $keys[] = $file;
1104
1105
        return $keys;
1106
    }
1107
1108
    private function loadImportFreshCallbacksNewSchema(
1109
        string $namespace,
1110
        Schema $schema,
1111
        string $file
1112
    ): Schema {
1113
        /**
1114
         * @var Schema $newSchema
1115
         */
1116
        $newSchema = $this->setLoadedFile(
1117
            $file,
1118
            (('' !== trim($namespace)) ? new Schema() : $schema)
1119
        );
1120
1121
        if ('' !== trim($namespace)) {
1122
            $newSchema->addSchema($this->getGlobalSchema());
1123
            $schema->addSchema($newSchema);
1124
        }
1125
1126
        return $newSchema;
1127
    }
1128
1129
    /**
1130
     * @return Closure[]
1131
     */
1132
    private function loadImportFreshCallbacks(
1133
        string $namespace,
1134
        Schema $schema,
1135
        string $file
1136
    ): array {
1137
        /**
1138
         * @var string
1139
         */
1140
        $file = $file;
1141
1142
        return $this->schemaNode(
1143
            $this->loadImportFreshCallbacksNewSchema(
1144
                $namespace,
1145
                $schema,
1146
                $file
1147
            ),
1148
            $this->getDOM(
1149
                isset($this->knownLocationSchemas[$file])
1150
                    ? $this->knownLocationSchemas[$file]
1151
                    : $file
1152
            )->documentElement,
1153
            $schema
1154
        );
1155
    }
1156
1157
    private function loadImportFresh(
1158
        string $namespace,
1159
        Schema $schema,
1160
        string $file
1161
    ): Closure {
1162
        return function () use ($namespace, $schema, $file) {
1163
            foreach (
1164
                $this->loadImportFreshCallbacks(
1165
                    $namespace,
1166
                    $schema,
1167
                    $file
1168
                ) as $callback
1169
            ) {
1170
                $callback();
1171
            }
1172
        };
1173
    }
1174
1175
    /**
1176
     * @var Schema|null
1177
     */
1178
    protected $globalSchema;
1179
1180
    /**
1181
     * @return string[]
1182
     */
1183
    public function getGlobalSchemaInfo(): array
1184
    {
1185
        return self::$globalSchemaInfo;
1186
    }
1187
1188
    /**
1189
     * @return Schema
1190
     */
1191
    public function getGlobalSchema(): Schema
1192
    {
1193
        if (!($this->globalSchema instanceof Schema)) {
1194
            $callbacks = array();
1195
            $globalSchemas = array();
1196
            /**
1197
             * @var string $namespace
1198
             */
1199
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
1200
                $this->setLoadedFile(
1201
                    $uri,
1202
                    $globalSchemas[$namespace] = $schema = new Schema()
1203
                );
1204
                if ($namespace === self::XSD_NS) {
1205
                    $this->globalSchema = $schema;
1206
                }
1207
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1208
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1209
            }
1210
1211
            $globalSchemas[(string) static::XSD_NS]->addType(new SimpleType($globalSchemas[(string) static::XSD_NS], 'anySimpleType'));
1212
            $globalSchemas[(string) static::XSD_NS]->addType(new SimpleType($globalSchemas[(string) static::XSD_NS], 'anyType'));
1213
1214
            $globalSchemas[(string) static::XML_NS]->addSchema(
1215
                $globalSchemas[(string) static::XSD_NS],
1216
                (string) static::XSD_NS
1217
            );
1218
            $globalSchemas[(string) static::XSD_NS]->addSchema(
1219
                $globalSchemas[(string) static::XML_NS],
1220
                (string) static::XML_NS
1221
            );
1222
1223
            /**
1224
             * @var Closure
1225
             */
1226
            foreach ($callbacks as $callback) {
1227
                $callback();
1228
            }
1229
        }
1230
1231
        /**
1232
         * @var Schema
1233
         */
1234
        $out = $this->globalSchema;
1235
1236
        return $out;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $out could return the type null which is incompatible with the type-hinted return GoetasWebservices\XML\XSDReader\Schema\Schema. Consider adding an additional type-check to rule them out.
Loading history...
1237
    }
1238
1239
    private function readNode(DOMElement $node, string $file = 'schema.xsd'): Schema
1240
    {
1241
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1242
        $this->setLoadedFile($fileKey, $rootSchema = new Schema());
1243
1244
        $rootSchema->addSchema($this->getGlobalSchema());
1245
        $callbacks = $this->schemaNode($rootSchema, $node);
1246
1247
        foreach ($callbacks as $callback) {
1248
            call_user_func($callback);
1249
        }
1250
1251
        return $rootSchema;
1252
    }
1253
1254
    /**
1255
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1256
     *
1257
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1258
     * file to distinguish between multiple schemas in a single file.
1259
     */
1260
    private function getNamespaceSpecificFileIndex(string $file, string $targetNamespace): string
1261
    {
1262
        return $file.'#'.$targetNamespace;
1263
    }
1264
1265
    /**
1266
     * @throws IOException
1267
     */
1268
    public function readString(string $content, string $file = 'schema.xsd'): Schema
1269
    {
1270
        $xml = new DOMDocument('1.0', 'UTF-8');
1271
        if (!$xml->loadXML($content)) {
1272
            throw new IOException("Can't load the schema");
1273
        }
1274
        $xml->documentURI = $file;
1275
1276
        return $this->readNode($xml->documentElement, $file);
1277
    }
1278
1279
    public function readFile(string $file): Schema
1280
    {
1281
        $xml = $this->getDOM($file);
1282
1283
        return $this->readNode($xml->documentElement, $file);
1284
    }
1285
1286
    /**
1287
     * @throws IOException
1288
     */
1289
    private function getDOM(string $file): DOMDocument
1290
    {
1291
        $xml = new DOMDocument('1.0', 'UTF-8');
1292
        if (!$xml->load($file)) {
1293
            throw new IOException("Can't load the file $file");
1294
        }
1295
1296
        return $xml;
1297
    }
1298
1299
    private static function againstDOMNodeList(
1300
        DOMElement $node,
1301
        Closure $againstNodeList
1302
    ) {
1303
        $limit = $node->childNodes->length;
1304
        for ($i = 0; $i < $limit; $i += 1) {
1305
            /**
1306
             * @var DOMNode
1307
             */
1308
            $childNode = $node->childNodes->item($i);
1309
1310
            if ($childNode instanceof DOMElement) {
1311
                $againstNodeList(
1312
                    $node,
1313
                    $childNode
1314
                );
1315
            }
1316
        }
1317
    }
1318
1319
    private function loadTypeWithCallback(
1320
        Schema $schema,
1321
        DOMElement $childNode,
1322
        Closure $callback
1323
    ) {
1324
        /**
1325
         * @var Closure|null $func
1326
         */
1327
        $func = null;
1328
1329
        switch ($childNode->localName) {
1330
            case 'complexType':
1331
                $func = $this->loadComplexType($schema, $childNode, $callback);
1332
                break;
1333
            case 'simpleType':
1334
                $func = $this->loadSimpleType($schema, $childNode, $callback);
1335
                break;
1336
        }
1337
1338
        if ($func instanceof Closure) {
1339
            call_user_func($func);
1340
        }
1341
    }
1342
1343
    private function loadElement(
1344
        Schema $schema,
1345
        DOMElement $node
1346
    ): Element {
1347
        $element = new Element($schema, $node->getAttribute('name'));
1348
        $element->setDoc($this->getDocumentation($node));
1349
1350
        $this->fillItem($element, $node);
1351
1352
        self::maybeSetMax($element, $node);
1353
        self::maybeSetMin($element, $node);
1354
1355
        $xp = new \DOMXPath($node->ownerDocument);
1356
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
1357
1358
        if ($xp->query('ancestor::xs:choice', $node)->length) {
1359
            $element->setMin(0);
1360
        }
1361
1362
        if ($node->hasAttribute('nillable')) {
1363
            $element->setNil($node->getAttribute('nillable') == 'true');
1364
        }
1365
        if ($node->hasAttribute('form')) {
1366
            $element->setQualified($node->getAttribute('form') == 'qualified');
1367
        }
1368
1369
        return $element;
1370
    }
1371
1372
    private function addAttributeFromAttributeOrRef(
1373
        BaseComplexType $type,
1374
        DOMElement $childNode,
1375
        Schema $schema,
1376
        DOMElement $node
1377
    ) {
1378
        $attribute = $this->getAttributeFromAttributeOrRef(
1379
            $childNode,
1380
            $schema,
1381
            $node
1382
        );
1383
1384
        $type->addAttribute($attribute);
1385
    }
1386
1387
    private function findSomethingLikeAttributeGroup(
1388
        Schema $schema,
1389
        DOMElement $node,
1390
        DOMElement $childNode,
1391
        AttributeContainer $addToThis
1392
    ) {
1393
        $attribute = $this->findAttributeGroup($schema, $node, $childNode->getAttribute('ref'));
1394
        $addToThis->addAttribute($attribute);
1395
    }
1396
1397
    private function setLoadedFile(string $key, Schema $schema): Schema
1398
    {
1399
        $this->loadedFiles[$key] = $schema;
1400
1401
        return $schema;
1402
    }
1403
1404
    private function setSchemaThingsFromNode(
1405
        Schema $schema,
1406
        DOMElement $node,
1407
        Schema $parent = null
1408
    ) {
1409
        $schema->setDoc($this->getDocumentation($node));
1410
1411
        if ($node->hasAttribute('targetNamespace')) {
1412
            $schema->setTargetNamespace($node->getAttribute('targetNamespace'));
1413
        } elseif ($parent instanceof Schema) {
1414
            $schema->setTargetNamespace($parent->getTargetNamespace());
1415
        }
1416
        $schema->setElementsQualification($node->getAttribute('elementFormDefault') == 'qualified');
1417
        $schema->setAttributesQualification($node->getAttribute('attributeFormDefault') == 'qualified');
1418
        $schema->setDoc($this->getDocumentation($node));
1419
    }
1420
}
1421