Mis à jour le 08/01/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 !

Le frontend

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

Nous allons enfin aborder quelque chose de concret en construisant notre première application : le frontend. Cette application est la partie visible par tout le monde. Nous allons construire un module de news avec commentaires, il y a de quoi faire, donc ne perdons pas une seconde de plus !

L'application

Nous allons commencer par créer les fichiers de base dont nous aurons besoin. Vous en connaissez quelques uns déjà : la classe représentant l'application, le layout, et les deux fichiers de configuration. Cependant, il nous en faut deux autres : un fichier qui instanciera notre classe et qui invoquera la méthode run(), et un .htaccess qui redirigera toutes les pages sur ce fichier. Nous verrons cela après avoir créé les quatre fichiers précédemment cités.

La classe FrontendApplication

Commençons par créer notre classe FrontendApplication. Avant de commencer, assurez-vous que vous avez bien créé le dossier /App/Frontend qui contiendra notre application. Créez à l'intérieur le fichier contenant notre classe, à savoir FrontendApplication.php.

Bien. Commencez par écrire le minimum de la classe avec le namespace correspondant (je le rappelle : le namespace est identique au chemin menant vers le fichier contenant la classe) en implémentant les deux méthodes à écrire, à savoir __construct() (qui aura pour simple contenu d'appeler le constructeur parent puis de spécifier le nom de l'application), et run(). Cette dernière méthode écrira cette suite d'instruction :

  • Obtention du contrôleur grâce à la méthode parente getController().

  • Exécution du contrôleur.

  • Assignation de la page créée par le contrôleur à la réponse.

  • Envoi de la réponse.

Votre classe devrait ressembler à ceci :

<?php
namespace App\Frontend;

use \OCFram\Application;

class FrontendApplication extends Application
{
  public function __construct()
  {
    parent::__construct();

    $this->name = 'Frontend';
  }

  public function run()
  {
    $controller = $this->getController();
    $controller->execute();

    $this->httpResponse->setPage($controller->page());
    $this->httpResponse->send();
  }
}

Finalement, le déroulement est assez simple quand on y regarde de plus près.

Le layout

Tout site web qui se respecte se doit d'avoir un design. Nous n'allons pas nous étaler sur ce type de création, ce n'est pas le sujet qui nous occupe. Nous allons donc nous servir d'un pack libre anciennement disponible sur un site de designs : j'ai nommé envision. C'est un design très simple et facilement intégrable, idéal pour ce que nous avons à faire.

Je vous laisse télécharger le pack et découvrir les fichiers qu'il contient. Pour rappel, les fichiers sont à placer dans /Web. Vous devriez ainsi vous retrouver avec deux dossiers dans /Web : /Web/css et /Web/images.

Revenons-en au layout. Celui-ci est assez simple et respecte les contraintes imposées par le design. Pour rappel, ce dernier doit être placé dans le fichier /App/Frontend/Templates/layout.php.

<!DOCTYPE html>
<html>
  <head>
    <title>
      <?= isset($title) ? $title : 'Mon super site' ?>
    </title>
    
    <meta charset="utf-8" />
    
    <link rel="stylesheet" href="/css/Envision.css" type="text/css" />
  </head>
  
  <body>
    <div id="wrap">
      <header>
        <h1><a href="/">Mon super site</a></h1>
        <p>Comment ça, il n'y a presque rien ?</p>
      </header>
      
      <nav>
        <ul>
          <li><a href="/">Accueil</a></li>
          <?php if ($user->isAuthenticated()) { ?>
          <li><a href="/admin/">Admin</a></li>
          <li><a href="/admin/news-insert.html">Ajouter une news</a></li>
          <?php } ?>
        </ul>
      </nav>
      
      <div id="content-wrap">
        <section id="main">
          <?php if ($user->hasFlash()) echo '<p style="text-align: center;">', $user->getFlash(), '</p>'; ?>
          
          <?= $content ?>
        </section>
      </div>
    
      <footer></footer>
    </div>
  </body>
</html>

Qu'est-ce que c'est que ces variables $user ?

La variable $user fait référence à l'instance de User. Elle doit être initialisée dans la méthode getGeneratedPage() de la classe Page (ligne 15) :

<?php
namespace OCFram;

class Page extends ApplicationComponent
{
  // ...

  public function getGeneratedPage()
  {
    if (!file_exists($this->contentFile))
    {
      throw new \RuntimeException('La vue spécifiée n\'existe pas');
    }

    $user = $this->app->user();

    extract($this->vars);

    ob_start();
      require $this->contentFile;
    $content = ob_get_clean();

    ob_start();
      require __DIR__.'/../../App/'.$this->app->name().'/Templates/layout.php';
    return ob_get_clean();
  }

  public function setContentFile($contentFile)
  {
    if (!is_string($contentFile) || empty($contentFile))
    {
      throw new \InvalidArgumentException('La vue spécifiée est invalide');
    }

    $this->contentFile = $contentFile;
  }
}

Les deux fichiers de configuration

Nous allons préparer le terrain en créant les deux fichiers de configuration dont nous avons besoin : les fichiers app.xml et routes.xml, pour l'instant quasi-vierges :

<?xml version="1.0" encoding="utf-8" ?>
<definitions>
</definitions>
<?xml version="1.0" encoding="utf-8" ?>
<routes>
</routes>

L'instanciation de FrontendApplication

Pour lancer notre application, nous avons au préalable besoin d'effectuer quelques opérations. En effet, nous n'avons toujours pas parlé de l'endroit où nous allons enregistrer nos autoload. Nous allons ainsi créer un fichier chargé d'enregistrer ces autoload puis de lancer l'application. En informatique, un tel programme est appelé un bootstrap. Il s'agit de manière plus générale d'un petit programme chargé d'en lancer un plus gros (ce plus gros programme est ici notre application). Je ne vais pas m'étaler sur la façon dont nous allons procéder car nous avons couvert ce sujet au chapitre précédent.

Nous allons donc créer un fichier bootstrap.php situé dans le dossier /Web. Celui-ci devra être capable de connaitre l'application à lancer. Nous allons lui transmettre cette donnée par une variable GET. Bien entendu, il faudra vérifier que cette variable existe et que l'application existe bien avant de procéder aux diverses opérations. Voici le fichier que vous devriez obtenir :

<?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();

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

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

Réécrire toutes les URL

Il faut que toutes les URL pointent vers ce fichier. Pour cela, nous allons nous pencher vers l'URL rewriting. Voici le contenu du .htaccess :

RewriteEngine On

# Si le fichier auquel on tente d'accéder existe (si on veut accéder à une image par exemple).
# Alors on ne réécrit pas l'URL.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ bootstrap.php?app=Frontend [QSA,L]

Le module de news

Nous allons commencer en douceur par un système de news. Pourquoi en douceur ? Car vous avez déjà fait cet exercice lors du précédent TP ! Ainsi, nous allons voir comment l'intégrer au sein de l'application, et vous verrez ainsi plus clair sur la manière dont elle fonctionne. Pour ne perdre personne, nous allons refaire le TP petit à petit pour mieux l'intégrer dans l'application. Ainsi, je vais commencer par vous rappeler ce que j'attends du système de news.

Fonctionnalités

Il doit être possible d'exécuter deux actions différentes sur le module de news :

  • Afficher l'index du module. Cela aura pour effet de dévoiler les cinq dernières news avec le titre et l'extrait du contenu (seuls les 200 premiers caractères seront affichés).

  • Afficher une news spécifique en cliquant sur son titre. L'auteur apparaîtra, ainsi que la date de modification si la news a été modifiée.

Comme pour tout module, commencez par créer les dossiers et fichiers de base, à savoir :

  • Le dossier /App/Frontend/Modules/News qui contiendra notre module.

  • Le fichier /App/Frontend/Modules/News/NewsController.php qui contiendra notre contrôleur.

  • Le dossier /App/Frontend/Modules/News/Views qui contiendra les vues.

  • Le fichier /lib/vendors/Model/NewsManager.php qui contiendra notre manager de base.

  • Le fichier /lib/vendors/Model/NewsManagerPDO.php qui contiendra notre manager utilisant PDO.

  • Le fichier /lib/vendors/Entity/News.php qui contiendra la classe représentant un enregistrement.

Structure de la table news

Une news est constituée d'un titre, d'un auteur et d'un contenu. Aussi, il faut stocker la date d'ajout de la news ainsi que sa date de modification. Cela nous donne une table news ressemblant à ceci :

CREATE TABLE IF NOT EXISTS `news` (
  `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `auteur` varchar(30) NOT NULL,
  `titre` varchar(100) NOT NULL,
  `contenu` text NOT NULL,
  `dateAjout` datetime NOT NULL,
  `dateModif` datetime NOT NULL,
  PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8 ;

Nous pouvons désormais écrire la classe représentant cette entité : News.

<?php
namespace Entity;

use \OCFram\Entity;

class News extends Entity
{
  protected $auteur,
            $titre,
            $contenu,
            $dateAjout,
            $dateModif;

  const AUTEUR_INVALIDE = 1;
  const TITRE_INVALIDE = 2;
  const CONTENU_INVALIDE = 3;

  public function isValid()
  {
    return !(empty($this->auteur) || empty($this->titre) || empty($this->contenu));
  }


  // SETTERS //

  public function setAuteur($auteur)
  {
    if (!is_string($auteur) || empty($auteur))
    {
      $this->erreurs[] = self::AUTEUR_INVALIDE;
    }

    $this->auteur = $auteur;
  }

  public function setTitre($titre)
  {
    if (!is_string($titre) || empty($titre))
    {
      $this->erreurs[] = self::TITRE_INVALIDE;
    }

    $this->titre = $titre;
  }

  public function setContenu($contenu)
  {
    if (!is_string($contenu) || empty($contenu))
    {
      $this->erreurs[] = self::CONTENU_INVALIDE;
    }

    $this->contenu = $contenu;
  }

  public function setDateAjout(\DateTime $dateAjout)
  {
    $this->dateAjout = $dateAjout;
  }

  public function setDateModif(\DateTime $dateModif)
  {
    $this->dateModif = $dateModif;
  }

  // GETTERS //

  public function auteur()
  {
    return $this->auteur;
  }

  public function titre()
  {
    return $this->titre;
  }

  public function contenu()
  {
    return $this->contenu;
  }

  public function dateAjout()
  {
    return $this->dateAjout;
  }

  public function dateModif()
  {
    return $this->dateModif;
  }
}

L'action index

La route

Commençons par implémenter cette action. La première chose à faire est de créer une nouvelle route : quelle URL pointera vers cette action ? Je vous propose simplement que ce soit la racine du site web, donc ce sera l'URL /. Pour créer cette route, il va falloir modifier notre fichier de configuration et y ajouter cette ligne :

<route url="/" module="News" action="index" ></route>

Vient ensuite l'implémentation de l'action dans le contrôleur.

Le contrôleur

Qui dit nouvelle action dit nouvelle méthode, et cette méthode c'est executeIndex(). Cette méthode devra récupérer les cinq dernières news (le nombre cinq devra être stocké dans le fichier de configuration de l'application, à savoir /App/Frontend/Config/app.xml). Il faudra parcourir cette liste de news afin de n'assigner aux news qu'un contenu de 200 caractères au maximum. Ensuite, il faut passer la liste des news à la vue :

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

use \OCFram\BackController;
use \OCFram\HTTPRequest;

class NewsController extends BackController
{
  public function executeIndex(HTTPRequest $request)
  {
    $nombreNews = $this->app->config()->get('nombre_news');
    $nombreCaracteres = $this->app->config()->get('nombre_caracteres');
    
    // On ajoute une définition pour le titre.
    $this->page->addVar('title', 'Liste des '.$nombreNews.' dernières news');
    
    // On récupère le manager des news.
    $manager = $this->managers->getManagerOf('News');
    
    // Cette ligne, vous ne pouviez pas la deviner sachant qu'on n'a pas encore touché au modèle.
    // Contentez-vous donc d'écrire cette instruction, nous implémenterons la méthode ensuite.
    $listeNews = $manager->getList(0, $nombreNews);
    
    foreach ($listeNews as $news)
    {
      if (strlen($news->contenu()) > $nombreCaracteres)
      {
        $debut = substr($news->contenu(), 0, $nombreCaracteres);
        $debut = substr($debut, 0, strrpos($debut, ' ')) . '...';
        
        $news->setContenu($debut);
      }
    }
    
    // On ajoute la variable $listeNews à la vue.
    $this->page->addVar('listeNews', $listeNews);
  }
}

Comme vous le voyez, j'utilise le fichier de configuration pour récupérer le nombre de news à afficher et le nombre maximum de caractères. Voici le fichier de configuration :

<?xml version="1.0" encoding="utf-8" ?>
<definitions>
  <define var="nombre_news" value="5" ></define>
  <define var="nombre_caracteres" value="200" ></define>
</definitions>
La vue

À toute action correspond une vue du même nom. Ici, la vue à créer sera /App/Frontend/Modules/News/Views/index.php. Voici un exemple très simple pour cette vue :

<?php
foreach ($listeNews as $news)
{
?>
  <h2><a href="news-<?= $news['id'] ?>.html"><?= $news['titre'] ?></a></h2>
  <p><?= nl2br($news['contenu']) ?></p>
<?php
}

Notez l'utilisation des news comme des tableaux : cela est possible du fait que l'objet est une instance d'une classe qui implémente ArrayAccess.

Le modèle

Nous allons modifier deux classes faisant partie du modèle, à savoir NewsManager et NewsManagerPDO. Nous allons implémenter à cette dernière classe une méthode : getList(). Sa classe parente doit donc aussi être modifiée pour déclarer cette méthode.

<?php
namespace Model;

use \OCFram\Manager;

abstract class NewsManager extends Manager
{
  /**
   * Méthode retournant une liste de news demandée
   * @param $debut int La première news à sélectionner
   * @param $limite int Le nombre de news à sélectionner
   * @return array La liste des news. Chaque entrée est une instance de News.
   */
  abstract public function getList($debut = -1, $limite = -1);
}
<?php
namespace Model;

use \Entity\News;

class NewsManagerPDO extends NewsManager
{
  public function getList($debut = -1, $limite = -1)
  {
    $sql = 'SELECT id, auteur, titre, contenu, dateAjout, dateModif FROM news ORDER BY id DESC';
    
    if ($debut != -1 || $limite != -1)
    {
      $sql .= ' LIMIT '.(int) $limite.' OFFSET '.(int) $debut;
    }
    
    $requete = $this->dao->query($sql);
    $requete->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, '\Entity\News');
    
    $listeNews = $requete->fetchAll();
    
    foreach ($listeNews as $news)
    {
      $news->setDateAjout(new \DateTime($news->dateAjout()));
      $news->setDateModif(new \DateTime($news->dateModif()));
    }
    
    $requete->closeCursor();
    
    return $listeNews;
  }
}

Vous pouvez faire le test : accédez à la racine de votre site et vous découvrirez... un gros blanc, car aucune news n'est présente en BDD :-° . Vous pouvez en ajouter manuellement via phpMyAdmin par exemple en attendant que nous ayons construit l'espace d'administration.

L'action show

La route

Je vous propose que les URL du type news-id.html pointent vers cette action. Modifiez donc le fichier de configuration des routes pour y ajouter celle-ci :

<?xml version="1.0" encoding="utf-8" ?>
<routes>
  <route url="/" module="News" action="index" ></route>
  <route url="/news-([0-9]+)\.html" module="News" action="show" vars="id"></route>
</routes>
Le contrôleur

Le contrôleur implémentera la méthode executeShow(). Son contenu est simple : le contrôleur ira demander au manager la news correspondant à l'identifiant puis, il passera cette news à la vue, en ayant pris soin de remplacer les sauts de lignes par des balises <br /> dans le contenu de la news.

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

use \OCFram\BackController;
use \OCFram\HTTPRequest;

class NewsController extends BackController
{
  public function executeIndex(HTTPRequest $request)
  {
    $nombreNews = $this->app->config()->get('nombre_news');
    $nombreCaracteres = $this->app->config()->get('nombre_caracteres');
    
    // On ajoute une définition pour le titre.
    $this->page->addVar('title', 'Liste des '.$nombreNews.' dernières news');
    
    // On récupère le manager des news.
    $manager = $this->managers->getManagerOf('News');
    
    $listeNews = $manager->getList(0, $nombreNews);
    
    foreach ($listeNews as $news)
    {
      if (strlen($news->contenu()) > $nombreCaracteres)
      {
        $debut = substr($news->contenu(), 0, $nombreCaracteres);
        $debut = substr($debut, 0, strrpos($debut, ' ')) . '...';
        
        $news->setContenu($debut);
      }
    }
    
    // On ajoute la variable $listeNews à la vue.
    $this->page->addVar('listeNews', $listeNews);
  }
  
  public function executeShow(HTTPRequest $request)
  {
    $news = $this->managers->getManagerOf('News')->getUnique($request->getData('id'));
    
    if (empty($news))
    {
      $this->app->httpResponse()->redirect404();
    }
    
    $this->page->addVar('title', $news->titre());
    $this->page->addVar('news', $news);
  }
}
La vue

La vue se contente de gérer l'affichage de la news. Faites comme bon vous semble, cela n'a pas trop d'importance. Voici la version que je vous propose :

<p>Par <em><?= $news['auteur'] ?></em>, le <?= $news['dateAjout']->format('d/m/Y à H\hi') ?></p>
<h2><?= $news['titre'] ?></h2>
<p><?= nl2br($news['contenu']) ?></p>

<?php if ($news['dateAjout'] != $news['dateModif']) { ?>
  <p style="text-align: right;"><small><em>Modifiée le <?= $news['dateModif']->format('d/m/Y à H\hi') ?></em></small></p>
<?php } ?>
Le modèle

Nous allons là aussi toucher à nos classes NewsManager et NewsManagerPDO en ajoutant la méthode getUnique().

<?php
namespace Model;

use \OCFram\Manager;

abstract class NewsManager extends Manager
{
  /**
   * Méthode retournant une liste de news demandée.
   * @param $debut int La première news à sélectionner
   * @param $limite int Le nombre de news à sélectionner
   * @return array La liste des news. Chaque entrée est une instance de News.
   */
  abstract public function getList($debut = -1, $limite = -1);
  
  /**
   * Méthode retournant une news précise.
   * @param $id int L'identifiant de la news à récupérer
   * @return News La news demandée
   */
  abstract public function getUnique($id);
}
<?php
namespace Model;

use \Entity\News;

class NewsManagerPDO extends NewsManager
{
  public function getList($debut = -1, $limite = -1)
  {
    $sql = 'SELECT id, auteur, titre, contenu, dateAjout, dateModif FROM news ORDER BY id DESC';
    
    if ($debut != -1 || $limite != -1)
    {
      $sql .= ' LIMIT '.(int) $limite.' OFFSET '.(int) $debut;
    }
    
    $requete = $this->dao->query($sql);
    $requete->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, '\Entity\News');
    
    $listeNews = $requete->fetchAll();
    
    foreach ($listeNews as $news)
    {
      $news->setDateAjout(new \DateTime($news->dateAjout()));
      $news->setDateModif(new \DateTime($news->dateModif()));
    }
    
    $requete->closeCursor();
    
    return $listeNews;
  }
  
  public function getUnique($id)
  {
    $requete = $this->dao->prepare('SELECT id, auteur, titre, contenu, dateAjout, dateModif FROM news WHERE id = :id');
    $requete->bindValue(':id', (int) $id, \PDO::PARAM_INT);
    $requete->execute();
    
    $requete->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, '\Entity\News');
    
    if ($news = $requete->fetch())
    {
      $news->setDateAjout(new \DateTime($news->dateAjout()));
      $news->setDateModif(new \DateTime($news->dateModif()));
      
      return $news;
    }
    
    return null;
  }
}

Ajoutons des commentaires

Cahier des charges

Nous allons ajouter une action à notre module de news : l'ajout d'un commentaire. Il ne faudra pas oublier de modifier notre module de news, et plus spécialement l'action show pour laisser apparaître la liste des commentaires ainsi qu'un lien menant au formulaire d'ajout de commentaire.

Avant toute chose, il va falloir créer les modèles nous permettant d'interagir avec la BDD pour accéder aux commentaires :

  • Le fichier /lib/vendors/Model/CommentsManager.php qui contiendra notre manager de base.

  • Le fichier /lib/vendors/Model/CommentsManagerPDO.php qui inclura notre manager utilisant PDO.

  • Le fichier /lib/vendors/Entity/Comment.php qui comportera la classe représentant un enregistrement.

Structure de la table comments

Un commentaire est assigné à une news. Il est constitué d'un auteur et d'un contenu, ainsi que de sa date d'enregistrement. Notre table comments doit donc être constituée de la sorte :

CREATE TABLE IF NOT EXISTS `comments` (
  `id` mediumint(9) NOT NULL AUTO_INCREMENT,
  `news` smallint(6) NOT NULL,
  `auteur` varchar(50) NOT NULL,
  `contenu` text NOT NULL,
  `date` datetime NOT NULL,
  PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8 ;

Puisque nous connaissons la structure d'un commentaire, nous pouvons écrire la classe représentant cette entité, à savoir la classe Comment :

<?php
namespace Entity;

use \OCFram\Entity;

class Comment extends Entity
{
  protected $news,
            $auteur,
            $contenu,
            $date;

  const AUTEUR_INVALIDE = 1;
  const CONTENU_INVALIDE = 2;

  public function isValid()
  {
    return !(empty($this->auteur) || empty($this->contenu));
  }

  public function setNews($news)
  {
    $this->news = (int) $news;
  }

  public function setAuteur($auteur)
  {
    if (!is_string($auteur) || empty($auteur))
    {
      $this->erreurs[] = self::AUTEUR_INVALIDE;
    }

    $this->auteur = $auteur;
  }

  public function setContenu($contenu)
  {
    if (!is_string($contenu) || empty($contenu))
    {
      $this->erreurs[] = self::CONTENU_INVALIDE;
    }

    $this->contenu = $contenu;
  }

  public function setDate(\DateTime $date)
  {
    $this->date = $date;
  }

  public function news()
  {
    return $this->news;
  }

  public function auteur()
  {
    return $this->auteur;
  }

  public function contenu()
  {
    return $this->contenu;
  }

  public function date()
  {
    return $this->date;
  }
}

L'action insertComment

La route

Nous n'allons pas faire dans la fantaisie pour cette action, nous allons prendre une URL basique : commenter-idnews.html. Rajoutez donc cette nouvelle route dans le fichier de configuration des routes :

<?xml version="1.0" encoding="utf-8" ?>
<routes>
  <route url="/" module="News" action="index" ></route>
  <route url="/news-([0-9]+)\.html" module="News" action="show" vars="id"></route>
  <route url="/commenter-([0-9]+)\.html" module="News" action="insertComment" vars="news" ></route>
</routes>
La vue

Dans un premier temps, nous allons nous attarder sur la vue car c'est à l'intérieur de celle-ci que nous allons construire le formulaire. Cela nous permettra donc de savoir quels champs seront à traiter par le contrôleur. Je vous propose un formulaire très simple demandant le pseudo et le contenu à l'utilisateur :

<h2>Ajouter un commentaire</h2>
<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>

L'identifiant de la news est stocké dans l'URL. Puisque nous allons envoyer le formulaire sur cette même page, l'identifiant de la news sera toujours présent dans l'URL et donc accessible via le contrôleur.

Le contrôleur

Notre méthode executeInsertComment() se chargera dans un premier temps de vérifier si le formulaire a été envoyé en vérifiant si la variable POST pseudo existe. Ensuite, elle procédera à la vérification des données et insérera le commentaire en BDD si toutes les données sont valides.

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

use \OCFram\BackController;
use \OCFram\HTTPRequest;
use \Entity\Comment;

class NewsController extends BackController
{
  // ...
  
  public function executeInsertComment(HTTPRequest $request)
  {
    $this->page->addVar('title', 'Ajout d\'un commentaire');
    
    if ($request->postExists('pseudo'))
    {
      $comment = new Comment([
        'news' => $request->getData('news'),
        'auteur' => $request->postData('pseudo'),
        'contenu' => $request->postData('contenu')
      ]);
      
      if ($comment->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');
      }
      else
      {
        $this->page->addVar('erreurs', $comment->erreurs());
      }
      
      $this->page->addVar('comment', $comment);
    }
  }
  
  // ...
}
Le modèle

Nous aurons besoin d'implémenter une méthode dans notre classe CommentsManager : save(). En fait, il s'agit d'un « raccourci », cette méthode appelle elle-même une autre méthode : add() ou modify() selon si le commentaire est déjà présent en BDD. Notre manager peut savoir si l'enregistrement est déjà enregistré ou pas grâce à la méthode isNew().

<?php
namespace Model;

use \OCFram\Manager;
use \Entity\Comment;

abstract class CommentsManager extends Manager
{
  /**
   * Méthode permettant d'ajouter un commentaire
   * @param $comment Le commentaire à ajouter
   * @return void
   */
  abstract protected function add(Comment $comment);
  
  /**
   * Méthode permettant d'enregistrer un commentaire.
   * @param $comment Le commentaire à enregistrer
   * @return void
   */
  public function save(Comment $comment)
  {
    if ($comment->isValid())
    {
      $comment->isNew() ? $this->add($comment) : $this->modify($comment);
    }
    else
    {
      throw new \RuntimeException('Le commentaire doit être validé pour être enregistré');
    }
  }
}
<?php
namespace Model;

use \Entity\Comment;

class CommentsManagerPDO extends CommentsManager
{
  protected function add(Comment $comment)
  {
    $q = $this->dao->prepare('INSERT INTO comments SET news = :news, auteur = :auteur, contenu = :contenu, date = NOW()');
    
    $q->bindValue(':news', $comment->news(), \PDO::PARAM_INT);
    $q->bindValue(':auteur', $comment->auteur());
    $q->bindValue(':contenu', $comment->contenu());
    
    $q->execute();
    
    $comment->setId($this->dao->lastInsertId());
  }
}

L'implémentation de la méthode modify() se fera lors de la construction de l'espace d'administration.

Affichage des commentaires

Modification du contrôleur

Il suffit simplement de passer la liste des commentaires à la vue. Une seule instruction suffit donc :

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

use \OCFram\BackController;
use \OCFram\HTTPRequest;
use \Entity\Comment;

class NewsController extends BackController
{
  // ...
  
  public function executeShow(HTTPRequest $request)
  {
    $news = $this->managers->getManagerOf('News')->getUnique($request->getData('id'));
    
    if (empty($news))
    {
      $this->app->httpResponse()->redirect404();
    }
    
    $this->page->addVar('title', $news->titre());
    $this->page->addVar('news', $news);
    $this->page->addVar('comments', $this->managers->getManagerOf('Comments')->getListOf($news->id()));
  }
}
Modification de la vue affichant une news

La vue devra parcourir la liste des commentaires passés pour les afficher. Les liens pointant vers l'ajout d'un commentaire devront aussi figurer sur la page.

<p>Par <em><?= $news['auteur'] ?></em>, le <?= $news['dateAjout']->format('d/m/Y à H\hi') ?></p>
<h2><?= $news['titre'] ?></h2>
<p><?= nl2br($news['contenu']) ?></p>

<?php if ($news['dateAjout'] != $news['dateModif']) { ?>
  <p style="text-align: right;"><small><em>Modifiée le <?= $news['dateModif']->format('d/m/Y à H\hi') ?></em></small></p>
<?php } ?>

<p><a href="commenter-<?= $news['id'] ?>.html">Ajouter un commentaire</a></p>

<?php
if (empty($comments))
{
?>
<p>Aucun commentaire n'a encore été posté. Soyez le premier à en laisser un !</p>
<?php
}

foreach ($comments as $comment)
{
?>
  <fieldset>
    <legend>
      Posté par <strong><?= htmlspecialchars($comment['auteur']) ?></strong> le <?= $comment['date']->format('d/m/Y à H\hi') ?>
    </legend>
    <p><?= nl2br(htmlspecialchars($comment['contenu'])) ?></p>
  </fieldset>
<?php
}
?>

<p><a href="commenter-<?= $news['id'] ?>.html">Ajouter un commentaire</a></p>
Modification du manager des commentaires

Le manager des commentaires devra implémenter la méthode getListOf() dont a besoin notre contrôleur pour bien fonctionner. Voici la version que je vous propose :

<?php
namespace Model;

use \OCFram\Manager;
use \Entity\Comment;

abstract class CommentsManager extends Manager
{
  /**
   * Méthode permettant d'ajouter un commentaire.
   * @param $comment Le commentaire à ajouter
   * @return void
   */
  abstract protected function add(Comment $comment);
  
  /**
   * Méthode permettant d'enregistrer un commentaire.
   * @param $comment Le commentaire à enregistrer
   * @return void
   */
  public function save(Comment $comment)
  {
    if ($comment->isValid())
    {
      $comment->isNew() ? $this->add($comment) : $this->modify($comment);
    }
    else
    {
      throw new \RuntimeException('Le commentaire doit être validé pour être enregistré');
    }
  }
  
  /**
   * Méthode permettant de récupérer une liste de commentaires.
   * @param $news La news sur laquelle on veut récupérer les commentaires
   * @return array
   */
  abstract public function getListOf($news);
}
<?php
namespace Model;

use \Entity\Comment;

class CommentsManagerPDO extends CommentsManager
{
  protected function add(Comment $comment)
  {
    $q = $this->dao->prepare('INSERT INTO comments SET news = :news, auteur = :auteur, contenu = :contenu, date = NOW()');
    
    $q->bindValue(':news', $comment->news(), \PDO::PARAM_INT);
    $q->bindValue(':auteur', $comment->auteur());
    $q->bindValue(':contenu', $comment->contenu());
    
    $q->execute();
    
    $comment->setId($this->dao->lastInsertId());
  }
  
  public function getListOf($news)
  {
    if (!ctype_digit($news))
    {
      throw new \InvalidArgumentException('L\'identifiant de la news passé doit être un nombre entier valide');
    }
    
    $q = $this->dao->prepare('SELECT id, news, auteur, contenu, date FROM comments WHERE news = :news');
    $q->bindValue(':news', $news, \PDO::PARAM_INT);
    $q->execute();
    
    $q->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, '\Entity\Comment');
    
    $comments = $q->fetchAll();
    
    foreach ($comments as $comment)
    {
      $comment->setDate(new \DateTime($comment->date()));
    }
    
    return $comments;
  }
}
Exemple de certificat de réussite
Exemple de certificat de réussite