TYPO3 Flow – Applikation mit Dynamischen Attributen 1/3

1/3: Dynamische Attribute

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öchte 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 aus der Sicht der dynamischen Applikation:

  • (plus) Wartungsarbeiten nach Abschluss des Projektes werden reduziert (d.h. Kosten nach Abschluss geringer)
  • (plus) Kunde kann seine Applikation jederzeit nach seinen Bedürfnissen gestalten
  • (minus) Initiale Entwicklung kostet Zeit und damit Geld
  • (minus) 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

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
identifier string
value string
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:

<?php
namespace MyCompany\MyApplication\Domain\Model;
 
use TYPO3\Flow\Annotations as Flow;
 
/**
 * @Flow\Entity
 */
class Product extends \MyCompany\MyApplication\Domain\Model\AbstractDataModel {
	...
}

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

<?php
namespace MyCompany\MyApplication\Domain\Model;

use TYPO3\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;
use MyCompany\MyApplication\Exception;


/**
 * @Flow\Entity
 * @ORM\InheritanceType("JOINED")
 */
abstract class AbstractDataModel {

	/**
	 * @var \Doctrine\Common\Collections\Collection<\MyCompany\MyApplication\Domain\Model\Data>
	 * @ORM\OneToMany(mappedBy="dataModel")
	 */
	protected $data;

	/**
	 * @var \MyCompany\MyApplication\Domain\Repository\DataRepository
	 * @Flow\Inject
	 */
	protected $dataRepository;
 
	/**
	 * Get and set all attribute data to Data objects
	 *
	 * @param string $name The name of the method to be called
	 * @param array $arguments The arguments provided
	 * @return mixed
	 */
	public function __call($name, $arguments) {
		$isSetterMethod = (substr($name, 0, 3) === 'set');
		$isGetterMethod = (substr($name, 0, 3) === 'get');
		$attribute = lcfirst(substr($name, 3));
		$value = (isset($arguments[0])) ? $arguments[0] : NULL;
		if($isSetterMethod) {
			if(!property_exists($this, $attribute)) {
				try {
					$data = $this->getData($attribute);
					if($value !== NULL && $value !== '' && $value !== 0 && $value !== '0') {
						$data->setValue($value);
						$this->dataRepository->update($data);
					}
					else {
						$this->removeData($data);
						$this->dataRepository->remove($data);
					}
				}
				catch(Exception\PropertyNotFoundException $e) {
					if($value !== NULL && $value !== '' && $value !== 0 && $value !== '0') {
						$data = new Data($attribute, $value);
						$this->addData($data);
						$this->dataRepository->add($data);
					}
				}
			}
			else {
				$this->$attribute = $value;
			}
		}
		if($isGetterMethod) {
			if(!property_exists($this, $attribute)) {
				try {
					return $this->getData($attribute)->getValue();
				}
				catch(Exception\PropertyNotFoundException $e) {
					return NULL;
				}
			}
		}
	}

	/**
	 * Returns the value of the data assigned to $identifier
	 *
	 * @param string $identifier
	 * @return \MyCompany\MyApplication\Domain\Model\Data
	 * @throws \MyCompany\MyApplication\Exception\PropertyNotFoundException
	 */
	public function getData($identifier = NULL) {
		if($identifier !== NULL) {
			/** @var \MyCompany\MyApplication\Domain\Model\Data $data */
			foreach($this->data as $data) {
				if($data->getIdentifier() === $identifier) {
					return $data;
				}
			}
			throw new Exception\PropertyNotFoundException('Property with key "'.$identifier.'" not found');
		}
		return $this->data;
	}

	/**
	 * @param \MyCompany\MyApplication\Domain\Model\Data $data
	 * @return void
	 */
	public function addData(Data $data) {
		$data->setDataModel($this);
		$this->data->add($data);
	}

	/**
	 * @param \MyCompany\MyApplication\Domain\Model\Data $data
	 * @return void
	 */
	public function removeData(Data $data) {
		$this->data->removeElement($data);
	}
}

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. Dafür gibt’s aber Teil 2 und 3, welche in Kürze folgen werden.

Kommentare (0)

Kommentar verfassen