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