Contao 4 Bundle (Plugin) erstellen - Backend und Frontend

Im ersten Teil haben wir ein Contao 4 Projekt erstellt und haben mittels Gulp und Bower im zweiten Teil der Beispielseite Leben eingehaucht. Im dritten Teil soll sich alles um die Erstellung eines Contao 4 Bundles drehen. Als Ergebnis erhalten wir ein Backend und Frontend-Modul. Für den Start eines solchen Bundles erstellen wir uns eine Grundstruktur:

contao4_part3_car_bundle_1

Die Bundle-Datei (src/Xuad/CarBundle/XuadCarBundle.php) bekommt folgenden Inhalt:

namespace Xuad\CarBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class XuadCarBundle extends Bundle
{

}

Mit dem einfachen Grundgerüst, können wir das Bundle im Symfony-Kernel registrieren. Mit einen einfachen "new Xuad\CarBundle\XuadCarBundle()" im Array des $bundle-Array (app/AppKernel.php » Methode: registerBundles), ist das Bundle registriert.

contao4_part3_car_bundle_2

Damit Composer unsere Bundle-Klassen laden kann, muss das Autoloading angepasst werden. Dazu die "composer.json" bearbeiten und folgenden Schnippsel in das json-Objekt einfügen:

"autoload": {
    "psr-0": { "": "src/" }
}

Die Änderungen werden mit einem beherzten "composer update" übernommen.

Konfiguration für Contao

Im ersten Schritt erstellen wir eine Contao DCA-Konfiguration (src/Xuad/CarBundle/Resource/contao/dca/tl_car.php). Damit soll man später einmal Autos erstellen, bearbeiten und verwalten können.

$GLOBALS['TL_DCA']['tl_car'] = [
    'config' => [
        'dataContainer' => 'Table',
        'switchToEdit' => true,
        'enableVersioning' => true,
        'sql' => [
            'keys' => [
                'id' => 'primary',
            ]
        ]
    ],
    'list' => [
        'sorting' => [
            'mode' => 1,
            'fields' => ['name'],
            'headerFields' => ['name'],
            'flag' => 1,
            'panelLayout' => 'debug;filter;sort,search,limit',
        ],
        'label' => [
            'fields' => ['name'],
            'format' => '%s',
            'showColumns' => true,
        ],
        'global_operations' => [
        ],
        'operations' => [
            'edit' => [
                'label' => &$GLOBALS['TL_LANG']['tl_car']['edit'],
                'href' => 'table=tl_car',
                'icon' => 'edit.gif'
            ],
            'editheader' => [
                'label' => &$GLOBALS['TL_LANG']['tl_car']['editheader'],
                'href' => 'act=edit',
                'icon' => 'header.gif',
            ],
            'copy' => [
                'label' => &$GLOBALS['TL_LANG']['tl_car']['copy'],
                'href' => 'act=copy',
                'icon' => 'copy.gif',
            ],
            'delete' => [
                'label' => &$GLOBALS['TL_LANG']['tl_car']['delete'],
                'href' => 'act=delete',
                'icon' => 'delete.gif',
                'attributes' => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"',
            ],
            'show' => [
                'label' => &$GLOBALS['TL_LANG']['tl_car']['show'],
                'href' => 'act=show',
                'icon' => 'show.gif'
            ]
        ]
    ],
    'palettes' => [
        '__selector__' => [],
        'default' => '
            brand,
            name,
            alias'
    ],
    'subpalettes' => [
        '' => ''
    ],
    'fields' => [
        'id' => [
            'sql' => "int(10) unsigned NOT NULL auto_increment"
        ],
        'tstamp' => [
            'sql' => "int(10) unsigned NOT NULL default '0'"
        ],
        'name' => [
            'label' => &$GLOBALS['TL_LANG']['tl_car']['name'],
            'exclude' => true,
            'search' => true,
            'sorting' => true,
            'flag' => 1,
            'inputType' => 'text',
            'eval' => ['mandatory' => true, 'maxlength' => 255],
            'sql' => "varchar(255) NOT NULL default ''"
        ],
        'brand' => [
            'label' => &$GLOBALS['TL_LANG']['tl_car']['brand'],
            'exclude' => true,
            'search' => true,
            'sorting' => true,
            'flag' => 1,
            'inputType' => 'text',
            'eval' => ['mandatory' => true, 'maxlength' => 255],
            'sql' => "varchar(255) NOT NULL default ''"
        ],
        'alias' => [
            'label' => &$GLOBALS['TL_LANG']['tl_car']['alias'],
            'exclude' => true,
            'search' => true,
            'inputType' => 'text',
            'eval' => ['rgxp' => 'alias', 'unique' => true, 'maxlength' => 128, 'tl_class' => 'w50'],
            'save_callback' => [
                function ($varValue, DataContainer $dataContainer)
                {
                    return \System::getContainer()->get('xuad_car.datacontainer.car')->generateAlias($varValue, $dataContainer);
                }
            ],
            'sql' => "varchar(128) COLLATE utf8_bin NOT NULL default ''"
        ],
    ]
];

Mit Hilfe des Installtools können wir uns automatisch unsere Datenbank-Tabelle erstellen lassen. Dazu http://tutorial-contao4.dev/install.php aufrufen und das Datenbank-Update ausführen.

contao4_part3_car_bundle_3

Services registrieren

Da ich gern den Alias automatisch aus der Automarke und dem -namen generiert haben möchte, rufe ich einen Callback (save_callback) beim Speichern auf. Den Service (xuad_car.datacontainer.car), welcher für den Callback auswertet und manipuliert, muss ich natürlich auch anlegen und registrieren. Dafür wird ein "services.yml" unter "src/xuad/CarBundle/Resources/config/" erstellt.

services:
  kernel_bundle:
    class: xuad\CarBundle\xuadCarBundle

  xuad_car.datacontainer.car:
    class: xuad\CarBundle\DataContainer\CarDataContainer
    arguments:
    - '@doctrine.orm.default_entity_manager'
    - '@xuad_car.service.carservice'

Doctrine ORM

Um Doctrine ORM Mapping benutzen zu können, benötigen wir noch den Service "doctrine.orm.default_entity_manager". Diese Pakete werden leider nicht in der Contao4-Standard-Edition mitgeliefert. Daher müssen die Pakete mit Composer bezogen (Packagist) werden.

Also wieder die "composer.json" anpassen:

"require-dev": {
    "doctrine/data-fixtures": "1.0.*",
    "doctrine/dbal": "~2.4",
    "doctrine/orm": "~2.4,>=2.4.5",
    "doctrine/doctrine-bundle": "~1.2"
},

Darüber hinaus muss Doctrine ORM in der "config.yml" aktiviert werden. Zuusätzlich müssen ebenfalls unsere Doctrine-Entitäten (Welche momentan noch nicht existieren) registriert werden:

# Doctrine configuration
doctrine:
    dbal:
        default_connection: default
        connections:
    # ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%
        entity_managers:
            default:
                mappings:
                    xuadCarBundle: ~

Jetzt sind wir bereit die entsprechende Entität anzulegen, welche als Mapper zu unserem DCA-Container dient. Diese wird unter "src/Xuad/CarBundle/Entity/Car.php" angelegt. Die Member besitzen die gleichen Eigenschaften, welche in der tl_car.php definiert sind.

/**
 * @ORM\Entity
 * @ORM\Table(name="tl_car")
 */
class Car
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $brand;

    /**
     * @ORM\Column(type="string", length=128)
     */
    protected $alias;

    /**
     * @ORM\Column(type="integer")
     */
    protected $tstamp;
}

Die Getter und Setter können wir uns bequem von Doctrine erstellen lassen:

php app/console doctrine:generate:entities xuad/CarBundle/Entity/Car

Weiterhin legen wir einen weiteren Service (CarService.php) an, welcher uns die Daten mittels Doctrine ORM bezieht (/src/Xuad/CarBundle/Service/CarService.php):

class CarService
{
    /**
     * @var EntityManager
     */
    private $entityManager;

    /**
     * CarService constructor.
     *
     * @param EntityManager $entityManager
     */
    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    /**
     * Find car by alias
     *
     * @param $alias
     *
     * @return array|\Xuad\CarBundle\Entity\Car[]
     */
    public function findByAlias($alias)
    {
        $carList = $this->entityManager->getRepository('XuadCarBundle:Car')->findBy(['alias' => $alias]);

        return $carList;
    }

    /**
     * Find all cars
     *
     * @return array|\Xuad\CarBundle\Entity\Car[]
     */
    public function findAll()
    {
        $carList = $this->entityManager->getRepository('XuadCarBundle:Car')->findAll();

        return $carList;
    }
}

Services registrieren

Die Services welche wir in der "services.yml" definiert haben, registrieren wir unter "app/config/config.yml".

imports:
    - { resource: "@xuadCarBundle/Resources/config/services.yml" })))))))

Jetzt können wir endlich unseren CallBack schreiben (src/Xuad/CarBundle/DataContainer/CarDataContainer.php). Dabei dient uns die abgewandelte Contao-Methode "generateAlias" als Vorbild.

class CarDataContainer
{
    /**
     * @var EntityManager
     */
    private $entityManager;

    /**
     * @var CarService
     */
    private $carService;

    /**
     * Constructor.
     *
     * @param \Doctrine\ORM\EntityManager $entityManager
     * @param \Xuad\CarBundle\Service\CarService $carService
     */
    public function __construct(EntityManager $entityManager, CarService  $carService)
    {
        $this->entityManager = $entityManager;
        $this->carService = $carService;
    }

    /**
     * Generate alias
     *
     * @param $varValue
     * @param \Contao\DataContainer $dc
     *
     * @return mixed
     * @throws \Exception
     */
    public function generateAlias($varValue, DataContainer $dc)
    {
        $autoAlias = false;

        if ($varValue === '')
        {
            $autoAlias = true;

            $varValue = \StringUtil::generateAlias($dc->activeRecord->brand . '-' . $dc->activeRecord->name);
        }

        $carList = $this->carService->findByAlias($varValue);

        // Check whether the news alias exists
        if (count($carList) > 1 && $autoAlias === false)
        {
            throw new \Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $varValue));
        }

        // Add ID to alias
        if (count($carList) > 0 && $autoAlias === true)
        {
            $varValue .= '-' . $dc->id;
        }

        return $varValue;
    }
}

Hinweis: Durch die Angabe der Argumentenliste (services.yml) werden über den Konstruktur automatisch per DependcyInjection die Servives injiziert.

Backend-Modul erstellen

Die Grundbasis ist nun geschaffen und wir können das erste Backend-Modul erstellen. Für das Verwalten von Autos, registrieren wir das Backend-Modul "xuadCarsManageCars" (src/xuad/CarBundle/Resources/contao/config/config.php):

array_insert($GLOBALS['BE_MOD']['XuadCars'], 1 ,[
    'XuadCarsManageCars' => [
        'tables' => ['tl_car'],
        'icon' => 'bundles/xuadcar/icon.png',
        'table' => ['TableWizard', 'importTable'],
        'list' => ['ListWizard', 'importList']
    ]
]);

...und zack ist das Backend-Module bereits schon im Backend sichtbar. Damit das auch nach etwas aussieht, legen wir noch ein Autologo (icon.png) in den public-Ordner des Bundles. Im Anschluss werden die Sprachen für Deutsch und Englisch hinzugefügt. Dafür werden jeweils eine "modules.php" und eine "tl_car.php" in englischer (src/Xuad/CarBundle/Resources/contao/languages/en) sowie in deutscher (src/Xuad/CarBundle/Resources/contao/languages/de) Sprache angelegt.

contao4_part3_car_bundle_4

Autos anlegen

Jetzt können wir mit den Anlegen von Autos beginnen.

contao4_part3_car_bundle_5

contao4_part3_car_bundle_6

Frontend-Modul erstellen

Im Backend die Autos zu verwalten ist ja ganz nett, aber wir wollen die Autos natürlich auch im Frontend anzeigen lassen. Dafür erstellen wir ein Frontend-Modul (src/Xuad/CarBundle/Module/ModuleCarList.php) welches die Autos auflistet.

class ModuleCarList extends Module
{
    /**
     * @var string
     */
    protected $strTemplate = 'mod_car_list';

    /**
     * Do not display the module if there are no menu items
     *
     * @return string
     */
    public function generate()
    {
        if (TL_MODE == 'BE')
        {
            /** @var \BackendTemplate|object $objTemplate */
            $objTemplate = new \BackendTemplate('be_wildcard');

            $objTemplate->wildcard = '### ' . utf8_strtoupper($GLOBALS['TL_LANG']['FMD']['ModuleCarList'][0]) . ' ###';
            $objTemplate->title = $this->headline;
            $objTemplate->id = $this->id;
            $objTemplate->link = $this->name;
            $objTemplate->href = 'contao?do=themes&table=tl_module&act=edit&id=' . $this->id;

            return $objTemplate->parse();
        }

        return parent::generate();
    }

    /**
     * Generate module
     */
    protected function compile()
    {
        $carService = \System::getContainer()->get('xuad_car.service.carservice');

        $carList = $carService->findAll();

        $this->Template->carList = $carList;
    }
}

Für die Anzeige der Autos legen wir eines neues HTML-Template an (src/Xuad/CarBundle/Resources/contao/templates/mod_car_list.html5).

<div class="<?= $this->class; ?> block"<?= $this->cssID; ?>>
    <div class="car-list">
        <?php if(count($this->carList) > 0): ?>
            <ul>
                <?php foreach($this->carList as $car): /** @var \Xuad\CarBundle\Entity\Car $car */ ?>
                    <li><strong><?= $car->getBrand() ?></strong><?= $car->getName() ?></li>
                <?php endforeach; ?>
            </ul>
        <?php endif; ?>
    </div>
</div>

Im Anschluss machen wir Contao, dass FrontEnd-Modul bekannt. (src/Xuad/CarBundle/Resources/contao/config/config.php)

$GLOBALS['FE_MOD']['xuadCars']['ModuleCarList'] = 'Xuad\\CarBundle\\Module\\ModuleCarList';

Damit das HTML-Template von Contao gefunden wird, müssen wir dafür eine "autoload.php" im Ordner "src/Xuad/CarBundle/Resources/contao/config/" anlegen. In dieser werden alle relevanten HTML-Templates für das Modul registriert.

TemplateLoader::addFiles([
    'mod_car_list' => 'src/Xuad/CarBundle/Resources/contao/templates'
]);

Jetzt legen wir das neues FrontEnd-Module im Contao-Backend an (Layout » Themes » default » FrontEnd-Module bearbeiten » Neues Modul ...

contao4_part3_car_bundle_7

...und binden es in die Startseite ein (Neues Element » Elementtyp: Modul » Autoliste (ID 1)

contao4_part3_car_bundle_8

contao4_part3_car_bundle_9

Ausgabe des fertigen FrontEnd-Modul

Im Frontend sollte auf der Startseite, die Ausgabe des Modul zu sehen sein.

contao4_part3_car_bundle_10

Noch ein allgemeiner Hinweis für das Entwickeln unter Symfony. Manchmal zickt der Cache einwenig rum. Daher folgende Befehle für das Cache löschen:

  • Cache löschen ohne Aufbau: php app/console cache:clear --no-warmup
  • Cache Produktion: php app/console cache:warmup -e prod
  • Cache Develop: php app/console cache:warmup -e dev

Das Contao 4 - Projekt kann auf GitHub bestaunt werden.

Für Fragen zur Optimierung von Contao 4 Bundles, bin ich jederzeit offen. Folgende Punkte brennen mir noch unter den Nägeln:

  • Ist das Registrieren der Services in der "app/config/config.yml" nötig, oder kann dies direkt im Modul untergebracht werden
  • Wie ist möglich die HTML-Ausgabe mittels Twig zu verwirklichen

Bild-Quelle: Icons made by Freepik from Flaticon is licensed by CC BY 3.0

Kommentar von iCodr8 am

"Ist das Registrieren der Services in der "app/config/config.yml" nötig, oder kann dies direkt im Modul untergebracht werden"

Ja, das kann innerhalb der Moduls gemacht werden.

Siehe:
http://symfony.com/doc/current/cookbook/bundles/extension.html

Kommentar von Mr. B am

Tolles Tutorial! Dumme Frage, aber in welchen Ordner wird denn der ganze contao-tutorial4-bundle Ordner hochgeladen?

Antwort von xuad

Wird automatisch über die composer.json installiert.

Kommentar von Florian am

Hallo Xuad,
bei mir taucht bereits bei der Contao Konfiguration ein Fehler auf, da die tl_cars Tabellen vom Installertool nicht erkannt werden und ich diese somit nicht Updaten kann. Später kann ich die Doctrine (ich denke wegen der fehlenden Tabellen in der Datenbank) dadurch nicht generieren.
Könnte dieser Fehler Versionen bedingt sein? Ich nutze aktuell Contao 4.4 .

Kommentar von Alexander am

Ist diese Methode noch gültig für Contao 4.4.7?

Antwort von xuad

Das Tutorial habe ich nur bis Version 4.3 getestet. Perspektivisch möchte ich ein Tutorial für das Erstellen eines Plugin für die Contao Managed Edition (4.4.* / 4.5.*) veröffentlichen.

Kommentar von egal am

Leider funktionieren die Links zum ersten und zweiten Teil nicht. Danke jedoch für diesen Artikel.

Antwort von xuad

Vielen Dank für die Informationen. Die Verlinkungen sind nun wieder erreichbar!

Kommentar von Manfred am

Das Tutorial funktioniert mit 4.4.x nicht mehr. Da scheinen sich noch einige Dinge geändert zu haben. Insbesondere das mit AppKernel.php. Scheint nicht mehr nötig zu sein. Trotzdem wäre ich stark daran interessiert dieses Tutorial für 4.4.x sehen zu können.

Einen Kommentar schreiben