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