• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

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 !

Itération 7 : affichage des détails sur un article

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

Le but de cette itération est de permettre au visiteur de consulter les détails sur un article en cliquant sur son titre.

Mise à jour de la base de données

Notre base de données actuelle doit évoluer pour intégrer le stockage des commentaires sur les articles. Un commentaire se caractérise par son identifiant, son auteur, son contenu ainsi que l'article auquel il se rapporte.

Au cours de cette itération, nous allons gérer l'auteur comme une simple chaîne de caractères. Plus loin, nous découvrirons comment obliger les visiteurs à s'authentifier avant pouvoir de commenter un article. On crée donc une tablet_comment pour stocker les commentaires, en lui ajoutant les champs requis ainsi qu'une clé étrangère vers la tablet_article. On aboutit au contenu ci-dessous pour le fichierdb/structure.sql.

drop table if exists t_comment;
drop table if exists t_article;

create table t_article (
    art_id integer not null primary key auto_increment,
    art_title varchar(100) not null,
    art_content varchar(2000) not null
) engine=innodb character set utf8 collate utf8_unicode_ci;

create table t_comment (
    com_id integer not null primary key auto_increment,
    com_author varchar(100) not null,
    com_content varchar(500) not null,
    art_id integer not null,
    constraint fk_com_art foreign key(art_id) references t_article(art_id)
) engine=innodb character set utf8 collate utf8_unicode_ci;

On enrichit le jeu de données de test de l'application (fichierdb/content.sql) en y ajoutant quelques commentaires.

insert into t_comment values
(1, 'John Doe', 'Great! Keep up the good work.', 1);
insert into t_comment values
(2, 'Ann Yone', "Thank you, I'll try my best.", 1);

Modifiez les scripts SQL comme indiqué ci-dessus, puis exécutez-les afin de mettre votre base de données à jour.

Mise à jour de l'application

Partie Modèle

En respectant les choix de modélisation objet effectués dans l'itération 4, on modélise un commentaire sous la forme d'une classeComment dans l'espace de nomsMicroCMS\Domain. Voici le diagramme UML associé.

Diagramme UML des classes Article et Comment
Diagramme UML des classes Article et Comment

Ce diagramme modélise l'association entre un article et ses commentaires.

Créez le fichier sourceComment.php dans le répertoiresrc/Domain, puis ajoutez-y le code source de la classeComment.

<?php

namespace MicroCMS\Domain;

class Comment 
{
    /**
     * Comment id.
     *
     * @var integer
     */
    private $id;

    /**
     * Comment author.
     *
     * @var string
     */
    private $author;

    /**
     * Comment content.
     *
     * @var integer
     */
    private $content;

    /**
     * Associated article.
     *
     * @var \MicroCMS\Domain\Article
     */
    private $article;

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

    public function setId($id) {
        $this->id = $id;
        return $this;
    }

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

    public function setAuthor($author) {
        $this->author = $author;
        return $this;
    }

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

    public function setContent($content) {
        $this->content = $content;
        return $this;
    }

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

    public function setArticle(Article $article) {
        $this->article = $article;
        return $this;
    }
}

 

L'association avec un article se traduit dans le code source par la présence d'une propriété$article. Il ne s'agit pas d'un simple identifiant de type entier, mais bien d'un objet de la classeArticle.

À présent, il faut créer la classe d'accès aux données pour les commentaires. Cette classe doit permettre de récupérer la liste des commentaires associés à un article donné. Elle est logiquement nomméeCommentDAO et se trouve dans l'espace de nomsMicroCMS\DAO. Voici une première version de cette classe (à ne pas recopier pour l'instant).

<?php

namespace MicroCMS\DAO;

use Doctrine\DBAL\Connection;
use MicroCMS\Domain\Comment;

class CommentDAO
{
    /**
     * Database connection
     *
     * @var \Doctrine\DBAL\Connection
     */
    private $db;

    /**
     * Constructor
     *
     * @param \Doctrine\DBAL\Connection The database connection object
     */
    public function __construct(Connection $db) {
        $this->db = $db;
    }

    /**
     * Return a list of all comments for an article, sorted by date (most recent first).
     *
     * @param integer $articleId The article id.
     *
     * @return array A list of all comments for the article.
     */
    public function findAllByArticle($articleId) {
        // ...
    }

    /**
     * Creates a Comment object based on a DB row.
     *
     * @param array $row The DB row containing Comment data.
     * @return \MicroCMS\Domain\Comment
     */
    private function buildComment(array $row) {
        // ...
    }
}

 

On peut remarquer que la propriété$db ainsi que le constructeur sont exactement les mêmes que dans la classeArticleDAO. Il s'agit d'une duplication de code.

Nous allons profiter de cette itération pour refactoriser le code d'accès aux données afin de supprimer cette duplication de code. Commençons par identifier les besoins communs à toutes les classes d'accès aux données :

  • la connexion à la base (propriété$db) ;

  • la construction d'un objet du domaine à partir d'une ligne de résultat SQL (méthodesbuildArticle  etbuildComment).

Nous factorisons ces besoins communs au sein d'une classe abstraiteDAO dont hériteront toutes nos classes d'accès aux données. Si vous avez besoin de détails sur l'héritage ou les autres concepts de la programmation orientée objet, consultez ce cours. Voici le code source de la classeDAO.

<?php

namespace MicroCMS\DAO;

use Doctrine\DBAL\Connection;

abstract class DAO 
{
    /**
     * Database connection
     *
     * @var \Doctrine\DBAL\Connection
     */
    private $db;

    /**
     * Constructor
     *
     * @param \Doctrine\DBAL\Connection The database connection object
     */
    public function __construct(Connection $db) {
        $this->db = $db;
    }

    /**
     * Grants access to the database connection object
     *
     * @return \Doctrine\DBAL\Connection The database connection object
     */
    protected function getDb() {
        return $this->db;
    }

    /**
     * Builds a domain object from a DB row.
     * Must be overridden by child classes.
     */
    protected abstract function buildDomainObject(array $row);
}

 

La connexion à la base de données est encapsulée sous la forme d'une propriété privée$db et d'un accesseur protégé (donc accessible uniquement aux classes dérivées)getDb. La construction d'un objet du domaine est spécifique à chaque entité métier : on factorise donc uniquement la déclaration de ce service (méthode protégéebuildDomainObject). Chaque classe d'accès aux données devra redéfinir cette méthode pour consstruire un objet du domaine particulier.

Créez le fichier sourcesrc/DAO/DAO.php, puis ajoutez-y le code source de la classeDAO.

L'existence de la classe abstraiteDAO  nous permet de modifier la définition de la classeArticleDAO comme indiqué ci-dessous.

<?php

namespace MicroCMS\DAO;

use MicroCMS\Domain\Article;

class ArticleDAO extends DAO
{
    /**
     * Return a list of all articles, sorted by date (most recent first).
     *
     * @return array A list of all articles.
     */
    public function findAll() {
        $sql = "select * from t_article order by art_id desc";
        $result = $this->getDb()->fetchAll($sql);

        // Convert query result to an array of domain objects
        $articles = array();
        foreach ($result as $row) {
            $articleId = $row['art_id'];
            $articles[$articleId] = $this->buildDomainObject($row);
        }
        return $articles;
    }

    /**
     * Creates an Article object based on a DB row.
     *
     * @param array $row The DB row containing Article data.
     * @return \MicroCMS\Domain\Article
     */
    protected function buildDomainObject(array $row) {
        $article = new Article();
        $article->setId($row['art_id']);
        $article->setTitle($row['art_title']);
        $article->setContent($row['art_content']);
        return $article;
    }
}

 

La classeArticleDAO hérite (mot-cléextends) de la classe abstraiteDAO. L'utilisation directe de la propriété$db est remplacée par l'appel de la méthodegetDb définie dans  DAO. La méthodebuildArticle est remplacée par la méthode redéfiniebuildDomainObject.

Voici maintenant le code source de la classeCommentDAO.

<?php

namespace MicroCMS\DAO;

use MicroCMS\Domain\Comment;

class CommentDAO extends DAO 
{
    /**
     * @var \MicroCMS\DAO\ArticleDAO
     */
    private $articleDAO;

    public function setArticleDAO(ArticleDAO $articleDAO) {
        $this->articleDAO = $articleDAO;
    }

    /**
     * Return a list of all comments for an article, sorted by date (most recent last).
     *
     * @param integer $articleId The article id.
     *
     * @return array A list of all comments for the article.
     */
    public function findAllByArticle($articleId) {
        // The associated article is retrieved only once
        $article = $this->articleDAO->find($articleId);

        // art_id is not selected by the SQL query
        // The article won't be retrieved during domain objet construction
        $sql = "select com_id, com_content, com_author from t_comment where art_id=? order by com_id";
        $result = $this->getDb()->fetchAll($sql, array($articleId));

        // Convert query result to an array of domain objects
        $comments = array();
        foreach ($result as $row) {
            $comId = $row['com_id'];
            $comment = $this->buildDomainObject($row);
            // The associated article is defined for the constructed comment
            $comment->setArticle($article);
            $comments[$comId] = $comment;
        }
        return $comments;
    }

    /**
     * Creates an Comment object based on a DB row.
     *
     * @param array $row The DB row containing Comment data.
     * @return \MicroCMS\Domain\Comment
     */
    protected function buildDomainObject(array $row) {
        $comment = new Comment();
        $comment->setId($row['com_id']);
        $comment->setContent($row['com_content']);
        $comment->setAuthor($row['com_author']);

        if (array_key_exists('art_id', $row)) {
            // Find and set the associated article
            $articleId = $row['art_id'];
            $article = $this->articleDAO->find($articleId);
            $comment->setArticle($article);
        }
        
        return $comment;
    }
}

 

Afin de pouvoir construire complètement une instance de la classeComment, la classeCommentDAO  doit pouvoir récupérer un article à partir de son identifiant et construire une instance de la classeArticle. Plutôt que d'ajouter cela dans le code source deCommentDAO, on ajoute dans la classeArticleDAO une nouvelle méthodefind définissant le service requis. La classeCommentDAO a besoin de ce service pour fonctionner : on dit qu'il existe une dépendance entre la classeCommentDAO et la classeArticleDAO. Cette dépendance se traduit dans le code source deCommentDAO par la présence d'une propriété privée$articleDAO et d'un accesseur en écriture (mutateur)setArticleDAO.

Créez le fichier sourcesrc/DAO/CommentDAO.php et ajoutez-y le code de la classeCommentDAO. Ensuite, ajoutez au fichierArticleDAO.php la méthodefind ci-dessous.

<?php

// ...

class ArticleDAO extends DAO
{
    // ...

    /**
     * Returns an article matching the supplied id.
     *
     * @param integer $id
     *
     * @return \MicroCMS\Domain\Article|throws an exception if no matching article is found
     */
    public function find($id) {
        $sql = "select * from t_article where art_id=?";
        $row = $this->getDb()->fetchAssoc($sql, array($id));

        if ($row)
            return $this->buildDomainObject($row);
        else
            throw new \Exception("No article matching id " . $id);
    }
    
    // ...

 

 

Notre refactorisation de la partie Modèle est maintenant terminée.

Partie Vue

L'évolution de cette partie consiste à ajouter une vue affichant les détails sur un article : titre, contenu et liste des commentaires. Cela se traduit par l'ajout d'un nouveau templatearticle.html.twig  dans le répertoireviews. Voici une première version de ce nouveau template (à ne pas recopier pour l'instant).

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="{{ asset('/lib/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('/css/microcms.css') }}" rel="stylesheet">
    <title>MicroCMS - {{ article.title }}</title>
</head>
<body>
    <div class="container">
        <nav class="navbar navbar-default navbar-fixed-top navbar-inverse" role="navigation">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-target">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="/">MicroCMS</a>
                </div>
                <div class="collapse navbar-collapse" id="navbar-collapse-target">
                </div>
            </div><!-- /.container -->
        </nav>
        <p>
            <h2>{{ article.title }}</h2>
            <p>{{ article.content }}</p>
            <h3>Comments</h3>
            {% for comment in comments %}
                <strong>{{ comment.author }}</strong> said : {{ comment.content }}<br>
            {% else %}
                No comments yet.
            {% endfor %}
        </p>
        <footer class="footer">
            <a href="https://github.com/bpesquet/OC-MicroCMS">MicroCMS</a> is a minimalistic CMS built as a showcase for modern PHP development.
        </footer>
    </div>
</body>
</html>

 

Ce template utilise la structure de contrôle Twigfor associée à unelse si aucune itération de boucle n'est réalisée (absence de tout commentaire).

Le template existantindex.html.twig et le nouveau templatearticle.html.twig partagent de nombreux éléments : partie<head>, barre de navigation, pied de page... Pour éviter la duplication de code, on aimerait centraliser la définition de ces éléments et les inclure dans nos templates.

Le moteur de templates Twig permet de faire encore mieux : il supporte le mécanisme d'héritage de templates. Cela permet de définir un template de base contenant les éléments communs, puis de créer chaque template spécifique par héritage du template commun.

Créez dans le répertoireviews un fichier textelayout.html.twig qui sera notre template commun :

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="{{ asset('/lib/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('/css/microcms.css') }}" rel="stylesheet">
    <title>MicroCMS - {% block title %}{% endblock %}</title>
</head>
<body>
    <div class="container">
        <nav class="navbar navbar-default navbar-fixed-top navbar-inverse" role="navigation">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-target">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="{{ path('home') }}">MicroCMS</a>
                </div>
                <div class="collapse navbar-collapse" id="navbar-collapse-target">
                </div>
            </div><!-- /.container -->
        </nav>
        <div id="content">{% block content %}{% endblock %}</div>
        <footer class="footer">
            <a href="https://github.com/bpesquet/OC-MicroCMS">MicroCMS</a> is a minimalistic CMS built as a showcase for modern PHP development.
        </footer>
    </div>
</body>
</html>

 

Ce template définit deux blocs, appeléstitle et  content. Les templates dérivés redéfinissent ces blocs afin d'ajouter les parties spécifiques de chaque vue. Observez la cible du lien<a class="navbar-brand">. Plutôt que de définir directement ce lien, on utilise une fonction nomméepath() qui permet de générer une URL dans un template (documentation). Pour pouvoir utiliser cette fonction, il faudra que toutes les routes de l'application portent un nom (voir plus bas). Le nom de la route utilisée ici est'home'.

Voici la nouvelle définition du fichierindex.html.twig qui affiche la liste des articles.

{% extends "layout.html.twig" %}

{% block title %}Home{% endblock %}

{% block content %}
{% for article in articles %}
<article>
    <h2><a class="articleTitle" href="{{ path('article', { 'id': article.id }) }}">{{ article.title }}</a></h2>
    <p>{{ article.content }}</p>
</article>
{% endfor %}
{% endblock %}

On constate l'utilisation du mot-cléextends  pour indiquer queindex.html.twig hérite du template communlayout.html.twig. Le reste du template définit les valeurs des blocstitle etcontent. Un lien (balise<a>) ajouté au titre de chaque article renvoie vers l'URL de la route nommée'article', en lui donnant un paramètre nomméid dont la valeur est l'identifiant de l'article (article.id).

Le templatearticle.html.twig suit le même modèle.

{% extends "layout.html.twig" %}

{% block title %}{{ article.title }}{% endblock %}

{% block content %}
<p>
    <h2>{{ article.title }}</h2>
    <p>{{ article.content }}</p>
    <h3>Comments</h3>
    {% for comment in comments %}
        <strong>{{ comment.author }}</strong> said : {{ comment.content }}<br>
    {% else %}
        No comments yet.
    {% endfor %}
</p>
{% endblock %}

 

La dernière modification de la partie Vue consiste à enrichir légèrement la feuille de styleweb/css/microcms.css pour améliorer la présentation des titres d'article cliquables. Ajoutez le code ci-dessous à la fin de ce fichier.

.articleTitle:hover, .articleTitle:focus {
    text-decoration: none;
}

Partie Contrôleur

 La partie Contrôleur de notre application fait le lien entre le Modèle et la Vue. Il faut  mettre à jour le fichierapp/app.php afin d'enregistrer le nouveau service d'accès aux commentaires. Modifiez ce fichier comme indiqué ci-dessous.

<?php

use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;

// Register global error and exception handlers
ErrorHandler::register();
ExceptionHandler::register();

// Register service providers
$app->register(new Silex\Provider\DoctrineServiceProvider());
$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__.'/../views',
));
$app->register(new Silex\Provider\AssetServiceProvider(), array(
    'assets.version' => 'v1'
));

// Register services
$app['dao.article'] = function ($app) {
    return new MicroCMS\DAO\ArticleDAO($app['db']);
};
$app['dao.comment'] = function ($app) {
    $commentDAO = new MicroCMS\DAO\CommentDAO($app['db']);
    $commentDAO->setArticleDAO($app['dao.article']);
    return $commentDAO;
};

C'est dans ce fichier que la dépendance envers la classeArticleDAO est injectée à l'instance deCommentDAO grâce au mutateursetArticleDAO

Ensuite, on fait évoluer le fichierapp/routes.php afin d'ajouter une nouvelle route. Il faut également nommer toutes les routes. Pour cela, on utilise la fonctionbind().

<?php

// Home page
$app->get('/', function () use ($app) {
    $articles = $app['dao.article']->findAll();
    return $app['twig']->render('index.html.twig', array('articles' => $articles));
})->bind('home');

// Article details with comments
$app->get('/article/{id}', function ($id) use ($app) {
    $article = $app['dao.article']->find($id);
    $comments = $app['dao.comment']->findAllByArticle($id);
    return $app['twig']->render('article.html.twig', array('article' => $article, 'comments' => $comments));
})->bind('article');

 

La route d'accueil se nomme'home' et la route des détails sur un article se nomme'article'. Ce sont les noms que nous avons utilisés en paramètres de la fonctionpath() dans nos vues. 

Le contrôleur associé à la nouvelle route génère le templatearticle.html.twig en lui passant en paramètres les données nécessaires, récupérées depuis les services de la partie Modèle : l'article identifié par le paramètre$id présent dans l'URL et la liste des commentaires associés à cet article.

Cette longue itération touche à sa fin et il est temps de tester l'application. Ouvrez l'URL http://microcms pour afficher la liste des articles, puis cliquez sur le titre de l'article du bas. Vous devriez obtenir l'affichage de ses détails.

Si vous cliquez sur le titre d'un article sans aucun commentaire, vous obtenez un résultat de la forme suivante.

Le code source associé à cette itération est disponible sur une branche du dépôt GitHub

Bilan

Au cours de cette itération, nous avons ajouté à l'application une fonctionnalité métier en nous appuyant sur les bases définies précédemment. Nous avons également saisi les occasions de refactoriser l'architecture afin qu'elle s'adapte aux nouveaux besoins.‌

Afin de réaliser de nouvelles fonctionnalités métier, l'itération suivante va s'intéresser à la sécurisation de l'application.

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