In den bisherigen beiden Symfony-Artikeln wurde gezeigt,
- wie man das Symfony-Modul von Drupal 7 aktiviert und
- wie man mit Synfony ohne eine Zeile Source-Code zu schreiben, eine simple Blog-Anwendung entwickelt.
Im Folgenden soll das simple Blog aus dem Tutorial als vollwertiges Modul für ein Drupal 7 - Portal entwickelt werden.
Man kann mit dem Symfony-Modul für Drupal 7 zwar auf Symfony-Klassen zugreifen, aber nicht den gewohnten Workflow von Symfony nutzen, insbesondere nicht die Scaffolding-Möglichkeiten auf Basis des Console-Interface app/console. Das Symfony-Modul ist also nicht mehr als ein Proof of Concept, wie die Autoren selber schreiben.
Wie man die vollwertige Integration erreicht, wird im folgenden gezeigt. Damit können dann Entwickler ähnlich wie bei Typo3 Vers. 4.7 mit der Extension Extbase für das MVC-Framework Flow3 im Falle Drupal 7 Module auf Basis des MVC-Frameworks Symfony entwickeln, die später leichter auf Drupal 8 portiert werden können. Eine solche Möglichkeit fehlte für Drupal 7 bisher. Es kann sich also für die nähere Zukunft schon lohnen, Module für Drupal auf Symfony-Basis zu entwickeln und man braucht dafür nicht zu warten, bis sich Drupal 8 etabliert hat.
Installation eines vollwertigen Symfony-Frameworks im Symfony-Modul-Ordner
Der erste Schritt besteht darin, dass wir ein vollwertiges Symfony-Framework im Symfony-Modul mittels Composer herunterladen. Wir deinstallieren deshalb ein schon installiertes Symfony-Modul komplett und löschen es inklusive der alten Symfony-Klassen. Dann laden wir uns ein frisches Modul von der Drupal-Seite herunter, installieren es aber noch nicht.
Als nächstes installieren wir ein frisch heruntergeladenes entzipptes Symfony2 in das Symfony-Modul von Drupal ohne Rücksicht auf Verluste.
Wir können alternativ auch ein schon installiertes Symfony - etwa unterhalb von htdocs - inklusive diverser selbst- oder fremdentwickelter Bundles in den Modulordner kopieren.
Wichtig ist dabei nur folgendes:
- Eine danach eventuell vorhandene Datei composer.lock muss gelöscht werden.
- Alle Dateien in app/cache müssen gelöscht werden (manuelles Clear Cache).
Dann wird composer.phar in das Symfony-Verzeichnis kopiert. Ich hatte ja im Artikel zur Installation des Symfony-Moduls gezeigt, wie man den Composer installiert. Eine Alternative zu der dort dargestellten Vorgehensweise mit Hilfe von curl ist hier beschrieben, falls noch kein Curl installiert ist oder man auf die sehr umständliche Installation verzichten möchte (s. etwa Mitte des verlinkten Textes, wo auf den Installer des Composers verwiesen wird).
Funktioniert der Composer, werden die Bezüge und Sourcen des Frameworks aktualisiert. Dazu führt man folgende Befehle mit cmd.exe im Modulordner aus:
php composer.phar self-update php composer.phar install
Den Update-Befehl führe ich vorsichtshalber als erstes aus. Ansonsten werde ich eventuell vom Composer auf das Vorliegen einer nicht aktuellen Version hingewiesen.
Abb. 1: Installation des Symfony-Frameworks in das Symfony-Module von Drupal
Das Composer-Install (s. Abb. 1) kann etwas dauern. Zu Beginn sehe ich die Meldung "Loading composer ....". Also nicht zu früh abbrechen, falls man ungeduldig wird.
Danach habe ich im Modulordner eine komplette Symfony-Installation, die ich allerdings so noch nicht für Drupal nutzen kann. Ich kann aber schon einmal zwei Tests vornehmen.
Test1:
Der erste Test besteht darin, zu prüfen, ob die Symfony-Installation erfolgreich war. Ich rufe sie deshalb auf z.B. mit
- http://localhost/<Drupal-Home-Verzeichnis>/sites/all/modules/symfony/web/app_dev.php
Wenn ich ein normales Entwickler-Fenster von Symfony2 bekomme (s. Abb. 2), war die Installation erfolgreich.
Abb. 2: Ergebnis von Test2: Eine vorläufige erfolgreiche Symfony-Installation
Test2:
Der zweite Test überprüft, ob sich das Framework in Drupal ansprechen lässt. Dazu installieren wir in Drupal jetzt das Modul Symfony und klicken in der Symfony-Modul-Zeile (unter admin/modules) auf den help-Link. Wenn es funktioniert, sollte ich einen kurzen Help-Text und eine Ausgabe "HttpFoundation Example ... " erhalten (s. Artikel: Installation des Symfony-Moduls).
Der Inhalt meines Drupal-Symfony-Modul-Ordners sieht nach erfolgreicher Installation ungefähr so aus:
Abb. 3: Vorläufiger Inhalt des Symfony-Modul-Verzeichnis
Vom Drupal-Modul stammen nur noch diverse Licence- und Readme-Dateien sowie die Modul-Dateien symfony.info und symfony.module.
Von Symfony selbst brauchen wir in diesem Verzeichnis nur die Ordner bin und vendor.
Die Ordner app, web und src kopieren wir jetzt in den Ordner whr_nrw/symfonyblog, in dem wir unser eigenes Symfony-Modul für Drupal erstellen wollen, natürlich unter Bezugnahme (dependencies[]) auf das Drupal-Symfony-Modul in dessen .info-Datei.
Wir werden aber auch das Drupal-Symfony-Modul für unsere Zecke noch anpassen müssen, damit die Symfony-Entwicklungsumgebung innerhalb von Drupal genauso funktioniert, wie wir das von der normalen Symfony-Entwicklung gewohnt sind.
Abb. 4: Inhalt des Modulordners whr_nrw/symfonyblog nach kopieren (Ctrl-X) von app, src und dev aus dem Symfony-Modul-Ordner
Im ursprünglichen Symfony-Modul-Ordner (vgl. Abb. 3) verbleiben die dort vorher schon vorhandenen Root-Dateien (sämtlich) sowie nur noch die beiden Ordner:
- vendor und
- bin.
Test1 (s. oben) wird jetzt nicht mehr funktionieren. Test2 aber immer noch.
Als nächstes passen wir die composer.json-Datei so an, dass die dort darin enthalten Pfade auf unser spezielles zu entwickelndes Drupal-Modul symfonyblog verweisen.
{ "name": "symfony/framework-standard-edition", "license": "MIT", "type": "project", "description": "The \"Symfony Standard Edition\" distribution", "autoload": { "psr-0": { "": "../whr_nrw/symfonyblog/src/" } }, "require": { "php": ">=5.3.3", "symfony/symfony": "~2.4", "doctrine/orm": "~2.2,>=2.2.3", "doctrine/doctrine-bundle": "~1.2", "twig/extensions": "~1.0", "symfony/assetic-bundle": "~2.3", "symfony/swiftmailer-bundle": "~2.3", "symfony/monolog-bundle": "~2.4", "sensio/distribution-bundle": "~2.3", "sensio/framework-extra-bundle": "~3.0", "sensio/generator-bundle": "~2.3", "incenteev/composer-parameter-handler": "~2.0" }, "scripts": { "post-install-cmd": [ "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile" ], "post-update-cmd": [ "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile" ] }, "config": { "bin-dir": "bin" }, "extra": { "symfony-app-dir": "../whr_nrw/symfonyblog/app", "symfony-web-dir": "../whr_nrw/symfonyblog/web", "incenteev-parameters": { "file": "../whr_nrw/symfonyblog/app/config/parameters.yml" }, "branch-alias": { "dev-master": "2.4-dev" } } }
Es handelt sich um eine leicht modifiziert Fassung einer composer.json-Datei der frischen Symfony-Installation.
Gegenüber dem Original wurden die Pfade unter "autoload..psr-0" und unter "extra" so angepasst, dass sie auf unser im folgenden zu erstellendes Modul auf Symfony-Basis whr_nrw/symfonyblog verweisen. Da sowohl das Symfony-Modul als auch unser zu erstellendes whr_nrw/symfonyblog-Modul im Orderner sites/all/modules liegen, reichen für diesen Bezug relative Pfade, z.B. "../whr_nrw/symfonyblog/app".
Leider gibt es auch Rückbezüge in den kopierten Dateien in den Ordnern src, web und app, die für eine ordentliche Verwaltung unserer Anwendung mit dem Composer composer.phar erforderlich sind.
Wenn wir nämlich jetzt nochmal ein Update mit php composer.phar install versuchen, erhalten wir Fehlermeldungen. Die Fehlermeldungen besagen z.B., dass die in unser Modul symfonyblog kopierten autoload.php-Sourcen die entsprechenden Eltern-autoload.php-Dateien im Symfony-Modul aufgrund fehlerhafter Pfadangaben nicht finden.
Wir müssen also in unserem symfonyblog-Ordner einige Dateipfade in diversen Dateien modifizieren, bis unser php composer.phar install rund läuft.
Wir fangen mal mit der Datei whr_nrw/symfonyblog/app/autoload.php an und passen die Pfade dort folgendermaßen an:
<?php use Doctrine\Common\Annotations\AnnotationRegistry; use Composer\Autoload\ClassLoader; /** * @var ClassLoader $loader */ $loader = require __DIR__.'/../../../symfony/vendor/autoload.php'; AnnotationRegistry::registerLoader(array($loader, 'loadClass')); return $loader;
Bei einer frischen Symfony-Installation sollten wir jetzt keine weiteren Fehlermeldungen mehr haben. Unser Composer läuft durch. Das ist schonmal ein sehr gutes Zeichen.
Test3:
Wir testen jetzt, ob wir unser Symfony wie in Test1 wieder über die Url erreichen können, aber diesmal mit folgender Url:
- http://localhost/<Drupal-Home-Verzeichnis>/sites/all/modules/whr_nrw/symfonyblog/web/app_dev.php
und siehe da, die Symfony-Entwicklungsumgebung lacht uns wieder an (vgl. Abb. 2).
Sollten bei diesen Tests Fehlermeldungen auftauchen, dann liegt das vielleicht daran, dass wir für die Installation kein taufrisches Symfony-Projekt verwendet haben und in den Konfigurationsdateien aus einem vorherigen Projekt (z.B. unserem Tutorial) noch Fehl-Parameter zu finden sind. Diese können wir anhand der Fehlermeldungen leicht identifizieren und korrigieren.
Nun haben wir aber unser Symfony so eingerichtet, wie wir das für die anschließende Modulentwicklung benötigen.
Tipp: Wir sollten uns die Sourcen in unserem Ordner symfonyblog jetzt sichern, da wir diese wiederverwenden können, wenn wir weitere Drupal-Module auf die gleiche Weise entwickeln.
Damit sind das Symfony-Modul und die symfonyblog-Ressourcen so präpariert, dass mit der Entwicklung eines spezifischen Drupal-Moduls symfonyblog auf Symfony-Basis begonnen werden kann.
Entwicklung eines Drupal-Blog-Moduls auf Symfony-Basis
Jedes Drupal-Modul beginnt mit einer .info-Datei. Wichtigster Punkt in unserer symfonyblog.info ist die Abhängigkeit zum Drupal-Symfony-Modul:
name = Symfony Blog description = A simple demo blog modul based on Symfony framework. package = whr_nrw core = 7.x php = 5.3.2 version = "7.x-0.1-alpha0" dependencies[] = symfony
Die zweite unverzichtbare Datei ist die .module-Datei, die dem Modul Leben einhaucht. Wir schreiben eine erste Version von symfonyblog.module.
<?php /** * Implements hook_menu() */ function symfonyblog_menu(){ /* Hello World */ $items['demo/hello'] = array( 'title' => 'Hello World', 'page callback' => 'symfonyblog_list_standard', 'access arguments' => array('access page manager'), 'expanded' => TRUE, ); return $items; } use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Debug\Debug; function symfonyblog_list_standard($arg = 0){ $loader = require_once __DIR__.'/app/bootstrap.php.cache'; Debug::enable(); require_once __DIR__.'/app/AppKernel.php'; $kernel = new AppKernel('dev', true); $kernel->loadClassCache(); $request = Request::createFromGlobals(); $response = $kernel->handle($request); $kernel->terminate($request, $response); $resp = $response->getContent(); return $resp; }
Der Zweck dieses ersten Moduls ist es, die Hello World-Demo von Symfony im Drupal-Portal anzuzeigen. Dafür reicht im Menu-Hook der Verweis auf die Callback-Funktion symfonyblog_list_standard(...). Wir betrachten das Ergebnis (Abb. 5).
Abb. 5: Die Hello-World-Demo von Symfony in Drupal noch mit Fehlermeldung
Trotz der Fehlermeldung ist das ein sehr erfreuliches Ergebnis, wenn man bedenkt, mit wie wenig Source-Code diese Integration in Drupal möglich ist.
Die Hello-World-Demo wird aufgerufen mit:
- http://localhost.sandbox.hauertmann.com/demo/hello/xy
Ich habe bewusst xy statt world gewählt, weil die Symfony-Demo, die im Framework enthalten ist, dann auch xy ausgibt und nicht world. Schaut man in den Sourcecode von symfonyblog.module, so erkennt man, dass $arg in der Funktion symfonyblog_develtoolbar gar nicht ausgewertet wird. Symfony wertet seine Argumente also selbstständig aus. Solange man keine weiteren Ansprüche an eine Anwendung hat, braucht man sich also von Drupal-Seite aus erstmal nicht um das Pfad-Management zu kümmern.
Es bleibt die Fehlermeldung:
An error occurred while loading the web debug toolbar (404: Not Found). Do you want to open the profiler?
Wir möchten ja auch den Javascript-Entwickler-Toolbar von Symfony in Drupal sehen. Dazu ändern wir jetzt die Datei app/config/routing_dev.yml folgendermaßen ab:
_wdt: resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" prefix: /dev/_wdt _profiler: resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" prefix: /dev/_profiler _configurator: resource: "@SensioDistributionBundle/Resources/config/routing/webconfigurator.xml" prefix: /dev/_configurator _main: resource: routing.yml # AcmeDemoBundle routes (to be removed) _acme_demo: resource: "@AcmeDemoBundle/Resources/config/routing.yml"
Wir haben dort nur dreimal die Zeile prefix: geändert, indem wir jeweils ein /dev vor den Pfad gesetzt haben. Wir brauchen für diese Tools einen definierten Pfad für unser hook_menu in unserem Drupal-Modul.
Unsere erste symfonyblog.module-Version vervollständigen wir jetzt so:
<?php /** * Implements hook_menu() */ function symfonyblog_menu(){ /* Hello World */ $items['demo/hello'] = array( 'title' => 'Hello World', 'page callback' => 'symfonyblog_list_standard', 'access arguments' => array('access page manager'), 'expanded' => TRUE, ); /* Profiler */ $items['dev'] = array( 'title' => 'Hr Blog', 'page callback' => 'symfonyblog_develtoolbar', 'access arguments' => array('access page manager'), 'expanded' => TRUE, ); return $items; } use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Debug\Debug; function symfonyblog_list_standard($arg = 0){ $loader = require_once __DIR__.'/app/bootstrap.php.cache'; Debug::enable(); require_once __DIR__.'/app/AppKernel.php'; $kernel = new AppKernel('dev', true); $kernel->loadClassCache(); $request = Request::createFromGlobals(); $response = $kernel->handle($request); $kernel->terminate($request, $response); $resp = $response->getContent(); return $resp; } // Profiler function symfonyblog_develtoolbar($arg = 0){ $loader = require_once __DIR__.'/app/bootstrap.php.cache'; Debug::enable(); require_once __DIR__.'/app/AppKernel.php'; $kernel = new AppKernel('dev', true); $kernel->loadClassCache(); $request = Request::createFromGlobals(); $response = $kernel->handle($request); $kernel->terminate($request, $response); $resp = $response->getContent(); if ($arg == '_wdt') { echo $resp; exit; } if ($arg == '_profiler') { exit; } if ($arg == '_configurator') { exit; } return $resp; }
Wir haben also einen Ajax-Handler für unseren Toolbar in unser hook_menu zusätzlich eingefügt.
Abb. 6: Die Hello-World-Demo von Symfony in Drupal mit Entwickler-Toolbar
Wir bekommen jetzt keine Fehlermeldung mehr und noch besser, der Toolbar sitzt ordentlich und funktionsfähig dort, wo er hingehört, im Fußbereich des Portals und kann dort jederzeit durch einen Klick auf den Button ganz rechts aus- und eingeblendet werden.
Voila! Die Integration von Symfony in Drupal 7 ist vollbracht. Ich möchte zwar nicht behaupten, dass alles, was dem reinen Symfony-Entwickler zur Verfügung steht, in Drupal ohne Verfeinerung dieses Ansatzes funktioniert, aber zumindest das Wichtigste.
Ich will aber noch beweisen, dass man den wesentlichen Symfony-Workflow zur Entwicklung eines Moduls in Drupal ebenfalls verwenden kann. Dies passiert im nächsten Abschnitt.
Nutzung des Symfony-Workflows in Drupal zur Erstellung eines CRUD-Moduls
Wir werden jetzt das aus dem Symfony-Tutorial bekannte Blog-Beispiel hier als Drupal-Modul implementieren.
Programmieren war dafür ja nicht erforderlich gewesen, sondern nur der Einsatz des Consolen-Interface app/console.
Wir begeben uns in das Rootverzeichnis unseres symfonyblog-Moduls und führen folgende Kommandos der Reihe nach aus. Dabei beantworten wir die Fragen genauso, wie im Symfony-Tutorial beschrieben.
php app/console --version php app/console generate:bundle php app/console doctrine:database:drop --force php app/console doctrine:database:create php app/console gen:doctrine:entity php app/console doctrine:schema:update --dump-sql php app/console doctrine:schema:update --force php app/console doctrine:generate:crud
Falls wir schon eine Datenbank-Tabelle haben, können wir natürlich auf den schema-update--force-Befehl verzichten. Oder wir legen eine neue Datenbank mit anderem Namen an. Dazu ändern wir vor dem dritten Kommando die Datenbank-Parameter in C:/xampp/htdocs/Symfony/app/config/parameters.yml.
Tipp: Falls es bei den Kommandos Zeile 2 oder 5 zu Fehler-Meldungen kommen sollte, kann man folgendes untersuchen: Die Modul-Datei whr_nrw/symfonyblog/app/config/routing.yml sollte ganz zu Beginn keine whr_blog-Definition enthalten, also leer sein. Es kann sein, dass dort aus den Tutorial-Experimenten durch das Kopieren des app-Ordners noch eine alte Definition steht. Bei mir war das jedenfalls so. Also immer besser mit einer ganz frischen Symfony-Installation arbeiten, was bei mir leider nicht der Fall war. Ansonsten hilft bei solchen Problemen, die auf Namenskonflikte hindeuten, manchmal auch, einfach einen anderen Namespace zu verwenden.
Wenn wir diesen Prozess erfolgreich durchlaufen haben, können wir das Ergebnis offline mit folgenden Urls testen:
- Entwicklungsumgebung: http://localhost/<Drupal-Home-Verzeichnis>/sites/all/modules/whr_nrw/symfonyblog/web/app_dev.php
- Blog: http://localhost/<Drupal-Home-Verzeichnis>/sites/all/modules/whr_nrw/symfonyblog/web/app_dev.php/post/
<Drupal-Home-Verzeichnis> müssen wir natürlich durch den Pfad ausgehend von htdocs zu unserem Drupal-Root-Verzeichnis ersetzen.
Mit dem ersten Link rufen wir die Symfony-Entwicklungsumgebung ähnlich Abb. 2 auf.
Mit dem zweiten Link erreichen wir unser Demo-Blog außerhalb der Drupal-Umgebung als pure Symfony-Applikation (s. Abb. 7).
Abb. 7: Erfolgreiche Implementierung unseres CRUD-Blog-Moduls
Integration der Symfony-Applikation in Drupal als Modul
Für eine Integration in unser Drupal-System müssen wir jetzt nur noch unser hook_menu erweitern. Wir fügen in die Funktion symfonyblog_menu() in symfonyblog.modul folgendes Snippet hinzu:
/* CRUD-Blog-Example */ $items['post'] = array( 'title' => 'Symfony Blog', 'page callback' => 'symfonyblog_list_standard', 'access arguments' => array('access page manager'), 'expanded' => TRUE, );
Wir sehen, dass wir keine weitere Callback-Funktion verwenden müssen. Wir können symfonyblog_list_standard aus dem Hello-World-Beispiel wiederverwenden.
Testen können wir unser Blog jetzt mit:
- http://localhost.sandbox.com/post/
Abb. 8: Demo-Blog-Symfony-Applikation als Drupal 7 Modul
Nach dem Anlegen einiger Posts kann man sich diese listen lassen, man kann sie editieren, einzeln anzeigen lassen und wieder löschen. Das alles, ohne Drupal damit beauftragen zu müssen. Alles spielt sich dann innerhalb der Symfony-Applikation ab.
Fazit
Auf diese Weise können wir beliebige weitere Funktionalitäten hinzufügen. Das ist jetzt alles ziemlich einfach.
Haben wir diese Einfachheit jetzt Drupal oder Symfony zu verdanken? Ich würde tippen: beiden! Die Tatsache, dass sich derartig komplexe Systeme so relativ problemlos integrieren lassen, spricht für die gute Konzeption jedes einzelnen Systems.