Für modulare Anwendungen ist es wünschenswert, dass sich das Anwendungsmenü dynamisch aus den aktivierten Modulen generiert. Das Typo3 Flow Framework organisiert Anwendungsmodule in Packages. Fügt man der Anwendung ein Package hinzu sollte sich automatisch das Menü erweitern. Um solch einen Mechanismus zu implementieren bietet sich das Signal-Slot-Konzept von Flow an.
Erster Versuch: ReflectionClass
Im ersten Anlauf hatte ich das dynamische Menü über die PHP Reflection Funktionen implementiert. Jeder Controller implementiert ein Interface mit Getter-Funktionen, die die Parameter für das Menü liefern. Per Reflection werden alle Controller, die dieses Interface implementieren ermittelt und die betreffenden Getter-Funktionen des Interfaces aufgerufen. Die gesammelten Menü-Informationen werden in einem Array gesammelt und dann an das Fluid-Template übergeben. Das hat funktioniert, erwies sich aber als recht aufwändig und vor allem unflexibel.
Package Settings
Besser wäre es, die Menü-Einstellungen in der Settings.yaml
-Datei eines jeden Packages festzulegen:
1 2 3 4 5 6 7 8 9 10 | Qbus: Bestdesq: Dashboard: packageLabel: Übersicht packageIcon: fa-dashboard packageOrder: 1 packageItems: Dashboard: 'Kennzahlen' Planning: 'Planung' TimePlanning: 'Zeitplanung' |
Dies definiert den Hauptmenüpunkt Übersicht
an Position 1 des Menüs mit den Unterpunkten Kennzahlen
, Planung
, Zeitplanung
. Die Settings können als Properties in einem Basiscontroller der Anwendung deklariert werden. Dank der Dependency Injection
des Flow Frameworks ist das sehr einfach.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * @var string * @FlowInject(setting="packageLabel") */ protected $packageLabel; /** * @var string * @FlowInject(setting="packageIcon") */ protected $packageIcon; /** * @var string * @FlowInject(setting="packageOrder") */ protected $packageOrder; /** * @var array * @FlowInject(setting="packageItems") */ protected $packageItems; |
Jeder abgeleitete Controller verfügt nun über diese Eigenschaften, die die Menüstruktur beschreiben.
Signal-Slot
Der Signal-Slot-Mechanismus von Typo3 Flow implementiert das Event-Observer-Entwurfsmuster. Die Umsetzung ist recht einfach. Es braucht drei Dinge:
- Eine Signalfunktion
- Slotfunktionen in jedem Package
- Verknüpfung von Signal und Slots
Die Signafunktion sendet eine Botschaft, auf die die Slotfunktionen der einzelnen Packages reagieren, indem sie die Eigenschaften zur Menüstruktur mitteilen. Beide Funktionen werden im Basiscontroller angelegt, von dem alle Controller der Anwendung abgeleitet sind. Die Signalfunktion muss nur als „Stummel“ deklariert werden:
1 2 3 4 5 6 7 8 | /** * signal-slot stub to build the menu * * @param array $menuItems * @return void * @FlowSignal */ protected function emitCallForMenu(array &$menuItems) {} |
Damit Flow dies als eine Signalfunktion erkennt, muss der Funktionsname den Präfix emit
enthalten und es muss die Anotation @FlowSignal
angegeben sein. Das Framework baut daraus eine Implementierung der Funktion zusammen – der Programmierer muss sich darum nicht weiter kümmern. Diese „Magic“ bedient sich der Aspect Orientierten Programmierung (AOP) in Typo3 Flow. Der Parameter $menuItems
wird als Referenz übergeben und beinhaltet so nach dem Aufruf die Menüstruktur. Die Slotfunktion setzt einen Eintrag im Array $menuItems
, der aus den Setting-Informationen besteht:
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * slot function to build the menu * * @param array $menuItems */ public function getMenuItemsOfPackage(array &$menuItems) { $menuItemOfPackage = array(); $menuItemOfPackage['packageKey'] = $this->objectManager->getPackageKeyByObjectName(get_class($this)); $menuItemOfPackage['packageLabel'] = $this->packageLabel; $menuItemOfPackage['packageIcon'] = $this->packageIcon; $menuItemOfPackage['packageItems'] = $this->packageItems; $menuItems[$this->packageOrder] = $menuItemOfPackage; } |
Auch hier ist die Übergabe des Parameters als Referenz wichtig. Die Verknüpfung zwischen Signal und Slots wird in der Initialisierungsklasse Package eines jeden Paketes vorgenommen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | namespace Qbus\Bestdesq\Dashboard; use \TYPO3\Flow\Core\Bootstrap; use \TYPO3\Flow\Package\Package as BasePackage; class Package extends BasePackage { /** * Invokes custom PHP code directly after the package manager has been initialized. * @param Bootstrap $bootstrap The current bootstrap * @return void */ public function boot(Bootstrap $bootstrap) { $dispatcher = $bootstrap->getSignalSlotDispatcher(); $dispatcher->connect( 'Qbus\Bestdesq\Controller\AbstractModuleController', 'callForMenu', 'Qbus\Bestdesq\Dashboard\Controller\DashboardController', 'getMenuItemsOfPackage' ); } } |
Signal- und Slotfunktionen können in jeder beliebigen Klasse angelegt werden. Die Funktion $dispatcher->connect()
hat die Parameter Klassenname und Funktionsname des Signals (ohne den Prefix emit
) sowie Klassenname und Funktionsname des antwortenden Slots. Als Klassenname wird hier eine beliebige vom Basiscontroller abgeleitete Controller-Klasse dieses Paketes angegeben. Im Basiscontroller wird nun in der Funktion initializeView
das Signal ausgelöst und die sich daraus ergebende Menüstruktur an ein Fluid-Template übergeben.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /** * @param ViewInterface $view */ protected function initializeView(ViewInterface $view) { parent::initializeView($view); /** @var \TYPO3\Fluid\View\TemplateView $view */ $view->setLayoutPathAndFilename('resource://Qbus.Bestdesq/Private/Layouts/Default.html'); // call for menu items of all packages $menuItems = array(); $this->emitCallForMenu($menuItems); $view->assign('packages', $menuItems); // set this controller to be the active one $active = substr(get_class($this), strpos(get_class($this), '\Controller\') + 12, -10); $view->assign('active', $active); // build the module title (section "Title") $moduleTitle = array( 'packageLabel' => $this->packageLabel, 'packageIcon' => $this->packageIcon ); $view->assign('moduleTitle', $moduleTitle); } |
Das ist alles! Ich hatte zuvor noch versucht, die Menüinformationen in einer eigenen MenuUtility
Klasse zu halten. Das hatte aber den Nachteil gehabt, dass ich zusätzlich zu den Settings
und der Package
-Klasse pro Package noch eine leere, abgeleitete Klasse dieser MenuUtility
-Klasse hätte anlegen müssen.