• 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 10 : back-office d'administration

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 pouvoir administrer l'application via un back-office dédié.

Objectifs du back-office

Le back-office d'une application Web désigne "la partie du site internet qui n'est visible que par l'administrateur du site et qui permet de gérer le contenu, les fonctionnalités..." (source).

L'accès au back-office sera réservé aux administrateurs de l'application. Il leur offrira les fonctionnalités suivantes :

  • Affichage des articles, commentaires et utilisateurs de l'application.

  • Modification et suppression d'un article, d'un commentaire ou d'un utilisateur.

  • Ajout d'un nouvel article ou d'un nouvel utilisateur (l'ajout de commentaires étant déjà permis par l'application).

Gestion des rôles par Symfony

Pour pouvoir réserver l'accès au back-office aux administrateurs, il faut pouvoir définir si un utilisateur connecté possède ou non le droit d'administration. Pour cela, nous allons utiliser la notion de rôle mise en œuvre dans Symfony. Ce framework permet d'associer aux utilisateurs un ou plusieurs rôle(s). L'accès aux ressources de l'application est ensuite conditionné à la possession de rôles particuliers. On peut relier les rôles par une hiérarchie, la possesion d'un rôle donnant automatiquement les droits associés à un autre rôle. Pour plus de détails sur les rôles, consultez la documentation Symfony.

Notre application distingue deux types d'utilisateurs :

  • les utilisateurs simples, qui ne peuvent qu'ajouter des commentaires aux articles ;

  • les administrateurs, qui ont en plus l'accès complet au back-office.

Dans l'itération 8, nous avions donné à nos utilisateurs le rôleROLE_USER : c'est le rôle par défaut pour Symfony. Nous définissons donc un second rôleROLE_ADMIN associé aux administrateurs.

Commençons par ajouter un administrateur à l'application en modifiant le fichierdb/content.sql.

/* ... */

/* raw password is '@dm1n' */
insert into t_user values
(3, 'admin', '$2y$13$A8MQM2ZNOi99EW.ML7srhOJsCaybSbexAj/0yXrJs4gQ/2BqMMW2K', 'EDDsl&fBCJB|a5XUtAlnQN8', 'ROLE_ADMIN');


/* ... */

Modifiez le fichierdb/content.sql comme indiqué ci-dessus puis exécutez successivementdb/structure.sql  etdb/content.sql pour mettre à jour votre base de données.

Il faut ensuite mettre à jour la configuration de la sécurité pour soumettre l'accès au back-office (zone /admin) à la possession du rôle ROLE_ADMIN. Voici les modifications à intégrer au fichier app/app.php.

<?php

// ...

$app->register(new Silex\Provider\SecurityServiceProvider(), array(
    'security.firewalls' => array(
        // ...
    ),
    'security.role_hierarchy' => array(
        'ROLE_ADMIN' => array('ROLE_USER'),
    ),
    'security.access_rules' => array(
        array('^/admin', 'ROLE_ADMIN'),
    ),
));

// ...

 

 

Comme indiqué plus haut, nous modifions la configuration du pare-feu pour définir une hiérarchie entreROLE_ADMIN etROLE_USER, puis pour protéger spécifiquement la zone/admin

Enfin, il faut signaler visuellement à l'administrateur connecté qu'il a accès au back-office d'administration. Pour cela, ajoutez le code ci-dessous à la ligne 25 du fichierviews/layout.html.twig.

{% if app.user and is_granted('ROLE_ADMIN') %}
    <li class="{% if adminMenu is defined %}active{% endif %}"><a href="{{ path('admin') }}"><span class="glyphicon glyphicon-cog"></span> Administration</a></li>
{% endif %}

A présent, il nous reste à écrire tout le code d'administration.

Page d'accueil du back-office

L'accueil du back-office (route/admin) doit afficher à l'administrateur l'ensemble des données de l'application. On ajoute pour cela une nouvelle route à la fin du fichierapp/routes.php.

<?php

// ...

// Admin home page
$app->get('/admin', function() use ($app) {
    $articles = $app['dao.article']->findAll();
    $comments = $app['dao.comment']->findAll();
    $users = $app['dao.user']->findAll();
    return $app['twig']->render('admin.html.twig', array(
        'articles' => $articles,
        'comments' => $comments,
        'users' => $users));
})->bind('admin');

Le contrôleur associé génère la vueadmin.html.twig en lui fournissant les listes des articles, des commentaires et des utilisateurs. La méthodefindAll existe déjà dans la classeArticleDAO. Il faut créer les deux autres.

Modifiez le fichiersrc/DAO/CommentDAO.php pour ajouter la méthodefindAll comme indiqué ci-dessous.

<?php

// ... 

class CommentDAO extends DAO 
{
    // ...

    /**
     * Returns a list of all comments, sorted by date (most recent first).
     *
     * @return array A list of all comments.
     */
    public function findAll() {
        $sql = "select * from t_comment order by com_id desc";
        $result = $this->getDb()->fetchAll($sql);

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

    // ...

Modifiez le fichiersrc/DAO/UserDAO.php pour ajouter la méthodefindAll comme indiqué ci-dessous.

<?php

// ...

class UserDAO extends DAO implements UserProviderInterface
{
    /**
     * Returns a list of all users, sorted by role and name.
     *
     * @return array A list of all users.
     */
    public function findAll() {
        $sql = "select * from t_user order by usr_role, usr_name";
        $result = $this->getDb()->fetchAll($sql);

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

Afin de limiter la taille du contenu des articles dans la page d'accueil du back-office, nous allons utiliser la fonctiontruncate fournie par l'extensionText de Twig. Au passage, nous allons intégrer également les composants Symfony nécessaires pour valider des formulaires.

Pour cela, modifiez votre fichiercomposer.json comme indiqué ci-dessous.

"require": {
    ...
    "twig/extensions": "~1.4",
    "symfony/validator": "~2.8|3.0.*"
},
...

Récupérez ces nouveaux composants grâce à la commande habituelle :

composer update

Il faut ensuite modifier le fichier de configuration de l'applicationapp/app.php  pour intégrer la nouvelle extension et les nouveaux composants. Ajoutez les lignes suivantes juste après avoir enregistréTwigServiceProvider (ligne 15).

<?php

// ...

$app['twig'] = $app->extend('twig', function(Twig_Environment $twig, $app) {
    $twig->addExtension(new Twig_Extensions_Extension_Text());
    return $twig;
});
$app->register(new Silex\Provider\ValidatorServiceProvider());

// ...

Ajoutez le contenu suivant à la fin du fichierweb/css/microcms.css afin d'améliorer l'affichage des pages du back-office.‌

.adminTable {
    margin-top: 20px;
    margin-bottom: 20px;
}

Enfin, créez le fichierviews/admin.html.twig en lui donnant le contenu ci-dessous.

{% extends "layout.html.twig" %}
{% set adminMenu = true %}

{% block title %}Administration{% endblock %}

{% block content %}
<h2 class="text-center">{{ block('title') }}</h2>
{% for flashMessage in app.session.flashbag.get('success') %}
<div class="alert alert-success">
    {{ flashMessage }}
</div>
{% endfor %}
<div class="row">
    <div class="col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3">
        <ul class="nav nav-tabs nav-justified">
            <li class="active"><a href="#articles" data-toggle="tab">Articles</a></li>
            <li><a href="#comments" data-toggle="tab">Comments</a></li>
            <li><a href="#users" data-toggle="tab">Users</a></li>
        </ul>
    </div>
</div>
<div class="tab-content">
    <div class="tab-pane fade in active adminTable" id="articles">
        {% if articles %}
        <div class="table-responsive">
            <table class="table table-hover table-condensed">
                <thead>
                    <tr>
                        <th>Title</th>
                        <th>Content</th>
                        <th></th>  <!-- Actions column -->
                    </tr>
                </thead>
                {% for article in articles %}
                <tr>
                    <td><a class="articleTitle" href="{{ path('article', { 'id': article.id }) }}">{{ article.title }}</a></td>
                    <td>{{ article.content | truncate(60) }}</td>
                    <td>
                        <a href="{{ path('admin_article_edit', { 'id': article.id }) }}" class="btn btn-info btn-xs" title="Edit"><span class="glyphicon glyphicon-edit"></span></a>
                        <button type="button" class="btn btn-danger btn-xs" title="Delete" data-toggle="modal" data-target="#articleDialog{{ article.id }}"><span class="glyphicon glyphicon-remove"></span>
                        </button>
                        <div class="modal fade" id="articleDialog{{ article.id }}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
                            <div class="modal-dialog">
                                <div class="modal-content">
                                    <div class="modal-header">
                                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                                        <h4 class="modal-title" id="myModalLabel">Confirmation needed</h4>
                                    </div>
                                    <div class="modal-body">
                                        Do you really want to delete this article ?
                                    </div>
                                    <div class="modal-footer">
                                        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                                        <a href="{{ path('admin_article_delete', { 'id': article.id }) }}" class="btn btn-danger">Confirm</a>
                                    </div>
                                </div><!-- /.modal-content -->
                            </div><!-- /.modal-dialog -->
                        </div><!-- /.modal -->
                    </td>
                </tr>
                {% endfor %}
            </table>
        </div>
        {% else %}
        <div class="alert alert-warning">No articles found.</div>
        {% endif %}
        <a href="{{ path('admin_article_add') }}"><button type="button" class="btn btn-primary"><span class="glyphicon glyphicon-plus"></span> Add article</button></a>
    </div>
    <div class="tab-pane fade adminTable" id="comments">
        <!-- TODO Insérer ici le code de gestion des commentaires -->
        
    </div>
    <div class="tab-pane fade adminTable" id="users">
        <!-- TODO Insérer ici le code de gestion des utilisateurs -->
        
    </div>
</div>
{% endblock %}

Cette vue comporte beaucoup de code Bootstrap et peut vous paraître complexe si vous connaissez peu ce framework. Elle affiche les données de l'application dans trois onglets Articles, Comments et Users (classe Bootstraptab-pane). Grâce au code JavaScript inclus dans Bootstrap, le clic sur un onglet déclenche automatiquement l'affichage du contenu de celui-ci.

À chaque article sont associés deux actions matérialisés par des boutons :

  • la modification ("Edit") ;

  • la suppression ("Delete"). Lors du clic sur ce bouton, une fenêtre modale permet de demander confirmation à l'utilisateur avant de lancer l'opération de suppression.

Les deux commentaires HTML TODO situés à la fin du fichier servent à indiquer où ajouter le code de gestion des commentaires et celui des utilisateurs (voir plus bas).

À présent, ouvrez l'URL http://microcms et tentez de vous connecter en tant qu'administrateur (nom d'utilisateur : "admin", mot de passe : "@dm1n"). Vous devriez obtenir l'affichage d'une nouvelle entrée "Administration" dans le barre de navigation de l'application.

 

Le clic sur cette entrée (URL http://microcms/admin) déclenche pour l'instant une erreur : nous avons indiqué des liens vers des routes qui n'existent pas encore (exemple :{{ path('admin_article_add') }}).

Pour que notre back-office fonctionne, il nous reste à écrire les routes d'administration et les contrôleurs associés.

Gestion des articles

Ajouter un nouvel article nécessite de saisir ses caractéristiques dans un formulaire. En suivant la méthode préconisée par Symfony, on définit ce formulaire dans une classeArticleType créée dans le fichiersrc/Form/Type/ArticleType.php.

<?php

namespace MicroCMS\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            ->add('content', TextareaType::class);
    }

    public function getName()
    {
        return 'article';
    }
}

Les deux champs du formulaire correspondent aux propriétés d'un article. Ce formulaire est affiché dans la vueviews/article_form.html.twig. Créez ce fichier avec le contenu suivant.

 

{% extends 'layout.html.twig' %}
{% set adminMenu = true %}

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

{% block content %}
<h2 class="text-center">{{ block('title') }}</h2>
{% for flashMessage in app.session.flashbag.get('success') %}
<div class="alert alert-success">
    {{ flashMessage }}
</div>
{% endfor %}

<div class="well">
{{ form_start(articleForm, { 'attr': {'class': 'form-horizontal'} }) }}
    <div class="form-group">
        {{ form_label(articleForm.title, null, { 'label_attr':  {
            'class': 'col-sm-4 control-label'
        }}) }}
        <div class="col-sm-6">
            {{ form_errors(articleForm.title) }}
            {{ form_widget(articleForm.title, { 'attr':  {
                'class': 'form-control'
            }}) }}
        </div>
    </div>
    <div class="form-group">
        {{ form_label(articleForm.content, null, { 'label_attr':  {
            'class': 'col-sm-4 control-label'
        }}) }}
        <div class="col-sm-6">
            {{ form_errors(articleForm.content) }}
            {{ form_widget(articleForm.content, { 'attr':  {
                'class': 'form-control',
                'rows': '8'
            }}) }}
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-4 col-sm-3">
            <input type="submit" class="btn btn-primary" value="Save" />
        </div>
    </div>
{{ form_end(articleForm) }}
</div>
{% endblock %}

Cette vue affiche les propriétés d'un article, ainsi qu'un bouton de validation.

À présent, modifiez le fichiersrc/DAO/ArticleDAO.php pour y ajouter les méthodes de sauvegarde et de suppression d'un article, comme indiqué ci-dessous.

<?php

// ...

class ArticleDAO extends DAO
{
    // ...

    /**
     * Saves an article into the database.
     *
     * @param \MicroCMS\Domain\Article $article The article to save
     */
    public function save(Article $article) {
        $articleData = array(
            'art_title' => $article->getTitle(),
            'art_content' => $article->getContent(),
            );

        if ($article->getId()) {
            // The article has already been saved : update it
            $this->getDb()->update('t_article', $articleData, array('art_id' => $article->getId()));
        } else {
            // The article has never been saved : insert it
            $this->getDb()->insert('t_article', $articleData);
            // Get the id of the newly created article and set it on the entity.
            $id = $this->getDb()->lastInsertId();
            $article->setId($id);
        }
    }

    /**
     * Removes an article from the database.
     *
     * @param integer $id The article id.
     */
    public function delete($id) {
        // Delete the article
        $this->getDb()->delete('t_article', array('art_id' => $id));
    }

    // ...
}

 

 

 

La suppression d’un article entraînant celle de tous ses commentaires, modifiez également le fichiersrc/DAO/CommentDAO.php pour ajouter la méthode suivante.

<?php

// ...

class CommentDAO extends DAO 
{
    // ...

    /**
     * Removes all comments for an article
     *
     * @param $articleId The id of the article
     */
    public function deleteAllByArticle($articleId) {
        $this->getDb()->delete('t_comment', array('art_id' => $articleId));
    }
    
    // ...
}

Il ne reste plus qu'à ajouter dans le fichierapp/routes.php les routes permettant la création, la modification et la suppresion d'un article. Ajoutez les directivesuse et les contrôleurs ci-dessous à ce fichier.

<?php

use Symfony\Component\HttpFoundation\Request;
use MicroCMS\Domain\Comment;
use MicroCMS\Domain\Article;
use MicroCMS\Form\Type\CommentType;
use MicroCMS\Form\Type\ArticleType;

// ...

// Add a new article
$app->match('/admin/article/add', function(Request $request) use ($app) {
    $article = new Article();
    $articleForm = $app['form.factory']->create(ArticleType::class, $article);
    $articleForm->handleRequest($request);
    if ($articleForm->isSubmitted() && $articleForm->isValid()) {
        $app['dao.article']->save($article);
        $app['session']->getFlashBag()->add('success', 'The article was successfully created.');
    }
    return $app['twig']->render('article_form.html.twig', array(
        'title' => 'New article',
        'articleForm' => $articleForm->createView()));
})->bind('admin_article_add');

// Edit an existing article
$app->match('/admin/article/{id}/edit', function($id, Request $request) use ($app) {
    $article = $app['dao.article']->find($id);
    $articleForm = $app['form.factory']->create(ArticleType::class, $article);
    $articleForm->handleRequest($request);
    if ($articleForm->isSubmitted() && $articleForm->isValid()) {
        $app['dao.article']->save($article);
        $app['session']->getFlashBag()->add('success', 'The article was successfully updated.');
    }
    return $app['twig']->render('article_form.html.twig', array(
        'title' => 'Edit article',
        'articleForm' => $articleForm->createView()));
})->bind('admin_article_edit');

// Remove an article
$app->get('/admin/article/{id}/delete', function($id, Request $request) use ($app) {
    // Delete all associated comments
    $app['dao.comment']->deleteAllByArticle($id);
    // Delete the article
    $app['dao.article']->delete($id);
    $app['session']->getFlashBag()->add('success', 'The article was successfully removed.');
    // Redirect to admin home page
    return $app->redirect($app['url_generator']->generate('admin'));
})->bind('admin_article_delete');

 

Les contrôleurs de création et de modification sont similaires. L'un crée un nouvel article, alors que l'autre le récupère dans la base de données à partir de l'identifiant passé en paramètre dans l'URL. Tous deux utilisent le formulaireArticleType et la vuearticle_form.html.twig définis précédemment.

Le contrôleur de suppression détruit l'article passé en paramètre de l'URL après avoir supprimé les commentaires associés. Il redirige ensuite le client vers la page d'accueil du back-office en utilisant le service de génération d'URL.

Il est temps de tester nos modifications. Connectez-vous à l'application en tant qu'administrateur puis accédez au back-office d'administration. Vous obtenez la page d'accueil ci-dessous.

Essayez d'ajouter un nouvel article en cliquant sur le bouton "Add article". Sauvegardez le nouvel article en cliquant sur "Save".

Vous pouvez ensuite le modifier...

Et enfin le supprimer.

Gestion des commentaires

La gestion des commentaires suit le même modèle que celles des articles. Le formulaireCommentType existe déjà : nous l'avions créé lors de l'itération 9, pour ajouter un commentaire à un article. Ce formulaire sera affiché par la vue existanteviews/article.html.twig et par la nouvelle vueviews/comment_form.html.twig : on constate ici l'intérêt d'isoler la définition des formulaires dans des classes dédiées.

Créez le fichierviews/comment_form.html.twig et donnez-lui le contenu suivant.

 

{% extends 'layout.html.twig' %}
{% set adminMenu = true %}

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

{% block content %}
<h2 class="text-center">{{ block('title') }}</h2>
{% for flashMessage in app.session.flashbag.get('success') %}
<div class="alert alert-success">
    {{ flashMessage }}
</div>
{% endfor %}

<div class="well">
{{ form_start(commentForm, { 'attr': {'class': 'form-horizontal'} }) }}
    <div class="form-group">
        {{ form_label(commentForm.content, null, { 'label_attr':  {
            'class': 'col-sm-4 control-label'
        }}) }}
        <div class="col-sm-6">
            {{ form_errors(commentForm.content) }}
            {{ form_widget(commentForm.content, { 'attr':  {
                'class': 'form-control',
                'rows': '4'                
            }}) }}
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-4 col-sm-3">
            <input type="submit" class="btn btn-primary" value="Save" />
        </div>
    </div>
{{ form_end(commentForm) }}
</div>
{% endblock %}

Modifiez la vue principale d'administrationviews/admin.html.twig pour ajouter le code ci-dessous, qui affiche les commentaires et les actions dédiées, à l'emplacement du premier commentaire TODO (que vous pouvez maintenant supprimer).

{% if comments %}
        <div class="table-responsive">
            <table class="table table-hover table-condensed">
                <thead>
                    <tr>
                        <th>Article</th>
                        <th>Author</th>
                        <th>Content</th>
                        <th></th>  <!-- Actions column -->
                    </tr>
                </thead>
                {% for comment in comments %}
                <tr>
                    <td><a class="articleTitle" href="{{ path('article', { 'id': comment.article.id }) }}">{{ comment.article.title }}</a></td>
                    <td>{{ comment.author.username }}</td>
                    <td>{{ comment.content | truncate(60) }}</td>
                    <td>
                        <a href="{{ path('admin_comment_edit', { 'id': comment.id }) }}" class="btn btn-info btn-xs" title="Edit"><span class="glyphicon glyphicon-edit"></span></a>
                        <button type="button" class="btn btn-danger btn-xs" title="Delete" data-toggle="modal" data-target="#commentDialog{{ comment.id }}"><span class="glyphicon glyphicon-remove"></span>
                        </button>
                        <div class="modal fade" id="commentDialog{{ comment.id }}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
                            <div class="modal-dialog">
                                <div class="modal-content">
                                    <div class="modal-header">
                                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                                        <h4 class="modal-title" id="myModalLabel">Confirmation needed</h4>
                                    </div>
                                    <div class="modal-body">
                                        Do you really want to delete this comment ?
                                    </div>
                                    <div class="modal-footer">
                                        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                                        <a href="{{ path('admin_comment_delete', { 'id': comment.id }) }}" class="btn btn-danger">Confirm</a>
                                    </div>
                                </div><!-- /.modal-content -->
                            </div><!-- /.modal-dialog -->
                        </div><!-- /.modal -->
                    </td>
                </tr>
                {% endfor %}
            </table>
        </div>
        {% else %}
        <div class="alert alert-warning">No comments found.</div>
        {% endif %}

La méthode de sauvegarde d'un commentaire existe déjà dans la classeCommentDAO. Modifiez le fichiersrc/DAO/CommentDAO.php pour y ajouter la méthode de recherche d'un commentaire et la méthode de suppression d'un commentaire définies ci-dessous.

<?php

// ...

class CommentDAO extends DAO 
{
    // ...

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

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

    // ...

    /**
     * Removes a comment from the database.
     *
     * @param @param integer $id The comment id
     */
    public function delete($id) {
        // Delete the comment
        $this->getDb()->delete('t_comment', array('com_id' => $id));
    }

    // ...
}

Enfin, ajoutez les contrôleurs ci-dessous à la fin du fichierapp/routes.php.

<?php

// ...

// Edit an existing comment
$app->match('/admin/comment/{id}/edit', function($id, Request $request) use ($app) {
    $comment = $app['dao.comment']->find($id);
    $commentForm = $app['form.factory']->create(CommentType::class, $comment);
    $commentForm->handleRequest($request);
    if ($commentForm->isSubmitted() && $commentForm->isValid()) {
        $app['dao.comment']->save($comment);
        $app['session']->getFlashBag()->add('success', 'The comment was successfully updated.');
    }
    return $app['twig']->render('comment_form.html.twig', array(
        'title' => 'Edit comment',
        'commentForm' => $commentForm->createView()));
})->bind('admin_comment_edit');

// Remove a comment
$app->get('/admin/comment/{id}/delete', function($id, Request $request) use ($app) {
    $app['dao.comment']->delete($id);
    $app['session']->getFlashBag()->add('success', 'The comment was successfully removed.');
    // Redirect to admin home page
    return $app->redirect($app['url_generator']->generate('admin'));
})->bind('admin_comment_delete');

 

Vous pouvez à présent tester, en tant qu'administrateur, la modification d'un commentaire.

Vous pouvez également supprimer un commentaire existant.

Gestion des utilisateurs

Il ne nous reste plus qu'à implémenter la gestion des utilisateurs pour finaliser le back-office. Commençons par définir le formulaire associé à un utilisateur dans le fichiersrc/Form/Type/UserType.php.

<?php

namespace MicroCMS\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username', TextType::class)
            ->add('password', RepeatedType::class, array(
                'type'            => PasswordType::class,
                 'invalid_message' => 'The password fields must match.',
                 'options'         => array('required' => true),
                 'first_options'   => array('label' => 'Password'),
                 'second_options'  => array('label' => 'Repeat password'),
             ))
            ->add('role', ChoiceType::class, array(
                'choices' => array('Admin' => 'ROLE_ADMIN', 'User' => 'ROLE_USER')
            ));
    }

    public function getName()
    {
        return 'user';
    }
}

Dans la plupart des applications web, les mots de passe des utilisateurs sont saisis deux fois pour éviter les erreurs de saisie. Symfony supporte cette fonctionnalité : le type de champrepeated permet de faire saisir un champ deux fois et effectue automatiquement la comparaison des deux valeurs. Le message défini parinvalid_message apparaîtra si les deux valeurs sont différentes. Nous définissons le champrole sous la forme d'une liste (choice). Les deux valeurs possibles sontROLE_ADMIN etROLE_USER. Nous observerons plus loin comment Symfony affiche et valide ce type de champ.

Ce formulaire est utilisé par la vueviews/user_form.html.twig que voici.

{% extends 'layout.html.twig' %}
{% set adminMenu = true %}

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

{% block content %}
<h2 class="text-center">{{ block('title') }}</h2>
{% for flashMessage in app.session.flashbag.get('success') %}
<div class="alert alert-success">
    {{ flashMessage }}
</div>
{% endfor %}
{% if form_errors(userForm.password.first) %}
<div class="alert alert-danger">
    {{ form_errors(userForm.password.first) }}
</div>
{% endif %}

<div class="well">
{{ form_start(userForm, { 'attr': {'class': 'form-horizontal'} }) }}
    <div class="form-group">
        {{ form_label(userForm.username, null, { 'label_attr':  {
            'class': 'col-sm-5 control-label'
        }}) }}
        <div class="col-sm-4">
            {{ form_errors(userForm.username) }}
            {{ form_widget(userForm.username, { 'attr':  {
                'class': 'form-control'
            }}) }}
        </div>
    </div>
    <div class="form-group">
        {{ form_label(userForm.password.first, null, { 'label_attr':  {
            'class': 'col-sm-5 control-label'
        }}) }}
        <div class="col-sm-4">
            {{ form_widget(userForm.password.first, { 'attr':  {
                'class': 'form-control'
            }}) }}
        </div>
    </div>
    <div class="form-group">
        {{ form_label(userForm.password.second, null, { 'label_attr':  {
            'class': 'col-sm-5 control-label'
        }}) }}
        <div class="col-sm-4">
            {{ form_widget(userForm.password.second, { 'attr':  {
                'class': 'form-control'
            }}) }}
        </div>
    </div>
    <div class="form-group">
        {{ form_label(userForm.role, null, { 'label_attr':  {
            'class': 'col-sm-5 control-label'
        }}) }}
        <div class="col-sm-2">
            {{ form_errors(userForm.role) }}
            {{ form_widget(userForm.role, { 'attr':  {
                'class': 'form-control'
            }}) }}
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-5 col-sm-3">
            <input type="submit" class="btn btn-primary" value="Save" />
        </div>
    </div>
{{ form_end(userForm) }}
</div>
{% endblock %}

 

 

Cette vue affiche les champs du formulaireUserType. Le mot de passe est affiché sous la forme de deux champs :userForm.password.first etuserForm.password.second.

Modifiez la vue principale d'administrationviews/admin.html.twig pour ajouter le code ci-dessous, qui affiche les utilisateur et les actions dédiées, à l'emplacement du second commentaire TODO (que vous pouvez maintenant supprimer).

{% if users %}
        <div class="table-responsive">
            <table class="table table-hover table-condensed">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Role</th>
                        <th></th>  <!-- Actions column -->
                    </tr>
                </thead>
                {% for user in users %}
                <tr>
                    <td>{{ user.username }}</a></td>
                    <td>
                        {% if user.role == 'ROLE_ADMIN' %}
                            Admin
                        {% else %}
                            User
                        {% endif %}
                    </td>
                    <td>
                        <a href="{{ path('admin_user_edit', { 'id': user.id }) }}" class="btn btn-info btn-xs" title="Edit"><span class="glyphicon glyphicon-edit"></span></a>
                        <button type="button" class="btn btn-danger btn-xs" title="Delete" data-toggle="modal" data-target="#userDialog{{ user.id }}"><span class="glyphicon glyphicon-remove"></span>
                        </button>
                        <div class="modal fade" id="userDialog{{ user.id }}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
                            <div class="modal-dialog">
                                <div class="modal-content">
                                    <div class="modal-header">
                                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                                        <h4 class="modal-title" id="myModalLabel">Confirmation needed</h4>
                                    </div>
                                    <div class="modal-body">
                                        Do you really want to delete this user ?
                                    </div>
                                    <div class="modal-footer">
                                        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                                        <a href="{{ path('admin_user_delete', { 'id': user.id }) }}" class="btn btn-danger">Confirm</a>
                                    </div>
                                </div><!-- /.modal-content -->
                            </div><!-- /.modal-dialog -->
                        </div><!-- /.modal -->
                    </td>
                </tr>
                {% endfor %}
            </table>
        </div>
        {% else %}
        <div class="alert alert-warning">No users found.</div>
        {% endif %}
        <a href="{{ path('admin_user_add') }}"><button type="button" class="btn btn-primary"><span class="glyphicon glyphicon-plus"></span> Add user</button></a>

Comme pour les articles, il faut ajouter les méthodes de modification et de suppression d'un utilisateur dans la classeUserDAO (fichiersrc/DAO/UserDAO.php).

<?php

// ...

class UserDAO extends DAO implements UserProviderInterface
{
    // ...

    /**
     * Saves a user into the database.
     *
     * @param \MicroCMS\Domain\User $user The user to save
     */
    public function save(User $user) {
        $userData = array(
            'usr_name' => $user->getUsername(),
            'usr_salt' => $user->getSalt(),
            'usr_password' => $user->getPassword(),
            'usr_role' => $user->getRole()
            );

        if ($user->getId()) {
            // The user has already been saved : update it
            $this->getDb()->update('t_user', $userData, array('usr_id' => $user->getId()));
        } else {
            // The user has never been saved : insert it
            $this->getDb()->insert('t_user', $userData);
            // Get the id of the newly created user and set it on the entity.
            $id = $this->getDb()->lastInsertId();
            $user->setId($id);
        }
    }

    /**
     * Removes a user from the database.
     *
     * @param @param integer $id The user id.
     */
    public function delete($id) {
        // Delete the user
        $this->getDb()->delete('t_user', array('usr_id' => $id));
    }

    // ...
}

 

Il faut également ajouter dans le fichiersrc/DAO/CommentDAO.php la possibilité de supprimer tous les commentaires associés à un utilisateur.

<?php

// ...

class CommentDAO extends DAO 
{
    // ...
    
    /**
     * Removes all comments for a user
     *
     * @param integer $userId The id of the user
     */
    public function deleteAllByUser($userId) {
        $this->getDb()->delete('t_comment', array('usr_id' => $userId));
    }
    
    // ...
}

Pour terminer, ajoutez les directivesuse et les contrôleurs nécessaires à la fin du fichierapp/routes.php.

<?php

use Symfony\Component\HttpFoundation\Request;
use MicroCMS\Domain\Comment;
use MicroCMS\Domain\Article;
use MicroCMS\Domain\User;
use MicroCMS\Form\Type\CommentType;
use MicroCMS\Form\Type\ArticleType;
use MicroCMS\Form\Type\UserType;

// ...

// Add a user
$app->match('/admin/user/add', function(Request $request) use ($app) {
    $user = new User();
    $userForm = $app['form.factory']->create(UserType::class, $user);
    $userForm->handleRequest($request);
    if ($userForm->isSubmitted() && $userForm->isValid()) {
        // generate a random salt value
        $salt = substr(md5(time()), 0, 23);
        $user->setSalt($salt);
        $plainPassword = $user->getPassword();
        // find the default encoder
        $encoder = $app['security.encoder.bcrypt'];
        // compute the encoded password
        $password = $encoder->encodePassword($plainPassword, $user->getSalt());
        $user->setPassword($password); 
        $app['dao.user']->save($user);
        $app['session']->getFlashBag()->add('success', 'The user was successfully created.');
    }
    return $app['twig']->render('user_form.html.twig', array(
        'title' => 'New user',
        'userForm' => $userForm->createView()));
})->bind('admin_user_add');

// Edit an existing user
$app->match('/admin/user/{id}/edit', function($id, Request $request) use ($app) {
    $user = $app['dao.user']->find($id);
    $userForm = $app['form.factory']->create(UserType::class, $user);
    $userForm->handleRequest($request);
    if ($userForm->isSubmitted() && $userForm->isValid()) {
        $plainPassword = $user->getPassword();
        // find the encoder for the user
        $encoder = $app['security.encoder_factory']->getEncoder($user);
        // compute the encoded password
        $password = $encoder->encodePassword($plainPassword, $user->getSalt());
        $user->setPassword($password); 
        $app['dao.user']->save($user);
        $app['session']->getFlashBag()->add('success', 'The user was successfully updated.');
    }
    return $app['twig']->render('user_form.html.twig', array(
        'title' => 'Edit user',
        'userForm' => $userForm->createView()));
})->bind('admin_user_edit');

// Remove a user
$app->get('/admin/user/{id}/delete', function($id, Request $request) use ($app) {
    // Delete all associated comments
    $app['dao.comment']->deleteAllByUser($id);
    // Delete the user
    $app['dao.user']->delete($id);
    $app['session']->getFlashBag()->add('success', 'The user was successfully removed.');
    // Redirect to admin home page
    return $app->redirect($app['url_generator']->generate('admin'));
})->bind('admin_user_delete');

Les contrôleurs de création et de modification doivent hacher le mot de passe saisi par l'utilisateur avant de le sauvegarder dans la base de données. Ils utilisent pour cela les services$app['security.encoder.bcrypt'] et$app['security.encoder_factory']. Le salage (variable $salt) est généré aléatoirement grâce à la fonction PHP md5.

Le back-office est maintenant terminé. Vous pouvez le tester en vous connectant à l'application en tant qu'administrateur, puis en tentant d'ajouter un nouvel utilisateur. Commencez par saisir deux mots de passe différents : l'application affiche le message d'erreur approprié. Merci Symfony !

On remarque également que le rôle (type de champchoice) est affiché sous la forme d'une liste déroulante. Saisissez ensuite le même mot de passe deux fois : l'utilisateur est cette fois-ci bien créé.

Vous pouvez ensuite modifier ses propriétés...

Et enfin le supprimer.

 

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

Bilan

Cette longue itération nous a permis d'intégrer à l'application des fonctionnalités d'administration. Les utilisateurs disposant du rôle d'administrateur peuvent à présent ajouter, modifier et supprimer des données dans la base. Pour cela, nous avons exploité les possibilités du framework Bootstrap pour obtenir un affichage moderne. Nous avons également découvert certaines fonctionnalités de Symfony en matière de sécurité (gestion des rôles) et de validation des formulaires (comparaison des mots de passe).

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