Inhaltstypen
Pages
Posts
01.03.2016

TYPO3 Flow: Applikation mit Dynamischen Attributen

Entscheidung

Manchmal weiss man nicht, was auf Einen zukommt. Vorbereiten kann man sich trotzdem.

Bei einem komplexen Projekt, bei dem bis zum Zeitpunkt des Projektstarts (und darüber hinaus) nicht klar ist, welche Informationen überhaupt wo angezeigt werden müssen, hat man die Wahl:

Möchten ich und mein Team im Laufe der Jahre immer und immer wieder Anpassungen am Code vornehmen? Oder gestalte ich meine Applikation so, dass sie dynamisch erweiterbar ist?

Entscheidung

Vor- und Nachteile gibt es natürlich bei Beiden. Hier sind diese aus Sicht einer dynamischen Applikation aufgeführt:

  1. Wartungsarbeiten nach Abschluss des Projektes werden reduziert. (d.h. Kosten nach Abschluss geringer)
  2. Kunde kann seine Applikation jederzeit nach seinen Bedürfnissen gestalten.
  3. Initiale Entwicklung kostet Zeit und damit Geld.
  4. Lösung ist komplexer.

Entscheidend sind natürlich meistens die finanziellen Mittel: Möchte der Kunde das Geld lieber während des Projektes investieren oder danach?

Wenn Ersteres, und Sie sich dieser Aufgabe widmen dürfen, erfahren Sie hier alles Wissenswerte dazu.

Ein Beitrag in drei Teilen

Da das Thema ziemlich komplex ist, werden wir es in drei Teilen besprechen:

  1. Backend: Dynamische Attribute (Dieser Beitrag)
  2. Frontend: Locations & DataFields
  3. Anwendung: DQL-Generator

Backend: Dynamische Attribute

Wir gehen von folgendem Fallbeispiel aus: Wir haben von einer Firma den Auftrag bekommen, eine webbasierte Verwaltungsapplikation für das Produktelager zu erstellen. Die Firma hat verschiedene Abteilungen, welche aber dasselbe Lager verwenden. Die Abteilungen haben alle unterschiedliche Produkte mit verschiedenen Attributen.

Wir möchten nun nicht für jede Abteilung die Attribute manuell bewirtschaften. Ausserdem kann es sein, dass eine weitere Abteilung mit neuen Produkten und neuen Attributen hinzukommt. Jedes Mal das ganze Prozedere noch einmal durchgehen? Fehlanzeige!

Domain\Model\Data

Wir müssen als erstes Flow beibringen, mit dynamischen Attributen umgehen zu können. Dazu brauchen wir ein neues Model-Objekt namens “Data”, welches den Namen des Attributes, dessen Wert und die Verknüpfung zum Objekt, zu dem es gehört, gespeichert werden kann:

Data 
identifierstring
valuestring
dataModel\MyCompany\MyProject\Domain\Model\AbstractDataModel

Obwohl es möglich ist, einen Persistence Objekt Identifier (Datenbank-ID) im “value”-Feld zu speichern und damit ein Objekt als dynamischer Wert zu speichern, wird dies von unserer Seite nicht empfohlen. Es kann dabei schnell zu unübersichtlichen Datenstrukturen kommen.

Domain\Model\AbstractDataModel

Wie Sie sehen können, gehen wir damit nahtlos zur nächsten benötigten Klasse: Dem AbstractDataModel. Diese ist der Wrapper für alle Klassen, welche die Funktionalität der dynamischen Attribute verwenden möchten.

In unserem Beispiel wird es also eine Klasse “Domain\Model\Product” geben. Dieses muss von der Abstrakten Data-Model-Klasse erben:

Die Klasse selber muss sicherstellen, dass das Schreiben und lesen der dynamischen Attribute möglich ist. Dazu muss sie folgende Methoden beinhalten:

Und das war’s schon. Jetzt kann TYPO3 Flow mit dynamischen Attributen umgehen! In den Fluid-Templates kann nun ein solches Property mit {product.myAttribute} aufgerufen werden. Da die Attribute von Fluid nicht direkt, sondern über die get*-Methode des Objektes aufgerufen werden, funktioniert das auch einwandfrei.

“unset”-Methoden sind nicht nötig, da dafür der “set”-Aufruf mit Parameter-Wert “0” verwendet werden kann. 

So einfach? Das kann ja gar nicht sein!

Stimmt. Im Moment müssen wir für die Ausgabe im Frontend den Namen des Attributes wissen, was ja nicht Sinn der Sache ist. Ausserdem gibt es noch viel mehr, was man dank (oder: trotz?) diesem Feature entwickeln kann.

Locations & DataFields

Wie wir in Teil 1 gesehen haben, ist es nicht schwierig, auf Model-Ebene dynamische Attribute hinzuzufügen. Die Ausgabe der Dynamischen Attribute im Frontend gestaltet sich – sobald man sich mit dem Konzept vertraut gemacht hat – auch nicht als grosse Hürde.

Anzeige im Frontend

Wo immer die dynamischen Attribute angezeigt werden sollen, wird ein Fluid-Partial eingebunden. Dieses nennen wir eine DataLocation. Als Parameter muss dabei jeweils ein Location-Objekt, sowie das Datenobjekt übergeben werden, wie folgendes Diagramm illustriert:

Wie wir in Teil 1 gesehen haben, ist es nicht schwierig, auf Model-Ebene dynamische Attribute hinzuzufügen. Die Ausgabe der Dynamischen Attribute im Frontend gestaltet sich – sobald man sich mit dem Konzept vertraut gemacht hat – auch nicht als grosse Hürde.

Ebenfalls daraus ersichtlich ist die n:n-Verknüfpung zwischen Locations und DataFields. Im Template wird das Partial wie folgt eingebunden:

Es wird dem Parameter data das von AbstractDataModel abgeleitete Objekt und dem Parameter Location eine Location-Instanz übergeben. Mit der Location können mehrere DataFields verknüpft sein, welche dann vom data-Objekt die dynamischen Attribute abfragen.

Ein DataField kann einen beliebigen Typ haben, welcher die Art des Feldes definiert. Möglich sind zum Beispiel:

  • Checkbox
  • Radio-Button
  • Inputfeld
  • Textfeld
  • Datumsfeld

Location-Partial

Die vorhin definierten Typen müssen im Location-Partial abgebildet sein. In unserem Beispiel sieht das so aus:

Dem geschulten Auge fallen dabei drei Dinge auf:

  1. Es werden custom ViewHelper verwendet.
  2. Für den POST-Request werden die Daten in einem separaten Daten-Array gespeichert; das muss beim Speichern berücksichtigt werden.
  3. Das Partial muss immer innerhalb eines <form>-Tags liegen.

ViewHelper

Format\ViewHelper

Ist soweit selbsterklärend. Es braucht nur einen Linien-Code in der render-Methode.

Data\CheckedViewHelper

Bestimmt, ob eine Checkbox oder ein Radio-Button gechecked ist oder nicht. Die render-Methode beinhaltet folgende Zeilen:

Data\ValueViewHelper

Liest die von den in der Location definierten DataFields angeforderten Daten aus einem AbstractDataModel-Objekt.

Actions

Die edit- und update-Actions für Views, welche das DataLocation-Partial laden, müssen angepasst werden.

editAction

updateAction

Hier ist $product natürlich das zu speichernde AbstractDataModel-Objekt.

Abschluss

Nun muss noch die Verwaltung der Location- und DataField-Objekte eingebaut werden. Diese CRUD-Views und -Actions können ganz bequem per Flow-Kommandozeilentool erstellt werden.

Damit ist auch der zweite Teil abgeschlossen. Das Feature kann nun in der Typo3-Flow-Applikation verwendet werden.

Bei uns wurde das Feature natürlich für die spezifischen Kundenwünsche noch weiter angepasst, was auch problemlos möglich ist. Dies würde aber den Scope dieses Beitrages sprengen. Etwas tiefer in die Materie geht’s dann beim dritten Teil: DQL-Generator!

DQL-Generator

Die in TYPO3 Flow verwendete Datenbankabstraktion namens Doctrine, erlaubt das Absetzen von Queries mittels der eigens entwickelten Sprache DQL. Hierbei werden im Unterschied zu herkömmlichen Query-Sprachen nicht die Datenbank und deren Tabellen, sondern die Objekte in der Applikation abgefragt. Es handelt sich also um eine Abstraktion der herkömmlichen Abfragesprachen.

In diesem Teil wird es vor allem PHP-Codes zu lesen geben, welche im Anschluss jeweils kurz erklärt werden. Dies bedeutet nicht, dass wir auf jede Kleinigkeit eingehen werden, da dies erstens einen ellenlangen Beitrag zur Folge hätte und zweitens davon ausgegangen wird, dass der Leser PHP- und TYPO3 Flow-Know-how mitbringt.

DQL in TYPO3 Flow

Der Einsatz von DQL in Flow ist einfach und schnell erklärt:

<?php /** @var \Doctrine\ORM\Query $query */ $query = $this->objectManager->get("Doctrine\Common\Persistence\ObjectManager")->createQuery("SELECT product FROM MyCompany\MyApplication\Domain\Model\Product product");
$this->results = new \Doctrine\Common\Collections\ArrayCollection($query->execute());

Es muss also zuerst ein DQL-Query-Objekt erstellt werden mit der entsprechenden Abfrage. Diese wird dann ausgeführt mit $query->execute, und in diesem Fall einer ArrayCollection hinzugefügt.

Weitere Informationen zu DQL-Syntax und -Funktionen finden Sie auf der DQL-Website. Entsprechendes Fachwissen wird hier jedoch vorausgesetzt.

Konzept

Ein DQL-Generator ist im Prinzip keine Anwendung, welche nur mit dynamischen Attributen Sinn macht, sondern kann genau so gut auch ohne diese funktionieren. Sie ist dann sogar um einiges einfacher zu programmieren.

Im Zuge dieses Blog-Artikels gehen wir aber verständlicherweise von dynamischen Attributen aus. Ausserdem ist es nur beschränkt nötig, dynamische Reporte zu generieren, wenn die Klassenattribute selber nicht dynamisch sind.

Wir wollen also eine Seite, wo unsere Benutzer folgende Eingaben machen können:

  1. Name des Reports,
  2. Datenquelle (entspricht einer oder mehreren Model-Klasse/n),
  3. Verknüpfungen (JOIN-Operationen),
  4. Anzuzeigende Felder der Datenquelle und deren Verknüpfungen,
  5. Bedingungen/Filter,
  6. Sortierung.

Nun wird das Ganze ein bisschen komplizierter…

Der Name des Reports stellt kein Problem dar, diesen können wir wie gewohnt mit Flow auf dem Objekt speichern. Danach wird es bereits komplizierter: Woher wissen wir, welche Datenquellen verfügbar sind? Wir werden dazu die Reflection Services von Flow benutzen (aber noch ein bisschen erweitern).

Dasselbe gilt für die anderen Werte, welche wir benötigen. Zusätzlich gibt es aber noch Syntaxelemente, welche wir statisch einfügen müssen. Dabei handelt es sich um Elemente wie “ist grösser als” oder “ist gleich” und so weiter. Ausserdem kann es bei den Bedingungen Freitext-Werte geben, welche ebenfalls behandelt werden müssen.

Gleichermassen gibt es unzählige weitere Spezialfälle wie Datumsfelder, was aber aus Gründen der Übersicht ebenfalls nicht hier behandelt wird.Wie also sollen wir all dies dem User anzeigen, ohne diesen gänzlich zu verwirren?Die Idee ist, dass wir bei der Auswahl der Datenquelle nicht den für den Anwender unschönen Wert “\MyCompany\MyApplication\Domain\Model\Product”, sondern nur “Produkt” anzeigen. Dasselbe gilt natürlich für alle Attribute. 

Für Dynamische können wir die im letzten Teil besprochenen DataFields verwenden, da dort der Name des Attributs für die Ausgabe gespeichert ist. Für alle Anderen und für die Datenquellen benutzen wir ein Translation-File und die Übersetzungsfunktionalität von TYPO3 Flow.

Und auch weiterhin werden wir den Kunden natürlich unterstützen.

Ausserdem wollen wir, dass der Nutzer weiss, was er eingeben kann. So können wir mit Autovervollständigung (z.B. typeahead.js von Twitter) und Hilfefenstern (z.B. in Form von Modals) ihn dabei unterstützen.

Nach der Eingabe werden die validierten Werte nicht als Freitext, sondern als so genannte Tags gespeichert (siehe z.B. https://github.com/bootstrap-tagsinput/bootstrap-tagsinput). So können Tippfehler oder versehentliches Löschen von einzelnen Buchstaben verhindert werden.

Trotzdem: Uns muss bewusst sein, dass ein gewisses Verständnis und Technikaffinität vorhanden sein muss, um diesen Generator zu bedienen.

Model

Wir brauchen für den DQL-Generator zwei Klassen:

  • Report: Speichert die Informationen zum Abfrage-Report und einen Link zu einem DqlStatement-Objekt.
  • DqlStatement: Alle DQL-Queries und Informationen dazu werden auf diesem Objekt gespeichert.

Die Trennung ist nicht zwingend erforderlich, da es sich hierbei um eine 1:1-Beziehung handelt. Trotzdem ist es der Übersicht halber sinnvoll.

CODE

https://gist.github.com/randm-ch/fa731a0a5d8fd3c422ff (Report.php)

Hier werden bei der Methode getResults() auch die Ergebnisse des Reports aus der Datenbank gelesen.

Da es möglich ist, dass gewisse Attributnamen bei mehreren Objekten vorkommen, wird bei der Abfrage jeweils eine Nummer angehängt. Diese wird bei der Anzeige mittels der Methode getPropertyNames() wieder entfernt.

CODE

https://gist.github.com/randm-ch/d95f35aa1f04d1ed745c (DqlStatement.php)

Die generate()-Methode ist hier der Zentrale Punkt. Sie erstellt die DQL-Statements aus den Formularwerten. Ausserdem werden in dieser Klasse die statischen Werte wie LEFT JOIN oder IS NULL definiert.

Controller

CODE

https://gist.github.com/randm-ch/66e7bcfada8d85608fcc (ReportController.php)

Im Controller müssen zweierlei Dinge erledigt werden:

  1. CRUD der Reports
  2. Laden der Zusatzinformationen (Helper-Modal-Informationen, Vorschläge für Auto-Suggestion)

Ausserdem gibt es hier zwei Helper-Methoden:

  1. getAllJoinableTags() – Liefert die Namen aller Klassen, mit den aktuell ausgewählten Datenquellen verknüpft werden können
  2. getAllTags() – Liefert alle Attribute der Datenquellen und deren Verknüpfungen (mit Pfad, z.B. product.category.name)

Utilities

Dem neugierigen Leser ist im Controller-Code aufgefallen: Eine weitere Klasse wird dringend benötigt. Richtig,  \MyCompany\MyApplication\Utility\DataClassReflection wird als Nächstes behandelt.

CODE

https://gist.github.com/randm-ch/adcacf8b9390aa563b38 (DataClassReflection.php)

In getForbiddenProperties() können Sie definieren, welche Attribute dem Benutzer nicht zur Auswahl stehen. Die drei im Code Implementierten sind mehr oder weniger Pflicht, ansonsten könnten Infinite-Loops erzeugt werden.

Ausserdem müssen wir, um den Code zum funktionieren zu bewegen, die TagsinputTag-Klasse entwickeln. Hierbei handelt es sich um einen Wrapper, welcher einen Tagsinput-Tag beschreibt:

CODE

https://gist.github.com/randm-ch/b535a380caa1a21d5d7e (TagsinputTag.php)

Ein Tag hat jeweils einen Typ, möglich sind folgende:

  • Freitext (für Bedinungen)
  • Attribut (einer Klasse, z.B. user.email)
  • Verknüpfung (JOIN Operationen)
  • Bedingungsoperation (gleich, ungleich, grösser/kleiner als, …)
  • Bedingungsverbindung (und/oder)
  • Funktion (DQL-Funktionen wie CURRENT_TIMESTAMP(), IS NULL etc.)
  • Sortierung aufsteigend
  • Sortierung absteigend

Der Typ definiert, in welchem Input-Feld der Tag eingesetzt werden kann und welche Farbe er hat. Mehr dazu weiter unten in der View-Beschreibung.

Translation

Diese Templates sind nötig, um den DQL-Generator laufen zu lassen. Ein TYPO3-Fluid-Layout, basierend auf Twitter Bootstrap, wird vorausgesetzt.
CODEReporte anzeigen (Templates/Report/Index.html)
CODEEinen neuen Report generieren (Templates/Report/New.html)
CODEEinen Report bearbeiten (Templates/Report/Edit.html)
CODE Einen Report anzeigen (Templates/Report/Show.html)
CODEHilfetext für Datenquelle (Templates/Report/FromQuery.html)
CODEHilfetext für Verknüpfungen (Templates/Report/JoinQuery.html)
CODEHilfetext für Felder (Templates/Report/SelectQuery.html)
CODEHilfetext für Bedingungen (Templates/Report/WhereQuery.html)

JavaScript

jQuery wird vorausgesetzt, um die Tagsinput-Library verwenden zu können.

CODE

https://gist.github.com/randm-ch/55b2b069c07fc62d7caf (Scripts/tagsinput.js)
Dieses Script aktiviert die Autosuggest-Funktion, erstellt ein Tag – mit entsprechendem Typ, wie oben beschrieben – und fügt dieses in das entsprechende Input-Feld

Abschliessend

Was haben wir erreicht?

  • Attribute von Datenobjekten können dynamisch erweitert werden
  • Diese Daten können ausgelesen und geschrieben werden
  • Wir können anhand dieser Daten Reports erstellen

Damit sind alle Arbeiten abgeschlossen und der Generator kann nun verwendet werden.