XML-Daten in PHP: Einfacher Serialisieren mit DTOs

Wer mit XML basierten Standards wie UBL, CII, XRechnung, ZUGFeRD, BiPRO oder anderen proprietären B2B-Formaten arbeitet, kennt das Problem: In Java (JAXB) und .NET lassen sich Datenobjekte elegant über Annotationen / Attribute mit XML Metadaten versehen. Eine native DTO-XML Serialisierung fehlt in PHP komplett.

Dieser Umstand führte zu:

  • komplexen, fehleranfälligen Serializern
  • externen Mapping-Konfigurationen (XML / YAML / JSON)
  • unübersichtlichen DOM-Manipulationen
  • fehlender Typ-Sicherheit und fehlender IDE-Unterstützung

Mit dem neuen Repository dto-xml-serializer ändert sich das grundlegend. In meiner langen Laufbahn als PHP Freelancer habe ich an zahlreichen Projekten mitwirken dürfen, in denen XML eine zentrale Rolle spielte. Oftmals blieb dabei aber strukturierter Code auf der Strecke und irgendwelche aufgeblähten DOM-Manipulationen mussten für diesen einen Use-Case herhalten.

Mit dieser Lösung gibt es endlich einen strukturierten, leicht wartbaren, erweiterbaren Weg Datenobjekte in XML zu wandeln. Eine DTO-XML Serialisierung ist ab sofort möglich.

Was das Repository ermöglicht

Das Projekt bringt XML-Attribute für PHP-Datenobjekte – analog zu .NET und Java.

Damit kannst Du:

  • XML-Strukturen direkt am Data Transfer Object definieren.
  • Serialisierung und Deserialisierung typsicher steuern
  • komplexe XML-Schemas modellieren, ohne externe Mapping Dateien
  • die Lesbarkeit und Wartbarkeit Deiner DTOs massiv verbessern

Kurz: PHP bekommt endlich ein Feature, dass in Enterprise-Sprachen seit Jahren Standard ist.

XML Leafs / Simple Content Types

Ein so genanntes XML-Leaf oder Simple Content Type ist ein Knoten, der im XML Baum keine Kind-Elemente enthält und lediglich einfache Werte, wie z.B. Text, Zahlen- oder Datumswerte, enthält. Je nach Umfang des anzuwendenden XML-Datenmodells können daher verschiedene Simple Content Types definiert werden.

use MMNewmedia\Model\ValueableInterface;
use MMNewmedia\Model\ValueableTrait;
use MMNewmedia\Xsd;

#[Xsd\SimpleContent]
#[Xsd\Extension(base: 'xsd:date')]
class DateType implements ValueableInterface
{
    use ValueableTrait;
}

Simple Content Types implementieren immer das ValuableInterface und das darauf basierende ValueableTrait. Zudem wird durch die Attribute #[XsdSimpleContent] und #[XsdExtension] die XSD-Basis festgelegt, von der sich dieses Leaf ableitet. Die hier gezeigte Klasse kann dann als Type Eigenschaft für das #[Xsd\Element] Attribut verwendet werden.

Das oben gezeigte Beispiel entspricht folgender XSD-Definition:

<xsd:complexType name="DateType">
    <xsd:simpleContent>
        <xsd:extension base="xsd:date"/>
    </xsd:simpleContent>
</xsd:complexType>

XML-Attribute für XML-Leafs

Natürlich kann so ein Simple Content Type auch Attribute. Mit simpler PHP Property Promotion und dem #[Xsd\Attribute] Attribut können Eigenschaften eines DTO als XML-Attribut deklariert werden.

use MMNewmedia\Model\ValueableInterface;
use MMNewmedia\Model\ValueableTrait;
use MMNewmedia\Xsd;

#[Xsd\SimpleContent]
#[Xsd\Extension(base: 'xsd:decimal')]
final class AmountType implements ValueableInterface
{
    use ValueableTrait;

    public function __construct(
        #[Xsd\Attribute(
            name: 'currencyID', 
            type: 'xsd:string', 
            use: Xsd\AttributeUseType::OPTIONAL
        )]
        public ?string $currencyID = null,
    ) {}
}

Das oben gezeigte Beispiel entspricht folgender XSD-Definition:

<xsd:complexType name="AmountType">
    <xsd:simpleContent>
        <xsd:extension base="xsd:decimal">
            <xsd:attribute name="currencyID" type="xsd:token"/>
            <xsd:attribute name="currencyCodeListVersionID" type="xsd:token"/>
        </xsd:extension>
    </xsd:simpleContent>
</xsd:complexType>

XML Komplexe Typen

An dieser Stelle wird es richtig komfortabel. Sind die XML Simple Types ersteinmal als PHP DTOs definiert, sind komplexe Typen ein Kinderspiel. Ein XML Complex Type ist, wie der Name eigentlich schon sagt, ein komplexer Datentyp. Ein XSD-Elementtyp, der andere Elemente und Attribute enthalten kann. Im Gegensatz zu Simple Types beschreiben Complex Types komplexe, verschachtelte Daten. Sie erlauben es, dass ein XML-Element Attribute, Kind-Elemente oder eine Kombination aus Text und Elementen besitzen kann.

In einem PHP DTO werden Complex Types mit dem #[Xsd\ComplexType] Attribut annotiert. Optional kann auch das #[Xsd\Schema] Attribut verwendet werden, um einen Target Namespace zu definieren. Das PHP-Attribut #[Xsd\Sequence] kann benutzt werden, um die Reihenfolge von Elementen innerhalb eines komplexen Typen festzulegen. Elemente, die n-fach (z.B. über das XSD Attribut maxOccurs="unbounded") werden in einer SplObjectStorage Instanz gesammelt.

use MMNewmedia\Xsd;
use SplObjectStorage;

#[Xsd\ComplexType]
#[Xsd\Schema(targetNamespace: 'urn:example:invoice')]
#[Xsd\Element(namespace: HeaderNamespaces::INV, name: 'Invoice', type: InvoiceType::class)]
final readonly class InvoiceType
{
    public function __construct(
        #[Xsd\Element(namespace: HeaderNamespaces::CBC, name: 'ID', type: IdType::class)]
        #[Xsd\Sequence(position: 1)]
        public IdType $id,

        #[Xsd\Element(name: 'Note', maxOccurs: 99, type: NoteType::class)]
        #[Xsd\Sequence(position: 2)]
        public SplObjectStorage $note,
    ) {}
}

JSON Serialisierung

Im Repository gibt es einen Serializer, der die PHP DTOs in ein valides JSON Format und aus einem validen JSON Format wieder PHP DTOs erzeugen kann.

use App\Model\CrossIndustryInvoice\CrossIndustryInvoiceType;
use MMNewmedia\Serializer\JsonDtoSerializer;

$invoice = new CrossIndustryInvoiceType(...);
$serializer = new JsonDtoSerializer();

// DTO -> JSON
$json = $serializer->serialize($invoice);

// JSON -> DTO
$serializer->setRootClasses([
    'CrossIndustryInvoice' => CrossIndustryInvoiceType::class,
]);
$invoice = $serializer->unserialize($json);

Simple Content Types werden als skalare Werte serialisiert. Wenn ein Simple Content Type Attribut deklariert ist, wird seine JSON-Darstellung zu einem Objekt, bei dem die XML Attribute als Schlüssel und der Wert unter dem Schlüssel value stehen.

Somit kann eine vollständige DTO-Struktur als valides JSON serialisiert in z.B. REST Webservices genutzt werden.

XML Serialisierung

Die XML Serialisierung ist das Herzstück dieses Repositories. Sie erlaubt es aus einer PHP DTO-Struktur valides XML zu erzeugen.

use App\Xml\HeaderNamespaces;
use MMNewmedia\Serializer\XmlDtoSerializer;

$serializer = new XmlDtoSerializer();

// map namespace URIs to prefixes via a BackedEnum (case name => xml namespace uris)
$serializer->setNamespaces(HeaderNamespaces::class);

// for model families that declare the namespace per element, prefer element namespaces:
$serializer->setUsePropertyNamespaces(true);

$xml = $serializer->serialize($invoice);

Wenn usePropertyNamespaces deaktiviert ist, erben untergeordnete Elemente den „targetNamespace“ des Attributs #[Schema] des übergeordneten komplexen Typs; die #[Element]-Namespaces auf Eigenschaftenebene dienen lediglich als Ausweichlösung.

Namespaces für die Serialisierung festlegen

Namespaces werden als PHP Enum bei der Initialisierung des XML Serializers übergeben. Die Serializer Klasse erwartet als einzigen Parameter eine BackedEnum Instanz, welche das Namespace Prefix als Enumeration und die dazu passende Namespace URI als Wert vorhält.

enum HeaderNamespaces: string
{
    case INV = 'urn:example:invoice';
    case CBC = 'urn:example:commonbasic';
    case CAC = 'urn:example:commonaggregate';
}

Der Vorteil hierbei ist, dass weder in den #[Schema] noch in den #[Element] PHP-Attributen vollständige Namespace URIs angegeben werden müssen. Die Enumeration muss immer alle zu verwendenden Namespaces enthalten.

Fazit zu DTO-XML Serialisierung

Mit diesem Repository können komplexe PHP DTO Strukturen entsprechende XML Bäume abbilden. Als Entwickler wird man in die Lage versetzt lediglich auf Basis von PHP DTOs zu arbeiten, die die mit diesem Repository zur Verfügung gestellten Attribute verwenden und somit einfach in JSON oder XML serialisiert werden können. Genau das, was andere Programmiersprachen bereits können.

XML Strukturen müssen nicht mehr mit individuellen DOM Orgien dargestellt werden. Der Vorteil von DTOs bleibt erhalten. Striktes Typehinting helfen nicht nur in Deiner IDE, sondern sorgen gleichzeitig für Typensicherheit.

Ist dieses Repository für Dich hilfreich? Lass es mich in den Kommentaren wissen.

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.