diff --git a/src/SchemaReader.php b/src/SchemaReader.php
index a1bf4846..42c06a4d 100644
--- a/src/SchemaReader.php
+++ b/src/SchemaReader.php
@@ -277,6 +277,9 @@ function (
case 'import':
$callback = $this->loadImport($schema, $childNode);
break;
+ case 'redefine':
+ $callback = $this->loadRedefine($schema, $childNode);
+ break;
case 'element':
$callback = $this->loadElementDef($schema, $childNode);
break;
@@ -1170,6 +1173,32 @@ private function loadImport(
return $this->loadImportFresh($namespace, $schema, $file);
}
+ private function loadRedefine(Schema $schema, DOMElement $node): Closure
+ {
+ $base = urldecode($node->ownerDocument->documentURI);
+ $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute('schemaLocation'));
+
+ if (isset($this->loadedFiles[$file])) {
+ /* @var $redefined Schema */
+ $redefined = clone $this->loadedFiles[$file];
+
+ if ($schema->getTargetNamespace() !== $redefined->getTargetNamespace()) {
+ $redefined->setTargetNamespace($schema->getTargetNamespace());
+ }
+
+ $schema->addSchema($redefined);
+
+ return function () use ($redefined, $node, $schema): void {
+ $callbacks = $this->schemaNode($redefined, $node, $schema);
+ foreach ($callbacks as $callback) {
+ $callback();
+ }
+ };
+ }
+
+ return $this->loadImportFresh((string) $schema->getTargetNamespace(), $schema, $file);
+ }
+
private function createOrUseSchemaForNs(
Schema $schema,
string $namespace
diff --git a/tests/RedefineTest.php b/tests/RedefineTest.php
new file mode 100644
index 00000000..d2782555
--- /dev/null
+++ b/tests/RedefineTest.php
@@ -0,0 +1,145 @@
+reader->readString(
+ '
+
+
+
+
+
+
+
+
+
+ ', 'http://www.example.com/xsd.xsd');
+
+ $schema = $this->reader->readString(
+ '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ');
+
+ // check if schema is not included
+ // we don't want to redefine original schema
+ $this->assertNotContains($remoteSchema, $schema->getSchemas(), '', true);
+
+ /* @var $localAttr \GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef */
+
+ // it should inherit namespace of main schema
+ $localAttr = $schema->findElement('addressee', 'http://www.user.com');
+ $this->assertNotNull($localAttr);
+
+ // find author element
+ $localAttr = $schema->findElement('author', 'http://www.user.com');
+ $this->assertNotNull($localAttr);
+
+ /* @var $type \GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType */
+ $type = $localAttr->getType();
+
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $type);
+
+ $children = array();
+ foreach ($type->getElements() as $element) {
+ $children[] = $element->getName();
+ }
+
+ $this->assertContains('generation', $children);
+ }
+
+ public function testReadSchemaLocation()
+ {
+ $schema = $this->reader->readFile(__DIR__.'/schema/extend-components.xsd');
+ $this->assertInstanceOf(Schema::class, $schema);
+
+ $this->assertEquals('spec:example:xsd:CommonBasicComponents-1.0', $schema->getTargetNamespace());
+
+ // defined in /schema/base-components.xsd
+ $dateElement = $schema->findElement('Date', 'spec:example:xsd:CommonBasicComponents-1.0');
+ $this->assertNotNull($dateElement);
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $dateElement);
+ $type = $dateElement->getType();
+ $this->assertEquals('DateType', $type->getName());
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $type);
+
+ $dateType = $schema->findType('DateType', 'spec:example:xsd:CommonBasicComponents-1.0');
+ $this->assertNotNull($dateType);
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $dateType);
+
+ // defined in /schema/extend-components.xsd
+ $deliveryDateElement = $schema->findElement('DeliveryDate', 'spec:example:xsd:CommonBasicComponents-1.0');
+ $this->assertNotNull($deliveryDateElement);
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $deliveryDateElement);
+ $type = $deliveryDateElement->getType();
+ $this->assertEquals('DateType', $type->getName());
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $type);
+ }
+
+ /**
+ * Ensure Semantics of are the same as described in the XSD specification
+ * @link https://www.w3.org/TR/xmlschema11-1/#modify-schema
+ */
+ public function testRedefineSemantics()
+ {
+ $schema = $this->reader->readFile(__DIR__.'/schema/extend-xsd-v2.xsd');
+ $this->assertInstanceOf(Schema::class, $schema);
+
+ // Definition from https://www.w3.org/TR/xmlschema11-1/#modify-schema
+
+ // The schema corresponding to v2.xsd has everything specified by v1.xsd, with the personName type redefined,
+ // as well as everything it specifies itself.
+ // According to this schema, elements constrained by the personName type may end with a generation element.
+ // This includes not only the author element, but also the addressee element.
+ $author = $schema->findElement('author');
+ $addressee = $schema->findElement('addressee');
+ $this->assertNotNull($author);
+ $this->assertNotNull($addressee);
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $author);
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $addressee);
+ $authorType = $author->getType();
+ $addresseeType = $addressee->getType();
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $authorType);
+ $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $addresseeType);
+
+ $this->assertEquals('personName', $authorType->getName());
+ $this->assertEquals('personName', $addresseeType->getName());
+
+ // ensure both types contain the same elements
+ foreach([$authorType, $addresseeType] as $type) {
+ /** @var $type \GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType */
+
+ $elements = $type->getElements();
+ $elementNames = array_map(function(Element $element) { return $element->getName(); }, $elements);
+ sort($elementNames);
+ $this->assertEquals(['forename', 'generation', 'title'], $elementNames);
+ }
+
+
+ //
+ // For any document D2 pointed at by a element in D1, it must be the case either (a) that tns(D1) = tns(D2) or else (b) that tns(D2) is ·absent·, in which case schema(D1) includes not redefine(E,schema(D2)) itself but redefine(E,schema(chameleon(tns(D1),D2))). That is, the redefinition pre-processing is applied not to the schema corresponding to D2 but instead to the schema corresponding to the schema document chameleon(tns(D1),D2), which is the result of applying chameleon pre-processing to D2 to convert it to target namespace tns(D1).
+
+ }
+}
diff --git a/tests/schema/base-components.xsd b/tests/schema/base-components.xsd
new file mode 100644
index 00000000..50a9d536
--- /dev/null
+++ b/tests/schema/base-components.xsd
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/schema/extend-components.xsd b/tests/schema/extend-components.xsd
new file mode 100644
index 00000000..290f3205
--- /dev/null
+++ b/tests/schema/extend-components.xsd
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ Lieferdatum
+
+
+
+ Indikator
+
+
+
\ No newline at end of file
diff --git a/tests/schema/redefine-xsd-v1.xsd b/tests/schema/redefine-xsd-v1.xsd
new file mode 100644
index 00000000..76a8edb0
--- /dev/null
+++ b/tests/schema/redefine-xsd-v1.xsd
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/schema/redefine-xsd-v2.xsd b/tests/schema/redefine-xsd-v2.xsd
new file mode 100644
index 00000000..a0cb5144
--- /dev/null
+++ b/tests/schema/redefine-xsd-v2.xsd
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+