goetas-webservices/xsd-reader
Reads XML Schema (XSD) documents into a clean PHP object model, handling includes, imports, simple and complex types, and namespaces. A lightweight foundation for WSDL readers, SOAP clients, and code generators.
Install the reader and confirm the DOM extension is available:
composer require goetas-webservices/xsd-reader
The first entry point is GoetasWebservices\Xml\XsdReader\SchemaReader. Load a file with readFile() when the schema has relative imports/includes, because the reader can resolve them relative to the document location:
use GoetasWebservices\Xml\XsdReader\SchemaReader;
$reader = new SchemaReader();
$schema = $reader->readFile(__DIR__ . '/xsd/order.xsd');
printf("Target namespace: %s\n", $schema->getTargetNamespace());
foreach ($schema->getElements() as $element) {
printf(
"element %s : %s\n",
$element->getName(),
$element->getType()?->getName() ?? 'anonymous/inline'
);
}
Look first at these classes:
SchemaReader — file/string loading and import resolution.Schema\Schema — target namespace, root elements, types, attributes, groups.Schema\Type\ComplexType / Schema\Type\SimpleType — type structure.Schema\Element\Element and Schema\Attribute\Attribute — declarations and references.A good first use case is schema inventory: point the reader at a vendor XSD, list root elements and types, and confirm namespaces and imports resolve as expected. Once that works, feed the same Schema objects into goetas-webservices/xsd2php for PHP class generation or into wsdl-reader for SOAP/WSDL tooling.
Namespace-aware lookup, not name-only lookup. XSDs routinely have the same local name in different namespaces. Use Clark-notation keys such as {urn:example}OrderType for your own maps, and pass the namespace explicitly when using finder methods:
$element = $schema->findElement('PurchaseOrder', 'urn:example:orders');
$type = $element?->getType();
$key = '{' . $type->getSchema()->getTargetNamespace() . '}' . $type->getName();
If finder methods return null, fall back to iterating getTypes()/getElements() and comparing both namespace and local name.
Build a type registry once per request. For code generation, validation metadata, or hydration maps, walk the schema graph and build a stable registry instead of repeatedly searching:
$types = [];
foreach ($schema->getTypes() as $type) {
$ns = $type->getSchema()->getTargetNamespace() ?? '';
$key = '{' . $ns . '}' . $type->getName();
$types[$key] = $type;
}
For each ComplexType, inspect getElements(), getAttributes(), and group/attribute-group references. For SimpleType, inspect restriction/list/union structure. Preserve the base type when following getExtension() or getRestriction() so generators can model inheritance instead of flattening everything.
Use the companion ecosystem rather than rebuilding generators. This package is a reader, not a code generator or validator. The highest-leverage workflow is:
SchemaReader.xsd2php to convert the object model into PHP classes and serialization metadata.wsdl-reader when the real input is a WSDL and you only need schema-level introspection for parts.This keeps your application code focused on consuming generated classes, while the reader handles imports, includes, namespaces, and type traversal.
Mirror remote schemas locally before parsing. Vendor XSDs often import from HTTP URLs. In CI and production, download the schema tree once into a directory, preserve relative paths, and call readFile() against the local copy. This avoids network dependencies during builds and makes import failures reproducible.
Normalize before generation. Anonymous inline complex types are common. If you are producing class names or metadata keys, assign synthetic stable names based on the parent element/type path. Do the normalization after reading, not by string-editing the XSD, so the reader’s namespace resolution stays intact.
Use extension callbacks for custom schema handling. The reader advertises extensible callbacks for custom schema handling. Use them for narrow concerns: rewriting a vendor schemaLocation, mapping a remote namespace to a local file, or post-processing a schema before traversal. Keep callbacks deterministic and side-effect free; they are easy to make global accidentally.
Cache the extracted metadata, not the object graph. The Schema objects are convenient to traverse but are tied to parsed XML state. Cache a plain array/JSON representation of what you need—element names, type names, namespaces, base types, cardinalities—or cache generated PHP files. Re-reading a small XSD is usually cheaper and safer than trying to serialize the object model.
readString() loses the base path. Relative import/include locations cannot be resolved reliably from an in-memory string. Prefer readFile(), or rewrite all schemaLocation values to absolute paths before calling readString().targetNamespace inherits the includer’s namespace. Do not cache solely by target namespace; include the schema location in your cache key.realpath() or a canonical URI as the cache key when you build your own schema registry.elementFormDefault and attributeFormDefault affect whether local declarations are in the target namespace. If you write custom traversal code, check qualification rather than assuming every name is namespace-qualified.getType() can be null or unresolved. Handle references, inline anonymous types, and unresolved external types defensively. A null type should produce a clear diagnostic with the element name and namespace, not a fatal error deep in a generator.DOMDocument::schemaValidate() or a dedicated validator.getTargetNamespace() and the class names returned by getTypes() before debugging generation logic.composer.json and run your schema fixtures in CI.group and attributeGroup references can be expanded during generation or preserved as structures. Pick one strategy early; mixing both usually produces duplicate or missing properties.How can I help you explore Laravel packages today?