Olivier El Mekki

Typolight et les modèles : M comme dans VC

Si on me posait la question du problème majeur dans le core de Typolight, je répondrais : l’implémentation incomplète du pattern MVC. La couche modèle est difficilement utilisable telle-quelle et provoque l’abondance de requêtes SQL dans les contrôleurs. Il y a aussi le problème de la présence de HTML dans les contrôleurs, mais cela ne concernera pas ce tutoriel. Nous verrons cette fois comment rendre aux modèles ce qui appartient aux modèles.

Le MVC, c’est quoi?

Pour ceux qui ne sont pas familiers à la notion de MVC, récapitulons brièvement. MVC est un acronyme pour : Model, View, Controller ( en français, ça marche bien aussi ). Il s’agit d’un design pattern qui structure une application selon trois tâches :

Typiquement, dans une application web, les modèles sont tous les calculs faits sur la base de donnée ; les vues sont les fichiers contenant le html ( ou autre, par exemple la structure d’un email ) ; les contrôleurs sont ce qui gèrent les requêtes ( POST, GET, etc ), leurs paramètres, et décident quels modèles utiliser et quelles variables fournir aux vues.

La couche modèle est en général gérée par un ORM ( Object Relational Mapping ). Celui-ci permet de manipuler la base de donnée sans faire soi-même de pénibles requêtes SQL. Si par exemple j’ai dans ma base de donnée une table posts avec des champs id, name, content et author, j’aurai une classe Post, héritant de Model, qui me permettra de manipuler les entrées de la base de donnée comme des objets. Voici un exemple de PHP fictif qui illustre la manipulation d’un modèle :

// récupérer l'entrée de la tables posts qui à l'id 10
$post = Post::find( 10 );
 
// retrouver son attribut "name"
echo $post->name;
 
// le modifier
$post->name = 'an other name';
 
// sauvegarder le changement dans la base de donnée
$post->save();
 
// récupérer tous les posts dans un array
$posts = Post::findAll();

On peut ensuite utiliser ce array dans une vue, avec par exemple :

<ul id="posts">
  < ?php foreach ( $posts as $post ) : ?>
  <li><a href="post-<?php echo $post->id ?>">< ?php echo $post->name ?></a>, by < ?php echo $post->author ?></li>
  < ?php endforeach ?>
</ul>

Dans Typolight, les vues sont les templates et les contrôleurs sont les FrontendModule et BackendModule. Il n’y a malheureusement pas d’équivalence pour la couche modèle, et les données sont généralement retrouvées en remplissant les contrôleurs de requêtes SQL, ce qui rend la base de donnée pénible à utiliser et encombre les contrôleurs.

Comment utiliser une couche modèle dans Typolight?

Heureusement, une couche modèle existe belle et bien. Le fichier est TL_ROOT/system/libraries/Model.php . Ce fichier dispose des méthodes de base d’une couche modèle, mais il semble avoir été sabordé en route. En effet, c’est un singleton ( ce qui signifie qu’un seul objet de cette classe peut exister en même temps ). Il a en fait été pensé pour ne servir qu’aux classes User, BackendUser et FrontendUser, permettant de gérer l’utilisateur actuellement loggé.

Il n’y a en fait qu’une simple chose à faire pour rendre une couche modèle utilisable dans Typolight : créer une classe qui descend de Model et rendre publique sa méthode __construct(). C’est par exemple la méthode choisie par les développeurs de Isotope, ou par ma propre extension framework.

Nous allons reprendre la base du précédent tutoriel. Nous avions deux tables SQL : tl_filiere_services et tl_filiere_filiales. Nous allons donc faire deux classes de modèles : Service et Filiale.

< ?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
 
/**
 * TYPOlight webCMS
 * Copyright (C) 2005 Leo Feyer
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation, either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, please visit the Free
 * Software Foundation website at http://www.gnu.org/licenses/.
 *
 * PHP version 5
 * @copyright  
 * @author    
 * @package    
 * @license    
 * @filesource
 */
 
 
/**
 * Class Service
 *
 * @copyright  
 * @author     
 * @package    Model
 */
 
class Service extends Model
{
  protected $strTable = 'tl_filiere_services';
 
  public function __construct()
  {
    parent::__construct();
  }
}
< ?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
 
/**
 * TYPOlight webCMS
 * Copyright (C) 2005 Leo Feyer
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation, either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, please visit the Free
 * Software Foundation website at http://www.gnu.org/licenses/.
 *
 * PHP version 5
 * @copyright  
 * @author    
 * @package    
 * @license    
 * @filesource
 */
 
 
/**
 * Class Filiale
 *
 * @copyright  
 * @author     
 * @package    Model
 */
 
class Filiale extends Model
{
  protected $strTable = 'tl_filiere_services';
 
  public function __construct()
  {
    parent::__construct();
  }
}

Et c’est tout. Nous disposons maintenant de tout ce qu’il faut pour trouver une entrée, récupérer la valeur de ses champs, enregistrer ses changements ou l’effacer. Par exemple :

// crée un objet service
$service = new Service();
 
// retrouver l'entrée qui a l'id 10
$service->findBy( 'id, 10 );
 
// retrouve l'attribut 'name'
echo $service->name;
 
// modifie l'attribut 'name'
$service->name = 'new service';
 
// enregistre les modifications dans la base de donnée
$service->save();
 
// efface l'entrée
$service->delete();

Nous pouvons maintenant manipuler simplement nos services et nos filiales sans avoir à écrire de fastidieuses requêtes SQL.

Mais ce n’est pas tout ce à quoi sert un modèle. Pour respecter le design pattern, il faut que toute manipulation de donnée soit faite dans le modèle. C’est son rôle, après tout. Dans le monde de Ruby on Rails, il y a même une devise : “thick models, thin controllers” ( modèles gros et contrôleurs minces ). L’idée est que puisque les modèles gèrent les données et que, précisément, un site est d’abord une question de données, c’est aux modèles de faire la majeure partie du travail. Il y a là un partie pris ( comme toujours avec rails ), vous en penserez ce que vous voudrez. Néanmoins, il reste exact que les manipulations des données doivent être opérées par les modèles. Reste a définir la limite entre “traitement des données” et “contrôle d’une requête” :)

Prenons un exemple concret. Les services de nos filiales ont un prix. Disons que ce prix est hors taxe. Il va me falloir, à un moment ou un autre, calculer le prix TTC pour un service. C’est ici typiquement une méthode qui doit résider dans le modèle Service. Comme nous sommes insouciants, nous allons considérer que les taxes sont fixes et que nous pouvons considérer qu’elles sont de 20% ( dans un cas réel, il faudrait rendre cette valeur configurable en backend ). Voici la méthode que nous allons rajouter dans la classe Service :

public function ttc()
{
  $ttc = $this->price + ( $this->price * 0.2 );
  return $ttc;
}

Désormais, dans le template qui affiche un service, nous n’aurons plus qu’à faire :

<p>Prix TTC : < ?php echo $this->service->ttc() ?></p>

( nous verrons dans un prochain tutoriel comment passer des variables à un template ).

Ce que la classe Model de Typolight ne permet pas

Aujourd’hui, les ORM des frameworks permettent toutes sortes de raffinements, telle que les associations, les validations, finders et autres.

Nos filiales ont des services. Nous voudrions, avec un objet $filiale, pouvoir faire $filiale->services() pour avoir la liste des services de cette filiale. C’est ce que permettent de faire les associations, par le biais de relations ( has many, has one, belongs to, many to many… ).

Nous voudrions aussi empêcher d’enregistrer les modifications sur une filiale si l’adresse mail fournie n’est pas dans un format valide ( par exemple “1HD23s” ). C’est le rôle des validations ( elle sont présentes dans les widgets backend, dans Typolight, mais pas dans la couche modèle ).

Enfin, nous voudrions pouvoir, par exemple, trouver toutes les filiales enregistrées depuis moins d’un mois. Ou simplement trouver toutes les filiales. Ou trouver la première filiale qui se trouve à Lyon. C’est le rôle des finders.

Avec la couche modèle de Typolight, vous devrez écrire ces méthodes vous-même. Si vous désirez avoir une solution générique pour ces problèmes relativement courant, je vous invite à utiliser ma classe EModel : http://github.com/oelmekki/typolight_framework/blob/master/system/modules/framework/EModel.php ( promis, la documentation arrive. Vous devrez pour l’instant lire le code ou me poser des questions pour comprendre comment s’en servir ).

Cela conclut mon article sur les modèles dans Typolight. Nous verrons ensuite les modules frontend.

Laisser un commentaire