Développer un blog avec Laravel : Partie 2/5

Développer un blog avec Laravel : Partie 2/5

Base des données et population

Dans la partie 1 nous avons parlé de notre Stack et avons initialisé un nouveau projet laravel prêt à l'emploi, passons maintenant à la structure de notre base des données.

Si vous n'avez pas lu la partie précédente:

  1. Stack et installation des outils

A cette étape, il est nécessaire de se poser 2 questions. Quelles sont les tables nécessaires et avec quelles colonnes ? Quelles sont les données de populations qu’on va générer pour que notre blog fonctionne dès le départ ? En répondant à ces questions nous saurons quelles sont les données que va contenir notre blog, en gardant en tête que cela peut changer au cours du développement.

A. LA BASE DES DONNÉES: LA STRUCTURE

Les utilisateurs

Commençons par les utilisateurs, nous en aurons de deux types:

  • les utilisateurs simples, qui lisent, commentent et partagent les articles.
  • les administrateurs qui gèrent les articles et les utilisateurs.

De base dans chaque projet laravel la table users comportent:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

les migrations se trouvent sous database/migrations

Capture-5.png

Nous allons avoir besoin de deux colonnes supplémentaires pour gérer les rôles et les droits d'accès, on ajoute donc:

  • rôle : pour savoir quelles sont les permissions de l’utilisateur : administrateur ou simple utilisateur.
  • valid : pour bloquer ou débloquer un utilisateur au cas où il ne respecterait pas les règles au niveau des commentaires.

Après ajout le fichier de migration ressemble a ca:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->enum('role', array('user', 'admin'))->default('user');  //ajout
    $table->boolean('valid')->default(true);         //ajout
    $table->rememberToken();
    $table->timestamps();
});

Nous allons ajouter les deux colonnes dans le model Users(déjà existant par défaut):

Les model sont sous App/Models

protected $fillable = [
    'name',
    'email',
    'password',
    'role', 
    'valid',
];

Les catégories

Nous allons organiser les articles en catégories(thèmes) qui vont faciliter la navigation sur le blog, on va donc créer une table pour ça. On va créer le modèle et la migration simultanément en exécutant dans le terminal de notre projet:

php artisan make:model Category -m

coup d’œil au nom du modèle en anglais qui nous donnera categories comme nom de la table dans la base. Laravel nomme les tables par le pluriel du nom du modèle.

Une catégorie n'aura que deux informations importantes:

  • title: le titre de la catégorie
  • slug: le texte qui va apparaître dans l’url et qui ne doit comporter que des caractères autorisés

Pour le modèle on crée la propriété $fillable et on supprime le trait HasFactory qui ne nous servira pas :

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $fillable = [
        'title', 
        'slug',
    ];

    public $timestamps = false;
}

et la migration :

Schema::create('categories', function(Blueprint $table) {
    $table->id();
    $table->string('title', 25)->unique();
    $table->string('slug')->unique();
    $table->timestamps();
});

Les articles

c'est la base même d'un blog, nous aurons donc pas mal d’informations dans cette table:

  • title : le titre de l'article
  • slug
  • overview : le texte d’introduction (le résumé qui apparaît à la liste des articles)
  • body : le corps de l’article
  • active : pour savoir si l’article est publié
  • image : le nom de l’image

On crée le modèle et la migration:

php artisan make:model Post -m

la migration ressemble a ça:

Schema::create('posts', function(Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('overview');
    $table->text('body');
    $table->boolean('active')->default(false);
    $table->string('image')->nullable();
});

le model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class Post extends Model
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'title', 
        'slug', 
        'overview', 
        'body', 
        'active', 
        'image', 
        'user_id',
    ];
}

Remarquer que pour la table article on aurait bien pu aller loin en prenant en compte des champs pour la gestion du SEO(référencement) mais j'ai voulu me limiter aux champs obligatoires pour raison de simplicité. Vous pouvez vous décidez d'aller plus loin.

Les commentaires

Pour les commentaires nous allons nous limiter aux commentaires a un seul niveau et pas des commentaires des commentaires de façon hiérarchisés.

On crée le modèle et migration :

php artisan make:model Comment -m

Nous avons besoin d'un champ body(on gérer les relations avec les articles et les utilisateurs plus bas dans l'article), on a donc:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentsTable extends Migration {

    public function up()
    {
        Schema::create('comments', function(Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->text('body');
        });
    }

    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

Pour le modèle Comment, on crée la propriété $fillable.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class Comment extends Model
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'body',
    ];
}

Les relations

Lions maintenant les tables qui dépendent les unes des autres. Nous allons avoir:

  • Un utilisateur peut avoir plusieurs articles et un article appartient à un seul utilisateur.
  • Une catégorie peut avoir plusieurs articles et réciproquement un article peut appartenir à plusieurs catégories.
  • Un utilisateur a plusieurs commentaires et un commentaire appartient à un utilisateur.
  • Un article a plusieurs commentaires et un commentaire appartient à un article.

Un utilisateur a plusieurs articles

Il faut mettre une clé étrangère dans la table posts, donc dans la migration : database/migrations/*****_create_posts_table.php

public function up()
{
    Schema::create('posts', function(Blueprint $table) {
        ...
        $table->foreignId('user_id')
              ->constrained()
              ->onDelete('cascade')
              ->onUpdate('cascade');
    });
}

Au niveau des modèles on ajoute la relation dans User : App/Models/User.php

public function posts()
{
    return $this->hasMany(Post::class);
}

Et dans Post : App/Models/Post.php

public function user()
{
    return $this->belongsTo(User::class);
}

entre catégories et articles

A ce niveau c'est une relation plusieurs à plusieurs, il faut donc ajouter une table intermédiaire entre les deux(on appelle aussi table pivot). on crée la migration :

php artisan make:migration category_post_table

et dans le fichier générer on créer les deux champs étrangères:

  • category_id : pour référencer la catégorie
  • post_id : pour référencer l’article
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CategoryPostTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('category_post', function(Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
            $table->foreignId('post_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('category_post');
    }
}

Au niveau des models, On ajoute la relation dans Category : App/Models/Category.php

public function posts()
{
    return $this->belongsToMany(Post::class);
}

Et dans Post :

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

entre commentaires, articles et utilisateurs

Dans le fichier de migration de commentaires, on ajoute les deux clés étrangères:

  • user_id : pour référencer l'utilisateur
  • post_id : pour référencer l’article

database/migrations/*****_create_comments_table.php

public function up()
{
    Schema::create('comments', function(Blueprint $table) {
        ...
        $table->foreignId('user_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
       $table->foreignId('post_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
    });
}

Dans le model Comment, on ajoute: App/Models/Comment.php

protected $fillable = [
        ...
        'post_id',          
        'user_id',
];

public function user()
{
    return $this->belongsTo(User::class);
}

public function post()
{
    return $this->belongsTo(Post::class);
}

Dans le model User:

public function comments()
{
    return $this->hasMany(Comment::class);
}

Dans le model Post:

public function comments()
{
    return $this->hasMany(Comment::class);
}

Pour nous assurer d'avoir les mêmes fichiers, Faisons un petit récap de ce à quoi ressemble nos modèles et migrations avant de lancer la migration vers la base des données:

Les utilisateurs App/Models/User.php

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var string[]
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'role', 
        'valid',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }


}

database/migrations/*****_create_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->enum('role', array('user', 'admin'))->default('user');  //ajout
            $table->boolean('valid')->default(true);
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Les catégories App/Models/Category.php

<?php

namespace App\Models;

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

class Category extends Model
{
    protected $fillable = [
        'title', 
        'slug',
    ];

    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }


    public $timestamps = false;
}

database/migrations/*****_create_categories_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('title', 25)->unique();
            $table->string('slug')->unique();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('categories');
    }
}

Les articles App/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Post extends Model
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'title', 
        'slug', 
        'overview', 
        'body', 
        'active', 
        'image', 
        'user_id',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

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

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

}

database/migrations/*****_create_posts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('overview');
            $table->text('body');
            $table->boolean('active')->default(false);
            $table->string('image')->nullable();
            $table->foreignId('user_id')
              ->constrained()
              ->onDelete('cascade')
              ->onUpdate('cascade');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

Les commentaires App/Models/Comment.php

<?php

namespace App\Models;

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

class Comment extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'body',
        'post_id',          
        'user_id',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

database/migrations/*****_create_comments_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->text('body');
            $table->foreignId('user_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
            $table->foreignId('post_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

La table pivot category_post database/migrations/*****_create_category_post_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CategoryPostTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('category_post', function(Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
            $table->foreignId('post_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('category_post');
    }
}

Maintenant que nous avons toutes les migrations nécessaires lançons la migration pour s'assurer que tout se passe bien:

php artisan migrate

Capture d’écran du 2021-10-15 03-29-22.png

vous devez obtenir ces tables:

Capture d’écran du 2021-10-15 03-30-03.png

Assurez-vous d'avoir les bons champs dans chaque table.

B. LES DONNÉES: LA POPULATION

Pour nos essais on va avoir besoin de données dans les tables, laravel dispose d'une bibliothèque permettant de générer du fake data selon le type et la quantité souhaitée, nous allons utiliser ces bibliothèques pour avoir des données des tests sans avoir à les saisir manuellement(un gros taffe archaïque ouais).

Les classes de la population se trouvent dans le dossier databases/seeds:

Capture-79.png

Lorsqu’on installe Laravel on dispose de la classe DatabaseSeeder avec ce code :

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
    }
}

Dans la fonction run on met tout le code qu’on veut pour remplir les tables. Dans cet article on va garder une seule classe pour remplir toutes les tables, ça nous évite d'avoir un article très long. On se retrouve avec ce code dans le seeder: databases/seeds/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Post;
use App\Models\User;
use App\Models\Comment;
use App\Models\Category;
use Illuminate\Support\Str;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Faker\Generator;
use Illuminate\Container\Container;

class DatabaseSeeder extends Seeder
{
    /**
     * The current Faker instance.
     *
     * @var \Faker\Generator
     */
    protected $faker;

    /**
     * Create a new seeder instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->faker = $this->withFaker();
    }

    /**
     * Get a new Faker instance.
     *
     * @return \Faker\Generator
     */
    protected function withFaker()
    {
        return Container::getInstance()->make(Generator::class);
    }

    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // utilisateurs
        // 5 utilisateurs simples
        foreach (range(1, 5) as $i) {
            User::create(
                [     
                    'name' => $this->faker->name(),
                    'email' => $this->faker->unique()->safeEmail(),
                    'email_verified_at' => now(),
                    'password' => '$2y$10$92IX',
                    'remember_token' => Str::random(10),
                    'valid' => true,
                ]
            );
        } 

        // 3 utilisateurs administrateur
        foreach (range(1, 3) as $i) {
            User::create(
                [
                    'name' => $this->faker->name(),
                    'email' => $this->faker->unique()->safeEmail(),
                    'email_verified_at' => now(),
                    'password' => Hash::make('$2y$10$92IX'),
                    'remember_token' => Str::random(10),
                    'valid' => true,
                    'role' => 'admin',
                ]
            );
        }

        // catégories
        Category::insert([
            [
                'title' => 'Category 1',
                'slug' => 'category-1'
            ],
            [
                'title' => 'Category 2',
                'slug' => 'category-2'
            ],
            [
                'title' => 'Category 3',
                'slug' => 'category-3'
            ],
        ]);

        // les articles
        foreach (range(1, 9) as $i) {
            Post::create([
                'title' => 'Post ' . $i,
                'slug' => 'post-' . $i,
                'overview' => $this->faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                'body' => $this->faker->paragraphs($nb = 8, $asText = true),
                'active' => true,
                'user_id' => 3,
                'image' => 'img0' . $i . '.jpg',
            ]);
        }
        // ici on considere qu'on a 9 images qui s’appellent img00, img01, img02…

        // on attache les categories aux articles
        $posts = Post::all();
        foreach ($posts as $post) {
            if ($post->id === 9) {
                DB::table('category_post')->insert([
                    'category_id' => 1,
                    'post_id' => 9,
                ]);
            } else {
                $numbers = range(1, 3);
                shuffle($numbers);
                $n = rand(1, 2);
                for ($i = 0; $i < $n; ++$i) {
                    DB::table('category_post')->insert([
                        'category_id' => $numbers[$i],
                        'post_id' => $post->id,
                    ]);
                }
            }
        }

        // les commentaires
        foreach (range(1, 8) as $i) {
            Comment::create([
                'body' => $this->faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                'post_id' => $i,
                'user_id' => rand(1, 5),
            ]);
        }
    }
}

Si vous n’êtes pas alaise avec la population(seeding) des base des données sous laravel, je vous recommande de consulter la documentation pour vous y former. Notez ici l'utilisation de faker pour générer du fake data, et la non utilisation de factory.

Il ne reste plus qu’à lancer la population :

php artisan db:seed

Il se peut que vous ayez des soucis avec certains champs de la table users, dans ce cas nous allons réactualiser nos table en exécutant: php artisan migrate:refresh

Vérifiez que dans votre base des données des nouvelles données ont été ajoutées et rendez-vous à la partie 3.