Passed
Push — static-analysis ( 67082a...81b9d9 )
by SignpostMarv
01:24
created

SchemaReader::loadImport()   C

Complexity

Conditions 11
Paths 7

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 26
nc 7
nop 2
dl 0
loc 43
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace GoetasWebservices\XML\XSDReader;
3
4
use Closure;
5
use DOMDocument;
6
use DOMElement;
7
use DOMNode;
8
use GoetasWebservices\XML\XSDReader\Exception\IOException;
9
use GoetasWebservices\XML\XSDReader\Exception\TypeException;
10
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Attribute;
11
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeDef;
12
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
13
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeRef;
14
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group as AttributeGroup;
15
use GoetasWebservices\XML\XSDReader\Schema\Element\Element;
16
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
17
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef;
18
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
19
use GoetasWebservices\XML\XSDReader\Schema\Element\ElementRef;
20
use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
21
use GoetasWebservices\XML\XSDReader\Schema\Element\GroupRef;
22
use GoetasWebservices\XML\XSDReader\Schema\Element\InterfaceSetMinMax;
23
use GoetasWebservices\XML\XSDReader\Schema\Exception\TypeNotFoundException;
24
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Base;
25
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Extension;
26
use GoetasWebservices\XML\XSDReader\Schema\Inheritance\Restriction;
27
use GoetasWebservices\XML\XSDReader\Schema\Item;
28
use GoetasWebservices\XML\XSDReader\Schema\Schema;
29
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
30
use GoetasWebservices\XML\XSDReader\Schema\Type\BaseComplexType;
31
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType;
32
use GoetasWebservices\XML\XSDReader\Schema\Type\ComplexTypeSimpleContent;
33
use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
34
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
35
use GoetasWebservices\XML\XSDReader\Utils\UrlUtils;
36
use RuntimeException;
37
38
class SchemaReader
39
{
40
41
    const XSD_NS = "http://www.w3.org/2001/XMLSchema";
42
43
    const XML_NS = "http://www.w3.org/XML/1998/namespace";
44
45
    /**
46
    * @var Schema[]
47
    */
48
    private $loadedFiles = array();
49
50
    /**
51
    * @var string[]
52
    */
53
    private $knownLocationSchemas = array();
54
55
    /**
56
    * @var string[]
57
    */
58
    private static $globalSchemaInfo = array(
59
        self::XML_NS => 'http://www.w3.org/2001/xml.xsd',
60
        self::XSD_NS => 'http://www.w3.org/2001/XMLSchema.xsd'
61
    );
62
63
    public function __construct()
64
    {
65
        $this->addKnownSchemaLocation('http://www.w3.org/2001/xml.xsd', __DIR__ . '/Resources/xml.xsd');
66
        $this->addKnownSchemaLocation('http://www.w3.org/2001/XMLSchema.xsd', __DIR__ . '/Resources/XMLSchema.xsd');
67
        $this->addKnownSchemaLocation('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', __DIR__ . '/Resources/oasis-200401-wss-wssecurity-secext-1.0.xsd');
68
        $this->addKnownSchemaLocation('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', __DIR__ . '/Resources/oasis-200401-wss-wssecurity-utility-1.0.xsd');
69
        $this->addKnownSchemaLocation('https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', __DIR__ . '/Resources/xmldsig-core-schema.xsd');
70
        $this->addKnownSchemaLocation('http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', __DIR__ . '/Resources/xmldsig-core-schema.xsd');
71
    }
72
73
    /**
74
    * @param string $remote
75
    * @param string $local
76
    */
77
    public function addKnownSchemaLocation($remote, $local)
78
    {
79
        $this->knownLocationSchemas[$remote] = $local;
80
    }
81
82
    /**
83
    * @return Closure
84
    */
85
    private function loadAttributeGroup(Schema $schema, DOMElement $node)
86
    {
87
        $attGroup = new AttributeGroup($schema, $node->getAttribute("name"));
88
        $attGroup->setDoc($this->getDocumentation($node));
89
        $schema->addAttributeGroup($attGroup);
90
91
        return function () use ($schema, $node, $attGroup) {
92
            foreach ($node->childNodes as $childNode) {
93
                switch ($childNode->localName) {
94
                    case 'attribute':
95
                        $attribute = $this->getAttributeFromAttributeOrRef(
96
                            $childNode,
97
                            $schema,
98
                            $node
99
                        );
100
                        $attGroup->addAttribute($attribute);
101
                        break;
102
                    case 'attributeGroup':
103
                        AttributeGroup::findSomethingLikeThis(
104
                            $this,
105
                            $schema,
106
                            $node,
107
                            $childNode,
108
                            $attGroup
109
                        );
110
                        break;
111
                }
112
            }
113
        };
114
    }
115
116
    /**
117
    * @return AttributeItem
118
    */
119
    private function getAttributeFromAttributeOrRef(
120
        DOMElement $childNode,
121
        Schema $schema,
122
        DOMElement $node
123
    ) {
124
        if ($childNode->hasAttribute("ref")) {
125
            /**
126
            * @var AttributeItem $attribute
127
            */
128
            $attribute = $this->findSomething('findAttribute', $schema, $node, $childNode->getAttribute("ref"));
129
        } else {
130
            /**
131
            * @var Attribute $attribute
132
            */
133
            $attribute = $this->loadAttribute($schema, $childNode);
134
        }
135
136
        return $attribute;
137
    }
138
139
    /**
140
    * @return Attribute
141
    */
142
    private function loadAttribute(Schema $schema, DOMElement $node)
143
    {
144
        $attribute = new Attribute($schema, $node->getAttribute("name"));
145
        $attribute->setDoc($this->getDocumentation($node));
146
        $this->fillItem($attribute, $node);
147
148
        if ($node->hasAttribute("nillable")) {
149
            $attribute->setNil($node->getAttribute("nillable") == "true");
150
        }
151
        if ($node->hasAttribute("form")) {
152
            $attribute->setQualified($node->getAttribute("form") == "qualified");
153
        }
154
        if ($node->hasAttribute("use")) {
155
            $attribute->setUse($node->getAttribute("use"));
156
        }
157
        return $attribute;
158
    }
159
160
    /**
161
    * @return Closure
162
    */
163
    private function loadAttributeDef(Schema $schema, DOMElement $node)
164
    {
165
        $attribute = new AttributeDef($schema, $node->getAttribute("name"));
166
167
        $schema->addAttribute($attribute);
168
169
        return function () use ($attribute, $node) {
170
            $this->fillItem($attribute, $node);
171
        };
172
    }
173
174
    /**
175
     * @param DOMElement $node
176
     * @return string
177
     */
178
    private function getDocumentation(DOMElement $node)
179
    {
180
        $doc = '';
181
        foreach ($node->childNodes as $childNode) {
182
            if ($childNode->localName == "annotation") {
183
                foreach ($childNode->childNodes as $subChildNode) {
184
                    if ($subChildNode->localName == "documentation") {
185
                        $doc .= ($subChildNode->nodeValue);
186
                    }
187
                }
188
            }
189
        }
190
        $doc = preg_replace('/[\t ]+/', ' ', $doc);
191
        return trim($doc);
192
    }
193
194
    private setSchemaPropertiesFromNode(
195
        Schema $schema,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $schema.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
196
        DOMElement $node,
197
        Schema $parent = null
198
    ) {
199
        $schema->setDoc($this->getDocumentation($node));
200
201
        if ($node->hasAttribute("targetNamespace")) {
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $node.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
202
            $schema->setTargetNamespace($node->getAttribute("targetNamespace"));
203
        } elseif ($parent) {
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $parent.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
204
            $schema->setTargetNamespace($parent->getTargetNamespace());
205
        }
206
        $schema->setElementsQualification($node->getAttribute("elementFormDefault") == "qualified");
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $schema.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
207
        $schema->setAttributesQualification($node->getAttribute("attributeFormDefault") == "qualified");
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $schema.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
208
        $schema->setDoc($this->getDocumentation($node));
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $schema.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
209
    }
210
211
    /**
212
     *
213
     * @param Schema $schema
214
     * @param DOMElement $node
215
     * @param Schema $parent
216
     * @return array
217
     */
218
    private function schemaNode(Schema $schema, DOMElement $node, Schema $parent = null)
219
    {
220
        $this->setSchemaPropertiesFromNode($schema, $node, $parent);
221
        $functions = array();
222
223
        foreach ($node->childNodes as $childNode) {
224
            switch ($childNode->localName) {
225
                case 'include':
226
                case 'import':
227
                    $functions[] = $this->loadImport($schema, $childNode);
228
                    break;
229
                case 'element':
230
                    $functions[] = $this->loadElementDef($schema, $childNode);
231
                    break;
232
                case 'attribute':
233
                    $functions[] = $this->loadAttributeDef($schema, $childNode);
234
                    break;
235
                case 'attributeGroup':
236
                    $functions[] = $this->loadAttributeGroup($schema, $childNode);
237
                    break;
238
                case 'group':
239
                    $functions[] = $this->loadGroup($schema, $childNode);
240
                    break;
241
                case 'complexType':
242
                    $functions[] = $this->loadComplexType($schema, $childNode);
243
                    break;
244
                case 'simpleType':
245
                    $functions[] = $this->loadSimpleType($schema, $childNode);
246
                    break;
247
            }
248
        }
249
250
        return $functions;
251
    }
252
253
    /**
254
    * @return Element
255
    */
256
    private function loadElement(Schema $schema, DOMElement $node)
257
    {
258
        $element = new Element($schema, $node->getAttribute("name"));
259
        $element->setDoc($this->getDocumentation($node));
260
261
        $this->fillItem($element, $node);
262
263
        static::maybeSetMax($element, $node);
264
        if ($node->hasAttribute("minOccurs")) {
265
            $element->setMin((int)$node->getAttribute("minOccurs"));
266
        }
267
268
        $xp = new \DOMXPath($node->ownerDocument);
269
        $xp->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema');
270
271
        if ($xp->query('ancestor::xs:choice', $node)->length) {
272
            $element->setMin(0);
273
        }
274
275
        if ($node->hasAttribute("nillable")) {
276
            $element->setNil($node->getAttribute("nillable") == "true");
277
        }
278
        if ($node->hasAttribute("form")) {
279
            $element->setQualified($node->getAttribute("form") == "qualified");
280
        }
281
        return $element;
282
    }
283
284
    /**
285
    * @return GroupRef
286
    */
287
    private function loadGroupRef(Group $referenced, DOMElement $node)
288
    {
289
        $ref = new GroupRef($referenced);
290
        $ref->setDoc($this->getDocumentation($node));
291
292
        static::maybeSetMax($ref, $node);
293
        if ($node->hasAttribute("minOccurs")) {
294
            $ref->setMin((int)$node->getAttribute("minOccurs"));
295
        }
296
297
        return $ref;
298
    }
299
300
    /**
301
    * @return ElementRef
302
    */
303
    private function loadElementRef(ElementDef $referenced, DOMElement $node)
304
    {
305
        $ref = new ElementRef($referenced);
306
        $this->setDoc($ref, $node);
307
308
        static::maybeSetMax($ref, $node);
309
        if ($node->hasAttribute("minOccurs")) {
310
            $ref->setMin((int)$node->getAttribute("minOccurs"));
311
        }
312
        if ($node->hasAttribute("nillable")) {
313
            $ref->setNil($node->getAttribute("nillable") == "true");
314
        }
315
        if ($node->hasAttribute("form")) {
316
            $ref->setQualified($node->getAttribute("form") == "qualified");
317
        }
318
319
        return $ref;
320
    }
321
322
    private function setDoc(Item $ref, DOMElement $node)
323
    {
324
        $ref->setDoc($this->getDocumentation($node));
325
    }
326
327
    private static function maybeSetMax(InterfaceSetMinMax $ref, DOMElement $node)
328
    {
329
        if (
330
            $node->hasAttribute("maxOccurs")
331
        ) {
332
            $ref->setMax($node->getAttribute("maxOccurs") == "unbounded" ? -1 : (int)$node->getAttribute("maxOccurs"));
333
        }
334
    }
335
336
    /**
337
    * @return AttributeRef
338
    */
339
    private function loadAttributeRef(AttributeDef $referencedAttribiute, DOMElement $node)
340
    {
341
        $attribute = new AttributeRef($referencedAttribiute);
342
        $this->setDoc($attribute, $node);
343
344
        if ($node->hasAttribute("nillable")) {
345
            $attribute->setNil($node->getAttribute("nillable") == "true");
346
        }
347
        if ($node->hasAttribute("form")) {
348
            $attribute->setQualified($node->getAttribute("form") == "qualified");
349
        }
350
        if ($node->hasAttribute("use")) {
351
            $attribute->setUse($node->getAttribute("use"));
352
        }
353
        return $attribute;
354
    }
355
356
    /**
357
    * @param int|null $max
358
    */
359
    private function loadSequence(ElementContainer $elementContainer, DOMElement $node, $max = null)
360
    {
361
        $max = $max || $node->getAttribute("maxOccurs") == "unbounded" || $node->getAttribute("maxOccurs") > 1 ? 2 : null;
362
363
        foreach ($node->childNodes as $childNode) {
364
365
            switch ($childNode->localName) {
366
                case 'choice':
367
                case 'sequence':
368
                case 'all':
369
                    $this->loadSequence($elementContainer, $childNode, $max);
370
                    break;
371
                case 'element':
372
                    if ($childNode->hasAttribute("ref")) {
373
                        /**
374
                        * @var ElementDef $referencedElement
375
                        */
376
                        $referencedElement = $this->findSomething('findElement', $elementContainer->getSchema(), $node, $childNode->getAttribute("ref"));
377
                        $element = $this->loadElementRef($referencedElement, $childNode);
378
                    } else {
379
                        $element = $this->loadElement($elementContainer->getSchema(), $childNode);
380
                    }
381
                    if ($max) {
382
                        $element->setMax($max);
383
                    }
384
                    $elementContainer->addElement($element);
385
                    break;
386
                case 'group':
387
                    /**
388
                    * @var Group $referencedGroup
389
                    */
390
                    $referencedGroup = $this->findSomething('findGroup', $elementContainer->getSchema(), $node, $childNode->getAttribute("ref"));
391
392
                    $group = $this->loadGroupRef($referencedGroup, $childNode);
393
                    $elementContainer->addElement($group);
394
                    break;
395
            }
396
        }
397
    }
398
399
    private function maybeLoadSequenceFromElementContainer(
400
        BaseComplexType $type,
401
        DOMElement $childNode
402
    ) {
403
        if (! ($type instanceof ElementContainer)) {
404
            throw new RuntimeException(
405
                '$type passed to ' .
406
                __FUNCTION__ .
407
                'expected to be an instance of ' .
408
                ElementContainer::class .
409
                ' when child node localName is "group", ' .
410
                get_class($type) .
411
                ' given.'
412
            );
413
        }
414
        $this->loadSequence($type, $childNode);
415
    }
416
417
    /**
418
    * @return Closure
419
    */
420
    private function loadGroup(Schema $schema, DOMElement $node)
421
    {
422
        $group = new Group($schema, $node->getAttribute("name"));
423
        $group->setDoc($this->getDocumentation($node));
424
425
        if ($node->hasAttribute("maxOccurs")) {
426
            static::maybeSetMax(new GroupRef($group), $node);
427
        }
428
        if ($node->hasAttribute("minOccurs")) {
429
            $group = new GroupRef($group);
430
            $group->setMin((int)$node->getAttribute("minOccurs"));
431
        }
432
433
        $schema->addGroup($group);
434
435
        return function () use ($group, $node) {
436
            foreach ($node->childNodes as $childNode) {
437
                switch ($childNode->localName) {
438
                    case 'sequence':
439
                    case 'choice':
440
                    case 'all':
441
                        $this->loadSequence($group, $childNode);
442
                        break;
443
                }
444
            }
445
        };
446
    }
447
448
    /**
449
    * @param Closure|null $callback
450
    *
451
    * @return Closure
452
    */
453
    private function loadComplexType(Schema $schema, DOMElement $node, $callback = null)
454
    {
455
        $isSimple = false;
456
457
        foreach ($node->childNodes as $childNode) {
458
            if ($childNode->localName === "simpleContent") {
459
                $isSimple = true;
460
                break;
461
            }
462
        }
463
464
        $type = $isSimple ? new ComplexTypeSimpleContent($schema, $node->getAttribute("name")) : new ComplexType($schema, $node->getAttribute("name"));
465
466
        $type->setDoc($this->getDocumentation($node));
467
        if ($node->getAttribute("name")) {
468
            $schema->addType($type);
469
        }
470
471
        return function () use ($type, $node, $schema, $callback) {
472
473
            $this->fillTypeNode($type, $node);
474
475
            foreach ($node->childNodes as $childNode) {
476
                switch ($childNode->localName) {
477
                    case 'sequence':
478
                    case 'choice':
479
                    case 'all':
480
                        $this->maybeLoadSequenceFromElementContainer(
481
                            $type,
482
                            $childNode
483
                        );
484
                        break;
485
                    case 'attribute':
486
                        $attribute = $this->getAttributeFromAttributeOrRef(
487
                            $childNode,
488
                            $schema,
489
                            $node
490
                        );
491
492
                        $type->addAttribute($attribute);
493
                        break;
494
                    case 'group':
495
                        if (! ($type instanceof ComplexType)) {
496
                            throw new RuntimeException(
497
                                '$type passed to ' .
498
                                __FUNCTION__ .
499
                                'expected to be an instance of ' .
500
                                ComplexType::class .
501
                                ' when child node localName is "group", ' .
502
                                get_class($type) .
503
                                ' given.'
504
                            );
505
                        }
506
507
                        /**
508
                        * @var Group $referencedGroup
509
                        */
510
                        $referencedGroup = $this->findSomething('findGroup', $schema, $node, $childNode->getAttribute("ref"));
511
                        $group = $this->loadGroupRef($referencedGroup, $childNode);
512
                        $type->addElement($group);
513
                        break;
514
                    case 'attributeGroup':
515
                        AttributeGroup::findSomethingLikeThis(
516
                            $this,
517
                            $schema,
518
                            $node,
519
                            $childNode,
520
                            $type
521
                        );
522
                        break;
523
                }
524
            }
525
526
            if ($callback) {
527
                call_user_func($callback, $type);
528
            }
529
        };
530
    }
531
532
    /**
533
    * @param Closure|null $callback
534
    *
535
    * @return Closure
536
    */
537
    private function loadSimpleType(Schema $schema, DOMElement $node, $callback = null)
538
    {
539
        $type = new SimpleType($schema, $node->getAttribute("name"));
540
        $type->setDoc($this->getDocumentation($node));
541
        if ($node->getAttribute("name")) {
542
            $schema->addType($type);
543
        }
544
545
        return function () use ($type, $node, $callback) {
546
            $this->fillTypeNode($type, $node);
547
548
            foreach ($node->childNodes as $childNode) {
549
                switch ($childNode->localName) {
550
                    case 'union':
551
                        $this->loadUnion($type, $childNode);
552
                        break;
553
                    case 'list':
554
                        $this->loadList($type, $childNode);
555
                        break;
556
                }
557
            }
558
559
            if ($callback) {
560
                call_user_func($callback, $type);
561
            }
562
        };
563
    }
564
565
    private function loadList(SimpleType $type, DOMElement $node)
566
    {
567
        if ($node->hasAttribute("itemType")) {
568
            /**
569
            * @var SimpleType $listType
570
            */
571
            $listType = $this->findSomeType($type, $node, 'itemType');
572
            $type->setList($listType);
573
        } else {
574
            $addCallback = function (SimpleType $list) use ($type) {
575
                $type->setList($list);
576
            };
577
578
            $this->loadTypeWithCallbackOnChildNodes(
579
                $type->getSchema(),
580
                $node,
581
                $addCallback
582
            );
583
        }
584
    }
585
586
    private function loadTypeWithCallbackOnChildNodes(
587
        Schema $schema,
588
        DOMNode $node,
589
        Closure $callback
590
    ) {
591
        foreach ($node->childNodes as $childNode) {
592
            $this->loadTypeWithCallback($schema, $childNode, $callback);
593
        }
594
    }
595
596
    private function loadTypeWithCallback(
597
        Schema $schema,
598
        DOMNode $childNode,
599
        Closure $callback
600
    ) {
601
        if (! ($childNode instanceof DOMElement)) {
602
            return;
603
        }
604
        switch ($childNode->localName) {
605
            case 'complexType':
606
                $childNode = $childNode;
607
                call_user_func(
608
                    $this->loadComplexType(
609
                        $schema,
610
                        $childNode,
611
                        $callback
612
                    )
613
                );
614
                break;
615
            case 'simpleType':
616
                call_user_func(
617
                    $this->loadSimpleType($schema, $childNode, $callback)
618
                );
619
                break;
620
        }
621
    }
622
623
    /**
624
    * @return SchemaItem
625
    */
626
    private function findSomeType(
627
        SchemaItem $fromThis,
628
        DOMElement $node,
629
        string $attributeName
630
    ) {
631
        return $this->findSomeTypeFromAttribute(
632
            $fromThis,
633
            $node,
634
            $node->getAttribute($attributeName)
635
        );
636
    }
637
638
    /**
639
    * @return SchemaItem
640
    */
641
    private function findSomeTypeFromAttribute(
642
        SchemaItem $fromThis,
643
        DOMElement $node,
644
        string $attributeName
645
    ) {
646
        /**
647
        * @var SchemaItem $out
648
        */
649
        $out = $this->findSomething(
650
            'findType',
651
            $fromThis->getSchema(),
652
            $node,
653
            $attributeName
654
        );
655
656
        return $out;
657
    }
658
659
    private function loadUnion(SimpleType $type, DOMElement $node)
660
    {
661
        if ($node->hasAttribute("memberTypes")) {
662
            $types = preg_split('/\s+/', $node->getAttribute("memberTypes"));
663
            foreach ($types as $typeName) {
664
                /**
665
                * @var SimpleType $unionType
666
                */
667
                $unionType = $this->findSomeTypeFromAttribute(
668
                    $type,
669
                    $node,
670
                    $typeName
671
                );
672
                $type->addUnion($unionType);
673
            }
674
        }
675
        $addCallback = function (SimpleType $unType) use ($type) {
676
            $type->addUnion($unType);
677
        };
678
679
        $this->loadTypeWithCallbackOnChildNodes(
680
            $type->getSchema(),
681
            $node,
682
            $addCallback
683
        );
684
    }
685
686
    /**
687
    * @param bool $checkAbstract
688
    */
689
    private function fillTypeNode(Type $type, DOMElement $node, $checkAbstract = true)
690
    {
691
692
        if ($checkAbstract) {
693
            $type->setAbstract($node->getAttribute("abstract") === "true" || $node->getAttribute("abstract") === "1");
694
        }
695
696
        foreach ($node->childNodes as $childNode) {
697
            switch ($childNode->localName) {
698
                case 'restriction':
699
                    $this->loadRestriction($type, $childNode);
700
                    break;
701
                case 'extension':
702
                    $this->maybeLoadExtensionFromBaseComplexType(
703
                        $type,
704
                        $childNode
705
                    );
706
                    break;
707
                case 'simpleContent':
708
                case 'complexContent':
709
                    $this->fillTypeNode($type, $childNode, false);
710
                    break;
711
            }
712
        }
713
    }
714
715
    private function loadExtension(BaseComplexType $type, DOMElement $node)
716
    {
717
        $extension = new Extension();
718
        $type->setExtension($extension);
719
720
        if ($node->hasAttribute("base")) {
721
            $this->findAndSetSomeBase(
722
                $type,
723
                $extension,
724
                $node
725
            );
726
        }
727
728
        foreach ($node->childNodes as $childNode) {
729
            switch ($childNode->localName) {
730
                case 'sequence':
731
                case 'choice':
732
                case 'all':
733
                    $this->maybeLoadSequenceFromElementContainer(
734
                        $type,
735
                        $childNode
736
                    );
737
                    break;
738
                case 'attribute':
739
                    $attribute = $this->getAttributeFromAttributeOrRef(
740
                        $childNode,
741
                        $type->getSchema(),
742
                        $node
743
                    );
744
                    $type->addAttribute($attribute);
745
                    break;
746
                case 'attributeGroup':
747
                    AttributeGroup::findSomethingLikeThis(
748
                        $this,
749
                        $type->getSchema(),
750
                        $node,
751
                        $childNode,
752
                        $type
753
                    );
754
                    break;
755
            }
756
        }
757
    }
758
759
    private function findAndSetSomeBase(
760
        Type $type,
761
        Base $setBaseOnThis,
762
        DOMElement $node
763
    ) {
764
        /**
765
        * @var Type $parent
766
        */
767
        $parent = $this->findSomeType($type, $node, 'base');
768
        $setBaseOnThis->setBase($parent);
769
    }
770
771
    private function maybeLoadExtensionFromBaseComplexType(
772
        Type $type,
773
        DOMElement $childNode
774
    ) {
775
        if (! ($type instanceof BaseComplexType)) {
776
            throw new RuntimeException(
777
                'Argument 1 passed to ' .
778
                __METHOD__ .
779
                ' needs to be an instance of ' .
780
                BaseComplexType::class .
781
                ' when passed onto ' .
782
                static::class .
783
                '::loadExtension(), ' .
784
                get_class($type) .
785
                ' given.'
786
            );
787
        }
788
        $this->loadExtension($type, $childNode);
789
    }
790
791
    private function loadRestriction(Type $type, DOMElement $node)
792
    {
793
        $restriction = new Restriction();
794
        $type->setRestriction($restriction);
795
        if ($node->hasAttribute("base")) {
796
            $this->findAndSetSomeBase($type, $restriction, $node);
797
        } else {
798
            $addCallback = function (Type $restType) use ($restriction) {
799
                $restriction->setBase($restType);
800
            };
801
802
            $this->loadTypeWithCallbackOnChildNodes(
803
                $type->getSchema(),
804
                $node,
805
                $addCallback
806
            );
807
        }
808
        foreach ($node->childNodes as $childNode) {
809
            if (in_array($childNode->localName,
810
                [
811
                    'enumeration',
812
                    'pattern',
813
                    'length',
814
                    'minLength',
815
                    'maxLength',
816
                    'minInclusive',
817
                    'maxInclusive',
818
                    'minExclusive',
819
                    'maxExclusive',
820
                    'fractionDigits',
821
                    'totalDigits',
822
                    'whiteSpace'
823
                ], true)) {
824
                $restriction->addCheck($childNode->localName,
825
                    [
826
                        'value' => $childNode->getAttribute("value"),
827
                        'doc' => $this->getDocumentation($childNode)
828
                    ]);
829
            }
830
        }
831
    }
832
833
    /**
834
    * @param string $typeName
835
    *
836
    * @return mixed[]
837
    */
838
    private static function splitParts(DOMElement $node, $typeName)
839
    {
840
        $namespace = null;
841
        $prefix = null;
842
        $name = $typeName;
843
        if (strpos($typeName, ':') !== false) {
844
            list ($prefix, $name) = explode(':', $typeName);
845
        }
846
847
        $namespace = $node->lookupNamespaceUri($prefix ?: '');
848
        return array(
849
            $name,
850
            $namespace,
851
            $prefix
852
        );
853
    }
854
855
    /**
856
     *
857
     * @param string $finder
858
     * @param Schema $schema
859
     * @param DOMElement $node
860
     * @param string $typeName
861
     * @throws TypeException
862
     * @return ElementItem|Group|AttributeItem|AttributeGroup|Type
863
     */
864
    public function findSomething($finder, Schema $schema, DOMElement $node, $typeName)
865
    {
866
        list ($name, $namespace) = self::splitParts($node, $typeName);
867
868
        $namespace = $namespace ?: $schema->getTargetNamespace();
869
870
        try {
871
            return $schema->$finder($name, $namespace);
872
        } catch (TypeNotFoundException $e) {
873
            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);
874
        }
875
    }
876
877
    /**
878
    * @return Closure
879
    */
880
    private function loadElementDef(Schema $schema, DOMElement $node)
881
    {
882
        $element = new ElementDef($schema, $node->getAttribute("name"));
883
        $schema->addElement($element);
884
885
        return function () use ($element, $node) {
886
            $this->fillItem($element, $node);
887
        };
888
    }
889
890
    private function fillItem(Item $element, DOMElement $node)
891
    {
892
        $localType = null;
893
        foreach ($node->childNodes as $childNode) {
894
            switch ($childNode->localName) {
895
                case 'complexType':
896
                case 'simpleType':
897
                    $localType = $childNode;
898
                    break 2;
899
            }
900
        }
901
902
        if ($localType) {
903
            $addCallback = function (Type $type) use ($element) {
904
                $element->setType($type);
905
            };
906
            $this->loadTypeWithCallback(
907
                $element->getSchema(),
908
                $localType,
909
                $addCallback
910
            );
911
        } else {
912
913
            if ($node->getAttribute("type")) {
914
                /**
915
                * @var Type $type
916
                */
917
                $type = $this->findSomeType($element, $node, 'type');
918
            } else {
919
                /**
920
                * @var Type $type
921
                */
922
                $type = $this->findSomeTypeFromAttribute(
923
                    $element,
924
                    $node,
925
                    ($node->lookupPrefix(self::XSD_NS) . ':anyType')
926
                );
927
            }
928
929
            $element->setType($type);
930
        }
931
    }
932
933
    /**
934
    * @return Closure
935
    */
936
    private function loadImport(Schema $schema, DOMElement $node)
937
    {
938
        $base = urldecode($node->ownerDocument->documentURI);
939
        $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute("schemaLocation"));
940
        if ($node->hasAttribute("namespace")
941
            && isset(self::$globalSchemaInfo[$node->getAttribute("namespace")])
942
            && isset($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]])
943
        ) {
944
945
            $schema->addSchema($this->loadedFiles[self::$globalSchemaInfo[$node->getAttribute("namespace")]]);
946
947
            return function () {
948
            };
949
        } elseif ($node->hasAttribute("namespace")
950
            && isset($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))])) {
951
            $schema->addSchema($this->loadedFiles[$this->getNamespaceSpecificFileIndex($file, $node->getAttribute("namespace"))]);
952
            return function () {
953
            };
954
        } elseif (isset($this->loadedFiles[$file])) {
955
            $schema->addSchema($this->loadedFiles[$file]);
956
            return function () {
957
            };
958
        }
959
960
        if (!$node->getAttribute("namespace")) {
961
            $this->loadedFiles[$file] = $newSchema = $schema;
962
        } else {
963
            $this->loadedFiles[$file] = $newSchema = new Schema();
964
            $newSchema->addSchema($this->getGlobalSchema());
965
        }
966
967
        $xml = $this->getDOM(isset($this->knownLocationSchemas[$file]) ? $this->knownLocationSchemas[$file] : $file);
968
969
        $callbacks = $this->schemaNode($newSchema, $xml->documentElement, $schema);
970
971
        if ($node->getAttribute("namespace")) {
972
            $schema->addSchema($newSchema);
973
        }
974
975
976
        return function () use ($callbacks) {
977
            foreach ($callbacks as $callback) {
978
                call_user_func($callback);
979
            }
980
        };
981
    }
982
983
    /**
984
    * @var Schema|null
985
    */
986
    private $globalSchema;
987
988
    /**
989
     *
990
     * @return Schema
991
     */
992
    public function getGlobalSchema()
993
    {
994
        if (!$this->globalSchema) {
995
            $callbacks = array();
996
            $globalSchemas = array();
997
            foreach (self::$globalSchemaInfo as $namespace => $uri) {
998
                $this->loadedFiles[$uri] = $globalSchemas [$namespace] = $schema = new Schema();
999
                if ($namespace === self::XSD_NS) {
1000
                    $this->globalSchema = $schema;
1001
                }
1002
                $xml = $this->getDOM($this->knownLocationSchemas[$uri]);
1003
                $callbacks = array_merge($callbacks, $this->schemaNode($schema, $xml->documentElement));
1004
            }
1005
1006
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anySimpleType"));
1007
            $globalSchemas[self::XSD_NS]->addType(new SimpleType($globalSchemas[self::XSD_NS], "anyType"));
1008
1009
            $globalSchemas[self::XML_NS]->addSchema($globalSchemas[self::XSD_NS], self::XSD_NS);
1010
            $globalSchemas[self::XSD_NS]->addSchema($globalSchemas[self::XML_NS], self::XML_NS);
1011
1012
            foreach ($callbacks as $callback) {
1013
                $callback();
1014
            }
1015
        }
1016
1017
        /**
1018
        * @var Schema $out
1019
        */
1020
        $out = $this->globalSchema;
1021
1022
        return $out;
1023
    }
1024
1025
    /**
1026
     * @param DOMElement $node
1027
     * @param string  $file
1028
     *
1029
     * @return Schema
1030
     */
1031
    public function readNode(DOMElement $node, $file = 'schema.xsd')
1032
    {
1033
        $fileKey = $node->hasAttribute('targetNamespace') ? $this->getNamespaceSpecificFileIndex($file, $node->getAttribute('targetNamespace')) : $file;
1034
        $this->loadedFiles[$fileKey] = $rootSchema = new Schema();
1035
1036
        $rootSchema->addSchema($this->getGlobalSchema());
1037
        $callbacks = $this->schemaNode($rootSchema, $node);
1038
1039
        foreach ($callbacks as $callback) {
1040
            call_user_func($callback);
1041
        }
1042
1043
        return $rootSchema;
1044
    }
1045
1046
    /**
1047
     * It is possible that a single file contains multiple <xsd:schema/> nodes, for instance in a WSDL file.
1048
     *
1049
     * Each of these  <xsd:schema/> nodes typically target a specific namespace. Append the target namespace to the
1050
     * file to distinguish between multiple schemas in a single file.
1051
     *
1052
     * @param string $file
1053
     * @param string $targetNamespace
1054
     *
1055
     * @return string
1056
     */
1057
    private function getNamespaceSpecificFileIndex($file, $targetNamespace)
1058
    {
1059
        return $file . '#' . $targetNamespace;
1060
    }
1061
1062
    /**
1063
     * @param string $content
1064
     * @param string $file
1065
     *
1066
     * @return Schema
1067
     *
1068
     * @throws IOException
1069
     */
1070
    public function readString($content, $file = 'schema.xsd')
1071
    {
1072
        $xml = new DOMDocument('1.0', 'UTF-8');
1073
        if (!$xml->loadXML($content)) {
1074
            throw new IOException("Can't load the schema");
1075
        }
1076
        $xml->documentURI = $file;
1077
1078
        return $this->readNode($xml->documentElement, $file);
1079
    }
1080
1081
    /**
1082
     * @param string $file
1083
     *
1084
     * @return Schema
1085
     */
1086
    public function readFile($file)
1087
    {
1088
        $xml = $this->getDOM($file);
1089
        return $this->readNode($xml->documentElement, $file);
1090
    }
1091
1092
    /**
1093
     * @param string $file
1094
     *
1095
     * @return DOMDocument
1096
     *
1097
     * @throws IOException
1098
     */
1099
    private function getDOM($file)
1100
    {
1101
        $xml = new DOMDocument('1.0', 'UTF-8');
1102
        if (!$xml->load($file)) {
1103
            throw new IOException("Can't load the file $file");
1104
        }
1105
        return $xml;
1106
    }
1107
}
1108