Tutoriel CMS - Création du Controller Articles

Maintenant que notre model est créé, nous avons besoin d’un controller pour nos articles. Dans CakePHP, les controllers se chargent de gérer les requêtes HTTP et exécutent la logique métier des méthodes des models pour préparer une réponse. Nous placerons le code de ce controller dans un nouveau fichier ArticlesController.php, dans le dossier src/Controller. La base du controller ressemblera à ceci:

<?php
// src/Controller/ArticlesController.php

namespace App\Controller;

class ArticlesController extends AppController
{
}

Ajoutons maintenant une action à notre controller. Les actions sont les méthodes des controllers qui sont connectées aux routes. Par exemple, quand un utilisateur appelle la page www.example.com/articles/index (ce qui est la même chose qu’appeler www.example.com/articles), CakePHP appelera la méthode index de votre controller ArticlesController. Cette méthode devra à son tour faire appel à la couche Model et préparer une réponse en faisant le rendu d’un Template via la couche de View. Le code de notre action index sera le suivant:

<?php
// src/Controller/ArticlesController.php

namespace App\Controller;

class ArticlesController extends AppController
{
    public function index()
    {
        $this->loadComponent('Paginator');
        $articles = $this->Paginator->paginate($this->Articles->find());
        $this->set(compact('articles'));
    }
}

Maintenant que nous avons une méthode index() dans notre ArticlesController, les utilisateurs peuvent maintenant y accéder via www.example.com/articles/index. De la même manière, si nous définissions une méthode foobar(), les utilisateurs pourraient y accéder via www.example.com/articles/foobar. Vous pourriez être tenté de nommer vos controllers et vos actions afin d’obtenir des URL spécifiques. Cependant, ceci est déconseillé. Vous devriez plutôt suivre les Conventions de CakePHP et créer des noms d’actions lisibles ayant un sens pour votre application. Vous pouvez ensuite utiliser le Routing pour obtenir les URLs que vous souhaitez et les connecter aux actions que vous avez créées.

Notre action est très simple. Elle récupère un jeu d’articles paginés dans la base de données en utilisant l’objet model Articles qui est chargé automatiquement via les conventions de nommage. Elle utilise ensuite la méthode set() pour passer les articles récupérés au Template (que nous créerons par la suite). CakePHP va automatiquement rendre le Template une fois que notre action de Controller sera entièrement exécutée.

Création du Template de liste des Articles

Maintenant que notre controller récupère les données depuis le model et qu’il prépare le contexte pour la view, créons le template pour notre action index.

Les templates de view de CakePHP sont des morceaux de PHP qui sont insérés dans le layout de votre application. Bien que nous créerons du HTML ici, les Views peuvent générer du JSON, du CSV ou même des fichiers binaires comme des PDFs.

Un layout est le code de présentation qui englobe la view d’une action. Les fichiers de layout contiennent les éléments communs comme les headers, les footers et les éléments de navigation. Votre application peut très bien avoir plusieurs layouts et vous pouvez passer de l’un à l’autre. Mais pour le moment, utilisons seulement le layout par défaut.

Les fichiers de template de CakePHP sont stockés dans src/Template et dans un dossier au nom du controller auquel ils sont attachés. Nous devons donc créer un dossier nommé “Articles” dans notre cas. Ajouter le code suivant dans ce fichier:

<!-- Fichier : src/Template/Articles/index.ctp -->

<h1>Articles</h1>
<table>
    <tr>
        <th>Titre</th>
        <th>Créé le</th>
    </tr>

    <!-- C'est ici que nous bouclons sur notre objet Query $articles pour afficher les informations de chaque article -->

    <?php foreach ($articles as $article): ?>
    <tr>
        <td>
            <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
        </td>
        <td>
            <?= $article->created->format(DATE_RFC850) ?>
        </td>
    </tr>
    <?php endforeach; ?>
</table>

Dans la précédente section, nous avons assigné la variable “articles” à la view en utilisant la méthode set(). Les variables passées à la view sont disponibles dans les templates de view comme des « variables locales », comme nous l’avons fait ci-dessus.

Vous avez peut-être remarqué que nous utilisons un objet appelé $this->Html. C’est une instance du HtmlHelper. CakePHP inclut plusieurs helpers de view qui rendent les tâches comme créer des liens, des formulaires et des éléments de paginations très faciles. Vous pouvez en apprendre plus à propos des Helpers (Assistants) dans le chapitre de la documentation qui leur est consacré, mais le plus important ici est la méthode link(), qui générera un lien HTML avec le texte fourni (le premier paramètre) et l’URL (le second paramètre).

Quand vous spécifiez des URLs dans CakePHP, il est recommandé d’utiliser des tableaux ou des routes nommées. Ces syntaxes vous permettent de bénéficier du reverse routing fourni par CakePHP.

A partir de maintenant, si vous accédez à http://localhost:8765/articles/index, vous devriez voir votre view qui liste les articles avec leur titre et leur lien.

Création de l’action View

Si vous cliquez sur le lien d’un article dans la page qui liste nos articles, vous tombez sur une page d’erreur vous indiquant que l’action n’a pas été implémentée. Vous pouvez corrigez cette erreur en créant l’action manquante correspondante:

// Ajouter au fichier existant src/Controller/ArticlesController.php

public function view($slug = null)
{
    $article = $this->Articles->findBySlug($slug)->firstOrFail();
    $this->set(compact('article'));
}

Bien que cette action soit simple, nous avons utilisez quelques-unes des fonctionnalités de CakePHP. Nous commençons par utiliser la méthode findBySlug() qui est un finder dynamique. Cette méthode nous permet de créer une requête basique qui permet de récupérer des articles par un « slug » donné. Nous utilisons ensuite la méthode firstOrFail() qui nous permet de récupérer le premier enregistrement ou lancera une NotFoundException si aucun article correspondant n’est trouvé.

Notre action attend un paramètre $slug, mais d’où vient-il ? Si un utilisateur requête /articles/view/first-post, alors la valeur “first-post” sera passé à $slug par la couche de routing et de dispatching de CakePHP. Si nous rechargeons notre navigateur, nous aurons une nouvelle erreur, nous indiquant qu’il manque un template de View.

Création du template View

Créons le template de view pour notre action « view » dans src/Template/Articles/view.ctp.

<!-- Fichier : src/Template/Articles/view.ctp -->

<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><small>Créé le : <?= $article->created->format(DATE_RFC850) ?></small></p>
<p><?= $this->Html->link('Modifier', ['action' => 'edit', $article->slug]) ?></p>

Vous pouvez vérifier que tout fonctionne en essayant de cliquer sur un lien de /articles/index ou en vous rendant manuellement sur une URL de la forme /articles/view/first-post.

Ajouter des articles

Maintenant que les views de lecture ont été créées, il est temps de rendre possible la création d’articles. Commencez par créer une action add() dans le ArticlesController. Notre controller doit maintenant ressembler à ceci:

// src/Controller/ArticlesController.php

namespace App\Controller;

use App\Controller\AppController;

class ArticlesController extends AppController
{

    public function initialize()
    {
        parent::initialize();

        $this->loadComponent('Paginator');
        $this->loadComponent('Flash'); // Inclusion du FlashComponent
    }

    public function index()
    {
        $articles = $this->Paginator->paginate($this->Articles->find());
        $this->set(compact('articles'));
    }

    public function view($slug)
    {
        $article = $this->Articles->findBySlug($slug)->firstOrFail();
        $this->set(compact('article'));
    }

    public function add()
    {
        $article = $this->Articles->newEntity();
        if ($this->request->is('post')) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());

            // L'écriture de 'user_id' en dur est temporaire et
            // sera supprimé quand nous aurons mis en place l'authentification.
            $article->user_id = 1;

            if ($this->Articles->save($article)) {
                $this->Flash->success(__('Votre article a été sauvegardé.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Impossible d\'ajouter votre article.'));
        }
        $this->set('article', $article);
    }
}

Note

Vous devez inclure le FlashComponent dans tous les controllers où vous avez besoin de l’utiliser. Il est souvent conseillé de le charger directement dans le AppController.

Voici ce que l’action add() fait :

  • Si la méthode HTTP de la requête est un POST, cela tentera de sauvegarder les données en utilisant le model Articles.

  • Si pour une quelconque raison la sauvegarde ne se fait pas, cela rendra juste la view. Cela nous donne ainsi une chance de montrer les erreurs de validation ou d’autres messages à l’utilisateur.

Toutes les requêtes de CakePHP incluent un objet request qui est accessible via $this->request. L’objet request contient des informations à propos de la requête qui vient d’être reçue. Nous utilisons la méthode Cake\Http\ServerRequest::is() pour vérifier que la requête possède bien le verbe HTTP POST.

Les données passées en POST sont disponibles dans $this->request->getData(). Vous pouvez utiliser les fonctions pr() ou debug() pour afficher les données si vous voulez voir à quoi elles ressemblent. Pour sauvegarder les données, nous devons tout d’abord « marshaller » les données du POST en une Entity Article. L’Entity sera ensuite persistée en utilisant la classe ArticlesTable que nous avons créée plus tôt.

Après la sauvegarde de notre article, nous utilisons la méthode success() du FlashComponent pour définir le message en Session. La méthode success est fournie via les méthodes magiquesde PHP. Les messages Flash seront affichés sur la page suivante après redirection. Dans notre layout, nous avons <?= $this->Flash->render() ?> qui affichera un message Flash et le supprimera du stockage de Session. Enfin, après la sauvegarde, nous utilisons Cake\Controller\Controller::redirect pour renvoyer l’utilisateur à la liste des articles. Le paramètre ['action' => 'index'] correspond à l’URL /articles, c’est-à-dire l’action index du ArticlesController. Vous pouvez vous référer à la méthode Cake\Routing\Router::url() dans la documentation API pour voir les formats dans lesquels vous pouvez spécifier une URL.

Création du Template Add

Voici le code de notre template de la view « add » :

<!-- Fichier : src/Template/Articles/add.ctp -->

<h1>Ajouter un article</h1>
<?php
    echo $this->Form->create($article);
    echo $this->Form->control('title');
    echo $this->Form->control('body', ['rows' => '3']);
    echo $this->Form->button(__('Sauvegarder l\'article'));
    echo $this->Form->end();
?>

Nous utilisons le FormHelper pour générer l’ouverture du form HTML. Voici le HTML que $this->Form->create() génère :

<form method="post" action="/articles/add">

Puisque nous appelons create() sans passer d’option URL, le FormHelper va partir du principe que le formulaire doit être soumis sur l’action courante.

La méthode $this->Form->control() est utilisée pour créer un élément de formulaire du même nom. Le premier paramètre indique à CakePHP à quel champ il correspond et le second paramètre vous permet de définir un très grand nombre d’options - dans notre cas, le nombre de lignes (rows) pour le textarea. Il y a un peu d’instrospection et de conventions utilisées ici. La méthode control() affichera des éléments de formulaire différents en fonction du champ du model spécifié et utilisera une inflection automatique pour définir le label associé. Vous pouvez personnaliser le label, les inputs ou tout autre aspect du formulaire en utilisant les options. La méthode $this->Form->end() ferme le formulaire.

Retournons à notre template src/Template/Articles/index.ctp pour ajouter un lien « Ajouter un article ». Avant le <table>, ajoutons la ligne suivante:

<?= $this->Html->link('Ajouter un article', ['action' => 'add']) ?>

Ajout de la génération de slug

Si nous sauvons un article tout de suite, la sauvegarde échouerait car nous ne créons pas l’attribut « slug » et la colonne correspondante est définie comme NOT NULL. Un slug est généralement une version « URL compatible » du titre d’un article. Nous pouvons utiliser le callback beforeSave() de l’ORM pour créer notre slug:

// dans src/Model/Table/ArticlesTable.php

// Ajoutez ce "use" juste sous la déclaration du namespace
// pour importer la classe Text
use Cake\Utility\Text;

// Ajouter la méthode suivante

public function beforeSave($event, $entity, $options)
{
    if ($entity->isNew() && !$entity->slug) {
        $sluggedTitle = Text::slug($entity->title);
        // On ne garde que le nombre de caractère correspondant à la longueur
        // maximum définie dans notre schéma
        $entity->slug = substr($sluggedTitle, 0, 191);
    }
}

Ce code est simple et ne prend pas en compte les potentiels doublons de slug. Mais nous nous occuperons de ceci plus tard.

Ajout de l’action Edit

Notre application peut maintenant sauvegarder des articles, mais nous ne pouvons pas modifier les articles existants. Ajoutez l’action suivante dans votre ArticlesController:

// dans src/Controller/ArticlesController.php

// Ajouter la méthode suivante.

public function edit($slug)
{
    $article = $this->Articles->findBySlug($slug)->firstOrFail();
    if ($this->request->is(['post', 'put'])) {
        $this->Articles->patchEntity($article, $this->request->getData());
        if ($this->Articles->save($article)) {
            $this->Flash->success(__('Votre article a été mis à jour.'));
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('Impossible de mettre à jour l\'article.'));
    }

    $this->set('article', $article);
}

Cette action va d’abord s’assurer que l’utilisateur essaie d’accéder à un enregistrement existant. Si vous n’avez pas passé de paramètre $slug ou que l’article n’existe pas, une NotFoundException sera lancée et le ErrorHandler rendra la page d’erreur appropriée.

Ensuite l’action va vérifier si la requête est une requête POST ou PUT. Si c’est le cas, nous utiliserons alors les données du POST/PUT pour mettre à jour l’entity de l’article en utilisant la méthode patchEntity(). Enfin, nous appelons la méthode save(), nous définissons un message Flash approprié et soit nous redirigeons, soit nous affichons les erreurs de validation en fonction du résultat de l’opération de sauvegarde.

Création du template Edit

Le template edit devra ressembler à ceci :

<!-- Fichier : src/Template/Articles/edit.ctp -->

<h1>Modifier un article</h1>
<?php
    echo $this->Form->create($article);
    echo $this->Form->control('user_id', ['type' => 'hidden']);
    echo $this->Form->control('title');
    echo $this->Form->control('body', ['rows' => '3']);
    echo $this->Form->button(__('Sauvegarder l\'article'));
    echo $this->Form->end();
?>

Ce template affiche le formulaire de modification (avec les valeurs déjà remplies), ainsi que les messages d’erreurs de validation.

Vous pouvez maintenant mettre à jour notre view index avec les liens pour modifier les articles :

<!-- Fichier : src/Template/Articles/index.ctp (liens de modification ajoutés) -->

<h1>Articles</h1>
<p><?= $this->Html->link("Ajouter un article", ['action' => 'add']) ?></p>
<table>
    <tr>
        <th>Titre</th>
        <th>Créé le</th>
        <th>Action</th>
    </tr>

    <!-- C'est ici que nous bouclons sur notre objet Query $articles pour afficher les informations de chaque article -->

<?php foreach ($articles as $article): ?>
    <tr>
        <td>
            <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
        </td>
        <td>
            <?= $article->created->format(DATE_RFC850) ?>
        </td>
        <td>
            <?= $this->Html->link('Modifier', ['action' => 'edit', $article->slug]) ?>
        </td>
    </tr>
<?php endforeach; ?>

</table>

Mise à jour des règles de validation pour les Articles

Jusqu’à maintenant, nos Articles n’avaient aucune validation de données. Occupons-nous de ça en utilisant un validator:

// src/Model/Table/ArticlesTable.php

// Ajouter ce "use" juste sous la déclaration du namespace pour importer
// la classe Validator
use Cake\Validation\Validator;

// Ajouter la méthode suivante.
public function validationDefault(Validator $validator)
{
    $validator
        ->notEmpty('title')
        ->minLength('title', 10)
        ->maxLength('title', 255)

        ->notEmpty('body')
        ->minLength('body', 10);

    return $validator;
}

La méthode validationDefault() indique à CakePHP comment valider les données quand la méthode save() est appelée. Ici, il est spécifié que les champs title et body ne peuvent pas être vides et qu’ils ont aussi des contraintes sur la taille.

Le moteur de validation de CakePHP est à la fois puissant et flexible. Il vous fournit un jeu de règles sur des validations communes comme les adresses emails, les adresses IP, etc. mais aussi la flexibilité d’ajouter vos propres règles de validation. Pour plus d’informations, rendez-vous dans la section Validation de la documentation.

Maintenant que nos règles de validation sont en place, utilisons l’application et essayons d’ajouter un article avec un title ou un body vide pour voir ce qu’il se passe. Puisque nous avons utiliser la méthode Cake\View\Helper\FormHelper::control() du FormHelper pour créer les éléments de formulaire, nos messages d’erreurs de validation seront affichés automatiquement.

Ajout de l’Action de Suppression

Donnons maintenant la possibilité à nos utilisateurs de supprimer des articles. Commencez par créer une action delete() dans ArticlesController:

// src/Controller/ArticlesController.php

public function delete($slug)
{
    $this->request->allowMethod(['post', 'delete']);

    $article = $this->Articles->findBySlug($slug)->firstOrFail();
    if ($this->Articles->delete($article)) {
        $this->Flash->success(__('L\'article {0} a été supprimé.', $article->title));
        return $this->redirect(['action' => 'index']);
    }
}

Ce code va supprimer l’article ayant le slug $slug et utilisera la méthode $this->Flash->success() pour afficher un message de confirmation à l’utilisateur après l’avoir redirigé sur /articles. Si l’utilisateur essaie d’aller supprimer un article avec une requête GET, la méthode allowMethod() lancera une exception. Les exceptions non capturées sont récupérées par le gestionnaire d’exception de CakePHP qui affichera une belle page d’erreur. Il existe plusieurs Exceptions intégrées qui peuvent être utilisées pour remonter les différentes erreurs HTTP que votre application aurait besoin de générer.

Avertissement

Permettre de supprimer des données via des requêtes GET est très dangereux, car il est possible que des crawlers suppriment accidentellement du contenu. C’est pourquoi nous utilisons la méthode allowMethod() dans notre controller.

Puisque nous exécutons seulement de la logique et redirigeons directement sur une autre action, cette action n’a pas de template. Vous devez ensuite mettre à jour votre template index pour ajouter les liens qui permettront de supprimer les articles :

<!-- Fichier : src/Template/Articles/index.ctp (ajout des liens de suppression) -->

<h1>Articles</h1>
<p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
<table>
    <tr>
        <th>Titre</th>
        <th>Créé le</th>
        <th>Action</th>
    </tr>

    <!-- C'est ici que nous bouclons sur notre objet Query $articles pour afficher les informations de chaque article -->

<?php foreach ($articles as $article): ?>
    <tr>
        <td>
            <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
        </td>
        <td>
            <?= $article->created->format(DATE_RFC850) ?>
        </td>
        <td>
            <?= $this->Html->link('Modifier', ['action' => 'edit', $article->slug]) ?>
            <?= $this->Form->postLink(
                'Supprimer',
                ['action' => 'delete', $article->slug],
                ['confirm' => 'Êtes-vous sûr ?'])
            ?>
        </td>
    </tr>
<?php endforeach; ?>

</table>

Utiliser View\Helper\FormHelper::postLink() va créer un lien qui utilisera du JavaScript pour faire une requête POST et supprimer notre article.

Note

Ce code de view utilise également le FormHelper pour afficher à l’utilisateur une boîte de dialogue de confirmation en JavaScript avant la suppression effective de l’article.

Maintenant que nous avons un minimum de gestion sur nos articles, il est temps de créer des actions basiques pour nos tables Tags et Users.