
Créer un blog – les catégories
Nous avons dans le précédent article codé la modification et la suppression d’un article et nous en avons fini avec cette entité. Dans le présent article nous allons nous intéresser aux catégories : création d’un tableau, ajout, modification et suppression.
Comme nous aurons plusieurs entités à traiter de façon similaire on va créer un contrôleur de ressource universel. Ca va un peu nous occuper mais ensuite nous faire gagner beaucoup de temps et de code, et puis ça permet de comprendre certaines choses sur notre framework favori.
Vous pouvez télécharger le code final de cet article ici.
Edit au 26/02/2021 : j’ai ajouté une majuscule à la classe du datatable dans le contrôleur de ressource.
Un contrôleur de ressource
On a vu précédemment que nous avons créé une vue partagée (back.shared.index) pour générer des tableaux. D’autre part de façon classique il nous faut les actions suivantes :
- create
- store
- edit
- update
- destroy
Ce qui change à chaque fois c’est :
- la Datatable
- les vues
- la validation
- le modèle
L’idée est donc de créer un contrôleur qui permette de s’adapter. On commence par créer la classe :
php artisan make:controller Back\ResourceController
Et on ajoute des propriétés pour différencier les entités :
<?php namespace App\Http\Controllers\Back; use App\Http\Controllers\Controller; use Illuminate\Support\Str; class ResourceController extends Controller { protected $dataTable; protected $view; protected $formRequest; protected $singular;
Le constructeur
On va se baser sur les urls pour différencier. Il faut adopter quelques conventions pour les appellations pour que ça fonctionne. C’est dans le contrôleur que le gros du travail va se passer :
public function __construct() { if(!app()->runningInConsole()) { $segment = getUrlSegment(request()->url(), 2); // categories ou newcategories if(substr($segment, 0, 3) === 'new') { $segment = substr($segment, 3); } $name = substr($segment, 0, -1); // categorie $this->singular = Str::singular($segment); // category $model = ucfirst($this->singular); // Category $this->model = 'App\Models\\' . $model; $this->dataTable = 'App\DataTables\\' . ucfirst($name) . 'sDataTable'; $this->view = 'back.' . $name . 's.form'; $this->formRequest = 'App\Http\Requests\Back\\' . $model . 'Request'; } }
J’ai mis en commentaire le résultat des calculs pour les catégories. Comme j’utilise un nouveau helper on l’ajoute dans app/helpers :
if (!function_exists('getUrlSegment')) { function getUrlSegment($url, $segment) { $url_path = parse_url(request()->url(), PHP_URL_PATH); $url_segments = explode('/', $url_path); return $url_segments[$segment]; } }
Prenons le cas de la liste des catégories, on a une url de la forme monblog.ext/admin/categories. Il nous faut récupérer le dernier segment de cette url :
$segment = getUrlSegment(request()->url(), 2);
On obtient alors categories.
Mais on doit prévoir le cas où on veut juste les nouvelles catégories (c’est un mauvais exemple parce que comme c’est l’administrateur qui crée les catégories ce n’est pas la peine d’en prévoir la liste, mais ça servira pour les utilisateurs, les contacts et les commentaires) et dans ce cas l’url est monblog.ext/admin/newcategories. Donc on doit supprimer les 3 premiers caractères pour ne garder que categories :
if(substr($segment, 0, 3) === 'new') { $segment = substr($segment, 3); }
On a ensuite besoin du nom sans le « s » :
$name = substr($segment, 0, -1);
On obtient donc categorie.
On aura besoin du singulier (pour la langue anglaise) pour les messages :
$this->singular = Str::singular($segment);
Maintenant on obtient category. De là il est facile d’en déduire le nom du modèle en mettant une majuscule :
$model = ucfirst($this->singular);
On obitent ainsi Category.
Avec tout ça on affecte nos propriétés :
$this->model = 'App\Models\\' . $model; $this->dataTable = 'App\DataTables\\' . $name . 'sDataTable'; $this->view = 'back.' . $name . 's.form'; $this->formRequest = 'App\Http\Requests\Back\\' . $model . 'Request';
index
Pour la méthode index on adopte ce code :
public function index() { return app()->make($this->dataTable)->render('back.shared.index'); }
On ne peut évidemment pas injecter la Datatable dans la méthode parce qu’on ne connait pas la classe. Alors on demande au container de Laravel (app, pour être plus précis l’application étendant le container) de générer une instance avec sa méthode make et ensuite de produire (render) la page avec la vue passée en paramètre.
Dans un contrôleur codé classiquement pour les catégories on aurait ce genre de code :
public function index(CategoriesDataTable $dataTable) { return $dataTable->render('back.shared.index'); }
Avec évidemment le même résultat.
create et store
Pour la méthode create c’est tout simple, on renvoie la vue dont on a défini le nom dans la propriété :
public function create() { return view($this->view); }
Pour la méthode store on ne peut pas injecter la validation et on doit la générer à partir du conteneur, de même que le modèle :
public function store() { $request = app()->make($this->formRequest); app()->make($this->model)->create($request->all()); return back()->with(['ok' => __('The ' . $this->singular . ' has been successfully created.')]); }
edit et update
Pour la méthode edit on ne peut pas utiliser le model binding :
public function edit($id) { $element = app()->make($this->model)->find($id); return view($this->view, [$this->singular => $element]); }
Pour la méthode update on doit encore utiliser le conteneur pour la validation et pour instancier le modèle :
public function update($id) { $request = app()->make($this->formRequest); app()->make($this->model)->find($id)->update($request->all()); return back()->with(['ok' => __('The ' . $this->singular . ' has been successfully updated.')]); }
destroy
Il ne reste plus qu’à s’occuper de la suppression qui se passera systématiquement an Ajax :
public function destroy($id) { app()->make($this->model)->find($id)->delete(); return response()->json(); }
Notre contrôleur de ressource est maintenant prêt et on va l’utiliser pour les catégories.
Les catégories
Les routes
Pour les routes c’est simple, on veut toute la ressource sauf show et c’est réservé à l’administrateur et on pointe notre contrôleur de ressource :
use App\Http\Controllers\Back\ ... ResourceController as BackResourceController }; ... Route::prefix('admin')->group(function () { ... Route::middleware('admin')->group(function () { ... Route::resource('categories', BackResourceController::class)->except(['show']); });
La configuration
Les titres
Dans app/config/titles on ajoute les titres pour les catégories :
'categories' => [ 'index' => 'Categories', 'create' => 'Category Creation', 'edit' => 'Category Edit', ],
Le menu
Dans app/config/menu on ajoute les items pour les catégories :
'Categories' => [ 'icon' => 'list', 'role' => 'admin', 'children' => [ [ 'name' => 'All categories', 'role' => 'admin', 'route' => 'categories.index', ], [ 'name' => 'Add', 'role' => 'admin', 'route' => 'categories.create', ], [ 'name' => 'fake', 'role' => 'admin', 'route' => 'categories.edit', ], ], ],
On doit avoir du nouveau dans le menu :
Si vous ne voyez pas ça c’est que vous n’êtes peut-être pas connecté avec l’administrateur !
Le tableau
On crée la Datatable :
php artisan datatables:make Categories
<?php namespace App\DataTables; use App\Models\Category; use Yajra\DataTables\Html\Column; use Yajra\DataTables\Services\DataTable; class CategoriesDataTable extends DataTable { use DataTableTrait; /** * Build DataTable class. * * @param mixed $query Results from query() method. * @return \Yajra\DataTables\DataTableAbstract */ public function dataTable($query) { return datatables() ->eloquent($query) ->editColumn('posts_count', function ($category) { return $this->badge($category->posts_count, 'secondary'); }) ->editColumn('action', function ($category) { return $this->button( 'categories.edit', $category->id, 'warning', __('Edit'), 'edit' ). $this->button( 'categories.destroy', $category->id, 'danger', __('Delete'), 'trash-alt', __('Really delete this category?') ); }) ->rawColumns(['posts_count', 'action']); } /** * Get query source of dataTable. * * @param \App\Models\Category $model * @return \Illuminate\Database\Eloquent\Builder */ public function query(Category $category) { return $category->withCount('posts'); } /** * Optional method if you want to use html builder. * * @return \Yajra\DataTables\Html\Builder */ public function html() { return $this->builder() ->setTableId('categories-table') ->columns($this->getColumns()) ->minifiedAjax() ->dom('Blfrtip') ->lengthMenu(); } /** * Get columns. * * @return array */ protected function getColumns() { return [ Column::make('title')->title(__('Title')), Column::make('slug')->title(__('Slug')), Column::computed('posts_count')->title(__('Posts'))->addClass('text-center align-middle'), Column::computed('action')->title(__('Action'))->addClass('align-middle text-center'), ]; } /** * Get filename for export. * * @return string */ protected function filename() { return 'Categories_' . date('YmdHis'); } }
Dans la requête on prévoit le nombre de commentaires pour l’afficher dans le tableau.
Vous devriez maintenant avoir le tableau :
La validation
Il faut créer la form request pour la validation :
php artisan make:request Back\CategoryRequest
<?php namespace App\Http\Requests\Back; use Illuminate\Foundation\Http\FormRequest; use App\Rules\Slug; class CategoryRequest extends FormRequest { public function authorize() { return true; } public function rules() { $id = $this->method() === 'PUT' ? ',' . basename($this->url()) : ''; return $rules = [ 'title' => 'required|max:255', 'slug' => ['required', 'max:255', new Slug, 'unique:posts,slug' . $id], ]; } }
Pour distinguer la création de la modification je regarde la méthode dans la requête. Dans le cas de la modification il faut récupérer l’identifiant pour l’exclusion pour la règle unique du slug.
On réutilise la classe Slug qu’on avait créée pour les articles.
La vue
On utilise la même vue pour le formulaire en création et en modification :
On profite pour cette vue de composants qu’on a créés pour les articles :
@extends('back.layout') @section('main') <form method="post" action="{{ Route::currentRouteName() === 'categories.edit' ? route('categories.update', $category->id) : route('categories.store') }}"> @if(Route::currentRouteName() === 'categories.edit') @method('PUT') @endif @csrf <div class="row"> <div class="col-md-12"> <x-back.validation-errors :errors="$errors" /> @if(session('ok')) <x-back.alert type='success' title="{!! session('ok') !!}"> </x-back.alert> @endif <x-back.card type='info' :outline="true" title=''> <x-back.input title='Title' name='title' :value="isset($category) ? $category->title : ''" input='text' :required="true"> </x-back.input> <x-back.input title='Slug' name='slug' :value="isset($category) ? $category->slug : ''" input='text' :required="true"> </x-back.input> </x-back.card> <button type="submit" class="btn btn-primary">@lang('Submit')</button> </div> </div> </form> @endsection @section('js') <script src="https://cdnjs.cloudflare.com/ajax/libs/speakingurl/14.0.1/speakingurl.min.js"></script> <script> $(function() { $('#slug').keyup(function () { $(this).val(getSlug($(this).val())) }) $('#title').keyup(function () { $('#slug').val(getSlug($(this).val())) }) }); </script> @endsection
On utilise aussi la même librairie pour la génération et le contrôle du slug.
Pour la création on obtient le formulaire vierge :
Pour la modification on obtient le formulaire complété :
Vérifiez que tout fonctionne : validation, création, modification et même suppression parce qu’on avait créé du code générique.
Conclusion
On a passé pas mal de temps à créer notre contrôleur de ressource mais on a déjà vu pour les catégories comme ça nous aide ! de même que les composants qu’on a créés précédemment pour les vues.


33 commentaires
oa
Très interessant ce RessourceControler. Il y aurait-il un moyen de l’utiliser avec un filtre, exemple l’id d’une autre ressource ?
ex: RessourceA en relation 1-n avec Ressource B
Mon idée serait d’ajouter une fonction listeB à la ressourceA qui m’affiche la liste des RessourceB (soit le RessourceControler de B) liée à l’Id de ma RessourceA sélectionné.
bestmomo
Salut,
Etant donné qu’on récupère l’instance du modèle dans le contrôleur à partir de la décomposition de l’url on peut facilement trouver les relations et donc trouver des enregistrement associés.
oa
Désolé je bloque pour le passer dans query de RessourceBDataTable
bestmomo
Je ne suis pas sûr de bien comprendre le problème.
Dans un DataTable on a une fonction query dans laquelle on récupère toutes les infos qu’on veut, y compris celles issues de relations. Et on peut les afficher dans le tableau.
pablitozer
Bonjour je vous envoie ce commentaire car je ne comprends pas du tout pourquoi j’ai une erreur Illuminate\Contracts\Container\BindingResolutionException
Target class [App\DataTables\PublisDataTable] does not exist à partir du moment ou je créé le datatable avec php artisan datatables:make Categories et que je met le code que vous avez donné
bestmomo
Bonjour,
C’est quoi cette DataTable Publis ?
pablitozer
et bien c’est ce que je me pose comme question et problème car c’est après avoir fait la commande php artisan datatables:make Categories et avoir rempli le fichier puis le ResourceController et après quand je veux accèder au tableau des catégories il me dit PublisDataTable does not exist
bestmomo
Il faudrait regarder dans le constructeur du contrôleur de ressource si le nom du DataTable se construit correctement à partir des données de l’url.
pablitozer
protected $dataTable;
protected $view;
protected $formRequest;
protected $singular;
public function __construct()
{
if(!app()->runningInConsole()) {
$segment = getUrlSegment(request()->url(), 2); // categories ou newcategories
if(substr($segment, 0, 3) === ‘new’) {
$segment = substr($segment, 3);
}
$name = substr($segment, 0, -1); // categorie
$this->singular = Str::singular($segment); // category
$model = ucfirst($this->singular); // Category
$this->model = ‘App\Models\\’ . $model;
$this->dataTable = ‘App\DataTables\\’ . ucfirst($name) . ‘sDataTable’;
$this->view = ‘back.’ . $name . ‘s.form’;
$this->formRequest = ‘App\Http\Requests\Back\\’ . $model . ‘Request’;
}
}
j’ai ça comme code
bestmomo
Regarde ce que ça donne la constitution du nom :
$this->dataTable = 'App\DataTables\\' . ucfirst($name) . 'sDataTable';
dd($this->dataTable);
pablitozer
effectivement il se construit pas correctement quand je fais le dd il m’affiche App\DataTables\PublisDataTable
bestmomo
On avance, c’est quoi l’url qui arrive ?
pablitozer
je peux le voir comment l’url qui arrive ?
pablitozer
désolé je suis bête je pense c’est ça l’url que tu demandes : http://localhost:8888/tutoReussi/public/admin/categories
bestmomo
Salut,
Le souci vient du fait que tu n’as pas de host du genre monblog.oo, du coup le code lit l’url et l’interprète mal.
pablitozer
ah mince et du coup y aurait un moyen de règler le problème ?
bestmomo
Salut,
Le seul moyen de résoudre le problème est de mettre en place un local host. Pour te simplifier la vie tu devrais utiliser Laragon.
pablitozer
ah oui et désolé j’ai zappé mais salut au passage et merci beaucoup déjà pour les informations que tu me donnes bestmomo 🙂
fabBlab
Vraiment très intéressant ce TP, avec une bonne illustration du principe « Don’t Repeat Yourself » !
Au-delà de Laravel, cela donne des idées d’organisation de son code dans des applications « cousues main » ou via des frameworks moins pointus.
HK
Bonjour Momo,
je recherche une piste pour ajouter une image par category d’article sans tout chambouler le code et pour une intégration optimale.
J’ai crée la migration integré le component input dans le formulaire des categories, créé un nouveau helper : getCatImage avec une injection de $category.
A ce stade je sauvegarde l’url complète dans ma base, maintenant pour récupérer la basename de l’url de l’upload avant la sauvegarde je devrait …. ? des pistes s’il te plait 😉
bestmomo
Bonjour,
Je ne suis pas sûr de bien comprendre la question mais pour récupérer un basename d’une url c’est la méthode basename() de PHP.
HK
Plus généralement, en suivant ta logique de RessourceController (générique), si on veut pour l’édition ou la création d’un Model (là Category) une fonction ou methode spécifique,vers quelle logique devons nous nous diriger ?
Ajouter une fonction dans le constructeur du RessourceController ?
Ajouter un Controller + routes spécifiques pour le Model ?
Quelle solution optimale donc a choisir pour juste un petit ajout sur un model ?
bestmomo
Bonjour,
Le plus logique est de créer un contrôleur qui étend ResourceController. Là on peut ajouter des méthodes ou surcharger celles de la classe mère. C’est d’ailleurs ce que je fais dans le chapitre suivant pour la gestion des utilisateurs. J’ajoute deux méthodes avec de nouvelles routes et je surcharge la méthode update parce qu’il y a une particularité.
slozano54
Up !
Petite kwak dans le controleur App\Http\Controllers\Back\ResourceController.php
La class est appelée avec une minuscule mais elle a été créée avec une majuscule. La casse est-elle importante sous tous les OS ? Sous Linux cela provoque une erreur.
J’ai donc ajouté une variable $nameDataTables = ucfirst($name); // Categorie
Puis j’ai modifié la propriété dataTable ainsi $this->dataTable = ‘App\DataTables\\’ . $nameDataTables . ‘sDataTable’;
bestmomo
Salut,
Oui c’est l’inconvénient de développer avec Windows qui est tolérant sur la casse, du coup on ne remarque pas ce genre de chose parce que tout fonctionne bien. J’ai déjà eu le cas dans un de mes packages. Je vais corriger ça. Merci !
Edit : j’ai modifié le code et l’article et fait suivre les ZIP…
slozano54
Up!
J’ai constaté que lorsque je duplique un article dont l’utilisateur connecté est propriétaire, l’image est dupliquée aussi.
Par contre lorsque je duplique un article dont l’utilisateur connecté n’est pas le propriétaire, l’image n’est pas dupliquée.
J’ai deux chapitres de retard, j’aimerais résoudre ce problème d’abord …
Je fouille mais si vous avez une piste …
Down …
bestmomo
Salut,
Le tableau des articles apparaît pour un rédacteur et il ne dispose dans ce tableau que de ses articles, il ne peut donc pas a priori dupliquer l’article d’un autre.
Il n’y a que l’administrateur qui peut dupliquer les articles de tout le monde et là effectivement s’il duplique l’article d’un rédacteur l’url de l’image ne sera pas générée correctement. Il faut qu’il change l’image pour qu’ensuite ça soit bon puisqu’il aura pris forcément une image à lui.
Dans le cas de l’administrateur on pourrait ne faire apparaître le bouton de duplication que pour ses articles.
Mais on peut aussi changer l’url pour dupliquer n’importe quel article donc il faudrait peut-être empêcher la duplication aussi dans cette situation. je vais voir ça…
Edit : j’ai mis à jour les articles et le code
slozano54
Salut,
Merci pour ta réactivité, effectivement j’aurais dû y penser, j’ai fait ce constat avec un profil admin !
Avec la modif proposée dans cet article, un admin peut toujours tout dupliquer mais s’il n’est pas l’auteur de l’article, l’article cloné est vierge.
Du coup pour le clonage par un admin on pourrait aussi le permettre en conservant tout sauf l’image.
Peut être en accompagnant d’un petit message d’alerte précisant qu’il faut en charger un nouvelle.
… à suivre
Merci encore
bestmomo
Salut,
Sur le fond je ne vois pas l’intérêt pour l’administrateur de cloner l’article d’un rédacteur.
Michel
Je fais le même constat:
L’image n’est pas dupliquée si l’action est faite par un autre useur que le rédacteur de l’article.
Elle est dupliquée qand l’auteur fit l’action.
bestmomo
Oui c’est ce que j’explique dans mon autre réponse, comme on n’enregistre que le nom de l’image et que l’url est générée avec l’identifiant de l’auteur si quelqu’un d’autre duplique ça coince. je vais proposer un patch pour bloquer ça.
Edit : j’ai mis à jour les articles et le code.
Michel
Bonsoir,
Les évolutions du jour sont superbes. Merci.
– 1 remarque: Quand je duplique un article l’image n’est pas reprise. Normal ?
Pour la question des images:
– Quand j’ai fermé monblg.test hier au soir, j’avais les images et tout fonctionnait bien.
– En ouvrant ce soir, sans avoir retouché à l’appli, aucune image !!!
Ne comprennant pas pourquoi, j’ai relancé via ma console
composer require unisharp/laravel-filemanager
php artisan vendor:publish –tag=lfm_config
php artisan vendor:publish –tag=lfm_public
php artisan storage:link
et les images sont apparues…..
J’ai foullé un peu plus et je ma suis aperçu que la route :
Route::group([‘prefix’ => ‘laravel-filemanager’, ‘middleware’ => ‘auth’], function () {
Lfm:: routes(); avait peut être une erreur de casse sur le L de lfm
J’ai remplacé, Lfm:: routes(); par lfm::routes(); qui est la casse exacte du nom du fichier lfm.php
Ai je bien fait ?
– J’ai relancé avec différends users (admin, redac, lecteur) et mes images sont toujours là.
2eme remarque: l’image 6 est beaucoup plus longue à apparaitre. Une idée du pourquoi ?
,
bestmomo
Salut,
– Normalement l’image est aussi dupliquée puisqu’on recopie tous les champs, mais ça doit rejoindre le souci de base pour les images.
– La syntaxe de la classe est Lfm avec une majuscule et le fichier a bien aussi une majuscule, j’ai vérifié dans le vendor. Quand on utilise Windows c’est assez bien digéré mais sous Linux la casse ne passe pas.
– pour la lenteur de l’image 6 je n’ai aucune explication, elle n’est pas plus lourde que les autres…