Generate PHP classes from an XSD schema.
TL;DR:
(new Generator())
->defaultDirectory(__DIR__ . 'src/Generated')
->defaultNamespace('MakinaCorpus\\XsdGen\\Tests\\Generated')
->namespace('http://schemas.makina-corpus.com/testing/common/1.0', 'MakinaCorpus\\Common')
->propertyGetter(true)
->propertyPromotion(true)
->propertyPublic(false)
->propertyReadonly(false)
->propertySetter(false)
->logger(new EchoLogger())
->file(__DIR__ . '/resources/xsd/some-file.xsd')
->generate()
;
... will give you working PHP code replicating XSD defined data structure.
Future plan is mostly to have a complete end-to-end SOAP exchange implementation for production projects.
Note: all examples in the following section removes some generated additional static methods that exists for object hydration and serialization purpose.
This API generates PHP classes from a given XSD schema.
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="https://schemas.makina-corpus.com/testing/inheritance"
targetNamespace="https://schemas.makina-corpus.com/testing/inheritance">
<xsd:complexType name="ShadowedClass">
<xsd:annotation>
<xsd:documentation>This class has shadowed properties.</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="shadowedCovariant" type="Address" minOccurs="1">
<xsd:annotation>
<xsd:documentation>This property is shadowed by a covariant type.</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="shadowedIncompatible" type="Address" minOccurs="1">
<xsd:annotation>
<xsd:documentation>This property is shadowed but is not compatible.</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="nonShadowedOther" type="Address">
<xsd:annotation>
<xsd:documentation>This property is not shadowed.</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Defaults\Inheritance;
class ShadowedClass
{
/**
* This property is shadowed by a covariant type.
*/
private readonly Address $shadowedCovariant;
/**
* This property is shadowed but is not compatible.
*/
private readonly Address $shadowedIncompatible;
/**
* This property is not shadowed.
*/
private readonly Address $nonShadowedOther;
public function __construct(
Address $shadowedCovariant,
Address $shadowedIncompatible,
Address $nonShadowedOther
) {
$this->shadowedCovariant = $shadowedCovariant;
$this->shadowedIncompatible = $shadowedIncompatible;
$this->nonShadowedOther = $nonShadowedOther;
}
public function getShadowedCovariant(): Address
{
return $this->shadowedCovariant;
}
public function getShadowedIncompatible(): Address
{
return $this->shadowedIncompatible;
}
public function getNonShadowedOther(): Address
{
return $this->nonShadowedOther;
}
}
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Legacy\Inheritance;
class ShadowedClass
{
public function __construct(
/**
* This property is shadowed by a covariant type.
*/
public readonly Address $shadowedCovariant,
/**
* This property is shadowed but is not compatible.
*/
public readonly Address $shadowedIncompatible,
/**
* This property is not shadowed.
*/
public readonly Address $nonShadowedOther
) {}
}
<xsd:restriction>
is type inheritance with properties override. And that's
what will do the generated PHP output.
Note: when a type restricts another, and change its properties, if property
definition is not covariant, generated PHP code may be invalid. In order to
avoid this from being a syntax error, set the property_public
to false
,
which makes all properties private and avoid covariance problems.
Tip: you probably want to set property_getter
to true
as well otherwise
you will not be able to read your properties.
Note: when using constructor property promotion and public properties altogether, redifining properties in a child class will raise PHP fatal errors at compile time. In order to prevent this happen, when this condition is fulfilled, the child class property will silentely removed.
<!-- ... -->
<xsd:complexType name="ShadowingClass">
<xsd:annotation>
<xsd:documentation>This class shadows properties.</xsd:documentation>
</xsd:annotation>
<xsd:complexContent>
<xsd:restriction base="ShadowedClass">
<xsd:sequence>
<xsd:element name="shadowedCovariant" type="FrenchAddress" minOccurs="1">
<xsd:annotation>
<xsd:documentation>This property shadows the parent one, and is covariant.</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="shadowedIncompatible" type="xsd:date" minOccurs="1">
<xsd:annotation>
<xsd:documentation>This property shadows the parent one, but is not covariant.</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
class ShadowingClass extends ShadowedClass
{
/**
* This property shadows the parent one, and is covariant.
*/
private readonly FrenchAddress $shadowedCovariant;
/**
* This property shadows the parent one, but is not covariant.
*/
private readonly \DateTimeImmutable $shadowedIncompatible;
public function __construct(
/** Inherited property. */
Address $nonShadowedOther,
FrenchAddress $shadowedCovariant,
\DateTimeImmutable $shadowedIncompatible
) {
$this->shadowedCovariant = $shadowedCovariant;
$this->shadowedIncompatible = $shadowedIncompatible;
parent::__construct(
shadowedCovariant: $shadowedCovariant,
shadowedIncompatible: $shadowedIncompatible,
nonShadowedOther: $nonShadowedOther,
);
}
public function getShadowedCovariant(): FrenchAddress
{
return $this->shadowedCovariant;
}
public function getShadowedIncompatible(): \DateTimeImmutable
{
return $this->shadowedIncompatible;
}
}
As you can see, the private $shadowedCovariant
and $shadowedIncompatible
properties are allowed to shadow their parent definitions, which is therefore
completely hidden now.
Warning: in this example, the $shadowedIncompatible
will cause PHP errors
because the type is not covariant, there is no way around this.
An alternative method would have been to merge classes with their parent definition in order to entirely drop the inheritance in the benefit of one huge class.
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Legacy\Inheritance;
class ShadowingClass extends ShadowedClass
{
public function __construct(
/** Inherited property. */
Address $shadowedCovariant,
/** Inherited property. */
Address $shadowedIncompatible,
/** Inherited property. */
Address $nonShadowedOther,
) {
parent::__construct(
shadowedCovariant: $shadowedCovariant,
shadowedIncompatible: $shadowedIncompatible,
nonShadowedOther: $nonShadowedOther,
);
}
}
As you can see, the private $shadowedCovariant
and $shadowedIncompatible
properties are not redefined because their are public and it would cause
fatal errors in case of incompatible types.
<xsd:extension>
allows type inheritance. And that's what will do the
generated PHP output.
<!-- ... -->
<xsd:complexType name="FrenchAddressWithPhone">
<xsd:annotation>
<xsd:documentation>Uses "xsd:extension" and add properties.</xsd:documentation>
</xsd:annotation>
<xsd:complexContent>
<xsd:extension base="FrenchAddress">
<xsd:sequence>
<xsd:element name="PhoneNumber" type="xsd:string" minOccurs="0"></xsd:element>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
Parent class:
class Address
{
/**
* A basic "xsd:string'.
*/
private readonly string $addressLine;
/**
* A basic "xsd:string'.
*/
private readonly ?string $country;
public function __construct(
string $addressLine,
?string $country,
) {
$this->addressLine = $addressLine;
$this->country = $country;
}
public function getAddressLine(): string
{
return $this->addressLine;
}
public function getCountry(): ?string
{
return $this->country;
}
}
Child class:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Defaults\Inheritance;
class AddressAndPhone extends Address
{
/**
* Additional property on extended type.
*/
private readonly ?string $phoneNumber;
public function __construct(
/** Inherited property. */
string $addressLine,
/** Inherited property. */
?string $country,
?string $phoneNumber,
) {
$this->phoneNumber = $phoneNumber;
parent::__construct(
addressLine: $addressLine,
country: $country,
);
}
public function getPhoneNumber(): ?string
{
return $this->phoneNumber;
}
}
Parent class:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Modern\Inheritance;
class Address
{
public function __construct(
/**
* A basic "xsd:string'.
*/
public readonly string $addressLine,
/**
* A basic "xsd:string'.
*/
public readonly ?string $country,
) {}
}
Child class:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Modern\Inheritance;
class AddressAndPhone extends Address
{
public function __construct(
/** Inherited property. */
string $addressLine,
/** Inherited property. */
?string $country,
/**
* Additional property on extended type.
*/
public readonly ?string $phoneNumber
) {
parent::__construct(
addressLine: $addressLine,
country: $country,
);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="https://schemas.makina-corpus.com/testing/inheritance" targetNamespace="https://schemas.makina-corpus.com/testing/inheritance" elementFormDefault="unqualified" attributeFormDefault="unqualified">
<xsd:complexType name="Address">
<xsd:annotation>
<xsd:documentation>Some random address</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="AddressLine" type="xsd:string" minOccurs="1">
<xsd:annotation>
<xsd:documentation>A basic "xsd:string'.</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="Country" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>A basic "xsd:string'.</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="RussianDollExample">
<xsd:annotation>
<xsd:documentation>This class shadows properties.</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="typeReference" type="Address">
<xsd:annotation><xsd:documentation>This is an existing complex type reference.</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="complexProperty">
<xsd:annotation><xsd:documentation>This is a russian doll complex type.</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="arbitraryProperty" type="xsd:string">
<xsd:annotation><xsd:documentation>This property is inside the internal type.</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
Named complex type:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Defaults\Inheritance;
class RussianDollExample
{
/**
* This is an existing complex type reference.
*/
private readonly Address $typeReference;
/**
* This is a russian doll complex type.
*/
private readonly RussianDollExample_complexProperty $complexProperty;
public function __construct(
Address $typeReference,
RussianDollExample_complexProperty $complexProperty
) {
$this->typeReference = $typeReference;
$this->complexProperty = $complexProperty;
}
public function getTypeReference(): Address
{
return $this->typeReference;
}
public function getComplexProperty(): RussianDollExample_complexProperty
{
return $this->complexProperty;
}
}
Internal complex property:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Defaults\Inheritance;
class RussianDollExample_complexProperty
{
/**
* This property is inside the internal type.
*/
private readonly string $arbitraryProperty;
public function __construct(string $arbitraryProperty)
{
$this->arbitraryProperty = $arbitraryProperty;
}
public function getArbitraryProperty(): string
{
return $this->arbitraryProperty;
}
}
As you can see, for the existing Address
type, a simple property using
the existing class is written.
For the nested complex type however, a RussianDollExample_complexProperty
new class is created.
Nested complex properties can nest complex properties themselves. Name will
always be ClassName_ComplexPropertyName
.
Named complex type:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Modern\Inheritance;
class RussianDollExample
{
public function __construct(
/**
* This is an existing complex type reference.
*/
public readonly Address $typeReference,
/**
* This is a russian doll complex type.
*/
public readonly RussianDollExample_complexProperty $complexProperty
) {}
}
Internal complex property:
<?php
namespace MakinaCorpus\XsdGen\Tests\Generated\Modern\Inheritance;
class RussianDollExample_complexProperty
{
public function __construct(
/**
* This property is inside the internal type.
*/
public readonly string $arbitraryProperty
) {}
}
Type restrictions are not implemented yet. They might be in the future but they will require extensive genereted guard code which is out of scope right now.
As of now, simple types will be arbitrarily replaced with PHP equivalents when available, the current conversion map is the following:
xsd:anyURI
:string
xsd:base64Binary
:string
xsd:boolean
:bool
xsd:date
:\DateTimeImmutable
,xsd:dateTime
:\DateTimeImmutable
,xsd:decimal
:float
xsd:double
:float
xsd:duration
:\DateInterval
,xsd:float
:float
xsd:gDay
:int
xsd:gMonth
:int
xsd:gMonthDay
:int
xsd:gYear
:int
xsd:gYearMonth
:int
xsd:hexBinary
:string
xsd:integer
:int
xsd:normalizedString
:string
xsd:NOTATION
:string
xsd:Qname
:string
xsd:string
:string
xsd:time
:string
It is planned to make simple types behaviour pluggable.
Enums are not implemented yet. It is a planned feature.
When spawning this API, you need to set two importants parameters:
-
default_namespace
: is the PSR-4 namespace prefix for all your generated code, if you need to split files in more specialized namespaces, read the documentation below. For example, you may setYourVendor\YourApp\Generated
in here. -
default_directory
: is the root folder for the PSR-4 namespace you chose. For example if your existing/path/to/project/src/
folder is the root for theYourVendor\YourApp
namespace, you probably want to use the/path/to/project/src/Generated
value.
Now consider the following XSD namespace:
https://schemas.makina-corpus.com/testing/inheritance
If you choose not to configure it further, this URI will be converted to the following namespace infix:
Schemas\Makina\Corpus\Com\Testing\Inheritance
Then the PSR-4 prefix will be applied, final class namespace will then be:
YourVendor\YourApp\Generated\Schemas\Makina\Corpus\Com\Testing\Inheritance`
Considering that raw namespace URI conversion to a PHP namespace is arbitrary and probably will not fit your need, you can choose to change on a per URI prefix basis the target PSR-4 namespace.
Here, we could, for example, set the namespaces
variable to the following:
default_namespace: YourVendor\YourApp\Generated
default_directory: /path/to/project/src/Generated
namespaces:
"https://schemas.makina-corpus.com/testing": MakinaCorpus\Testing
The given URI will match, and trailing namespace remaining will simply
be /inheritance
. It then converts to the following fully qualifed PHP
namespace name:
YourVendor\YourApp\Generated\MakinaCorpus\Testing\Inheritance`
Note: the namespaces you configure will always be a namespace infix, relative to the default PSR-4 prefix.
You can set as many namespaces as you wish, they will be evuluated in order, consider writing starting with the most specific and ending with the least specific namespace URI prefix.
On any node parsed, additional aliased namespaces will be parsed, registered and looked-up for type dependencies resolution. As expected, when a namespace is defined and aliased at a node level, only this node and its children will be able to resolve this namespace alias.
Remote URI resource fetching is not implemented yet. It is planned.
When the namespace resource URI is a local file, it will be fetched and read as an XSD file.
<xsd:import/>
are handled and will import the given namespace at its direct
parent node level, and make it accessible to children.
schemaLocation
attribute if present will override the namespace URI for
resource fetching. Hence you can import a remote URI namespace while fetching
the content from a local file.
<xsd:element/>
found in <xsd:sequence/>
will spawn the class properties.
@todo explain type=foo vs complexType @todo explain property russian doll type naming
Properties may have multiple values, which make them collections. When
a collection is detected (see algorithm in the next two sections) it
its type in the generated PHP code is array
.
There is no planned support for a collection API, yet in the future
doctrine/persistence
collections may be implemented as well.
Whenever a collection property is written, its PHP doc string will contain
the value type for IDE to be able to resolve the value type. The generated
value accessors will also inherit from their respective @returns
and
@param
PHP doc type annotations.
minOccurs
and maxOccurs
parameters will drive the collection status of
a <xsd:sequence><xsd:element>
generated property.
If maxOccurs
exists with a value equal to unbounded
or an integer value
greater than 1
, then the property type will be set to array
.
If maxOccurs
is omited, and minOccurs
value is greate than 1
, the the
property will be set to array
.
In all other cases, the property will have the element target type.
If minOccurs
is not defined, or its value is 0
, then the property will
be made nullable.
nillable
attribute in <xsd:sequence><xsd:element>
will make the
property nullable. This takes precedence over the minOccurs
parameter.
nillable
is ignored for collection, an empty collection will always be
created no matter it is nullable or not.
When a type class A { int $c }
is extended by some type
class B extends A { int $c }
which redefine an existing property, multiple
scenarios may occur:
-
If the constructor property promotion is enabled, the property in the
B
type will be ignored, and type specialization will be lost. This is mandatory otherwise PHP will detect the property redefinition and cause a crash when compiling the code. Note that when the properties are private, the constructor promoted property will remain defined. -
If the properties are normal properties defined at the class level, we let it be overriden, but pass the value to the parent constructor if the property is not nullable. Note that if the
B::$c
property is not contravariant with theA::$c
property you will experience runtime errors.
Please note that handling <xsd:restriction>
shadowed properties is still
experimental, but a quite rare use case.
Per default the XSD reader is tolerant and will not break on recoverable errors. Most error behaviours can be tuned at the configuration level.
All errors are logged through a Psr\Log\LoggerInterface
instance that
you can inject in the configuration.
When a remote resource cannot be fetched, all types from the given namespace will be ignored, properties using those types will be omited from the target PHP classes.
This validation check is made during XSD file parsing recursion.
This can be made stricter and raise exception by setting the
resource_missing_remote_error
option to true
.
When a remote resource cannot be fetched, an exception will be raised and XSD parsing will stop.
This can be made looser in order to simply ignore types from the unfound
resource file by setting the resource_missing_local_error
option to false
.
When a type is not found the same behaviour applies, all properties using it will be omited from the target PHP class.
This validation check is made during type resolution after the XSD has been fully parsed.
This can be made stricter and raise exception by setting the type_missing_error
option to true
.
When a type is found more than once, for example when you parse a set of multiple WSDL files at once which do not import a common resource but all embed all their common types, types redefinitions will be ignored.
This can be made stricter and raise exception by setting the type_override_error
option to true
.
When invalid or unexpected XML elements are found during XSD schema read
they will simply be ignored. Tolerance level will the same as the XML
parser in use, which is PHP core \DOMDocument
API.
This behaviour has no related configuration options.
The MakinaCorpus\XsdGen\Generator
class is a method-chaining based
configuration builder and runner.
Here a simple working example from the unit tests:
(new Generator())
->defaultDirectory(__DIR__ . '/Generated')
->defaultNamespace('MakinaCorpus\\XsdGen\\Tests\\Generated')
->namespace('http://schemas.makina-corpus.com/testing/common/1.0', 'MakinaCorpus\\Common')
->propertyGetter(true)
->propertyPromotion(true)
->propertyPublic(false)
->propertyReadonly(false)
->propertySetter(false)
->logger(new EchoLogger())
->file(__DIR__ . '/resources/random-dependencies.xsd')
->generate()
;
All configuration options have associated methods you can chain on the object to configure the generator behaviour.
When spawning the generator, you can pass numerous options to drive how will be generated the code, to satisfy your own conventions.
If you intend to use these objects only in a scenario where they are
automatically hydrated from XML (default is false
):
In configuration:
xsd_gen:
class_constructor: false
With the Generator
class:
(new Generator())
->classConstructor(false)
Generate a create(array|self)
factory method for hydration tooling (default is true
):
In configuration:
xsd_gen:
class_factory_method: false
With the Generator
class:
(new Generator())
->classFactoryMethod(false)
Will always be prepend to generated PHP class namespaces (no default):
In configuration:
xsd_gen:
default_namespace: YourVendor\YourApp\Generated
With the Generator
class:
(new Generator())
->defaultNamespace('YourVendor\\YourApp\\Generated')
Where is the source code, this path is the default namespace prefix PSR-4 folder where the source code will be put (no default):
In configuration:
xsd_gen:
default_directory: /path/to/app/src/Generated
With the Generator
class:
(new Generator())
->defaultDirectory('/path/to/app/src/Generated')
Should property names be camed cased, otherwise they simply keep the XSD
given name (default is true
):
In configuration:
xsd_gen:
property_camel_case: false
With the Generator
class:
(new Generator())
->propertyCamelCase(false)
Should default values be set when applyable (default is true
).
In configuration:
xsd_gen:
property_default: false
With the Generator
class:
(new Generator())
->propertyDefault(false)
Warning: this is not implemented yet.
Generated code will use constructor promoted properties instead of normal
class properties (default is false
):
In configuration:
xsd_gen:
property_promotion: true
With the Generator
class:
(new Generator())
->propertyPromotion(true)
Make properties public
instead of private
(default is false
):
In configuration:
xsd_gen:
property_public: true
With the Generator
class:
(new Generator())
->propertyPublic(true)
Make properties readonly
(default is true
):
In configuration:
xsd_gen:
property_readonly: false
With the Generator
class:
(new Generator())
->propertyReadonly(false)
Generate property getters (default is true
):
In configuration:
xsd_gen:
property_getter: false
With the Generator
class:
(new Generator())
->propertyGetter(false)
Generate property setters (default is false
):
In configuration:
xsd_gen:
property_setter: true
With the Generator
class:
(new Generator())
->propertySetter(true)
Warning: this is not compatible with property_readonly
and will set back
to false if readonly
is detected.
When a type is referenced but unfound in the present XSD document or any
other one loaded by import magic, raise an exception and prevent code
generation to happen. Otherwise simply drop the type and all properties
using it (default is false
).
In configuration:
xsd_gen:
type_missing_error: true
With the Generator
class:
(new Generator())
->typeMissingError(true)
When a type definition with the same namespace and name is found more than
once, raise an exception and prevent code generation to happen. Otherwise
simply keep the last one (default is false
).
In configuration:
xsd_gen:
type_override_error: true
With the Generator
class:
(new Generator())
->typeOverrideError(true)
All messages, informational, warnings and errors are being sent to a logger
implementing interfaces from psr/log
. You can pass the logger
option to the
generator using any Psr\Log\LoggerInterface
instance.
Generated code will have the default nikic/php-parser
pretty printer style,
there is no way around that. This means that it will lack some empty lines
and all methods will be single line per default. This is not easy to read for
humans.
We recommend to configure your favorite CS fixer tool to act upon the generated PHP code in order to make it fit your own conventions.
- Use
#[\Override]
attribute when applyable in generated code. - Propagate
<xsd:annotation>
from<xsd:element>
to the underlaying complex type in order to add PHP-doc to generated class (current state of XSD reader doesn't allow this). - Add more unit test, a lot of unit tests, many many unit tests.
- Add support for the
<xsd:enum>
element, by either using class constants or using PHPBackedEnum
, choice between the two being user configurable. - Provide an option to skip inheritance which will merge all parenting tree into a single class, thus elimating completly the property shadowing variancy problem.
- Implement remote resource download.
- Allow user to choose another exception than
\InvalidArgumentException
for hydrator method validation. - Provide a Symfony bundle with configuration inside.
- Provide a complete per-source-file configuration.
- Provide alternative ways to spawn object collections, first implementation
with
doctrine/collections
to validate a proof-of-concept, and make this pluggable for users to replace with something else.
- Generate XML tooling for hydrating values from XML content.
- Generate XML tooling for serializing values to XML content.
- Generate SOAP tooling for calling remote methods.
- Allow user to deal with simple types and provide its own conversion matrix.
- Allow user to plug static methods or functions to deal with simple types or class conversion (both ways).