Mis à jour le lundi 8 janvier 2018
  • 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Gérer les formulaires

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Il est possible que quelque chose vous chiffonne un petit peu. En effet, dans le frontend, nous avons créé un formulaire pour ajouter un commentaire. Dans le backend, nous avons recréé quasiment le même : nous avons fait de la duplication de code. Or, puisque vous êtes un excellent programmeur, cela devrait vous piquer les yeux !

Pour pallier ce problème courant de duplication de formulaires, nous allons externaliser nos formulaires à l'aide d'une API, c'est-à-dire que le code créant le formulaire sera accessible à un autre endroit, par n'importe quel module de n'importe quelle application. Cette technique fera d'une pierre deux coups : non seulement nos formulaires seront décentralisés (donc réutilisables une infinité de fois), mais la création se fera de manière beaucoup plus aisée ! Bien sûr, comme pour la conception de l'application, cela deviendra rapide une fois l'API développée. ;)

Le formulaire

Conception du formulaire

Commençons dans ce chapitre par créer un premier formulaire. Un formulaire, vous le savez, n'est autre qu'un ensemble de champs permettant d'interagir avec le contenu du site. Par exemple, voici notre formulaire d'ajout de commentaire :

<form action="" method="post">
  <p>
    <?= isset($erreurs) && in_array(\Entity\Comment::AUTEUR_INVALIDE, $erreurs) ? 'L\'auteur est invalide.<br />' : '' ?>
    <label>Pseudo</label>
    <input type="text" name="pseudo" value="<?= isset($comment) ? htmlspecialchars($comment['auteur']) : '' ?>" /><br />
    
    <?= isset($erreurs) && in_array(\Entity\Comment::CONTENU_INVALIDE, $erreurs)) 'Le contenu est invalide.<br />' : '' ?>
    <label>Contenu</label>
    <textarea name="contenu" rows="7" cols="50"><?= isset($comment) ? htmlspecialchars($comment['contenu']) : '' ?></textarea><br />
    
    <input type="submit" value="Commenter" />
  </p>
</form>

Cependant, vous conviendrez qu'il est long et fastidieux de créer ce formulaire. De plus, si nous voulons éditer un commentaire, il va falloir le dupliquer dans l'application backend. Dans un premier temps, nous allons nous occuper de l'aspect long et fastidieux : laissons un objet générer tous ces champs à notre place !

L'objet Form

Comme nous venons de le voir, un formulaire n'est autre qu'une liste de champs. Vous connaissez donc déjà le rôle de cet objet : il sera chargé de représenter le formulaire en possédant une liste de champs.

Commençons alors la liste des fonctionnalités de notre formulaire. Notre formulaire contient divers champs. Nous devons donc pouvoir ajouter des champs à notre formulaire. Ensuite, que serait un formulaire si on ne pouvait pas l'afficher ? Dans notre cas, le formulaire ne doit pas être capable de s'afficher mais de générer tous les champs qui lui sont attachés afin que le contrôleur puisse récupérer le corps du formulaire pour le passer à la vue. Enfin, notre formulaire doit posséder une dernière fonctionnalité : le capacité de déclarer si le formulaire est valide ou non en vérifiant que chaque champ l'est.

Pour résumer, nous avons donc trois fonctionnalités. Un objet Form doit être capable :

  • D'ajouter des champs à sa liste de champs.

  • De générer le corps du formulaire.

  • De vérifier si tous les champs sont valides.

Ainsi, au niveau des caractéristiques de l'objet, nous en avons qui saute aux yeux : la liste des champs !

Cependant, un formulaire est également caractérisé par autre chose. En effet, si je vous demande de me dire comment vous allez vérifier si tous les champs sont valides, vous sauriez comment faire ? À aucun moment nous n'avons passé des valeurs à notre formulaire, donc aucune vérification n'est à effectuer. Il faudrait donc, dans le constructeur de notre objet Form, passer un objet contenant toutes ces valeurs. Ainsi, lors de l'ajout d'un champ, la méthode irait chercher la valeur correspondante dans cet objet et l'assignerait au champ (nous verrons plus tard comment la méthode sait à quel attribut de l'entité correspond le champ). À votre avis, à quoi vont ressembler ces objets ? En fait, vous les avez déjà créés ces objets : ce sont toutes les classes filles de Entity ! Par exemple, si vous voulez modifier un commentaire, vous allez créer un objet Comment que vous allez hydrater, puis vous créerez un objet Form en passant l'objet Comment au constructeur.

Ainsi, voici notre classe Form schématisée (voir la figure suivante).

Modélisation de la classe Form
Modélisation de la classe Form
L'objet Field

Puisque l'objet Form est intimement lié à ses champs, intéressons-nous à la conception de ces champs (ou fields en anglais). Vous l'aurez peut-être deviné : tous nos champs seront des objets, chacun représentant un champ différent (une classe représentera un champ texte, une autre classe représentera une zone de texte, etc.). À ce stade, un tilt devrait s'être produit dans votre tête : ce sont tous des champs, ils doivent donc hériter d'une même classe représentant leur nature en commun, à savoir une classe Field !

Commençons par cette classe Field. Quelles fonctionnalités attendons-nous de cette classe ? Un objet Field doit être capable :

  • De renvoyer le code HTML représentant le champ.

  • De vérifier si la valeur du champ est valide.

Je pense que vous aviez ces fonctionnalités plus ou moins en tête. Cependant, il y a encore une autre fonctionnalité que nous devons implémenter. En effet, pensez aux classes qui hériteront de Field et qui représenteront chacune un type de champ. Chaque champ a des attributs spécifiques. Par exemple, un champ texte (sur une ligne) possède un attribut maxlength, tandis qu'une zone de texte (un textarea) possède des attributs rows et cols. Chaque classe fille aura donc des attributs à elles seules. Il serait pratique, dès la construction de l'objet, de passer ces valeurs à notre champ (par exemple, assigner 50 à l'attribut maxlength). Pour résoudre ce genre de cas, nous allons procéder d'une façon qui ne vous est pas inconnue : nous allons créer une méthode permettant à l'objet de s'hydrater ! Ainsi, notre classe Field possédera une méthode hydrate(), comme les entités.

Attends, la classe Entity possède déjà une telle méthode, on va se contenter de la dupliquer ?

Effectivement, cela ne serait pas très propre. Y a-t-il, à votre connaissance, une façon de pallier ce problème de duplication de méthodes ? Oui, il y en a une ! Vous souvenez-vous des traits ? Ils correspondent exactement à ce que nous voulons. Nous allons donc créer un trait Hydrator qui implémentera cette méthode hydrate() et que nos classes Entity et Field utiliseront !

Ce trait ne contient aucune difficulté à réaliser. Voici ce que vous devez obtenir (ce trait est à placer dans le fichier /lib/OCFram/Hydrator.php) :

<?php
namespace OCFram;

trait Hydrator
{
  public function hydrate($data)
  {
    foreach ($data as $key => $value)
    {
      $method = 'set'.ucfirst($key);
      
      if (is_callable([$this, $method]))
      {
        $this->$method($value);
      }
    }
  }
}

Vous pouvez dès à présent modifier la classe Entity de notre framework afin d'utiliser ce trait (il faudra donc penser à supprimer la méthode hydrate() qui y est présente).

<?php
namespace OCFram;

abstract class Entity implements \ArrayAccess
{
  // Utilisation du trait Hydrator pour que nos entités puissent être hydratées
  use Hydrator;
  
  // La méthode hydrate() n'est ainsi plus implémentée dans notre classe
}

Voici à la figure suivante le schéma représentant notre classe Field liée à la classe Form, avec deux classes filles en exemple (StringField représentant un champ texte sur une ligne et la classe TextField représentant un textarea).

Modélisation de la classe Field
Modélisation de la classe Field

Développement de l'API

La classe Form

Pour rappel, voici de quoi la classe Form est composée :

  • D'un attribut stockant la liste des champs.

  • D'un attribut stockant l'entité correspondant au formulaire.

  • D'un constructeur récupérant l'entité et invoquant le setter correspondant.

  • D'une méthode permettant d'ajouter un champ à la liste des champs.

  • D'une méthode permettant de générer le formulaire.

  • D'une méthode permettant de vérifier si le formulaire est valide.

Voici la classe Form que vous auriez du obtenir :

<?php
namespace OCFram;

class Form
{
  protected $entity;
  protected $fields = [];
  
  public function __construct(Entity $entity)
  {
    $this->setEntity($entity);
  }
  
  public function add(Field $field)
  {
    $attr = $field->name(); // On récupère le nom du champ.
    $field->setValue($this->entity->$attr()); // On assigne la valeur correspondante au champ.
    
    $this->fields[] = $field; // On ajoute le champ passé en argument à la liste des champs.
    return $this;
  }
  
  public function createView()
  {
    $view = '';
    
    // On génère un par un les champs du formulaire.
    foreach ($this->fields as $field)
    {
      $view .= $field->buildWidget().'<br />';
    }
    
    return $view;
  }
  
  public function isValid()
  {
    $valid = true;
    
    // On vérifie que tous les champs sont valides.
    foreach ($this->fields as $field)
    {
      if (!$field->isValid())
      {
        $valid = false;
      }
    }
    
    return $valid;
  }
  
  public function entity()
  {
    return $this->entity;
  }
  
  public function setEntity(Entity $entity)
  {
    $this->entity = $entity;
  }
}
La classe Field et ses filles

Voici un petit rappel sur la composition de la classe Field. Cette classe doit être composée :

  • D'un attribut stockant le message d'erreur associé au champ.

  • D'un attribut stockant le label du champ.

  • D'un attribut stockant le nom du champ.

  • D'un attribut stockant la valeur du champ.

  • D'un constructeur demandant la liste des attributs avec leur valeur afin d'hydrater l'objet.

  • D'une méthode (abstraite) chargée de renvoyer le code HTML du champ.

  • D'une méthode permettant de savoir si le champ est valide ou non.

Les classes filles, quant à elles, n'implémenteront que la méthode abstraite. Si elles possèdent des attributs spécifiques (comme l'attribut maxlength pour la classe StringField), alors elles devront implémenter les mutateurs correspondant (comme vous le verrez plus tard, ce n'est pas nécessaire d'implémenter les accesseurs).

Voici les trois classes que vous auriez du obtenir (la classe Field avec deux classes filles en exemple, StringField et TextField) :

<?php
namespace OCFram;

abstract class Field
{
  // On utilise le trait Hydrator afin que nos objets Field puissent être hydratés
  use Hydrator;
  
  protected $errorMessage;
  protected $label;
  protected $name;
  protected $value;
  
  public function __construct(array $options = [])
  {
    if (!empty($options))
    {
      $this->hydrate($options);
    }
  }
  
  abstract public function buildWidget();
  
  public function isValid()
  {
    // On écrira cette méthode plus tard.
  }
  
  public function label()
  {
    return $this->label;
  }
  
  public function name()
  {
    return $this->name;
  }
  
  public function value()
  {
    return $this->value;
  }
  
  public function setLabel($label)
  {
    if (is_string($label))
    {
      $this->label = $label;
    }
  }
  
  public function setName($name)
  {
    if (is_string($name))
    {
      $this->name = $name;
    }
  }
  
  public function setValue($value)
  {
    if (is_string($value))
    {
      $this->value = $value;
    }
  }
}
<?php
namespace OCFram;

class StringField extends Field
{
  protected $maxLength;
  
  public function buildWidget()
  {
    $widget = '';
    
    if (!empty($this->errorMessage))
    {
      $widget .= $this->errorMessage.'<br />';
    }
    
    $widget .= '<label>'.$this->label.'</label><input type="text" name="'.$this->name.'"';
    
    if (!empty($this->value))
    {
      $widget .= ' value="'.htmlspecialchars($this->value).'"';
    }
    
    if (!empty($this->maxLength))
    {
      $widget .= ' maxlength="'.$this->maxLength.'"';
    }
    
    return $widget .= ' />';
  }
  
  public function setMaxLength($maxLength)
  {
    $maxLength = (int) $maxLength;
    
    if ($maxLength > 0)
    {
      $this->maxLength = $maxLength;
    }
    else
    {
      throw new \RuntimeException('La longueur maximale doit être un nombre supérieur à 0');
    }
  }
}
<?php
namespace OCFram;

class TextField extends Field
{
  protected $cols;
  protected $rows;
  
  public function buildWidget()
  {
    $widget = '';
    
    if (!empty($this->errorMessage))
    {
      $widget .= $this->errorMessage.'<br />';
    }
    
    $widget .= '<label>'.$this->label.'</label><textarea name="'.$this->name.'"';
    
    if (!empty($this->cols))
    {
      $widget .= ' cols="'.$this->cols.'"';
    }
    
    if (!empty($this->rows))
    {
      $widget .= ' rows="'.$this->rows.'"';
    }
    
    $widget .= '>';
    
    if (!empty($this->value))
    {
      $widget .= htmlspecialchars($this->value);
    }
    
    return $widget.'</textarea>';
  }
  
  public function setCols($cols)
  {
    $cols = (int) $cols;
    
    if ($cols > 0)
    {
      $this->cols = $cols;
    }
  }
  
  public function setRows($rows)
  {
    $rows = (int) $rows;
    
    if ($rows > 0)
    {
      $this->rows = $rows;
    }
  }
}

Testons nos nouvelles classes

Testons dès maintenant nos classes. Dans notre contrôleur de news du frontend, nous allons modifier l'action chargée d'ajouter un commentaire. Créons notre formulaire avec nos nouvelles classes, en commançant par modifier le fichier NewsController.php du frontend :

<?php
namespace App\Frontend\Modules\News;

use \OCFram\BackController;
use \OCFram\HTTPRequest;
use \Entity\Comment;
use \OCFram\Form;
use \OCFram\StringField;
use \OCFram\TextField;

class NewsController extends BackController
{
  public function executeInsertComment(HTTPRequest $request)
  {
    // Si le formulaire a été envoyé, on crée le commentaire avec les valeurs du formulaire.
    if ($request->method() == 'POST')
    {
      $comment = new Comment([
        'news' => $request->getData('news'),
        'auteur' => $request->postData('auteur'),
        'contenu' => $request->postData('contenu')
      ]);
    }
    else
    {
      $comment = new Comment;
    }
    
    $form = new Form($comment);
    
    $form->add(new StringField([
        'label' => 'Auteur',
        'name' => 'auteur',
        'maxLength' => 50,
       ]))
       ->add(new TextField([
        'label' => 'Contenu',
        'name' => 'contenu',
        'rows' => 7,
        'cols' => 50,
       ]));
    
    if ($form->isValid())
    {
      // On enregistre le commentaire
    }
    
    $this->page->addVar('comment', $comment);
    $this->page->addVar('form', $form->createView()); // On passe le formulaire généré à la vue.
    $this->page->addVar('title', 'Ajout d\'un commentaire');
  }
}

La vue correspondante, insertComment.php, ressemble maintenant à ceci :

<h2>Ajouter un commentaire</h2>
<form action="" method="post">
  <p>
    <?= $form ?>
    
    <input type="submit" value="Commenter" />
  </p>
</form>

Cependant, avouez que ce n'est pas pratique d'avoir ceci en plein milieu de notre contrôleur. De plus, si nous avons besoin de créer ce formulaire à un autre endroit, nous devrons copier/coller tous ces appels à la méthode add() et recréer tous les champs. Niveau duplication de code, nous sommes servis ! Nous résoudrons ce problème dans la suite du chapitre. Mais avant cela, intéressons-nous à la validation du formulaire. En effet, le contenu de la méthode isValid() est resté vide : faisons appel aux validateurs !

Les validateurs

Un validateur, comme son nom l'indique, est chargé de valider une donnée. Mais attention : un validateur ne peut valider qu'une contrainte. Par exemple, si vous voulez vérifier que votre valeur n'est pas nulle et qu'elle ne dépasse pas les cinquante caractères, alors vous aurez besoin de deux validateurs : le premier vérifiera que la valeur n'est pas nulle, et le second vérifiera que la chaine de caractères ne dépassera pas les cinquante caractères.

Là aussi, vous devriez savoir ce qui vous attend au niveau des classes : nous aurons une classe de base (Validator) et une infinité de classes filles (dans le cas précédent, on peut imaginer les classes NotNullValidator et MaxLengthValidator). Attaquons-les dès maintenant !

Conception des classes

La classe Validator

Notre classe de base, Validator, sera chargée, comme nous l'avons dit, de valider une donnée. Et c'est tout : un validateur ne sert à rien d'autre que valider une donnée. Au niveau des caractéristiques, il n'y en a là aussi qu'une seule : le message d'erreur que le validateur doit pouvoir renvoyer si la valeur passée n'est pas valide.

Voici donc notre classe schématisée (voir la figure suivante).

Modélisation de notre classe Validator
Modélisation de notre classe Validator
Les classes filles

Les classes filles sont elles aussi très simples. Commençons par la plus facile : NotNullValidator. Celle-ci, comme toute classe fille, sera chargée d'implémenter la méthode isValid($value). Et c'est tout ! La seconde classe, MaxLengthValidator, implémente elle aussi cette méthode. Cependant, il faut qu'elle connaisse le nombre de caractères maximal que la chaîne doit avoir ! Pour cela, cette classe implémentera un constructeur demandant ce nombre en paramètre, et assignera cette valeur à l'attribut correspondant.

Ainsi, voici nos deux classes filles héritant de Validator (voir la figure suivante).

Modélisation des classes MaxLengthValidator et NotNullValidator
Modélisation des classes MaxLengthValidator et NotNullValidator

Développement des classes

La classe Validator

Cette classe (comme les classes filles) est assez simple à développer. En effet, il n'y a que l'accesseur et le mutateur du message d'erreur à implémenter, avec un constructeur demandant ledit message d'erreur. La méthode isValid(), quant à elle, est abstraite, donc rien à écrire de ce côté-là !

<?php
namespace OCFram;

abstract class Validator
{
  protected $errorMessage;
  
  public function __construct($errorMessage)
  {
    $this->setErrorMessage($errorMessage);
  }
  
  abstract public function isValid($value);
  
  public function setErrorMessage($errorMessage)
  {
    if (is_string($errorMessage))
    {
      $this->errorMessage = $errorMessage;
    }
  }
  
  public function errorMessage()
  {
    return $this->errorMessage;
  }
}
Les classes filles

Comme la précédente, les classes filles sont très simples à concevoir. J'espère que vous y êtes parvenus !

<?php
namespace OCFram;

class NotNullValidator extends Validator
{
  public function isValid($value)
  {
    return $value != '';
  }
}
<?php
namespace OCFram;

class MaxLengthValidator extends Validator
{
  protected $maxLength;
  
  public function __construct($errorMessage, $maxLength)
  {
    parent::__construct($errorMessage);
    
    $this->setMaxLength($maxLength);
  }
  
  public function isValid($value)
  {
    return strlen($value) <= $this->maxLength;
  }
  
  public function setMaxLength($maxLength)
  {
    $maxLength = (int) $maxLength;
    
    if ($maxLength > 0)
    {
      $this->maxLength = $maxLength;
    }
    else
    {
      throw new \RuntimeException('La longueur maximale doit être un nombre supérieur à 0');
    }
  }
}

Modification de la classe Field

Comme nous l'avions vu, pour savoir si un champ est valide, il lui faut des validateurs. Il va donc falloir passer, dans le constructeur de l'objet Field créé, la liste des validateurs que l'on veut imposer au champ. Dans le cas du champ auteur par exemple, nous lui passerons les deux validateurs : nous voulons à la fois que le champ ne soit pas vide et que la valeur ne dépasse pas les cinquante caractères. La création du formulaire ressemblerait donc à ceci :

<?php
// $form représente le formulaire que l'on souhaite créer.
// Ici, on souhaite lui ajouter le champ « auteur ».
$form->add(new StringField([
  'label' => 'Auteur',
  'name' => 'auteur',
  'maxLength' => 50,
  'validators' => [
    new \OCFram\MaxLengthValidator('L\'auteur spécifié est trop long (50 caractères maximum)', 50),
    new \OCFram\NotNullValidator('Merci de spécifier l\'auteur du commentaire'),
  ]
]));

De cette façon, quelques modifications au niveau de notre classe Field s'imposent. En effet, il va falloir créer un attribut $validators, ainsi que l'accesseur et le mutateur correspondant. De la sorte, notre méthode hydrate() assignera automatiquement les validateurs passés au constructeur à l'attribut $validators. Je vous laisse faire cela.

Vient maintenant l'implémentation de la méthode isValid(). Cette méthode doit parcourir tous les validateurs et invoquer la méthode isValid($value) sur ces validateurs afin de voir si la valeur passe au travers du filet de tous les validateurs. De cette façon, nous sommes sûrs que toutes les contraintes ont été respectées ! Si un validateur renvoie une réponse négative lorsqu'on lui demande si la valeur est valide, alors on devra lui demander le message d'erreur qui lui a été assigné et l'assigner à notre tour à l'attribut correspondant. Ainsi, voici la nouvelle classe Field :

<?php
namespace OCFram;

abstract class Field
{
  use Hydrator;
  
  protected $errorMessage;
  protected $label;
  protected $name;
  protected $validators = [];
  protected $value;
  
  public function __construct(array $options = [])
  {
    if (!empty($options))
    {
      $this->hydrate($options);
    }
  }
  
  abstract public function buildWidget();
  
  public function isValid()
  {
    foreach ($this->validators as $validator)
    {
      if (!$validator->isValid($this->value))
      {
        $this->errorMessage = $validator->errorMessage();
        return false;
      }
    }
    
    return true;
  }
  
  public function label()
  {
    return $this->label;
  }
  
  public function length()
  {
    return $this->length;
  }
  
  public function name()
  {
    return $this->name;
  }
  
  public function validators()
  {
    return $this->validators;
  }
  
  public function value()
  {
    return $this->value;
  }
  
  public function setLabel($label)
  {
    if (is_string($label))
    {
      $this->label = $label;
    }
  }
  
  public function setLength($length)
  {
    $length = (int) $length;
    
    if ($length > 0)
    {
      $this->length = $length;
    }
  }
  
  public function setName($name)
  {
    if (is_string($name))
    {
      $this->name = $name;
    }
  }
  
  public function setValidators(array $validators)
  {
    foreach ($validators as $validator)
    {
      if ($validator instanceof Validator && !in_array($validator, $this->validators))
      {
        $this->validators[] = $validator;
      }
    }
  }
  
  public function setValue($value)
  {
    if (is_string($value))
    {
      $this->value = $value;
    }
  }
}

Le constructeur de formulaires

Comme nous l'avons vu, créer le formulaire au sein du contrôleur présente deux inconvénients. Premièrement, cela encombre le contrôleur. Imaginez que vous ayez une dizaine de champs, cela deviendrait énorme ! Le contrôleur doit être clair, et la création du formulaire devrait donc se faire autre part. Deuxièmement, il y a le problème de duplication de code : si vous voulez utiliser ce formulaire dans un autre contrôleur, vous devrez copier/coller tout le code responsable de la création du formulaire. Pas très flexible vous en conviendrez ! Pour cela, nous allons donc créer des constructeurs de formulaire. Il y aura par conséquent autant de constructeurs que de formulaires différents.

Conception des classes

La classe FormBuilder

La classe FormBuilder a un rôle bien précis : elle est chargée de construire un formulaire. Ainsi, il n'y a qu'une seule fonctionnalité à implémenter... celle de construire le formulaire ! Mais, pour ce faire, encore faudrait-il avoir un objet Form. Nous le créerons donc dans le constructeur et nous l'assignerons à l'attribut correspondant.

Nous avons donc :

  • Une méthode abstraite chargée de construire le formulaire.

  • Un attribut stockant le formulaire.

  • L'accesseur et le mutateur correspondant.

Voici notre classe schématisée (voir la figure suivante).

Modélisation de la classe FormBuilder
Modélisation de la classe FormBuilder
Les classes filles

Un constructeur de base c'est bien beau, mais sans classe fille, difficile de construire grand-chose. Je vous propose donc de créer deux constructeurs de formulaire : un constructeur de formulaire de commentaires, et un constructeur de formulaire de news. Nous aurons donc notre classe FormBuilder dont hériteront deux classes, CommentFormBuilder et NewsFormBuilder (voir la figure suivante).

Modélisation des classes CommentFormBuilder et NewsFormBuilder
Modélisation des classes CommentFormBuilder et NewsFormBuilder

Développement des classes

La classe FormBuilder

Cette classe est assez simple à créer, j'espère que vous y êtes parvenus !

<?php
namespace OCFram;

abstract class FormBuilder
{
  protected $form;
  
  public function __construct(Entity $entity)
  {
    $this->setForm(new Form($entity));
  }
  
  abstract public function build();
  
  public function setForm(Form $form)
  {
    $this->form = $form;
  }
  
  public function form()
  {
    return $this->form;
  }
}
Les classes filles

Les classes filles sont simples à créer. En effet, il n'y a que la méthode build() à implémenter, en ayant pour simple contenu d'appeler successivement les méthodes add() sur notre formulaire. Pour l'emplacement des fichiers stockant les classes, je vous propose de les placer dans le dossier /lib/vendors/FormBuilder.

<?php
namespace FormBuilder;

use \OCFram\FormBuilder;
use \OCFram\StringField;
use \OCFram\TextField;
use \OCFram\MaxLengthValidator;
use \OCFram\NotNullValidator;

class CommentFormBuilder extends FormBuilder
{
  public function build()
  {
    $this->form->add(new StringField([
        'label' => 'Auteur',
        'name' => 'auteur',
        'maxLength' => 50,
        'validators' => [
          new MaxLengthValidator('L\'auteur spécifié est trop long (50 caractères maximum)', 50),
          new NotNullValidator('Merci de spécifier l\'auteur du commentaire'),
        ],
       ]))
       ->add(new TextField([
        'label' => 'Contenu',
        'name' => 'contenu',
        'rows' => 7,
        'cols' => 50,
        'validators' => [
          new NotNullValidator('Merci de spécifier votre commentaire'),
        ],
       ]));
  }
}
<?php
namespace FormBuilder;

use \OCFram\FormBuilder;
use \OCFram\StringField;
use \OCFram\TextField;
use \OCFram\MaxLengthValidator;
use \OCFram\NotNullValidator;

class NewsFormBuilder extends FormBuilder
{
  public function build()
  {
    $this->form->add(new StringField([
        'label' => 'Auteur',
        'name' => 'auteur',
        'maxLength' => 20,
        'validators' => [
          new MaxLengthValidator('L\'auteur spécifié est trop long (20 caractères maximum)', 20),
          new NotNullValidator('Merci de spécifier l\'auteur de la news'),
        ],
       ]))
       ->add(new StringField([
        'label' => 'Titre',
        'name' => 'titre',
        'maxLength' => 100,
        'validators' => [
          new MaxLengthValidator('Le titre spécifié est trop long (100 caractères maximum)', 100),
          new NotNullValidator('Merci de spécifier le titre de la news'),
        ],
       ]))
       ->add(new TextField([
        'label' => 'Contenu',
        'name' => 'contenu',
        'rows' => 8,
        'cols' => 60,
        'validators' => [
          new NotNullValidator('Merci de spécifier le contenu de la news'),
        ],
       ]));
  }
}

Ajout de l'autoload

Nous venons à l'instant de créer un nouveau vendor. Afin de pouvoir charger automatiquement les classes qui le composent, nous devons modifier notre bootstrap (situé dans /Web/bootstrap.php).

<?php
const DEFAULT_APP = 'Frontend';

// Si l'application n'est pas valide, on va charger l'application par défaut qui se chargera de générer une erreur 404
if (!isset($_GET['app']) || !file_exists(__DIR__.'/../App/'.$_GET['app'])) $_GET['app'] = DEFAULT_APP;

// On commence par inclure la classe nous permettant d'enregistrer nos autoload
require __DIR__.'/../lib/OCFram/SplClassLoader.php';

// On va ensuite enregistrer les autoloads correspondant à chaque vendor (OCFram, App, Model, etc.)
$OCFramLoader = new SplClassLoader('OCFram', __DIR__.'/../lib');
$OCFramLoader->register();

$appLoader = new SplClassLoader('App', __DIR__.'/..');
$appLoader->register();

$modelLoader = new SplClassLoader('Model', __DIR__.'/../lib/vendors');
$modelLoader->register();

$entityLoader = new SplClassLoader('Entity', __DIR__.'/../lib/vendors');
$entityLoader->register();

$formBuilderLoader = new SplClassLoader('FormBuilder', __DIR__.'/../lib/vendors');
$formBuilderLoader->register();


// Il ne nous suffit plus qu'à déduire le nom de la classe et de l'instancier
$appClass = 'App\\'.$_GET['app'].'\\'.$_GET['app'].'Application';

$app = new $appClass;
$app->run();

Modification des contrôleurs

L'ajout de commentaire (frontend)

Effectuons des premières modifications, en commençant par le formulaire d'ajout de commentaire dans le frontend. En utilisant nos classes, voici les instructions que nous devons exécuter :

  • Si la requête est de type POST (formulaire soumis), il faut créer un nouveau commentaire en le remplissant avec les données envoyées, sinon on crée un nouveau commentaire.

  • On instancie notre constructeur de formulaire en lui passant le commentaire en argument.

  • On invoque la méthode de construction du formulaire.

  • Si le formulaire est valide, on enregistre le commentaire en BDD.

  • On passe le formulaire généré à la vue.

Voilà ce que ça donne :

<?php
namespace App\Frontend\Modules\News;

use \OCFram\BackController;
use \OCFram\HTTPRequest;
use \Entity\Comment;
use \FormBuilder\CommentFormBuilder;
use \OCFram\FormHandler;

class NewsController extends BackController
{
  // ...
  
  public function executeInsertComment(HTTPRequest $request)
  {
    // Si le formulaire a été envoyé.
    if ($request->method() == 'POST')
    {
      $comment = new Comment([
        'news' => $request->getData('news'),
        'auteur' => $request->postData('auteur'),
        'contenu' => $request->postData('contenu')
      ]);
    }
    else
    {
      $comment = new Comment;
    }

    $formBuilder = new CommentFormBuilder($comment);
    $formBuilder->build();

    $form = $formBuilder->form();

    if ($request->method() == 'POST' && $form->isValid())
    {
      $this->managers->getManagerOf('Comments')->save($comment);
      $this->app->user()->setFlash('Le commentaire a bien été ajouté, merci !');
      $this->app->httpResponse()->redirect('news-'.$request->getData('news').'.html');
    }

    $this->page->addVar('comment', $comment);
    $this->page->addVar('form', $form->createView());
    $this->page->addVar('title', 'Ajout d\'un commentaire');
  }
  
  // ...
}

La vue correspondante, insertComment.php, ne change pas par rapport à celle que l'on a créée au début de ce chapitre.

La modification de commentaire, l'ajout et la modification de news (backend)

Normalement, vous devriez être capables, grâce à l'exemple précédent, de parvenir à créer ces trois autres formulaires. Voici le nouveau contrôleur :

<?php
namespace App\Backend\Modules\News;

use \OCFram\BackController;
use \OCFram\HTTPRequest;
use \Entity\Comment;
use \Entity\News;
use \FormBuilder\CommentFormBuilder;
use \FormBuilder\NewsFormBuilder;
use \OCFram\FormHandler;

class NewsController extends BackController
{
  // ...
  
  public function executeInsert(HTTPRequest $request)
  {
    $this->processForm($request);

    $this->page->addVar('title', 'Ajout d\'une news');
  }

  public function executeUpdate(HTTPRequest $request)
  {
    $this->processForm($request);

    $this->page->addVar('title', 'Modification d\'une news');
  }

  public function executeUpdateComment(HTTPRequest $request)
  {
    $this->page->addVar('title', 'Modification d\'un commentaire');

    if ($request->method() == 'POST')
    {
      $comment = new Comment([
        'id' => $request->getData('id'),
        'auteur' => $request->postData('auteur'),
        'contenu' => $request->postData('contenu')
      ]);
    }
    else
    {
      $comment = $this->managers->getManagerOf('Comments')->get($request->getData('id'));
    }

    $formBuilder = new CommentFormBuilder($comment);
    $formBuilder->build();

    $form = $formBuilder->form();

    if ($request->method() == 'POST' && $form->isValid())
    {
      $this->managers->getManagerOf('Comments')->save($comment);
      $this->app->user()->setFlash('Le commentaire a bien été modifié');
      $this->app->httpResponse()->redirect('/admin/');
    }

    $this->page->addVar('form', $form->createView());
  }

  public function processForm(HTTPRequest $request)
  {
    if ($request->method() == 'POST')
    {
      $news = new News([
        'auteur' => $request->postData('auteur'),
        'titre' => $request->postData('titre'),
        'contenu' => $request->postData('contenu')
      ]);

      if ($request->getExists('id'))
      {
        $news->setId($request->getData('id'));
      }
    }
    else
    {
      // L'identifiant de la news est transmis si on veut la modifier
      if ($request->getExists('id'))
      {
        $news = $this->managers->getManagerOf('News')->getUnique($request->getData('id'));
      }
      else
      {
        $news = new News;
      }
    }

    $formBuilder = new NewsFormBuilder($news);
    $formBuilder->build();

    $form = $formBuilder->form();

    if ($request->method() == 'POST' && $form->isValid())
    {
      $this->managers->getManagerOf('News')->save($news);
      $this->app->user()->setFlash($news->isNew() ? 'La news a bien été ajoutée !' : 'La news a bien été modifiée !');
      $this->app->httpResponse()->redirect('/admin/');
    }

    $this->page->addVar('form', $form->createView());
  }
}

Bien sûr, il va falloir modifier les vues s'occupant d'afficher ces formulaires. Vous pouvez aussi supprimer le fichier _form.php qui ne nous est plus d'aucune utilité.

Les vues insert.phpupdate.php et updateComment.php deviennent respectivement :

<h2>Ajouter une news</h2>
<form action="" method="post">
  <p>
    <?= $form ?>
    
    <input type="submit" value="Ajouter" />
  </p>
</form>
<h2>Modifier une news</h2>
<form action="" method="post">
  <p>
    <?= $form ?>
    
    <input type="submit" value="Modifier" />
  </p>
</form>
<h2>Modifier un commentaire</h2>
<form action="" method="post">
  <p>
    <?= $form ?>
    
    <input type="submit" value="Modifier" />
  </p>
</form>

Le gestionnaire de formulaires

Terminons ce chapitre en améliorant encore notre API permettant la création de formulaire. Je voudrais attirer votre attention sur ce petit passage, que l'on retrouve à chaque fois (que ce soit pour ajouter ou modifier une news ou un commentaire) :

<?php
// Nous sommes ici au sein d'un contrôleur
if($request->method() == 'POST' && $form->isValid())
{
  $this->managers->getManagerOf('Manager')->save($comment);
  // ...
}

Bien que réduit, ce bout de code est lui aussi dupliqué. De plus, si l'on veut vraiment externaliser la gestion du formulaire, alors il va falloir le sortir du contrôleur. Ainsi, il ne restera plus d'opération de traitement dans le contrôleur. On séparera donc bien les rôles : le contrôleur n'aura plus à réfléchir sur le formulaire qu'il traite. En effet, il ne fera que demander au constructeur de formulaire de construire le formulaire qu'il veut, puis demandera au gestionnaire de formulaire de s'occuper de lui s'il a été envoyé. On ne se souciera donc plus de l'aspect interne du formulaire !

Conception du gestionnaire de formulaire

Comme nous venons de le voir, le gestionnaire de formulaire est chargé de traiter le formulaire une fois qu'il a été envoyé. Nous avons donc d'ores et déjà une fonctionnalité de notre classe : celle de traiter le formulaire. Concernant les caractéristiques, penchons-nous du côté des éléments dont notre gestionnaire a besoin pour fonctionner. Le premier élément me paraît évident : comment s'occuper d'un formulaire si on n'y a pas accès ? Ce premier élément est donc bien entendu le formulaire dont il est question. Le deuxième élément, lui, est aussi évident : comment enregistrer l'entité correspondant au formulaire si on n'a pas le manager correspondant ? Le deuxième élément est donc le manager correspondant à l'entité. Enfin, le troisième élément est un peu plus subtil, et il faut réfléchir au contenu de la méthode qui va traiter le formulaire. Cette méthode devra savoir si le formulaire a été envoyé pour pouvoir le traiter (si rien n'a été envoyé, il n'y a aucune raison de traiter quoi que ce soit). Ainsi, pour savoir si le formulaire a été envoyé, il faut que notre gestionnaire de formulaire ait accès à la requête du client afin de connaitre le type de la requête (GET ou POST). Ces trois éléments devront être passés au constructeur de notre objet.

Schématiquement, voici notre gestionnaire de formulaire (voir la figure suivante).

Modélisation de la classe FormHandler
Modélisation de la classe FormHandler

Développement du gestionnaire de formulaire

Voici le résultat que vous auriez du obtenir :

<?php
namespace OCFram;

class FormHandler
{
  protected $form;
  protected $manager;
  protected $request;

  public function __construct(Form $form, Manager $manager, HTTPRequest $request)
  {
    $this->setForm($form);
    $this->setManager($manager);
    $this->setRequest($request);
  }

  public function process()
  {
    if($this->request->method() == 'POST' && $this->form->isValid())
    {
      $this->manager->save($this->form->entity());

      return true;
    }

    return false;
  }

  public function setForm(Form $form)
  {
    $this->form = $form;
  }

  public function setManager(Manager $manager)
  {
    $this->manager = $manager;
  }

  public function setRequest(HTTPRequest $request)
  {
    $this->request = $request;
  }
}

Modification des contrôleurs

Ici, la modification est très simple. En effet, nous avons juste décentralisé ce bout de code :

<?php
if($request->method() == 'POST' && $form->isValid())
{
  $this->managers->getManagerOf('Manager')->save($comment);
  // Autres opérations (affichage d'un message informatif, redirection, etc.).
}

Il suffit donc de remplacer ce code par la simple invocation de la méthode process() sur notre objet FormHandler :

<?php
// On récupère le gestionnaire de formulaire (le paramètre de getManagerOf() est bien entendu à remplacer).
$formHandler = new \OCFram\FormHandler($form, $this->managers->getManagerOf('Comments'), $request);

if ($formHandler->process())
{
  // Ici ne résident plus que les opérations à effectuer une fois l'entité du formulaire enregistrée
  // (affichage d'un message informatif, redirection, etc.).
}

Je vous fais confiance pour mettre à jour vos contrôleurs comme il se doit !

Exemple de certificat de réussite
Exemple de certificat de réussite