Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Créer un blog - les catégories
Dimanche 21 février 2021 14:36

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.

 


Par bestmomo

Nombre de commentaires : 33