Maintenant que nous avons implémenté une gestion basique de la création d’articles,
il est temps de permettre à plusieurs auteurs de travailler sur notre CMS. Dans les
étapes précédentes, nous avons créé nos models, nos views et nos controllers à la
main. Cette fois, nous allons utiliser Console Bake pour créer la base de notre
code. Bake est un outil CLI de génération de
code qui se base sur les conventions de CakePHP pour créer des applications
CRUD basique très rapidement. Nous allons
utiliser bake
pour créer le code relatif à la gestion d’utilisateurs :
cd /path/to/our/app
bin/cake bake model users
bin/cake bake controller users
bin/cake bake template users
Ces 3 commandes vont générer :
Les fichiers de Table, Entity, et Fixture.
Le Controller
Les templates CRUD.
Les fichiers de Tests pour chaque classe générée.
Bake va aussi utiliser les conventions CakePHP pour définir les associations et les validations pour vos models.
Il serait utile et pratique d’avoir, pour notre application CMS, un moyen
de catégoriser notre contenu. Nous allons donc utiliser des tags pour permettre aux
utilisateurs d’ajouter des catégories et des labels à leurs contenus. Une fois de plus,
nous allons utiliser bake
pour générer rapidement un code de base :
# Génère tout le code d'un coup.
bin/cake bake all tags
Une fois que le code de base est généré, créez quelques tags en vous rendant sur la page http://localhost:8765/tags/add.
Maintenant que nous avons une table Tags, nous pouvons créer une association entre
la table Articles et la table Tags. Nous pouvons le faire en ajoutant le code suivant
à la méthode initialize
de ArticlesTable:
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
$this->belongsToMany('Tags'); // Ajoutez cette ligne
}
Cette association fonctionnera avec cette définition qui tient sur une seule ligne car nous avons suivi les conventions de CakePHP lors de la création de nos tables. Pour plus d’informations, rendez-vous dans la section Associations - Lier les Tables Ensemble.
Maintenant que notre application gère les tags, nous devons donner la possibilité
à nos utilisateurs d’ajouter les tags sur les articles. Premièrement, mettez à jour
l’action add
pour qu’elle ressemble à ceci:
<?php
// dans src/Controller/ArticlesController.php
namespace App\Controller;
use App\Controller\AppController;
class ArticlesController extends AppController
{
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Hardcoding the user_id is temporary, and will be removed later
// when we build authentication out.
$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 de sauvegarder l\'article.'));
}
// Récupère une liste des tags.
$tags = $this->Articles->Tags->find('list');
// Passe les tags au context de la view
$this->set('tags', $tags);
$this->set('article', $article);
}
// Les autres actions
}
Les lignes de code ajoutées chargent une liste des tags sous forme de tableau associatif
de la forme id => title
. Ce format nous permet de créer un nouvel input de tags dans
notre template. Ajoutez la ligne suivante dans le bloc PHP avec les autres appels à
control()
dans src/Template/Articles/add.ctp:
echo $this->Form->control('tags._ids', ['options' => $tags]);
Cela rendra un select multiple qui utilisera la variable $tags
pour générer
les options du select. Vous devriez maintenant créer quelques articles en leur
mettant des tags car dans la section suivante, nous allons ajouter la possibilité
de trouver des articles par leurs tags.
Vous devriez également mettre à jour la méthode edit
pour permettre l’ajout
et la modification de tags sur les articles existant. La méthode edit
devrait
maintenant ressemble à ceci:
public function edit($slug)
{
$article = $this->Articles
->findBySlug($slug)
->contain('Tags') // charge les Tags associés
->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é modifié.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Impossible de mettre à jour votre article.'));
}
// Récupère une liste des tags.
$tags = $this->Articles->Tags->find('list');
// Passe les tags au context de la view
$this->set('tags', $tags);
$this->set('article', $article);
}
Pensez à ajouter le nouveau select multiple qui permet de sélectionner les tags comme nous l’avons fait dans le template add.ctp au template src/Template/Articles/edit.ctp.
Une fois que les utilisateurs ont catégorisé leurs contenus, ils voudront probablement retrouver ces contenus en fonction des tags utilisés. Pour développer ces fonctionnalités, nous allons implémenter une nouvelle route, une nouvelle action de controller et une fonction de finder pour chercher les articles par tags.
Idéalement, nous voulons une URL qui ressemblera à http://localhost:8765/articles/tagged/funny/cat/gifs. Cela nous permettra de trouver tous les articles avec le tag “funny”, “cat” ou “gifs”. Nous avons tout d’abord besoin d’ajouter une nouvelle route. Votre fichier config/routes.php devra ressembler à:
<?php
use Cake\Routing\Route\DashedRoute;
use Cake\Routing\Router;
Router::defaultRouteClass(DashedRoute::class);
// Ceci est la route à ajouter pour notre nouvelle action.
// Le `*` à la fin permet de préciser à CakePHP que cette action
// a des paramètres qui lui seront passés
Router::scope(
'/articles',
['controller' => 'Articles'],
function ($routes) {
$routes->connect('/tagged/*', ['action' => 'tags']);
}
);
Router::scope('/', function ($routes) {
// Connect the default home and /pages/* routes.
$routes->connect('/', [
'controller' => 'Pages',
'action' => 'display', 'home'
]);
$routes->connect('/pages/*', [
'controller' => 'Pages',
'action' => 'display'
]);
// Connect the conventions based default routes.
$routes->fallbacks();
});
Plugin::routes();
Le code ci-dessus définit une nouvelle “route” qui permet de connecter le chemin
URL /articles/tagged/ à ArticlesController::tags()
. En définissant une nouvelle
route, vous pouvez isoler le format de vos URLs de la manière dont elles sont implémentées.
Si nous venions à visiter http://localhost:8765/articles/tagged, nous verrions
une page d’erreur de CakePHP vous indiquant que l’action du controller n’existe
pas. Créons de ce pas cette nouvelle méthode. Dans src/Controller/ArticlesController.php,
ajoutez ce qui suit:
// Ajouter ce 'use' juste sous la déclaration du namespace pour importer
// la classe Query
use Cake\ORM\Query;
public function tags()
{
// La clé 'pass' est fournie par CakePHP et contient tous les
// segments d'URL passés dans la requête
$tags = $this->request->getParam('pass');
// Utilisation de ArticlesTable pour trouver les articles taggés
$articles = $this->Articles->find('tagged', [
'tags' => $tags
]);
// Passage des variable dans le contexte de la view du template
$this->set([
'articles' => $articles,
'tags' => $tags
]);
}
Pour accéder aux autres parties des données de la requête, consultez la section ServerRequest.
Puisque les arguments passés sont aussi fournis comme paramètres de la méthode d’action, nous pourrions également écrire l’action en utilisant les arguments variadic de PHP:
public function tags(...$tags)
{
// Utilisation de ArticlesTable pour trouver les articles taggés
$articles = $this->Articles->find('tagged', [
'tags' => $tags
]);
// Passage des variable dans le contexte de la view du template
$this->set([
'articles' => $articles,
'tags' => $tags
]);
}
Dans CakePHP, nous aimons garder nos actions de controller le plus minimaliste
possible et mettons la majorité de la logique de notre application dans la couche
model. Si vous veniez à visiter l’URL /articles/tagged, vous verriez une erreur
vous indiquant que la méthode findTagged()
n’existe pas. Dans
src/Model/Table/ArticlesTable.php, ajoutez le code suivant:
// Ajouter ce 'use' juste sous la déclaration du namespace pour importer
// la classe Query
use Cake\ORM\Query;
// L'argument $query est une instance du Query builder.
// Le tableau $options va contenir l'option 'tags' que nous avons passé
// à find('tagged') dans notre action de controller.
public function findTagged(Query $query, array $options)
{
$columns = [
'Articles.id', 'Articles.user_id', 'Articles.title',
'Articles.body', 'Articles.published', 'Articles.created',
'Articles.slug',
];
$query = $query
->select($columns)
->distinct($columns);
if (empty($options['tags'])) {
// si aucun tag n'est fourni, trouvons les articles qui n'ont pas de tags
$query->leftJoinWith('Tags')
->where(['Tags.title IS' => null]);
} else {
// Trouvons les articles qui ont au moins un des tags fourni
$query->innerJoinWith('Tags')
->where(['Tags.title IN' => $options['tags']]);
}
return $query->group(['Articles.id']);
}
Nous venons d’implémenter un custom finder. Ce concept
très pratique de CakePHP vous permet de définir des requêtes réutilisables. Les
méthodes finder récupèrent toujours en paramètres un objet Query Builder
et un tableau d’options. Les finders peuvent manipuler la requête et ajouter
n’importe quels condition ou critère. Une fois la logique terminée, le finder doit
retourner une instance modifiée de l’objet query. Dans notre finder, nous utilisons
les méthodes distinct()
et leftJoin()
qui nous permettent de trouver les articles
différents qui ont les tags correspondant.
Si vous visitez à nouveau /articles/tagged, CakePHP vous affichera une nouvelle erreur qui vous fait savoir qu’il manque le fichier de view. Créez le fichier src/Template/Articles/tags.ctp et ajoutez le contenu suivant:
<h1>
Articles avec les tags
<?= $this->Text->toList(h($tags), 'ou') ?>
</h1>
<section>
<?php foreach ($articles as $article): ?>
<article>
<!-- Utilisation du HtmlHelper pour créer le lien -->
<h4><?= $this->Html->link(
$article->title,
['controller' => 'Articles', 'action' => 'view', $article->slug]
) ?></h4>
<span><?= h($article->created) ?>
</article>
<?php endforeach; ?>
</section>
Dans le code ci-dessus, nous utilisons les Helpers Html et
Text pour nous aider à générer le contenu de notre view.
Nous utilisons également la fonction raccourcie h
pour échapper le
contenu HTML. Pensez à utiliser h()
quand vous affichez des données pour
éviter les injections de HTML.
Le fichier tags.ctp que nous venons de créer suit les conventions CakePHP pour les templates de view. La convention est d’utiliser le nom de l’action du controller en minuscule et avec un underscore en séparateur.
Vous avez peut-être remarqué que nous utilisons les variables $tags
et
$articles
dans notre template de view. Quand nous utilisons la méthode
set()
dans notre controller, nous définissons les variables qui doivent
être envoyées à notre view. La classe View fera alors en sorte de passer les
variables au scope du template comme variables « locales ».
Vous devriez maintenant être capable de visiter la page /articles/tagged/funny et voir tous les articles avec le tag “funny”.
Pour le moment, ajouter des tags est assez fastidieux puisque les rédacteurs auront besoin de créer les tags à utiliser avant de les assigner. Nous pouvons améliorer l’UI de notre gestion de tag en utilisant une liste de valeurs séparées par des virgules. Cela nous permettra d’améliorer l’expérience utilisateur et de découvrir d’autres fonctionnalités de l’ORM.
Puisque nous souhaitons une manière simple d’accéder aux tags formattés pour une entity, nous ajoutons un champ virtuel / pré-calculé pour l’entity. Dans src/Model/Entity/Article.php ajoutez la méthode suivante:
// Ajouter ce 'use' juste sous la déclaration du namespace pour importer
// la classe Collection
use Cake\Collection\Collection;
protected function _getTagString()
{
if (isset($this->_properties['tag_string'])) {
return $this->_properties['tag_string'];
}
if (empty($this->tags)) {
return '';
}
$tags = new Collection($this->tags);
$str = $tags->reduce(function ($string, $tag) {
return $string . $tag->title . ', ';
}, '');
return trim($str, ', ');
}
Cela nous permettra d’accéder à la propriété virtuelle $article->tag_string
.
Nous utiliserons cette propriété plus tard.
Maintenant que notre entity est mise à jour, nous pouvons ajouter un nouvel
élément de contrôle pour nos tags. Dans
src/Template/Articles/add.ctp et src/Template/Articles/edit.ctp,
remplacez l’élément de contrôle existant tags._ids
avec la déclaration
suivante:
echo $this->Form->control('tag_string', ['type' => 'text']);
Maintenant que nous voyons les tags existant sous forme d’une chaîne, nous avons
besoin de sauvegarder les tags sous ce format. Puisque que nous avons rendu tag_string
accessible, l’ORM copiera les données de la requête dans notre entity. Nous
pouvons utiliser le hook beforeSave()
pour parser la chaîne de tags et trouver /
construire les entities correspondantes. Ajoutez le code suivant à
src/Model/Table/ArticlesTable.php:
public function beforeSave($event, $entity, $options)
{
if ($entity->tag_string) {
$entity->tags = $this->_buildTags($entity->tag_string);
}
// Le code déjà existant
}
protected function _buildTags($tagString)
{
// Trim des tags
$newTags = array_map('trim', explode(',', $tagString));
// Retire les tags vides
$newTags = array_filter($newTags);
// Dé-doublonne les tags
$newTags = array_unique($newTags);
$out = [];
$query = $this->Tags->find()
->where(['Tags.title IN' => $newTags]);
// Retire les tags existant de la liste des nouveaux tags.
foreach ($query->extract('title') as $existing) {
$index = array_search($existing, $newTags);
if ($index !== false) {
unset($newTags[$index]);
}
}
// Ajout des tags existant.
foreach ($query as $tag) {
$out[] = $tag;
}
// Ajout des nouveaux tags.
foreach ($newTags as $tag) {
$out[] = $this->Tags->newEntity(['title' => $tag]);
}
return $out;
}
Bien que ce code soit plus compliqué que tout ce que nous avons fait jusqu’ici, il permet de mettre en avant les fonctions avancées de l’ORM : vous pouvez manipuler le résultat de la requête en utilisant les méthodes de la classe Collection (voir la section Collections) et pouvez également gérer les scénarios où vous avez besoin de créer des entities à la volée.
Dans le chapitre suivant, nous ajouter une couche d’authentification.