Freaky Friday: Objekte in XML umwandeln

Im letzten XML bezogenen Beitrag ging es darum komplexe XML Strukturen in Objekte mit der SimpleXMLIterator Klasse umzuwandeln. Tom fragte in den Kommentaren zu diesem Artikel, wie man denn den umgekehrten Weg mit PHP darstellen könnte? Also aus einer komplexen Objekt-Struktur sauberes XML zu formen. Genau darum soll es heute gehen.

Kleiner Hinweis vorweg: Nein, mit dem hier gezeigten Ansatz sollte man keine SOAP XML Requests und Responses formen. Das XML für SOAP Requests und Responses wird allein von den SoapServer und SoapClient Klassen erstellt. Bitte verwendet folgendes Beispiel nicht für eine SOAP XML Anwendung.

Für die Umwandlung von PHP Objektstrukturen in valides XML benötigen wir zu allererst mal ein wenig Ordnung und Struktur in unseren PHP Objekten. Kann ein Attribut in einem Objekt mehr als ein Element enthalten, ist es eine Collection, die wir mit der PHP nativen Klasse SplObjectStorage darstellen werden. Kommt ein Attribut nur einfach vor, kann es eine Klasse, ein String oder eine Zahl sein.

Darüber hinaus wähle ich zur vereinfachten Veranschaulichung das Beispiel eines Autos, welches einen Fahrer und Insassen haben kann. Sowohl Fahrer und Insassen sind Personen, die einen Namen haben können. Der Fahrer kann zusätzlich einen Führerschein haben.

Wie sieht das als PHP Code aus?

Bevor wir uns den Personen und dem Auto selbst widmen, werfen wir zunächst mal einen Blick auf das, was wir eigentlich erreichen möchten. Am Ende soll eine valide XML Struktur stehen, die unserer Objektstruktur entspricht. Mit diesem Vorhaben im Hinterkopf, programmieren wir uns erstmal ein Interface, welches alle Objekte implementieren, die in XML umgewandelt werden sollen.

namespace Marcel;
use DOMDocument;

interface ToXmlInterface
{
	public function toXml(): DOMDocument;
}

Dieses Interface ist einfach gehalten. Es gibt lediglich eine Methode vor, die von allen Klassen umgesetzt werden muss, die in XML umgewandelt werden sollen. Somit hätten wir eine Basis geschaffen, die uns später noch helfen wird. Weiter geht es mit den Personen im Fahrzeug.

Gehen wir einfach mal von dem Beispiel einer Person aus, die in diesem Auto sein kann. Sowohl Fahrer als auch Insassen sind Personen, die einen Namen haben können. Also programmieren wir uns hier eine abstrakte Klasse.

namespace Marcel;
use DOMDocument;
use ReflectionClass;

abstract class Person implements ToXmlInterface
{
	protected $name;

	public function getName(): ?string
	{
		return $this->name;
	}

	public function setName(string $name): self
	{
		$this->name = $name;
		return $this;
	}

	public function toXml(): DOMDocument
	{
		$doc = new DOMDocument();
		$parent = $doc->createElement(strtolower((new ReflectionClass($this))->getShortName()));

		foreach (get_object_vars($this) as $key => $value) {
			$element = $doc->createElement($key, $value);
			$parent->appendChild($element);
		}

		$doc->appendChild($parent);
		return $doc;
	}
}

Hier haben wir nun also unsere Person. Sowohl Fahrer als auch Insassen können einen Namen haben. Dem entsprechend definiert diese abstrakte Klasse schon mal die Getter- und Setter-Methode für den Namen. Weiterhin wird das gerade erstellte ToXmlInterface implementiert. Da Fahrer als auch Insassen einfache Objekte sind, definieren wir die toXml Methode schon hier in der abstrakten Klasse, so dass wir diese nicht in jeder Ableitung neu definieren müssen. Die toXml Methode macht nichts anderes, als den Namen der Klasse über die Reflection API zu ermitteln und daraus dann eine XML Struktur zu erstellen, die den Namen der abgeleiteten Klasse als Elternelement und die Eigenschaften der abgeleiteten Klasse als Kindelemente der XML Struktur darzustellen. Es findet noch keine Rekursion beim Erstellen der XML Elemente statt. Das Resultat der toXml Methode ist eine DOMDocument Instanz, die all unsere Daten zu einer Person beinhaltet.

Jetzt müssen wir nur noch Fahrer und Insassen genauer definieren. Sowohl Fahrer als auch Insasse leiten sich von der abstrakten Klasse Person ab.

namespace Marcel;

class Driver extends Person
{
	protected $fuehrerschein;

	public function getFuehrerschein(): ?bool
	{
		return $this->fuehrerschein;
	}

	public function setFuehrerschein(bool $fuehrerschein): self
	{
		$this->fuehrerschein = $fuehrerschein;
		return $this;
	}
}

class Passenger extends Person
{

}

Die Driver Klasse stellt den Fahrer dar. Ein Fahrer kann einen Führerschein haben. Man könnte die Führerschein-Eigenschaft dann später validieren. Ist sie false, könnte man eine entsprechende Exception werfen. Aber so weit wollen wir in diesem Beispiel nicht gehen. Eine mitfahrende Person als Insasse hat keine weiteren Eigenschaften. Sie leitet sich lediglich von der abstrakten Klasse Person ab und hat einen Namen. Die toXml Methode muss hier nicht mehr definiert werden, weil wir diese schon in der abstrakten Klasse, von der sich Fahrer und Insasse ableiten, definiert haben. Somit haben wir also Fahrer mit Insassen des Autos definiert.

Kommen wir nun zu unserem Auto. Es ist ein einfaches Auto, welches einen Namen, einen Fahrer und mehrere Insassen haben kann. Da es mehr als einen Mitfahrer geben kann, müssen wir diese in einer Collection darstellen.

namespace Marcel;
use DOMDocument;
use InvalidArgumentException;
use SplObjectStorage;

class PassengerCollection extends SplObjectStorage implements ToXmlInterface
{
	public function attach($object, $data = null): self
	{
		if (!$object instanceof Passenger) {
			throw new InvalidArgumentException(sprintf(
				'Das angegebene Objekt muss vom Typ Passenger sein. Angegeben: "%s"',
				get_class($object)
			));
		}

		parent::attach($object);
		return $this;
	}

	public function toXml(): DOMDocument
	{
		$doc = new DOMDocument();

		foreach ($this as $object) {
			if ($object instanceof ToXmlInterface) {
				$element = $object->toXml();
				foreach ($element->childNodes as $node) {
					$doc->appendChild($doc->importNode($node, true));
				}
			}
		}

		return $doc;
	}
}

Warum gibt es diese Collection? Rein theoretisch könnten wir auch einfach direkt die SplObjectStorage Klasse für unser vorhaben nehmen. Die Ableitung für die Insassen legt allerdings fest, dass nur Insassen des Fahrzeugs dieser Collection hinzugefügt werden können. Ist dies nicht der Fall, wird eine InvalidArgumentException geworfen. Man könnte jetzt noch prüfen, ob die maximale Anzahl an Insassen erreicht ist, oder ob Insassen mit einem bestimmten Namen ausgeschlossen werden sollen. Das kann die SplObjectStorage Klasse allein nicht darstellen. Aus meiner Sicht ergibt es also immer Sinn Collections für eine spezifische Klasse anzulegen. Diese Umsetzung gibt uns einfach Typensicherheit.

Zuletzt brauchen wir noch unser Auto, in dem unser Fahrer und unsere Insassen dann sitzen. Unser Auto hat dazu noch einen Namen.

namespace Marcel;
use DOMDocument;
use ReflectionClass;

class Car implements ToXmlInterface
{
	protected $name;

	protected $driver;

	protected $passengers;

	public function __construct()
	{
		$this->passengers = new PassengerCollection();
	}

	public function getName(): ?string
	{
		return $this->name;
	}

	public function setName(string $name): self
	{
		$this->name = $name;
		return $this;
	}

	public function getDriver(): ?Driver
	{
		return $this->driver;
	}

	public function setDriver(Driver $driver): self
	{
		$this->driver = $driver;
		return $this;
	}

	public function getPassengers(): PassengerCollection
	{
		return $this->passengers;
	}

	public function setPassenger(Passenger $passenger): self
	{
		$this->passengers->attach($passenger);
		return $this;
	}

	public function setPassengers(PassengerCollection $passengers): self
	{
		$this->passengers = $passengers;
		return $this;
	}

	public function toXml(): DOMDocument
	{
		$doc = new DOMDocument();
		$doc->formatOutput = true;

		$parent = $doc->createElement(strtolower((new ReflectionClass($this))->getShortName()));

		foreach (get_object_vars($this) as $key => $value) {
			if ($value instanceof ToXmlInterface) {
				$element = $value->toXml();
				foreach ($element->childNodes as $node) {
					$parent->appendChild($doc->importNode($node, true));
				}
			}

			if (is_string($value)) {
				$element = $doc->createElement($key, $value);
				$parent->appendChild($element);
			}
		}
		
		$doc->appendChild($parent);
		return $doc;
	}
}

Wie man hier sehr schön sehen kann, besitzt unser Auto einen Namen, einen Fahrer und es können mehrere Insassen mitfahren. Zudem wird das ToXmlInterface eingebunden, was die toXml Methode erzwingt. Hier iterieren wir rekursiv über alle Eigenschaften unseres Autos und erstellen daraus unsere XML Struktur. Hier sieht man auch, wie wichtig das zum Anfang erstellte Interface ist. Wir prüfen, ob eine Eigenschaft das ToXmlInterface implementiert. Tut es das, wissen wir, dass wir die toXml Methode aufrufen können und die Eigenschaft als XML dargestellt werden kann. Ist die Eigenschaft ein einfacher String, wie der Name des Autos, so wird ein normales Element erstellt, welches wir in unserem XML darstellen können. Eine toXml Methode existiert in diesem Fall nicht.

Dann fahren wir mal los

Mit den erstellten Klassen können wir eine komplexe Objektstruktur erstellen, aus der wir dann automatisch eine XML Struktur erzeugen.

namespace Marcel;

// der Fahrer
$marcel = (new Driver())
	->setName('Marcel')
	->setFuehrerschein(true);

// Nina fährt mit
$nina = (new Passenger())
	->setName('Nina');

// Steffen fährt auch mit
$steffen = (new Passenger())
	->setName('Steffen');

// alle zusammen im MM Newmedia Mobil
$car = (new Car())
    ->setName('MMNewmedia Mobil')
	->setDriver($marcel)
	->setPassenger($nina)
	->setPassenger($steffen)
	->toXml()
	->saveXml();

Sieht easy aus, oder? Ich taufe unser Auto einfach mal MM Newmedia Mobil. Ich fahre das Auto mal und nehme Nina und Steffen mit. Somit haben wir alle Eigenschaften für unser Auto besetzt. Die Erwartungshaltung ist nun, dass all diese Eigenschaften als vollständiges XML erscheinen. Ganz praktisch ist, dass wir lediglich die toXml Methode des Autos aufrufen.

<?xml version="1.0"?>
<car>
  <name>MMNewmedia Mobil</name>
  <driver>
    <fuehrerschein>1</fuehrerschein>
    <name>Marcel</name>
  </driver>
  <passenger>
    <name>Nina</name>
  </passenger>
  <passenger>
    <name>Steffen</name>
  </passenger>
</car>

Sauber. Somit haben wir aus einer komplexen Objektstruktur das entsprechende XML erstellt.

Fazit

Aus Erfahrung kann ich sagen, dass man für das Umwandeln von Objekten in XML äußerst strukturiert arbeiten muss. Sofern es nicht schon vorher Sinn ergeben hat sich vor dem Programmieren einen genauen Überblick zu verschaffen, was eigentlich realisiert werden soll und wie man das Ziel erreichen möchte, muss man sich zwangsweise jetzt auf seinen Popo setzen und sich eine genaue Struktur für seine Daten zurecht legen. Denn ohne eine genaue Struktur wird es nicht funktionieren. Ich muss mir im Klaren sein, wie mein XML am Ende aussehen soll. Sind Mehrfachnennungen von XML Elementen vorhanden? Wie sollen meine XML Elemente lauten? Welche Werte sollen dargestellt werden? Müssen die XML Elemente in einer festgelegten Reihenfolge erscheinen? Auf alle Eventualitäten sollten bereits vor der eigentlichen Programmierung die entsprechenden Lösungen vorliegen.

Mir ist klar, dass dies ein sehr einfaches Beispiel ist. Für eine Umsetzung die XML Attribute oder Namespaces beachten soll, ist mehr Aufwand notwendig.

Kommentar verfassen

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