Dynamisches Menü für Typo3 Flow Anwendungen mit Signal & Slot

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:

  1. Eine Signalfunktion
  2. Slotfunktionen in jedem Package
  3. 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.

Die Kommentarfunktion ist geschlossen.