Generischer O/R-Mapper - Eigene Domänen-Objekte

1. Einleitung

Um einen zusätzlichen Schritt zur Objektorientierung zu gehen, wurde der genericormapper mit Version 1.14 des APF um die optionale Möglichkeit erweitert, eigene Domänen-Objekte zu verwenden. Durch eine zusätzliche Konfigurationsdatei kann festgelegt werden, welche Objekttypen (respektive Datenbanktabellen) statt durch das GenericDomainObject durch ein eigenes Domänen-Objekt abgebildet werden sollen, und wo dieses Objekt zu finden ist.
Hierdurch kann man dem Domänen-Objekt eigene, objektspezifische Methoden spendieren, um Funktionen, die man früher über einen eigenen Manager abbilden musste, direkt im Objekt verankern zu können. Dies ist vorteilhafter für die Lesbarkeit des Codes, und vereinfacht die Anwendung in vielen Fällen merklich.

Mitgeliefert wird außerdem ein GenericORMapperDomainObjectGenerator, welcher in der Lage ist anhand der Konfigurationsdateien des genericormapper ein fertiges Grundgerüst für ein Domänen-Objekt an der konfigurierten Stelle zu erzeugen. Das Grundgerüst besteht zum einen aus einem Basisobjekt, welches für jede in der Datenbank konfigurierte Eigenschaft eine Getter- und Setter-Methode beinhaltet, und zwingend vom GenericDomainObject erben muss oder von einem anderen Objekt, welches von diesem erbt. Dieses Basisobjekt kann durch die Konfiguration bestimmt werden, sodass nach korrekter Konfiguration und Verwendung des GenericORMapperDomainObjectGenerator im einfachsten Fall keine weiteren Anpassungen notwendig sind. Desweiteren beinaltet das Grundgerüst das eigentliche Gerüst des Domänen-Objekts, welches von seinem jeweiligen Basisobjekt erbt.
Das Basisobjekt darf NICHT bearbeitet werden, da Änderungen an diesem durch erneutes ausführen des Generators (welcher gleichzeitig ein Updater darstellt, um beispielsweise neue Eigenschaften in die API aufzunehmen) unwiederruflich gelöscht werden. Für Änderungen ist das eigentliche Domänen-Objekt zur Verfügung, dieses wird bei einem Updatevorgang nicht mehr bearbeitet.

Als weiteres Feature kann das Domänen Objekt ein paar bestimmte "Event-Funktionen" implementieren, welche der GORM bei bestimmten Aktionen bei der Arbeit mit dem Objekt aufruft, beispielsweise vor und nach der Speicherung eines Objekts. Dies kann beispielsweise verwendet werden, um Daten bei der Verwendung des Domänen-Objekts als Array/Objekt bereitzuhalten, diese vor der Speicherung in das besser speicherbare JSON-Format umzuwandeln, und um nach der Speicherung wieder das Array/Objekt zur Verfügung zu stellen. Somit muss sich die Anwendung nichtmehr um die Umwandlung der Daten in das benötigte Format kümmern, dies kann jetzt das Domänen-Objekt automatisch erledigen.
Eine Übersicht der verfügbaren Event-Methoden finden Sie im Kapitel Event Methoden

Die folgenden Kapitel zeigen, wie eigene Domänen-Objekte konfiguriert, generiert und verwendet werden können, und welche Events sie unterstützen.

2. Konfiguration der Domänen-Objekte

Wenn der O/R-Mapper Domänen-Objekte verwenden soll, muss am selben Speicherort der Mapper-Konfigurationen noch eine zusätzliche Konfigurationsdatei angelegt werden:

  • {ENVIRONMENT}_{NAMEAFFIX}_domainobjects.ini

Die Platzhalter {ENVIRONMENT} und {NAMEAFFIX} werden einfach an die bereits bestehenden Konfigurationsdateien des O/R-Mapper angeglichen.

In dieser Konfigurationsdatei wird für jedes Objekt, welches durch ein eigenes Domänen-Objekt abgebildet werden soll, eine Sektion mit dem Name des Objektes, welcher in der *_objects.ini des O/R-Mappers definiert ist, angelegt. Objekte, die kein eigenes Domänen-Objekt benötigen, brauchen nicht konfiguriert werden, für diese wird automatisch das GenericDomainObject verwendet.

Jede Sektion benötigt hier zwingend folgende Werte:

  • Namespace: Den Namespace, unter welchem die Objektdefinition zu finden/anzulegen ist
  • Class: Den Klassen- und Dateinamen des Objektes

Falls ein Basisobjekt nicht direkt vom GenericDomainObject, sondern von einem anderen Objekt (welches wiederum vom GenericDomainObject erbt) erben soll, und der Generator zum Erzeugen des Grundgerüstes verwendet werden soll, müssen noch folgende, ansonsten nicht benötigte, Werte definiert werden:

  • Base.Namespace: Der Namespace, unter welchem die Objektdefinition des Basisobjektes zu finden ist
  • Base.Class: Der Klassen- und Dateinamen des zu verwendenden Basisobjektes

Nachfolgendes Beispiel ist ein Teil der Konfiguration der Postbox Extension, in welcher die Domänen-Objekte als erstes verwendet wurden, und welche daher auch als weiterführendes Beispiel für die Verwendung herangezogen werden kann.

APF-Konfiguration
[Message] Namespace = "extensions::postbox::biz" Class = "Message" Base.Namespace = "extensions::postbox::biz" Base.Class = "AbstractMessage" [MessageChannel] Namespace = "extensions::postbox::biz" Class = "MessageChannel" Base.Namespace = "extensions::postbox::biz" Base.Class = "AbstractMessageChannel"
Definiert wurden hier 2 Objekte, welche beide von einem speziellen Basisobjekt erben sollen.

3. Generierung der Domänen-Objekte

Mithilfe des mitgelieferten GenericORMapperDomainObjectGenerator gestaltet sich das Erstellen der Domänen-Objekte sehr einfach. Sobald Sie nach obigem Schema die Konfigurationen angelegt haben, muss nurnoch ein kleines Script zur Generierung der Objekte angelegt und ausgeführt werden:

PHP-Code
include('./apps/core/pagecontroller/pagecontroller.php') import('modules::genericormapper::data::tools','GenericORMapperDomainObjectGenerator'); $generator = new GenericORMapperDomainObjectGenerator(); $generator->setContext('{CONTEXT}'); // Set your context here $generator->generateServiceObjects('{NAMESPACE}', '{NAMEAFFIX}'); // set namespace and nameaffix of configuration files here
Der Platzhalter {CONTEXT} muss durch ihren Context ersetzt werden, {NAMESPACE} durch den Namespace unter dem die Konfigurationsdatei abgelegt wurde (ohne Context) und {NAMEAFFIX} durch den im Dateinamen definierten Affix (siehe oben).

Der GenericORMapperDomainObjectGenerator wird beim Aufruf dieses Scriptes für jedes definierte Objekt ein entsprechendes Domänen-Objekt am, in der Konfiguration definierten, Speicherort erzeugen. Sollte dort bereits eine Datei mit dem selben Namen gefunden werden, wird versucht das darin enthaltene Basisobjekt neu zu generieren, um API-Änderungen in der *_objects.ini zu übernehmen. Hierfür sind in der erzeugten Datei bestimmte, entsprechend gekennzeichnete, Kommentare enthalten, die auf keinen Fall entfernt oder geändert werden dürfen, genausowenig wie alles zwischen diesen Kommentaren, da sonst Datenverlust droht! Beim Update wird das Domänen-Objekt selber nichtmehr geändert, Änderungen an diesem sollten nicht verloren gehen. Alle Änderungen am Basisobjekt werden unwiederruflich und ohne Warnung verworfen.

Achtung: Wir übernehmen generell keine Haftung für Fehler durch die automatische Generierung, trotz sorgfältiger Prüfung können Fehler immer unerwartet auftreten, daher ist eine vorherige Sicherung der bereits bestehenden Dateien anzuraten, um diese im Fehlerfall wiederherstellen zu können!!

Ein Beispiel für eine der erzeugten Dateien (hier für das Message-Objekt aus obiger Definition) könnte in etwa so aussehen:

PHP-Code
//<*MessageBase:start*> DO NOT CHANGE THIS COMMENT! /** * Automatically generated BaseObject for Message. !!DO NOT CHANGE THIS BASE-CLASS!! * CHANGES WILL BE OVERWRITTEN WHEN UPDATING!! * You can change class "Message" which will extend this base-class. */ import('extensions::postbox::biz', 'AbstractMessage'); class MessageBase extends AbstractMessage { public function __construct($objectName = null){ parent::__construct('Message'); } public function getText() { return $this->getProperty('Text'); } public function setText($value) { $this->setProperty('Text', $value); return $this; } public function getAuthorNameFallback() { return $this->getProperty('AuthorNameFallback'); } public function setAuthorNameFallback($value) { $this->setProperty('AuthorNameFallback', $value); return $this; } } // DO NOT CHANGE THIS COMMENT! <*MessageBase:end*> /** * @package extensions::postbox::biz * @class Message * * Domain object for "Message" * Use this class to add your own functions. */ class Message extends MessageBase { /** * Call parent's function because the objectName needs to be set. */ public function __construct($objectName = null){ parent::__construct(); } }
Das Basisobjeckt MessageBase erbt vom, explizit in der Konfiguration definierten, Objekt AbstractMessage. Auch um den Import der benötigten Datei hat der Generator sich natürlich selbst gekümmert.
In der *_objects.ini des O/R-Mapper wurden dem Message-Objekt die Eigenschaften "Text" und "AuthorNameFallback" gegeben, dementsprechend wurden für diese beiden Eigenschaften Getter- und Setter-Methoden generiert. Die Setter-Methoden erhalten dabei immer automatisch ein Fluent-Interface.
Zuletzt die Definition des Message-Objektes, welches vom Basisobjekt erbt. In dieser Klasse können sie nun Ihre eigenen Funktionen definieren, denkbar wäre in diesem Fall z.B. die Funktion "delete()" um die aktuelle Nachricht zu löschen.

Sollten sie sich die Postbox-Extension einmal näher angesehen haben, werden sie feststellen, dass dort die delete-Methode bereits in AbstractMessage definiert wurde. Auch dies ist problemlos möglich, und im Falle der Postbox angewendet worden, im Normalfall aber nicht notwendig.

4. Verwendung der Domänen-Objekte

Wenn die Objekte entsprechend obiger Anleitung angelegt wurden, kann es nun zur praktischen Verwendung gehen. Hierbei gibt es eigentlich nichts zu beachten, da die Objekte immer zwingend vom GenericDomainObject (direkt oder indirekt durch Weitervererbung ist egal) erben, können Sie auch wie jedes normale GenericDomainObject verwendet werden und sind somit abwärtskompatibel. Dies ist besonders praktisch, da eine nachträgliche Verwendung der eigenen Domänen-Objekte keinerlei Anpassung ihrer bereits vorhanden Codeteile benötigt.

Der O/R-Mapper erkennt beim Laden von Daten automatisch anhand der Konfiguration, dass er ein spezielles Domänen-Objekt verwenden muss, und erzeugt dieses anstatt des GenericDomainObject. Laden Sie also ab sofort ein Message-Objekt aus der Datenbank, erhalten Sie ein Objekt vom Typ "Message", auf welches sie ihre eigenen Funktionen anwenden können.
Um ein neues Objekt zu erzeugen, müssen Sie lediglich die Objektdatei mittels eines import() einbinden, und danach eine Instanz des Objektes erzeugen, anschließend können Sie wie gewohnt damit arbeiten:

PHP-Code
import('extensions::postbox::biz', 'Message'); $Message = new Message(); $Message->setText('ExampleText'); $Orm->saveObject($Message);

5. Event Methoden

Wie bereits angesprochen beherrscht der O/R-Mapper auch ein paar "Events". Die Domänen-Objekte können für jedes Event eine Event-Methode definieren, welche bei entsprechender Aktion aufgerufen wird. Zur Verfügung stehen derzeit folgende Methoden, welche der O/R-Mapper aufruft:

  • afterLoad(): Wird nach dem Laden des Objektes aus der Datenbank aufgerufen.
  • beforeSave(): Wird direkt vor dem Speichern des Objektes in die Datenbank aufgerufen.
  • afterSave(): Wird direkt nach dem Speichern des Objektes in die Datenbank aufgerufen.

Ein mögliches Anwendungsbeispiel wäre das bereits in der Einleitung erwähnte kodieren und dekodieren von Arrays oder Objekten vor bzw. nach dem Speichern oder Laden.

6. Objekt-Bäume

Seit der Version 1.15 bietet der Generische O/R-Mapper einen Mechanismus, mittels dem sich hierarchische Objekt-Listen - also Objekt-Bäume - erstellen lassen. Dieses Feature ist vergleichbar mit dem Nested-Sets- oder ParentID-Prinzip.

Um dieses Feature nutzen zu können, muss in der Datei {ENVIRONMENT}_{NAMEAFFIX}_domainobjects.ini ein Domain-Objekt definiert sein, welches als Basis-Klasse die Klasse TreeItem verwendet oder aber man definiert direkt die Klasse TreeItem als Domain-Object:

APF-Konfiguration
; Variante 1 [NavigationNode] Namespace = "namespace::to" Class = "NavigationNode" Base.Namespace = "modules::genericormapper::data" Base.Class = "TreeItem" ; Variante 2 [NavigationNode] Namespace = "modules::genericormapper::data" Class = "TreeItem"

Die Hierarchie der Objekte muss über eine Kompositions-Tabelle abgebildet werden:

APF-Konfiguration
[NavigationNode2NavigationNode] Type = "COMPOSITION" SourceObject = "NavigationNode" TargetObject = "NavigationNode"

Einen Objekt-Baum erhält man nun durch den Aufrauf der Methode loadObjectTree() des GenericORRelationMapper:

PHP-Code
$ObjectTree = $ORM->loadObjectTree('NavigationNode', 'NavigationNode2NavigationNode');
Diese Methode kennt noch 3 weitere, optionale Parameter:

  • criterion: Hier kann ein GenericCriterionObject übergeben werden, um die Abfrage der Objekte, die für den Baum verwendet werden, zu beinflussen.
  • rootObjectId: Wird hier eine ID eine Objektes übergeben, wird dieses Objekt als Wurzel-Objekt für den Baum verwendet.
  • maxDepth: Mit diesem Parameter lässt sich die Tiefe des Baumes begrenzen.

Sobald ein Objekt-Baum erstellt/abgefragt wurde, ist es möglich über zwei Methoden, welche in der Klasse TreeItem definiert sind, die Kind- bzw. das Eltern-Objekt des aktuellen Tree-Items abzufragen. Üer Rekursion wäre es nun möglich, den Objekt-Baum bspw. mittels einer verschachtelten ungeordneten Liste auszuzeichen.

PHP-Code
function printChildObjects($objects) { echo '<ul>'; foreach ($objects as $object) { echo '<li>'; echo $object->getProperty('DisplayName'); $children = $object->getChildren(); if (count($children) > 0) { printChildObjects($children); } echo '</li>'; } echo '</ul>'; } $ObjectTree = $ORM->loadObjectTree('NavigationNode', 'NavigationNode2NavigationNode'); printChildObjects($ObjectTree);

Kommentare

Möchten Sie den Artikel eine Anmerkung hinzufügen, oder haben Sie ergänzende Hinweise? Dann können Sie diese hier einfügen. Die bereits verfassten Anmerkungen und Kommentare finden Sie in der untenstehenden Liste.
Für diesen Artikel liegen aktuell keine Kommentare vor.