Aller au contenu

2. Gestion des catégories⚓︎

Dans ce TP nous allons réaliser une application web, « TODO » ou aussi appelé liste de tâches.
Une TODO List est un procédé qui se veut simple et efficace pour gérer les tâches d'un projet. Ces tâches peuvent être indépendantes ou devoir, au contraire, être accomplies dans un certain ordre. Ces tâches pourront également être catégorisées (pro, perso, famille, ...).

Voilà la liste des fonctionnalités de l’application que l'on va ajouter :
- attribuer une ou plusieurs catégories pour chaque Todo - Création d'une page de recherche

2.1 Gestion de la catégorie⚓︎

2.1.1 Relation Many To Many⚓︎

On va avoir besoin de déclarer qu'un todo peut appartenir à plusieurs catégories et une catégorie classifie plusieurs todos. La relation est symétrique.

On ajoute l'espace de noms nécessaire à la gestion de cette relation dans les 2 models.

PHP
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

Puis à l'intérieur de la classe, on ajoute les méthodes d'association.

Dans Todos.php

PHP
    public function categories(): BelongsToMany
        {
            return $this->belongsToMany(Categories::class);
        }
code complet de Todos.php
PHP
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Todos extends Model
{
    use HasFactory;

    protected $fillable = ['texte', 'termine', 'important'];

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Categories::class);
    }
}

Dans Categories.php

PHP
    public function todos(): BelongsToMany
        {
            return $this->belongsToMany(Todos::class);
        }
code complet de catégories.php
PHP
<?php

namespace App\Models;


use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Categories extends Model
{
    use HasFactory;

    public function todos(): BelongsToMany
    {
        return $this->belongsToMany(Todos::class);
    }
}

On déclare ici avec la méthode todos (au pluriel) qu’une catégorie appartient à plusieurs (belongsToMany) todos (Todos). On aura ainsi une méthode pratique pour récupérer les todos d’une catégorie. Idem pour les catégories.

2.1.2 Le seeder⚓︎

On fait le choix de ne pas créer d'interface pour ajouter les catégories (Mais vous pouvez le faire ).
On va donc faire une saisie directement en Base de données. Mais en phase de mise au ppoint d'application, vous vous êtes rendu compte que l'on peut avoir besoin de flasher sa base. Ce qui peut être facheux lorsque l'on souhaite conserver quleques données. On va donc passer par de seeders, intégré à la Laravel, ce sont des scripts qui permettront des injections de données en Base, comme pour les tables de paramétrages ou des jeux de données.

Chat GPT

Il peut être tres pratique d'utiliser une IA pour générer ses seeders. Vous trouverez ci dessous la question poser à chatGPT pour générer le CategoriesTableSeeder.

Je travaille dans le contexte Laravel 10 pour créer une application de gestion de taches.
Une tache peut être affecter à une catégorie : pro, famille, sport, associatif par exemple.
Ci dessous le code de la migration de la table :

```php public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->integer('idcat'); $table->string('libelle'); $table->primary('idcat'); $table->timestamps(); }); } ```` Peux tu me donner le seeder correspondant ?

On va créer un nouveau fichier dans le répertoire database\seeders, qui contiendra les données à injecter en table. On pourra l'exécuter directement en utilisant la commande suivante :

📋 Texte
```prompt 
php artisan db:seed --class=CategoriesSeeder

```
Code de CategoriesSeeder
PHP
<?php
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class CategoriesSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $categories = [
            ['idcat' => 1, 'libelle' => 'Pro'],
            ['idcat' => 2, 'libelle' => 'Famille'],
            ['idcat' => 3, 'libelle' => 'Sport'],
            ['idcat' => 4, 'libelle' => 'Associatif'],
            // Ajoutez d'autres catégories au besoin
        ];

        // Insertion des données dans la table 'categories'
        DB::table('categories')->insert($categories);
    }
}
?>

On va également ajouter un seeder à la propriété $seeders du fichier DatabaseSeeder.php afin qu'il soit exécuté lors de la commande php artisan db:seed.

Code de DatabaseSeeder
PHP
class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        //Ajout de la classe CategoriesSeeder
        $this->call([
            CategoriesSeeder::class,
        ]);
    }
}

Lorsque est lancé la commande de (re)création de la base, on peut ajouter l'option --seed pour alimenter les tables avec un jeu de test

📋 Texte
php artisan migrate:fresh --seed

php artisan migrate:fresh --seed --seeder=CategoriesSeeder
▶️ A faire : Ajouter un seeder pour Todo

2.1.3 Le controller Catégorie⚓︎

▶️ Générer le controller de Catégorie en ligne de commande.

solution au cas où
PHP
php artisan make:controller CategorieController

▶️ Ajouter le code pour récupérer la liste des catégorie

code de categoriesController
PHP
/**
 * Affiche la liste des catégories.
 *
 * @return \Illuminate\Http\Response
 */
public function listeCatégories()
{
    return view("home", ["categories" => Categories::all()]);
}

▶️ Pour afficher la liste des Todos, on utilise le controller Todo. On va donc ajouter à l'appel de la liste des Todos, la liste des catégories. ‼️ Ne pas oublier de faire les import de librairies nécessaires.

code liste() dans todocontroller
PHP
//Liste
public function liste()
{
    return view("home", ["todos" => Todos::all(), "categories" => Categories::all()]);
}

▶️ Conclusion : ici on ne se sert pas du controller Categories.

2.1.4 La vue home pour ajouter les catégories⚓︎

On va d'abord ajouter dans la liste des Todos, l'affichage des catégories.

Dans la liste des Todos

Code dans home pour la liste des catégorie
PHP
<!-- Affichage du texte -->
<span>{{ $todo->texte }}</span>
    <!-- Affichage de la catégorie -->
    <p>Catégories :</p>
        <ul>
            @foreach($todo->categories as $category)
                <li>{{ $category->libelle }}</li>
             @endforeach
            </ul>
    </p>

Puis dans le formulaire de saisie d'un Todo

Code dans home pour les boites à cocher
PHP
 <!-- boites à cocher pour les catégories -->
<div class="form-group">
<label>Catégories</label>
    @foreach($categories as $categorie)
        <div class="form-check">
            <input class="form-check-input" type="checkbox" name="categories[]" value="{{ $categorie->id }}">
            <label class="form-check-label">{{ $categorie->libelle }}</label>
        </div>
    @endforeach
</div>

Remarquez que le nom du select est accompagné de crochets (categories[]) pour signifier qu’on va envoyer un tableau de valeurs dans la requête HTTP.

👀 Ne pas tester encore à cette étape, le travail n'est pas fini.

2.1.5 Afficher les catégories d'un Todo⚓︎

On est dans une relation n:n, il faudrait donc gérer en théorie des requêtes de jointures entre les tables pour afficher le libellé de chaque catégorie pour un Todo. On va utiliser de la ✨ magie de Laravel pour lui laisser cette tâche.

Dans le provider RouteServiceProvider qui, comme son nom l’indique, est consacré aux routes, on va ajouter le code ci dessous. L'idée est de charger pour chaque Route l'ensemble des catégories associées à un Todo.

PHP
    Route::bind('todos', function ($value) {
            return Todos::with('categories')->find($value) ?? abort(404);
        });

💥 Le souci est que cette méthode va nous obliger à revoir le nom des champs des noms pour laisser à Laravel le soin de faire ses jointures.
💢 Je vous invite à debugger par vous même votre application. Il va falloir supprimer la migration categorise pour recréer une nouvelle migration "propre".

Solutions

▶️ Créer la migration de la table d'association create_categories_todos_table. et supprimer celle decategorise`.

PHP
public function up(): void
{
    Schema::create('categories_todos', function (Blueprint $table) {
        $table->integer('categories_id');
        $table->integer('todos_id');

        $table->foreign('categories_id')->references('id')->on('categories');
        $table->foreign('todos_id')->references('id')->on('todos');

        $table->primary(['categories_id', 'todos_id']);
        $table->timestamps();
    });
}
▶️ Reprendre la migration de la table catégories

PHP
 public function up(): void
{
    Schema::create('categories', function (Blueprint $table) {
        $table->integer('id');
        $table->string('libelle');
        $table->primary('id');
        $table->timestamps();
    });
}

▶️ Reprendre la migration de la table Todos

PHP
public function up(): void
{
    Schema::create('todos', function (Blueprint $table) {
        $table->integer('id')->autoIncrement();
        #$table->primary('id');
        $table->string('texte');
        $table->boolean('termine')->default(0);
        $table->boolean('important')->default(0);

        $table->timestamps();
    });
}

▶️ Relancer la migration et les seeders php artisan migrate:fresh --seed

2.1.6 Adapter le code de SaveTodo pour enregister la catégorie du Todo⚓︎

Pour la gestion de la catégorie, je vous rappelle que nous avons déjà géré les jointures par magie ✨. On va continuer à utiliser cette mécanique pour la sauvegarde.
On envoie par la vue home un tableau de catégories name="categories[]". Lisez un peu de documentation ici et essayer de l'adapter à votre projet.

Solution
PHP
public function saveTodo(Request $request){
    //....
        $todo->save();
        $todo->categories()->attach($cats); /*A ajouter */
        return redirect()->route('todo.liste');
    } else{
        return redirect()->route('todo.liste')->with('message', "Veuillez saisir une note à ajouter");
    }

}

2.1.7 Delete un ToDo⚓︎

Si vous essayer de supprimer un Todo, à cette étape, cela va provoquer une erreur d'intégrité référentielle.

▶️ A Faire : Trouver la solution pour supprimer l'association catégorie_todo en cascade d'un ToDo.

solution

Il faut modifier la migration de Todo pour ajouter la fonction SQL de deleteOnCascade.

PHP
public function up(): void
{
    Schema::create('categories_todos', function (Blueprint $table) {
        $table->integer('categories_id');
        $table->integer('todos_id');

        $table->foreign('categories_id')->references('id')->on('categories');
        $table->foreign('todos_id')->references('id')->on('todos')
            ->onDelete('cascade');

        $table->primary(['categories_id', 'todos_id']);
        $table->timestamps();
    });
}

👀 relancer la migration et Tester !

2.2 La notion de softDelete⚓︎

Documentation

les soft deletes permettent de marquer un enregistrement comme "supprimé" sans le supprimer réellement de la base de données. Cela peut être utile dans de nombreux cas, par exemple lorsque vous avez des enregistrements que vous ne voulez pas supprimer définitivement, mais que vous voulez simplement cacher aux utilisateurs.

Pour utiliser les soft deletes sur vos modèles, vous devez d'abord ajouter le trait softDeletes à votre classe Model Todos.php:

code de Todos.php
PHP
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

use Illuminate\Database\Eloquent\SoftDeletes;

class Todos extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $fillable = ['texte', 'termine', 'important'];

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Categories::class);
    }


}

Ensuite, vous devez ajouter une colonne de type "deleted_at" à votre table. Vous pouvez le faire en ajoutant simplement la date colonne suivante à votre migration :

migration Todo
PHP
public function up(): void
{
    Schema::create('todos', function (Blueprint $table) {
        $table->integer('id')->autoIncrement();
        #$table->primary('id');
        $table->string('texte');
        $table->boolean('termine')->default(0);
        $table->boolean('important')->default(0);

        $table->timestamps();
        /*Utilisation du softDelete*/
        $table->softDeletes();
    });
}

Une fois que vous avez ajouté la colonne, vous pouvez commencer à utiliser la fonctionnalité soft deletes. Pour "supprimer" un enregistrement, en appelant la méthode delete(), cela va mettre à jour la colonne deleted_at avec la date et l'heure actuelles, ce qui marquera l'enregistrement comme supprimé. ❗️ Attention, dans notre cas, on ne peut supprimer un ToDo que si il est "terminé". Vous devez donc laisser la structure conditionnelle dans lequel est votre appel de delete().

PHP
$todo = Todos::find($id);
        if($todo->termine == 1){
            $todo->delete();
        }

2.3 Les groupes de routes⚓︎

Si plusieurs routes ont des informations communes il est possible de les grouper ensemble.

Code de web.php
PHP
    Route::controller(TodosController::class)->group(function () {

    Route::post('/action/add', 'saveTodo')->name('todo.save');
    Route::get('/action/done/{id}' , 'markAsDone')->name('todo.done');
    Route::get('/action/delete/{id}', 'deleteTodo')->name('todo.delete');
    Route::get('/action/lower/{id}',  'lowerPriority')->name('todo.lower');
    Route::get('/action/raise/{id}', 'raisePriority')->name('todo.raise');
});

2.4 Une nouvelle page : Recherche⚓︎

User story : On souhaite créer une page de Recherche par mot clé. Cette page disposera d'un champ texte pour saisir un mot-clé. A la validation de ce formulaire, l'application retournera tous les Todos contenant ce mot-clé dans leur texte, y compris dans les ToDos déjà supprimés.