Skip to content

Instantly share code, notes, and snippets.

@hh-com
Created October 24, 2018 22:13
Show Gist options
  • Save hh-com/f7685239436923b14f59335c0a2d4c88 to your computer and use it in GitHub Desktop.
Save hh-com/f7685239436923b14f59335c0a2d4c88 to your computer and use it in GitHub Desktop.
Contao Hello World Bundle Tutorial

Contao Hello World Bundle Tutorial

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.

  1. Einen Vendor- und Bundle-Namen wählen
  2. Das Skeleton Bundle verstehen und anpassen
  3. Ein Repository für das Bundle anlegen
  4. Das Bundle in der Entwicklungsinstallation installieren
  5. Die grundlegende Funktionalität implementieren
  6. Unit Tests schreiben
  7. Eine Klasse erstellen, die die Anforderungen der Tests erfüllt
  8. Die Klasse als Service über Dependeny Injection nutzen
  9. Ein anderes Paket als Abhängigkeit hinzufügen
  10. Den Code mit PHP-CS-Fixer formatieren
  11. 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.

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.

1. Einen Vendor- und Bundle-Namen wählen

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

2. Das Skeleton Bundle verstehen und anpassen

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.

.editorconfig

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.

.gitignore

Die Datei .gitignore gibt an, welche Dateien nicht in das Git Repository kommen sollen. Hier sind keine Anpassungen nötig.

LICENSE

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/.

.php_cs.dist

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;

composer.json

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"
    }
}

phpunit.xml.dist

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>

README.md

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.

src/ContaoSkeletonBundle.php

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
{
}

src/ContaoManager/Plugin.php

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]),
        ];
    }
}

src/DependencyInjection/ContaoSkeletonExtension.php & src/Resources/config/services.yml

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 ContaoSkeletonExtensionlä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 FrameworkAwareInterfacebzw. 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');
    }
}

tests/ContaoSkeletonBundleTest.php

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);
    }
}

Zusammenfassung

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 die LICENSE und die README.md.
  • Die Datei src/ContaoHelloWorldBundle.php enthält die Klasse ContaoHelloWorldBundle, die von der Symfony Klasse Bundle abgeleitet wird. Sie repräsentiert unser Bundle.
  • In src/ContaoManager/Plugin.php binden wir die Klasse ContaoHelloWorldBundle 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 und src/Resources/config/services.yml dienen der Registrierung von Services über DependencyInjection.
  • Die Datei tests/ContaoHelloWorldBundle.php enthält einen Test für die Klasse ContaoHelloWorldBundle (die in der Datei src/ContaoHelloWorldBundle liegt), der Überprüft, ob das Bundle instanziiert werden kann.

Über Namespaces

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 Pugindes 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

3. Ein Repository für das Bundle anlegen

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.

4. Das Bundle in der Entwicklungsinstallation installieren

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:

  1. Den Root der Contao Installation. Hier können wir den Cache leeren und andere Operationen im Contao System ausführen.
  2. Den Root unserer Erweiterung (hier also vendor/acme/hello-world-bundle. Hier können wir PHP-CS-Fixer, PHPUnit und andere Tools aufrufen.

5. Die grundlegende Funktionalität implementieren

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.

Struktur

Module
    HelloWorldModule.php
Resources
    contao
        config
            config.php
        dca
            tl_module.php
        templates
            modules
                mod_helloWorld.html5
        languages
            de
                modules.php

Code

src/Module/HelloWorldModule.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&amp;table=tl_module&amp;act=edit&amp;id='.$this->id;

            return $template->parse();
        }

        return parent::generate();
    }

    /**
     * Generates the module.
     */
    protected function compile()
    {
        $this->Template->message = 'Hello World';
    }
}

src/Resources/contao/config/config.php

<?php

// Frontend modules
$GLOBALS['FE_MOD']['miscellaneous']['helloWorld'] = 'Acme\ContaoHelloWorldBundle\Module\HelloWorldModule';

src/Resources/contao/dca/tl_module.php

<?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';

src/Resources/contao/templates/modules/mod_helloWorld.html5

<?php $this->extend('block_searchable'); ?>

<?php $this->block('content'); ?>

<?= $this->message; ?>

<?php $this->endblock(); ?>

src/Resources/contao/languages/de/modules.php

<?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.

6. Unit Tests schreiben

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.

  1. 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.
  2. 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 MessageGeneratornicht gibt. Das haben wir auch erwartet, da wir sie noch nicht implementiert haben.

7. Eine Klasse erstellen, die die Anforderungen der Tests erfüllt

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 sayHelloTonicht 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.

8. Die Klasse als Service über Dependeny Injection nutzen

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

9. Ein anderes Paket als Abhängigkeit hinzufügen

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.

  1. Unserer Erweiterung das fremde Paket als Abhängigkeit hinzufügen
  2. Den neuen Stand ins GitHub Repository pushen
  3. 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 generatefliegt raus. Die Datei HelloWorldModule.phpsieht 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

10. Den Code mit PHP-CS-Fixer formatieren

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 -vsteht 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

11. Das Bundle auf Packagist veröffentlichen

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.

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment