Möchte man eine Contao Erweiterung in mehreren Projekten einsetzen und/oder der Allgemeinheit zur Verfügung stellen, legt man dazu am besten ein Bundle an und veröffentlicht es auf Packagist. Dann lässt sich die Erweiterung einfach über Composer oder den Contao Manager installieren. Dieses Tutorial beschreibt wie das unter Verwendung des Skeleton Bundles funktioniert. Dabei wird ein Bundle erstellt, das ein Inhaltselement erzeugt, das "Hello World" ausgibt. Folgendes werden wir machen.
- Einen Vendor- und Bundle-Namen wählen
- Das Skeleton Bundle verstehen und anpassen
- Ein Repository für das Bundle anlegen
- Das Bundle in der Entwicklungsinstallation installieren
- Die grundlegende Funktionalität implementieren
- Unit Tests schreiben
- Eine Klasse erstellen, die die Anforderungen der Tests erfüllt
- Die Klasse als Service über Dependeny Injection nutzen
- Ein anderes Paket als Abhängigkeit hinzufügen
- Den Code mit PHP-CS-Fixer formatieren
- Das Bundle auf Packagist veröffentlichen
Dieses Tutorial richtet sich an Entwickler, die bereits mit der Modul Erstellung für ältere Contao Versionen vertraut sind und nun wissen wollen, wie der Weg unter Contao ab Version 4 mit dem Symfony Framework geht. Daher werden grundlegende Kenntnisse in der Erweiterungsentwicklung für Contao vorausgesetzt. Zudem wird folgendes benötigt.
- Eine gute IDE (z.B. PhpStorm) oder ein guter Editor (z.B. Atom)
- Eine lokale Contao Installation
- Git (wahlweise über die Konsole oder mit einem GUI wie SourceTree)
- Composer
- Ein GitHub Account
- Ein Packagist Account
Auf der Contao Konferenz 2018 gab es einen ausgezeichneten Vortrag von Leo Feyer zur Erweiterungsentwicklung unter Contao 4, in dem auch das Skeleton Bundle vorgestellt wurde. Dieser Vortrag wurde mittlerweile auf YouTube veröffentlicht: https://www.youtube.com/watch?v=VzDYjEd7OyQ.
Unsere Erweiterung benötigt einen Vendor- und Bundle-Namen. Daraus setzt sich dann auch der Namespace zusammen. Der Vendor-Name ist der Name des Herstellers, also unserer. In diesem Tutorial wird "Acme" verwendet, ihr solltet aber euren eigenen oder euren Firmennamen einsetzen. Idealerweise ist das dann auch der Name, mit dem ihr bei GitHub und Packagist angemeldet seid. Der Bundle-Name ist der Name der Erweiterung. Er erhält das Prefix "Contao" und das Suffix "Bundle". Das Bundle in diesem Tutorial heißt somit "Contao Hello World Bundle".
Für den Paketnamen, den wir später in die composer.json eintragen, ergibt sich somit:
acme/contao-hello-world-bundle
Der zugehörige Namespace hat die UpperCamelCase Schreibweise:
Acme\ContaoHelloWorldBundle
Das Skeleton Bundle gibt es hier: https://github.com/contao/skeleton-bundle
Wir können es entweder wie dort beschreiben über die Konsole herunterladen oder über die Weboberfläche. In letzterem Fall klicken wir einfach auf Clone or download > Download ZIP und entpacken das Zip in einen beliebigen Ordner. Wohin wir die Dateien entpacken ist nicht wichtig. Wir werden sie wieder löschen, sobald das Bundle auf GitHub und im Vendor-Ordner unserer Testinstallation liegt. Vorher müssen aber noch ein paar Dateien angepasst werden.
Los geht es mit den Dateien, die im Root liegen. Die Datei .editorconfig
enthält grundlegende Einstellungen für unsere IDE bzw. unseren Editor (z.B. dass die Einrückung mit Spaces statt Tabs erfolgen soll). Damit das funktioniert müsst ihr für eure gewählte Entwicklungsumgebung eventuell noch ein Plugin installieren (PhpStorm: https://plugins.jetbrains.com/plugin/7294-editorconfig, Atom: https://atom.io/packages/editorconfig). An der Datei .editorconfig
müssen wir keine Anpassungen vornehmen.
Die Datei .gitignore
gibt an, welche Dateien nicht in das Git Repository kommen sollen. Hier sind keine Anpassungen nötig.
Die Datei LICENSE
können wir ebenfalls in Ihrem Urzustand belassen, außer wir wollen eine andere Lizenz verwenden. Eine gute Hilfe bei der Wahl der Lizenz ist diese Website: https://choosealicense.com/.
Die Datei .php_cs.dist
enthält die Konfiguration für das Tool PHP-CS-Fixer, mit dem wir später unseren Code aufräumen. Hier muss die Kopfzeile angepasst werden. Diese wird später in jede PHP-Datei eingefügt werden. Für das Hello World Bundle schreiben wir dort folgendes.
$header = <<<EOF
This file is part of Contao Hello World Bundle.
(c) Acme
@license LGPL-3.0-or-later
EOF;
Die composer.json
enthält die Konfiguration unseres Bundles für Composer. Sie ist wichtig, damit wir unser Bundle bequem über die Konsole oder den Contao Manager installieren können. Zudem wird hier festgelegt, welche anderen Packete unsere Erweiterungen als Abhängigkeiten benötigt. In der Datei müsst wir an mehreren Stellen Vendor- und Package-Namen anpassen und Metainformationen wie Autor und Beschreibung aktualisieren. Die composer.json
für das Hello World Bundle sollte dann so aussehen.
{
"name": "acme/contao-hello-world-bundle",
"type": "contao-bundle",
"description": "Hello World extension for Contao Open Source CMS",
"license": "LGPL-3.0-or-later",
"authors": [
{
"name": "Acme",
"homepage": "https://github.com/acme"
}
],
"require": {
"php": "^5.6 || ^7.0",
"contao/core-bundle": "4.4.*",
"symfony/framework-bundle": "^3.3"
},
"conflict": {
"contao/core": "*",
"contao/manager-plugin": "<2.0 || >=3.0"
},
"require-dev": {
"contao/manager-plugin": "^2.0",
"doctrine/doctrine-cache-bundle": "^1.3",
"friendsofphp/php-cs-fixer": "^2.6",
"leofeyer/optimize-native-functions-fixer": "^1.1",
"php-http/guzzle6-adapter": "^1.1",
"php-http/message-factory": "^1.0.2",
"phpunit/phpunit": "^5.7.26",
"symfony/phpunit-bridge": "^3.2"
},
"extra": {
"contao-manager-plugin": "Acme\\ContaoHelloWorldBundle\\ContaoManager\\Plugin"
},
"autoload": {
"psr-4": {
"Acme\\ContaoHelloWorldBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\ContaoHelloWorldBundle\\Tests\\": "tests/"
}
},
"support": {
"issues": "https://github.com/acme/contao-hello-world-bundle/issues",
"source": "https://github.com/acme/contao-hello-world-bundle"
}
}
Die Datei phpunit.xml.dist
ist die Konfigurationsdatei für unsere Unit Tests. Dort steht, dass sich die Tests im Ordner test
befinden und dass die Dateien im Ordner src
(mit Außnahme des Unterordners Resources
) getestet werden sollen. Wir müssen hier nur den Package Namen anpassen.
<testsuites>
<testsuite name="Contao Hello World Bundle">
<directory>./tests</directory>
</testsuite>
</testsuites>
In der README.md
fügen wir eine Beschreibung unseres Bundles in Markdown ein. Diese wird auf GitHub und Packagist angezeigt. In einem echten Bundle sollten wir hier möglichst gut beschreiben, was die Erweiterung macht und wie sie zu benutzen ist. Dabei sind folgende Punkte wichtig: Was ist bei der Installation zu beachten? Wie kann die Erweiterung konfiguriert werden? Welche Module, Inhaltselemente, Inserttags oder sonstige Funktionalitäten werden zur Verfügung gestellt? Für das Hello World Bundle beschränken wir uns auf folgenden kurzen Text.
# Contao Hello World Bundle
This bundle is just a test and not developed for usage in production.
Weiter geht es mit dem Ordner src
. Dieser enthält den eigentlichen Code unserer Erweiterung. Direkt unter src
liegt die Datei ContaoSkeletonBundle.php
. Diese Datei enthält die Hauptklasse unseres Bundles, die von der Symfony Klasse Bundle
abgeleitet wird. Wir benennen die Datei um in ContaoHelloWorldBundle.php
und ändern in der Datei den Klassennamen ebenfalls in ContaoHelloWorldBundle
sowie den Namespace in Acme\ContaoHelloWorldBundle
. Der Inhalt der Datei sieht dann so aus (ohne den Header, der später von PHP-CS-Fixer hinzugefügt wird).
<?php
namespace Acme\ContaoHelloWorldBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ContaoHelloWorldBundle extends Bundle
{
}
Nun zum Unterordner ContaoManager
. Darin liegt die Datei Plugin.php
. Sieh enthält eine Klasse Plugin
, die das BundlePluginInterface
implementiert. Die Klasse, die wir in der vorherigen Datei erstellt habt, wird hier im Contao System eingebunden. In der Methode getBundles
sagen wir, dass unser Bundle nach dem Contao Core Bundle
geladen werden soll. Würden wir beispielsweise zusätzlich sicherstellen wollen, dass unsere Erweiterung erst nach dem News Bundle
geladen wird, könten wir das dort entsprechend eintragen. Für jetzt reicht es aber, wie gewohnt SkeletonBundle
durch ContaoHelloWordBundle
zu ersetzen und den Namespace anzupassen.
<?php
namespace Acme\ContaoHelloWorldBundle\ContaoManager;
use Contao\CoreBundle\ContaoCoreBundle;
use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
use Acme\ContaoHelloWorldBundle\ContaoHelloWorldBundle;
class Plugin implements BundlePluginInterface
{
/**
* {@inheritdoc}
*/
public function getBundles(ParserInterface $parser)
{
return [
BundleConfig::create(ContaoHelloWorldBundle::class)
->setLoadAfter([ContaoCoreBundle::class]),
];
}
}
Die Dateien src/DependencyInjection/ContaoSkeletonExtension.php
und src/Resources/config/services.yml
sind dazu da um Services für das Bundle zu definieren. Die Klasse ContaoSkeletonExtension
lädt die Datei services.yml
und weißt die dort definierten Services entsprechend zu. Das Skeleton Bundle hat hier definiert, dass die Services contao.framework
und service_container
für alle Instanzen der Interfaces FrameworkAwareInterface
bzw. ContainerAwareInterface
über Setter Injection verwendet werden sollen. Für unsere Erweiterung spielt das nicht wirklich eine Rolle, wir könnten die vorhandenen Zeilen also auch löschen. Sie stören aber auch nicht. Wir werden im Verlauf dieses Tutorials einen eigenen Service hinzufügen und verwenden. Zunächst aber zum Anpassen der Dateien. Die Datei services.yml
muss nicht angepasst werden. Die Datei ContaoSkeletonExtension.php
benennen wir um in ContaoHelloWorldExtension.php
. Der Name entspricht unserem Bundle-Namen, nur das Bundle
durch Extension
ersetzt wird. Die Klasse in der Datei heißt dementsprechend auch ContaoHelloWorldExtension
. Die Konventionen einzuhalten ist wichtig, damit Symfony die KLasse automatisch lädt. Hier ist die komplette Datei.
<?php
namespace Acme\ContaoHelloWorldBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class ContaoHelloWorldExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $mergedConfig, ContainerBuilder $container)
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yml');
}
}
Der src
Ordner ist nun fertig angepasst. Bleibt der Ordner test
. Darin sind unsere Unit-Tests. Bis jetzt gibt es nur die Datei ContaoSkeletonBundleTest.php
die testet, ob das Bundle instanziiert werden kann. Wir benennen die Datei um in ContaoHelloWorldBundleTest.php
und passen den Inhalt folgendermaßen an. Mit Unit-Tests werden wir uns im weiteren Verlauf des Tutorials beschäftigen.
<?php
namespace Acme\ContaoHelloWorlddBundle\Tests;
use Acme\ContaoHelloWorldBundle\ContaoHelloWorldBundle;
use PHPUnit\Framework\TestCase;
class ContaoHelloWorldBundleTest extends TestCase
{
public function testCanBeInstantiated()
{
$bundle = new ContaoHelloWorldBundle();
$this->assertInstanceOf('Acme\ContaoHelloWorldBundle\ContaoHelloWorldBundle', $bundle);
}
}
Das waren nun ziemlich viele Dateien, die am Anfang vielleicht etwas verwirrend sein können. Deshalb nochmal eine Zusammenfassung der wichtigsten Elemente.
- Im Root liegen Konfigurationsdateien für die Tools, die wir später kennenlernen werden, sowie die
composer.json
, die Metadaten und Abhängigkeiten unseres Bundles enthält. Daneben gibt es noch dieLICENSE
und dieREADME.md
. - Die Datei
src/ContaoHelloWorldBundle.php
enthält die KlasseContaoHelloWorldBundle
, die von der Symfony KlasseBundle
abgeleitet wird. Sie repräsentiert unser Bundle. - In
src/ContaoManager/Plugin.php
binden wir die KlasseContaoHelloWorldBundle
im Contao System ein. Wir können dabei bestimmen an welcher Stelle (also nach welchen anderen Bundles) unser Bundle geladen werden soll. - Die Dateien
src/DependencyInjection/ContaoHelloWorldExtension.php
undsrc/Resources/config/services.yml
dienen der Registrierung von Services über DependencyInjection. - Die Datei
tests/ContaoHelloWorldBundle.php
enthält einen Test für die KlasseContaoHelloWorldBundle
(die in der Dateisrc/ContaoHelloWorldBundle
liegt), der Überprüft, ob das Bundle instanziiert werden kann.
Ein paar Worte noch zu Namespaces. In modernen Applikationen wie Contao werden sehr viele Pakete von unterschiedlichen Herrstellern verwendet. Da kann es schon mal passieren, dass zwei oder mehr Klassen den selben Namen haben, was einen Fehler erzeugen würde. Woher soll PHP auch wissen, welche Klasse gemeint ist? Hier helfen Namespaces bei der Strukturierung. Der Name muss dann nur noch innerhalb des Namespaces eindeutig sein. Wenn wir also unseren Vendor- und Bundle-Namen als Grundlage unserer Namespaces verwenden, müssen wir uns keine Sorgen über Namenskonflikte mit anderen Paketen machen.
Bei den Namespaces so wie sie in Symfony und dem SkeletonBundle verwendet werden, gibt es zudem noch eine nützliche Besonderheit. Es handelt sich um sogenannte PSR-4 Namespaces. Das funktioniert folgendermaßen. Wir sagen in der composer.json
, dass der Namespace Acme/ContaoHelloWorldBundle
im Ordner src
liegt. Somit weiß der Autoloader, wo die Dateien der einzelnen Klassen liegen. Die Namespaces, die in Unterordnern von src
liegen, erhalten zusätzlich den Namen dieses Ordners. Die Klasse Pugin
des Namespace Acme\ContaoHelloWorldBundle\ContaoManager
liegt beispielsweise in der Datei src/ContaoManager/Plugin.php
. Der Autoloader setzt den Pfad folgendermaßen zusammen.
Acme\ContaoHelloWorldBundle
=>src
ContaoManager
=>ContaoManager
Plugin
=>Plugin.php
Damit wir unser Bundle über Composer instalieren können, brauchen wir entweder ein lokales oder entferntes Repository. In diesem Tutorial verwenden wir ein Repository auf GitHub. Wir loggen uns also bei GitHub ein und erstellen ein neues Repository mit dem Namen "contao-hello-world-bundle". Nun müssen wir unsere lokalen Dateien in dieses Repository pushen. Dazu nehmen wir entweder ein Git GUI oder geben auf der Konsole die folgenden Befehle ein ("acme" wird dabei durch unseren GitHub Namen ersetzt).
git init
git add .
git commit -m "Initial commit"
git remote add origin git@github.com:acme/contao-hello-world-bundle.git
git push -u origin master
Nun sollte unser GitHub Respository die Dateien enthalten, die wir im vorherigen Schritt erstellt haben.
Jetzt wird es Zeit, unser Bundle in unserer Entwicklungsinstallation einzubinden, damit wir mit der eigentlichen Implementierung anfangen können. Dazu öffnen wir die composer.json
, die im Root unserer Contao Installation liegt. Im Feld require
tragen wir am Ende der Liste unser Bundle ein. Wir wollen für die Entwicklung immer den aktuellsten Stand haben, deshalb geben wir keine Versionsnummer an sondern dev-master
. Da unser Paket noch nicht auf Packagist registriert ist, müssen wir Composer sagen, wo er es findet. Der Ausschnitt aus der composer.json sieht dann so aus.
"require": {
"...": "...",
"acme/contao-hello-world-bundle": "dev-master"
},
"repositories": [
{
"type": "git",
"url": "https://github.com/acme/contao-hello-world-bundle.git"
}
],
Zudem wollen wir nicht nur die aktuellsten Dateien einbinden sondern auch das gesamte Git Repository, damit wir lokal damit arbeiten und unsere Änderungen einchecken können. Das machen wir im Feld config
, wo wir hinter der anderen Konfiguration den preferred-install
Abschnitt aus folgendem Listing eintragen. Diese Zeilen bedeuten einfach nur, dass alle Pakete des Vendors "acme" komplett installiert werden sollen, von allen anderen Paketen nur die nötigen Distributionsdateien.
"config": {
"preferred-install": {
"acme/*": "source",
"*": "dist"
}
},
Nun gehen wir auf der Konsole in den Root der Contao Installation und starten den Import des Bundles.
composer update
Was haben wir nun erreicht? Wir können lokal im Vendor Ordner der Contao Installation arbeiten, sehen also direkt die Auswirkungen unserer Änderungen im Contao Back- oder Frontend. Zusätzlich können wir unsere Fortschritte bequem in unser Repository auf GitHub pushen. Den alten Ordner contao-hello-world-bundle
benötigen wir nun nicht mehr und können ihn daher löschen. In unserer IDE bzw. unserem Editor öffnen wir stattdessen den Ordner vendor/acme/contao-hello-world-bundle
und arbeiten von jetzt an in diesem Projekt.
Jetzt müssen wir noch die Development Abhängigkeiten (also insbesondere PHP-CS-Fixer und PHPUnit) für unser Paket installieren. Das sind die, die in der composer.json
unter require-dev
stehen. Composer installiert diese nicht mit, wenn der Update Befehl aus einer anderen Anwendung (in unserem Fall war das unsere Contao Installation) aufgerufen wird. Das ist auch gut so, denn wir brauchen sie nicht für unsere Contao Installation sondern nur für die Entwicklung unseres Pakets.
Wenn wir composer update
in unserem Bundle-Ordner aufrufen, werden zudem auch noch alle Abhängigkeiten unter "require" geladen, also zum Beispiel das Contao Core Bundle. Dies ist jedoch nicht schlimm, da die Dateien nicht eingebunden werden und wegen einer Regel in der .gitignore
auch nicht in unserem Repository landen. Es ist sogar gut, die Dateien nochmal im Bundle-Ordner zu haben, da unsere IDE sie dann für Autocomplete verwenden kann. Wir wechseln auf der Konsole also in unseren Projektordner und lassen Composer die Abhängigkeiten installieren.
cd vendor/acme/contao-hello-world-bundle
composer update
Es empfiehlt sich während der Entwicklung immer zwei Konsolenfenster offen zu haben:
- Den Root der Contao Installation. Hier können wir den Cache leeren und andere Operationen im Contao System ausführen.
- Den Root unserer Erweiterung (hier also
vendor/acme/hello-world-bundle
. Hier können wir PHP-CS-Fixer, PHPUnit und andere Tools aufrufen.
Wir wollen nun ein Frontend Modul erstellen, das den Text "Hello World" ausgibt. Das funktioniert im Wesentlichen noch so, wie es von alten Contao Erweiterungen gewohnnt ist, nur das die Contao spezifischen Dateien (config
, dca
, templates
, languages
) im Ordner src/Resources/contao
liegen müssen. Die Module Klassen könnte dagegen an einer beliebigen Stelle liegen, solange sie über den Namespace gefunden wird. Wir werden Sie unter src/Module
ablegen. Es gibt mittlerweile zwar auch einen Weg, wie man Frontend Module und Inhaltselemente im Sinne von Symfony als Fragments anlegen kann. Damit es in diesem Tutorial aber nicht zu viel neues auf einmal wird, beschränken wir uns diesmal auf den alten Contao Weg. Wir legen also folgende Dateistruktur unter src
an und tragen den danach aufgeführten Code ein.
Module
HelloWorldModule.php
Resources
contao
config
config.php
dca
tl_module.php
templates
modules
mod_helloWorld.html5
languages
de
modules.php
<?php
namespace Acme\ContaoHelloWorldBundle\Module;
class HelloWorldModule extends \Module
{
/**
* @var string
*/
protected $strTemplate = 'mod_helloWorld';
/**
* Displays a wildcard in the back end.
*
* @return string
*/
public function generate()
{
if (TL_MODE == 'BE') {
$template = new \BackendTemplate('be_wildcard');
$template->wildcard = '### '.utf8_strtoupper($GLOBALS['TL_LANG']['FMD']['helloWorld'][0]).' ###';
$template->title = $this->headline;
$template->id = $this->id;
$template->link = $this->name;
$template->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id='.$this->id;
return $template->parse();
}
return parent::generate();
}
/**
* Generates the module.
*/
protected function compile()
{
$this->Template->message = 'Hello World';
}
}
<?php
// Frontend modules
$GLOBALS['FE_MOD']['miscellaneous']['helloWorld'] = 'Acme\ContaoHelloWorldBundle\Module\HelloWorldModule';
<?php
// Add palette to tl_module
$GLOBALS['TL_DCA']['tl_module']['palettes']['helloWorld'] = '{title_legend},name,headline,type;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space';
<?php $this->extend('block_searchable'); ?>
<?php $this->block('content'); ?>
<?= $this->message; ?>
<?php $this->endblock(); ?>
<?php
// Frontend modules
$GLOBALS['TL_LANG']['FMD']['helloWorld'] = ['Hello World', 'Gibt den Text "Hello World" aus'];
Um während der Entwicklung den Cache zu umgehen, können wir Frontend sowie Backend über die app_dev.php
aufrufen. Den Cache leeren können wir mit folgendem Konsolenbefehl im Root der Contao Installation.
vendor/bin/contao-console cache:clear
Wir sollten nun unser Hello World Modul im Contao Backend anlegen können. Es befindet sich unter "Verschiedenes". Wir fügen es zum Testen an einer beliebigen Stelle ein. Dort sollte im Frontend nun der Text "Hello World" ausgegeben werden. Wenn alles geklappt hat, erzeugen wir einen neuen Commit mit der Nachricht "Create hello-world frontend module" und pushen ihn in unser GitHub Repository. Commit Messages werden üblicherweise im Präsens Imperativ geschrieben. Weiteres dazu gibt es hier: https://github.com/agis/git-style-guide. Wir benutzen entweder unser Git GUI oder geben die folgenden Befehle im Konsolenfenster, in dem unser Bundle-Ordner geöffnet ist, ein.
git add .
git commit -m "Create hello-world frontend module"
git push origin master
Das war auch schon der grundsätzliche Weg, wie Contao Bundles erzeugt werden. In den folgenden Kapiteln beschäftigen wir uns mit den nützlichen Hilfsmittlen PHPUnit und PHP-CS-Fixer, lernen Dependency Injection kennen, fügen unserem Bundle ein anderes Paket als Abhängigkeit hinzu und veröffentlichen unsere Erweiterung schließlich auf Packagist.
Unit Tests sind ein großartiger Weg, sicherzustellen, dass der Code das tut, was er tun soll. Wie der Name schon sagt, testet ein Unit Test nicht die komplette Anwendung, sondern einen Teil davon, eine "Unit". Wir können damit also sicherstellen, dass die Methoden unserer Klassen das tun, was sie sollen.
Da PHPUnit im Skeleton Bundle als Abhängigkeit aufgeführt ist, haben wir es auch schon installiert und können anfangen zu testen (falls noch nicht geschehen, sollte wie weiter oben beschrieben im Verzeichnis unseres Bundles der Befehl composer update
ausgeführt werden um die Entwicklungsabhängigkeiten zu installieren). Einen Test bringt das Skeleton Bundle auch schon mit, wie wir am Anfang beim Anpassen der Dateien gesehen haben. Dieser Überprüft, ob das Bundle instanziiert werden kann. Wir können daher auf der Konsole (im Ordner unseres Bundles) folgendes aufrufen.
vendor/bin/phpunit
Dabei sollten wir diese Meldung bekommen: OK (1 test, 1 assertion)
. Etwas ausführlicher wird es, wenn wir den Befehl folgendermaßen abwandeln.
vendor/bin/phpunit --testdox --colors
Nun erhalten wir die Meldung, dass unser Bundle instanziiert werden kann. Wenn wir eine gute Konsole haben sehen wir die Ausgabe sogar in Farbe.
Eigentlich ist die Funktionalität unseres Bundles schon fertig, so dass wir keine weiteren Klassen benötigen. Wir müssen daher ein bisschen unsere Fantasie spielen lassen und einen Fall für die folgenden Beispiele konstruieren. Stellen wir uns vor, dass wir die Nachricht "Hello World", die wir in src/Module/HelloWorldModule.php
ausgeben nicht einfach als String einfügen wollen, sondern von der Methode sayHelloTo('World')
der Klasse MessageGenerator
erzeugen lassen wollen. Diese Klasse werden wir in den Ordner src/Library
legen, sie bekommt also den Namespace Acme/ContaoHelloWorldBundle/Library
.
Im Sinne des Test-Driven Development schreiben wir zuerst die Unit Tests und implementieren dann eine Klasse, die die Tests erfolgreich besteht. Folgendes sind die Anforderungen an den MessageGenerator.
- Er soll die Welt begrüßen können, d.h. wird der Methode
sayHelloTo
der String "World" übergeben, soll der String "Hello World" zurückgegeben werden. - Er soll keine Begrüßung ohne Empfänger durchführen, d.h. wird der Methode ein leerer String übergeben, soll eine Ausnahme ausgelöst werden.
Diese Anforderungen können wir direkt in Unit Tests übersetzen. Wir erstellen also im Ordner tests
eine neue Datei MessageGenaratorTest.php
mit folgendem Inhalt.
<?php
declare(strict_types=1);
namespace Acme\ContaoHelloWorlddBundle\Tests;
use Acme\ContaoHelloWorldBundle\Library\MessageGenerator;
use PHPUnit\Framework\TestCase;
class MessageGeneratorTest extends TestCase
{
public function testCanSayHelloToWorld()
{
$messageGenerator = new MessageGenerator();
$message = $messageGenerator->sayHelloTo('World');
$this->assertSame('Hello World', $message);
}
public function testCanNotSayHelloToEmptyTarget()
{
$messageGenerator = new MessageGenerator();
$this->expectException(\InvalidArgumentException::class);
$message = $messageGenerator->sayHelloTo('');
}
}
Danach führen wir wieder den Befehl für die Unit Tests aus.
vendor/bin/phpunit
PHPUnit sollte uns sagen, dass es die Klasse MessageGenerator
nicht gibt. Das haben wir auch erwartet, da wir sie noch nicht implementiert haben.
Nun machen wir uns daran, eine Klasse zu erstellen, die die Anforderungen unserer Tests erfüllt. Dazu legen wir im Ordner src
einen neuen Ordner library
an und darin die Datei MessageGenerator.php
mit folgendem Inhalt.
<?php
declare(strict_types=1);
namespace Acme\ContaoHelloWorldBundle\Library;
class MessageGenerator
{
}
Wenn wir nun die Tests mit vendor/bin/phpunit
ausführen, erhalten wir die Fehlermeldung, dass die Methode sayHelloTo
nicht exisitiert. Die Meldungen sagen uns also immer, was als nächstes zu tun ist. So könnten wir uns, wenn wir die Tests nicht selbst geschrieben, sondern von einem anderen Entwickler bekommen hätten, nach und nach vorarbeiten, bis keine Fehler mehr kommen und alle Test erfolgreich absolviert sind. UnitTests sind also ein guter Weg, die Anforderungen einer Software festzulegen. Fügen wir nun also die Methode, zunächst leer, hinzu. Die Datei sieht dann so aus.
<?php
declare(strict_types=1);
namespace Acme\ContaoHelloWorldBundle\Library;
class MessageGenerator
{
public function sayHelloTo(string $target): string
{
}
}
Bei einem erneuten Ausführen der Tests mit vendor/bin/phpunit
treten nun zwar keine Fehler mehr auf, uns wird aber gesagt, dass zwei Anforderungen nicht erfüllt sind. Versuchen wir, die erste zu erfüllen. Wir erweitern die Methode folgendermaßen.
<?php
declare(strict_types=1);
namespace Acme\ContaoHelloWorldBundle\Library;
class MessageGenerator
{
public function sayHelloTo(string $target): string
{
$message = 'Hello '.$target;
return $message;
}
}
Zum testen verwenden wir nun wieder den auführlicheren Befehl.
vendor/bin/phpunit --testdox --colors
Wir sehen, dass die erste Anforderung erfüllt ist.
Acme\ContaoHelloWorlddBundle\Tests\MessageGenarator
[x] Can say hello to world
[ ] Can not say hello to empty target
Nun erweitern wir ein letztes Mal den Code um auch die andere Anforderung zu erfüllen.
<?php
declare(strict_types=1);
namespace Acme\ContaoHelloWorldBundle\Library;
/**
* @throws InvalidArgumentException
*/
class MessageGenerator
{
public function sayHelloTo(string $target): string
{
if (empty($target)) {
throw new \InvalidArgumentException('Target must not be empty.');
}
$message = 'Hello '.$target;
return $message;
}
}
Ein Aufruf von vendor/bin/phpunit --testdox --colors
sollte nun ergeben, dass alle Anforderungen erfüllt sind. Wir sind mit unserer Klasse also fertig.
Acme\ContaoHelloWorlddBundle\Tests\MessageGenarator
[x] Can say hello to world
[x] Can not say hello to empty target
Das Beispiel ist zugegebenermaßen ziemlich konstruiert, zeigt dafür aber gut die grundlegenden Konzepte von PHPUnit. Weitere Informationen zu PHPUnit gibt es auf der offiziellen Website: https://phpunit.de.
Da wir nun eine schöne Klasse haben und mit UnitTest sichergestellt haben, dass sie auch erwartungsgemäß funktioniert, wollen wir sie natürlich in unserem Frontend Modul verwenden. Dies machen wir mithilfe von Dependency Injection. Wir werden unsere Klasse als Service registrieren und dann über den Dependency Injection Container in unserem Frontend Modul verwenden.
Dazu tragen wir einen neuen Service in der Datei src/Resources/config/services.yml
ein. Als Namen wählen wir contao_hello_world_bundle.message_generator
. Also unser Bundle Name gefolgt von einem Punkt und dem eigentlichen Service Namen. Die Datei schaut dann so aus:
services:
_instanceof:
Contao\CoreBundle\Framework\FrameworkAwareInterface:
calls:
- ["setFramework", ["@contao.framework"]]
Symfony\Component\DependencyInjection\ContainerAwareInterface:
calls:
- ["setContainer", ["@service_container"]]
acme.contao_hello_world_bundle.message_generator:
class: Acme\ContaoHelloWorldBundle\Library\MessageGenerator
Nun müssen wir den Cache mit folgendem Befehl im Root unserer Contao Installation leeren.
vendor/bin/contao-console cache:clear
Den neuen Service verwenden wir nun in der Datei src/Module/HelloWorldModule.php
. Wir greifen dazu in der Methode compile
über den Service Container auf den Service zu.
protected function compile()
{
$messageGenerator = \Contao\System::getContainer()->get('acme.contao_hello_world_bundle.message_generator');
$message = $messageGenerator->sayHelloTo('World');
$this->Template->message = $message;
}
Jetzt sollte wieder "Hello World" im Frontend ausgegeben werden. Das war ein sehr simples Beispiel wie Dependeny Injection funktioniert. Man kann damit jedoch noch viel mehr machen. Ein guter Startpunkt sich mit dem Thema auseinander zu setzen findet sich in der Symfony Dokumentation: https://symfony.com/doc/current/service_container.html.
Es ist nun mal wieder Zeit für einen Commit.
git add .
git commit -m "Use message generator service to compose 'hello world' message"
git push origin master
Arbeitet man an einer größeren Erweiterung, kann es sinnvoll sein, für bestimmte Funktionalitäten auf fertige Pakete zurückzugreifen. Das können andere Contao Erweiterungen sein, andere Symfony Bundles oder ganz allgemein PHP Bibliotheken. Auf fertige Lösungen zurückzugreifen ersparrt einiges an Arbeitszeit, die ein anderer Entwickler schon für uns investiert hat. Dank Composer ist es zudem kinderleicht, andere Pakete einzubinden. Folgendes müssen wir machen.
- Unserer Erweiterung das fremde Paket als Abhängigkeit hinzufügen
- Den neuen Stand ins GitHub Repository pushen
- Composer die Abhängigkeit in unserer Contao Installation instalieren lassen
Das schwierigste dabei ist, zu überlegen, was wir für unsere Hello World Anwendung überhaupt noch benötigen könnten. Wir werden in diesem Tutorial Contao Haste als Beispiel verwenden, da es zahlreiche nützliche Funktionen enthält. Der Weg ist aber für jedes Paket, das es auf Packegist gibt, der gleiche.
Wir gehen auf https://packagist.org und suchen dort nach "contao-haste". Da sollten wir das Paket "codefog/contao-haste" finden (https://packagist.org/packages/codefog/contao-haste). Direkt unter dem Namen steht auch schon der Composer Befehl, mit dem wir das Paket einbinden können. Diesen führen auf der Konsole im Root unserer Erweiterung (also unter vendor/acme/contao-hello-world-bundle
) aus.
composer require codefog/contao-haste
Dabei passieren zwei Sachen. Zum einen lädt Composer die benötigten Dateien in den Vendor-Ordner unserer Erweiterung herunter. Dies ist nützlich, da unsre IDE sie nun für Autocomplete verwenden kann. Zum anderen - und das ist das Entscheidende - fügt Composer in der Datei composer.json
die neue Abhängigkeit hinzu. Dort sollte unter require
nun folgendes ergänzt sein.
"codefog/contao-haste": "^4.20"
Das bedeutet, dass jeder, der unsere Erweiterung installiert (egal ob über die Konsole oder den Contao Manager), automatisch auch Contao Haste mit dazu bekommt, falls es nicht schon installiert ist. Hier wird deutlich, wie hilfreich Composer ist. Wir müssen lediglich sagen, dass wir die Erweiterung einsetzen wollen, Composer kümmert sich um den Rest. Den Eintrag in der composer.json
kann man natürlich auch von Hand einsetzen. Der require
Befehl ist aber nochmal ein Stückchen bequemer.
Nun haben wir Contao Haste aber noch nicht in unserer Contao Installation. Das müssen wir Composer noch erledigen lassen. Doch zuerst muss der neue Stand mit der Abhängigkeit in unser GitHub Repository.
git add .
git commit -m "Require Contao Haste"
git push origin master
Wenn wir nun composer update
im Root unserer Contao Installation aufrufen, wird Composer sehen, dass das "contao-hello-world-bundle" eine neue Abhängigkeit hat, und diese installieren.
composer update
Nun sollte im Ordner vendor
der Contao Installation ein neuer Ordner codefog
mit dem Unterordner contao-haste
liegen. Das Paket wurde also installiert. Wir können es zum Testen in unserem Hello World Bundle verwenden. Zum Beispiel können wir das AbstractFrontendModule
von Contao Haste verwenden (https://github.com/codefog/contao-haste/blob/master/docs/Frontend/index.md), um uns in unserem HelloWorldModule
die Deklaration der Wildcard in der generate
Methode zu sparen. Dazu lassen wir unsere Klasse nicht mehr von \Module
erben, sondern von \Haste\Frontend\AbstractFrontendModule
. Die Methode generate
fliegt raus. Die Datei HelloWorldModule.php
sieht dann so aus.
<?php
namespace Acme\ContaoHelloWorldBundle\Module;
class HelloWorldModule extends \Haste\Frontend\AbstractFrontendModule
{
/**
* @var string
*/
protected $strTemplate = 'mod_helloWorld';
/**
* Generates the module.
*/
protected function compile()
{
$messageGenerator = \Contao\System::getContainer()->get('acme.contao_hello_world_bundle.message_generator');
$message = $messageGenerator->sayHelloTo('World');
$this->Template->message = $message;
}
}
Wir haben nun im Contao Backend weiterhin eine schöne Wildcard, müssen uns aber nicht mehr selber um deren Erzeugung kümmern. Mit Contao Haste kann man noch viel mehr machen, zum Beispiel schnell und einfach Formulare generieren. Details dazu gibt es in der Dokumentaion auf GitHub durch. Nun wollen wir unseren neuen Stand noch in Git einchecken.
git add .
git commit -m "Use Haste to generate HelloWorldModule wildcard"
git push origin master
Bevor wir unser Bundle auf Packagist veröffentlichen, wollen wir mit Hilfe von PHP-CS-Fixer dafür sorgen, dass die Coding Standards eingehalten werden. Dazu zunächst die Überlegung, warum es eigentlich wichtig ist einen konstanten Code Stil zu verwenden.
Wir wissen alle, dass Code zu lesen schwerer ist, als Code zu schreiben. Alles, was die Lesbarkeit erhöht, ist daher von Vorteil. Ein einheitlicher Code Stil trägt einen Teil dazu bei. Stellt euch zum Beispiel einen normalen Text vor, bei dem Zeichensetzung und Abstände nicht konsistent wären und entgegen den üblichen Regeln eingesetzt würden. Man könnte den Text immer noch verstehen, es würde aber länger dauern ihn zu lesen. So ist es auch beim Code. Wenn die Formatierung einheitlich ist und dem entspricht, was üblich ist, ist er für einen selbst und für andere ein ganzes Stück leichter zu lesen. Für PHP gibt es mittlerweile einen allgemeinen Standard, in dem vieles festgelegt ist: PSR-2. Darauf aufbauend hat Symfony noch einen eigenen Standard: https://symfony.com/doc/current/contributing/code/standards.html.
Doch auch wenn man die Regeln kennt, macht man mitunter Flüchtigkeitsfehler. Zudem ist es bei der Zusammenarbeit mit Anderen gar nicht so leicht, einen allgemeinen Code Standard für alle umzusetzen. Auch Code, der von mehreren Personen stammt, sollte so aussehen, als wäre er von einer einzigen Person geschrieben worden. Deshalb ist PHP-CS-Fixer ein sehr nützliches Tool. Damit lässt sich Code automatisch anhand der Regeln, die in einer Konfigurationsdatei festgelegt werden, formatieren. Diese Konfigurationsdatei ist die .php_cs.dist
im Root unseres Bundles. Hier ist festgelegt, dass der Symfony Coding Standard (und damit auch PSR-2) umgesetzt werden soll. Zusätzlich sind noch einige andere nützliche Regeln definiert. Wenn ihr wissen wollt, was die einzelnen Regeln bedeuten ist diese Website eine gute Quelle: https://mlocati.github.io/php-cs-fixer-configurator/.
Wir haben PHP-CS-Fixer auch schon installiert, da es in unseren Entwicklungsabhängigkeiten steht. Daher können wir einfach auf der Konsole im Root unseres Bundles folgenden Befehl eingeben (das -v
steht dabei für eine erweiterte Ausgabe und kann je nach Bedarf hinzugefügt oder weggelassen werden).
vendor/bin/php-cs-fixer fix -v
PHP-CS-Fixer hat nun unseren Code schön formatiert und überall den Header Kommentar eingefügt, der in der .php_cs.dist
definiert ist. Wir haben diesen Schritt in diesem Tutorial ganz zum Schluss gemacht, es empfiehlt sich aber sowohl die Unit Tests als auch PHP-CS-Fixer vor jedem Commit auszuführen. Es lässt sich auch ein Hook in Git einrichten, der die beiden Tools vor jedem Commit automatisch laufen lässt.
PHP-CS-Fixer lässt sich auch in viele IDEs und Editoren integrieren. Für PhpStorm gibt es hier eine Anleitung: https://hackernoon.com/how-to-configure-phpstorm-to-use-php-cs-fixer-1844991e521f. Für Atom gibt es dieses nützliche Plugin: https://atom.io/packages/atom-beautify (das kann auch Dateien in anderen Sprachen schön formatieren, für PHP-Dateien kann eingestellt werden, dass PHP-CS-Fixer verwendet werden soll).
Nun machen wir den letzten Commit vor dem Veröffentlichen unserer Erweiterung.
git add .
git commit -m "Fix coding standards"
git push origin master
Damit unser Bundle leichter installiert werden kann und auch vom Contao Manager gefunden wird, wollen wir es schließlich noch auf Packagist veröffentlichen. Der eigentliche Code bleibt dabei auf GitHub, Packagist fungiert lediglich als Inhaltsverzeichnis, das Composer sagt, wo die Dateien des Pakets zu finden sind.
Zunächst sollten wir noch eine Version unseres Bundles anlegen. Wir halten uns dabei an Semantic Versioning und beginnen mit Version 1.0.0. Würden wir in Zukunft ein Bugfix Release veröffentlichen, würden wir die letzte Zahl erhöhen (1.0.1), bei einem neuen Feature die mittlere Zahl (1.1.0) und bei einer nicht rückwärtskompatiblen Änderung die erste Zahl (2.0.0).
Wir öffnen unser Bundle auf GitHub und klicken dort auf releases und dann den Button Create a new Release. Das sorgt dafür, dass der letzte Commit mit einem Tag versehen wird, über den er referenziert werden kann. Im Feld Tag Version geben wir 1.0.0
ein. Ebenso im Feld Release title. Als Beschreibung tragen wir Initial release
ein. Bei zukünftigen Releases würden hier die Änderungen (Changelog) aufgelistet werden. Mit dem Button Publish release veröffentlichen wir unsere neue Version.
Jetzt loggen wir uns auf Packagist ein und klicken dort auf den Menüpunkt Submit. Im Feld Repository URL tragen wir die Adresse unseres GitHub Repositorys ein: https://github.com/acme/contao-hello-world-bundle
und klicken auf Check. Packagist überprüft nun, ob es die Adresse auch gibt. Hat das geklappt, klicken wir auf den Button Submit. Das war's auch schon. Unsere Erweiterung ist nun auf Packagist veröffentlicht.
Auf der zugehörigen Packagist Seite werden automatisch ein paar Informationen angezeigt. Es werden die Kurzbeschreibung aus unserer composer.json
, die Abhängigkeiten unseres Pakets sowie unsere README.md
ausgegeben. Zudem sehen wir ganz oben den Composer Befehl um unser Paket zu installieren: composer require acme/contao-hello-world-bundle
. Nun sollte das Paket auch im Contao Manager gefunden werden.
Ich hoffe, dieses Tutorial hat gezeigt, dass die Erweiterungsentwicklung unter Contao ab Version 4 gar nicht so viel anderes ist als in älteren Contao Versionen. Wir haben nichts verloren, sondern einige Hilfsmittel und Best-Practices gewonnen, die uns die Arbeit erleichtern und unseren Code besser machen.
- Contao Skeleton Bundle: https://github.com/contao/skeleton-bundle
- Erweiterungen für Contao 4 - Vortrag auf der Cotao Konferenz 2018: https://www.youtube.com/watch?v=VzDYjEd7OyQ
- Git: https://git-scm.com
- GitHub: https://github.com
- Composer: https://getcomposer.org
- Packagist: https://packagist.org
- Choose an open source license: https://choosealicense.com
- EditorConfig: https://editorconfig.org
- Mastering Markdown: https://guides.github.com/features/mastering-markdown/
- Semantic Versioning: https://semver.org
- PHPUnit: https://phpunit.de
- PHP-CS-Fixer: https://github.com/FriendsOfPHP/PHP-CS-Fixer
- PHP-CS-Fixer Configurator: https://mlocati.github.io/php-cs-fixer-configurator/
- PSR-2: Coding Style Guide: https://www.php-fig.org/psr/psr-2/
- PSR-4: Autoloader: https://www.php-fig.org/psr/psr-4/
- Git Style Guide: https://github.com/agis/git-style-guide
- Symfony Coding Standards: https://symfony.com/doc/current/contributing/code/standards.html
- Symfony Setter Injection: https://symfony.com/doc/current/service_container/calls.html
- Symfony Service Container: https://symfony.com/doc/current/service_container.html
- Contao Haste: https://github.com/codefog/contao-haste
- PhpStorm: https://www.jetbrains.com/phpstorm/
- EditorConfig Plugin für PhpStorm: https://plugins.jetbrains.com/plugin/7294-editorconfig
- How to configure PHPStorm to use PHP-CS-Fixer: https://hackernoon.com/how-to-configure-phpstorm-to-use-php-cs-fixer-1844991e521f
- Atom: https://atom.io
- EditorConfig Plugin für Atom: https://atom.io/packages/editorconfig
- Beautify Plugin für Atom: https://atom.io/packages/atom-beautify
- Sourcetree: https://www.sourcetreeapp.com