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.
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:
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.
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.
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.
Autos anlegen
Jetzt können wir mit den Anlegen von Autos beginnen.
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 ...
...und binden es in die Startseite ein (Neues Element » Elementtyp: Modul » Autoliste (ID 1)
Ausgabe des fertigen FrontEnd-Modul
Im Frontend sollte auf der Startseite, die Ausgabe des Modul zu sehen sein.
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