Comment développer une extension Contao ? (2ème partie)

par Julien

Photo source par Pexels

Alors, tout d'abord, si vous n'avez pas lu le premier article, c'est par là. Allez, hop hop hop, on a besoin des mêmes pré-requis :D

Aujourd'hui, on va parler du développement de la partie Frontend sous le CMS Contao. Il existe beaucoup de choses à savoir, mais globalement, si vous savez faire du PHP, vous saurez faire une extension Contao.

Structure MVC & Autoloader

Le modèle MVC (Model/View/Controller) est bien connu de tout développeur. Utilisé désormais dans tous les frameworks et tous les CMS, Contao n'y fait pas exception.

L'essentiel dans n'importe quel développement est d'arriver à anticiper la structure que l'on va utiliser. Depuis la version 4, Contao passe par Composer pour charger ses fichiers, mais il existe toujours la possibilité de le faire à la main via la classe ClassLoader.

Vous pouvez choisir d'utiliser le système que vous souhaitez. Je vous recommande toutefois le PSR-0 ou le PSR-4, bien plus souvent utilisé.

News - Composer
News - Dossiers
PSR0 - Composer
PSR0 - Dossiers

Attention : La dépendance "contao-community-alliance/composer-plugin":"~2.4 || ~3.0" est requise pour les modules “Contao 3 friendly” et pas en mode Bundle Symfony. Elle va avoir pour objectif de créer automatiquement de symlinks entre le dossier "system/modules" et "vendor" qui vont permettre à vos extensions d'être compatibles sur les deux systèmes.

Models

Dans les structures MVC, le Model sert à interagir avec la base de données. Plus précisément, il est censé représenter un ensemble précis de données, bien souvent une ligne d’une table.

L’informatique étant ce qu’elle est, c’est une définition assez vague, car en réalité, vous pouvez faire ce que vous voulez de vos Models. Chez Web ex Machina par exemple, on s’en sert pour récupérer des Collections de Models, ce qui n’est pas très recommandé, mais qui fait gagner beaucoup de temps sur des petites structures.

Une Collection est une sorte de liste de variables appartenant toutes au même type et répondant toutes aux mêmes règles. Une sorte de tableau avec des contraintes quoi.

Pour ajouter votre Model à la structure globale de Contao, il vous suffit d’ouvrir votre fichier config.php dans le répertoire du module, et de l’ajouter sous la forme suivante, en ayant bien pris soin de vérifier que vos fichiers soient correctement chargés :

/**
 * Models
 */
$GLOBALS['TL_MODELS'][\WEM\Form\Model\Submission::getTable()] = 'WEM\Form\Model\Submission';
$GLOBALS['TL_MODELS'][\WEM\Form\Model\Field::getTable()] = 'WEM\Form\Model\Field';
$GLOBALS['TL_MODELS'][\WEM\Form\Model\Log::getTable()] = 'WEM\Form\Model\Log';
$GLOBALS['TL_MODELS'][\WEM\Form\Model\Answer::getTable()] = 'WEM\Form\Model\Answer';

L’avantage d’utiliser la fonction getTable du fichier est que si un jour, vous modifier son nom, pas besoin de revenir modifier votre fichier config ;)

Dans votre fichier Model, vous devez impérativement mentionner deux choses :

  • Héritage de la classe Contao\Model
  • Présence de la propriété “protected static $strTable” avec le nom de la dite table en valeur
    • protected static $strTable = 'tl_wem_map';

Le reste, c’est à vous. Vous devriez avoir des fonctions utilisées fréquemment, qui seraient un peu complexes à mémoriser. Le module “news” par exemple, prépare des fonctions permettant de filtrer les actualités publiées, par archive et par date. C’est utilisé dans quasiment tous les modules d’actualités, donc ça évite de devoir réécrire des règles communes.

Chez Web ex Machina, on a une approche assez générique car nos Models contiennent minimum 3 fonctions “fourre-tout” : findItems, countItems, formatColumns. Ces trois fonctions nous permettent d’anticiper la quasi totalité des besoins que l’on pourra avoir avec nos Models, et nous offre une assez bonne flexibilité pour les faire évoluer.

<?php

/**
 * Module Planning for Contao Open Source CMS
 *
 * Copyright (c) 2018 Web ex Machina
 *
 * @author Web ex Machina <https://www.webexmachina.fr>
 */

namespace WEM\Form\Model;

use Exception;
use Contao\Model;

/**
 * Reads and writes items
 */
class Answer extends Model
{
	/**
	 * Table name
	 * @var string
	 */
	protected static $strTable = 'tl_wem_form_submission_answer';

	/**
	 * Find items, depends on the arguments
	 * @param Array
	 * @param Int
	 * @param Int
	 * @param Array
	 * @return Collection
	 */
	public static function findItems($arrConfig = array(), $intLimit = 0, $intOffset = 0, $arrOptions = array())
	{
		try
		{
			$t = static::$strTable;
			$arrColumns = static::formatColumns($arrConfig);
				
			if($intLimit > 0)
				$arrOptions['limit'] = $intLimit;

			if($intOffset > 0)
				$arrOptions['offset'] = $intOffset;

			if(!isset($arrOptions['order']))
				$arrOptions['order'] = "$t.tstamp DESC";

			if(empty($arrColumns))
				return static::findAll($arrOptions);
			else
				return static::findBy($arrColumns, null, $arrOptions);
		}
		catch(Exception $e)
		{
			throw $e;
		}
	}

	/**
	 * Count items, depends on the arguments
	 * @param Array
	 * @param Array
	 * @return Integer
	 */
	public static function countItems($arrConfig = array(), $arrOptions = array())
	{
		try
		{
			$t = static::$strTable;
			$arrColumns = static::formatColumns($arrConfig);

			if(empty($arrColumns))
				return static::countAll($arrOptions);
			else
				return static::countBy($arrColumns, null, $arrOptions);
		}
		catch(Exception $e)
		{
			throw $e;
		}
	}

	/**
	 * Format ItemModel columns
	 * @param  [Array] $arrConfig [Configuration to format]
	 * @return [Array]            [The Model columns]
	 */
	public static function formatColumns($arrConfig)
	{
		try
		{
			$t = static::$strTable;
			$arrColumns = array();

			if($arrConfig["pid"])
				$arrColumns[] = "$t.pid = ". $arrConfig["pid"];

			if($arrConfig["not"])
				$arrColumns[] = $arrConfig["not"];

			return $arrColumns;
		}
		catch(Exception $e)
		{
			throw $e;
		}
	}
}

Controllers

Si les Controllers sont généralement indispensables, c’est parce qu’ils représentent une couche intermédiaire entre les Models et les Views.

En effet, on considère que le job d’un Controller est de traiter/formater la donnée pour qu’elle soit en conformité avec la destination.
Au sein de Contao, on peut très bien considérer que le Module remplira ce rôle, dans les extensions simples.

Si toutefois, vous êtes plus à l’aise avec ces fichiers, vous devriez les faire hériter de la classe Controller de Contao, car cela vous permettra d’accéder à toutes les fonctions “système”.

Modules

Dans Contao, les modules sont déclarés dans le fichier config.php et sont utilisables soit directement dans les templates, soit au sein d’éléments de contenu.

Ils héritent d’une classe abstraite native à Contao, appelée ingénieusement “Module”. Dingue hein ?

Même s’il n'y a pas d’obligation technique, votre classe devra disposer d’une propriété strTemplate qui contiendra le nom du template que le module devra utiliser. Nous parlerons de l’utilisation des templates après.

Vous devrez également avoir deux fonctions, qui n’acceptent pas de paramètres : generate & compile. Ces deux fonctions sont appelées au cours de la génération des modules Contao, à différents moments, pour permettre l’utilisation de hooks (ça sent le futur article ?). generate est appelée en premier, puis compile.

/**
 * Contao Open Source CMS
 *
 * Copyright (c) 2005-2017 Leo Feyer
 *
 * @license LGPL-3.0+
 */

namespace Contao;

/**
 * Class ModuleName
 */
class ModuleName extends \Module
{

	/**
	 * Template
	 * @var string
	 */
	protected $strTemplate = 'template_name';

	/**
	 * Display a wildcard in the back end
	 *
	 * @return string
	 */
	public function generate()
	{

	}


	/**
	 * Generate the module
	 */
	protected function compile()
	{

	}
}

La fonction generate étant appelée en première, une sorte de convention s’est établie naturellement : si votre module ne doit pas être généré pour x ou y raison, c’est dans cette fonction qu’il doit être “cassé”. Cela évite de consommer des ressources pour rien puisque la fonction compile ne sera ensuite pas appelée si generate n’appelle pas “return parent::generate();”

Par exemple, si le module faqlist ne trouve pas d’item lui permettant d’afficher une faq , il fera un “return ‘’”, via les instructions ci-dessous :

/**
 * Display a wildcard in the back end
 *
 * @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']['faqlist'][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();
	}

	$this->faq_categories = \StringUtil::deserialize($this->faq_categories);

	// Return if there are no categories
	if (!is_array($this->faq_categories) || empty($this->faq_categories))
	{
		return '';
	}

	// Show the FAQ reader if an item has been selected
	if ($this->faq_readerModule > 0 && (isset($_GET['items']) || (\Config::get('useAutoItem') && isset($_GET['auto_item']))))
	{
		return $this->getFrontendModule($this->faq_readerModule, $this->strColumn);
	}

	return parent::generate();
}

Et ensuite, dans compile, il y a tout le reste. Tout le traitement que doit faire votre module, découpé comme vous le souhaitez. Le reste n’est qu’utilisation globale du PHP. Vous pouvez par exemple décider de créer une espèce de classe module parente de tous les modules de votre extension, pour rassembler des fonctions communes, et les appeler via un $this->votreFonction();

Vous pouvez également ajouter autant de propriétés que vous souhaitez, faites la ratatouille dont vous avez besoin :)

Dernier détail, chaque module va automatiquement contenir une propriété Template, accessible via $this->Template, et qui permet de transmettre des variables à ce dernier. Vous pouvez transmettre ce que vous voulez au Template, des chaînes, des tableaux ou des objets etc...

Ex : $this->Template->title = “Hello world !”; sera utilisable sous la forme <?php echo $this->title; ?> dans le fichier du template.

Digression : Concept de tableau de propriétés

L’utilisation d’un tableau de variable simulant les propriétés d’une classe PHP est un concept assez intéressant, bien que carrément abstrait.

Il consiste à créer une unique propriété, appelons la “data”, et de programmer les fonctions “get” et “set” de votre classe pour qu’elles accèdent à ce tableau plutôt qu’à une propriété classique. Voyez plutôt :

class ClassName
{
	protected $data;
	
	public function __get($key){
	    return $this->data[$key];
	}
	
	public function __set($key, $value){
	    $this->data[$key] = $value;
	}
}

Les tableaux PHP étant ce qu’ils sont, on peut y fourrer ce qu’on veut ! Cela donne ainsi une excellente flexibilité quand on n’a aucune idée de comment peut évoluer notre classe. Cependant, comme tout système ultra flexible, on perd forcément en contrôle, donc c’est à utiliser en connaissance de cause.

Templates

Contao utilise sa propre logique de template, en surcouche de Twig, utilisé par Symfony. Via l’extension html5, vous pouvez déclarer autant de templates que vous voulez via le fichier autoload.php et la fonction TemplateLoader::addFiles() qui accepte un tableau de paramètres, contenant en clé le nom du template, et en valeur, son emplacement sur le serveur :

/**
 * Register the templates
 */
TemplateLoader::addFiles(array
(
    'mod_newsarchive'   => 'system/modules/news/templates/modules',
    'mod_newslist'      => 'system/modules/news/templates/modules',
    'mod_newsmenu'      => 'system/modules/news/templates/modules',
    'mod_newsmenu_day'  => 'system/modules/news/templates/modules',
    'mod_newsmenu_year' => 'system/modules/news/templates/modules',
    'mod_newsreader'    => 'system/modules/news/templates/modules',
    'news_full'         => 'system/modules/news/templates/news',
    'news_latest'       => 'system/modules/news/templates/news',
    'news_short'        => 'system/modules/news/templates/news',
    'news_simple'       => 'system/modules/news/templates/news',
));

Petit bémol, cela veut dire qu’il va vous falloir trouver des noms uniques à vos fichiers, car sinon, Contao ne pourra pas déterminer lequel vous voulez (enfin, il prendra le dernier inséré).

Ensuite, il n’y a pas vraiment de règles. Vous pouvez faire hériter vos templates les uns les autres via le code ci-dessous, où vous insérez le nom du template à “étendre”.

<?php $this->extend('block_unsearchable'); ?>
<?php $this->block('content'); ?>
… votre code ...
<?php $this->endblock(); ?>

Pour le reste, c’est de l’html et du php de base !

Insérer du CSS ou du JS conditionnel

Pour optimiser nos modules, il est bien souvent nécessaire de n’inclure des librairies CSS ou JS que lorsque c’est nécessaire.
Pour cela, rien de plus simple, dans votre module, vous pouvez utiliser les tableaux globaux suivants :

// CSS
$GLOBALS['TL_HEAD'][] = ‘<link rel=”stylesheet” href="path/to/css">’;

// JS
$GLOBALS['TL_JAVASCRIPT'][] = ‘path/to/js’;

Ces tableaux globaux seront aussi formaté par le système de Contao, dans la balise <head> de votre page web pour le premier, et juste avant la balise </body> pour le second.

Il est aussi possible de générer du SASS (les fichiers scss) avec la classe Combiner de Contao. Cette classe a pour objectif de compiler des fichiers de même type en un seul.

Voilà un exemple d’utilisation ci-dessous :

$objCombiner = new \Combiner();
$objCombiner->add('system/modules/wem-contao-lightbox/assets/wem_contao_lightbox.scss', 1);
$GLOBALS['TL_HEAD'][] = '<link rel="stylesheet" ref="'.$objCombiner->getCombinedFile().'">';

Où l’on compile et ajoute le fichier wem_contao_lightbox.scss aux balises <head> de notre page.

A noter le “1” en second paramètre, qui correspond à la version du fichier combiné. Cela permet de mettre en cache les fichiers générés sous un numéro particulier. Si vous voulez générer le fichier à chaque fois, vous pouvez utiliser “time()” qui générera un numéro de version toujours différent.

What else ?

Vous avez désormais tout ce qu’il faut pour développer une extension de base, du backend au frontend !

Il existe encore beaucoup de trucs et astuces à savoir quand on souhaite développer sous Contao. Comme pour chaque framework / CMS, je vous conseille de parcourir les fichiers formant le coeur du système, ainsi que des extensions simples et complexes, pour vous en inspirer.

Les prochains articles sur le développement de Contao seront consacrés à des problématiques précises, comme la gestion du multilingue ou l’utilisation de l’Ajax !