Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Créer un blog - les données
Samedi 30 janvier 2021 14:58

On a vu dans le précédent article la présentation générale du projet de blog et on a aussi créé un Laravel 8 de base avec l'authentification avec Laravel Breeze. On va maintenant se lancer et on va commencer par s'intéresser aux données. Que va contenir notre base ? Quelles tables nous sont nécessaires avec quelles colonnes ? Que va-t-on avoir comme relations ? Quelle sont les données de populations qu'on va générer pour que notre blog fonctionne dès le départ ? Ce sont les bonnes questions à se poser quand on démarre un projet, même si ça évolue forcément en cours de route

Vous pouvez télécharger le code final de cet article ici.

Les données de base de Laravel

Pour le moment avec notre Laravel de base on a ces migrations :

Donc :

  • une migration pour la table users
  • une migration pour la table password_resets pour le renouvellement des mots de passe
  • une migration pour la table failed_jobs pour les tâches qui ont échoué

Comme on va utiliser le système de notifications de Laravel pour notre blog (on va même un peu le détourner) on va ajouter la migration correspondante :

php artisan notifications:table

Les utilisateurs

Par défaut la table users comporte ces colonnes :
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();
});
On va avoir besoin d'autres informations :
  • role : pour savoir quelles sont les permissions de l'utilisateur : administrateur, rédacteur ou simple utilisateur. Ce n'est pas la peine d'utiliser un package pour ce genre de chose.
  • valid : pour valider un utilisateur au niveau des commentaires, le premier commentaire d'un utilisateur n'apparaît pas tout de suite, il faut que ce soit validé par l'administrateur ou l'auteur de l'article, les commentaires suivants par contre apparaitront tout de suite, c'est une façon d'éviter les trolls.
On ajoute donc ces deux colonnes :
Schema::create('users', function (Blueprint $table) {
    ...
    $table->enum('role', array('user', 'redac', 'admin'))->default('user');
    $table->boolean('valid')->default(false);
    ...
});

On a déjà un modèle User. Toutefois on va ajouter les deux colonnes qu'on a créées dans la propriété $fillable :

protected $fillable = [
    'name',
    'email',
    'password',
    'role', 
    'valid',
];
C'est facile à oublier et ensuite on cherche le bug un bon moment :)

On dispose déjà d'un factory pour cette table (database/factories/UserFactory.php). On va juste le compléter pour mettre la colonne valid à true :

return [
    'name' => $this->faker->name,
    'email' => $this->faker->unique()->safeEmail,
    'email_verified_at' => now(),
    'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
    'remember_token' => Str::random(10),
    'valid' => true,
];

Les catégories

Les articles vont être organisées en catégories, on va donc créer une table pour mémoriser ces informations. On va créer simultanément le modèle et la migration :

php artisan make:model Category -m
Pour les catégories on va avoir besoin de deux informations :
  • title : le titre
  • slug : c'est le texte qui va apparaître dans l'url et qui ne doit comporter que des caractères autorisés

D'autre part on ne va pas utiliser les timestamps, quel intérêt de savoir quand une catégorie a été créée ou modifiée ? On a donc :

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

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

    public $timestamps = false;
}

Les étiquettes (tags)

Les articles vont être aussi identifiables avec des étiquettes, ce qui permet une sélection transversale alors que les catégories créent une organisation verticale. On crée le modèle et la migration :

php artisan make:model Tag -m
Pour les étiquettes on va avoir besoin de deux informations comme pour les cétégories :
  • tag : le titre
  • slug : c'est le texte qui va apparaître dans l'url et qui ne doit comporter que des caractères autorisés
D'autre part on ne va pas non plus utiliser les timestamps, quel intérêt de savoir quand une étiquette a été créée ou modifiée ? On a donc :
Schema::create('tags', function(Blueprint $table) {
    $table->id();
    $table->string('tag', 50)->unique();
    $table->string('slug')->unique();
});
Pour le modèle on va aussi supprimer le trait HasFactory qui ne nous servira pas et on crée la propriété $fillable :
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model {

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

    public $timestamps = false;
}

Les articles (posts)

Là c'est le gros morceau évidemment ! On crée le modèle, la migration et le factory :
php artisan make:model Post -mfc
  On va avoir besoin de pas mal d'informations :
  • title : le titre
  • slug
  • seo_title : le titre pour le SEO
  • meta_keywords : les mots clés pour le SEO
  • meta_description : la description pour le SEO
  • excerpt : le texte d'introduction (celui qui apparaît en résumé sur la page d'accueil)
  • body : le corps de l'article
  • active : pour savoir si l'article est publié
  • image : pour le nom de l'image réduite pour la page d'accueil
 
Schema::create('posts', function(Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->string('title');
    $table->string('slug')->unique();
    $table->string('seo_title')->nullable();
    $table->text('excerpt');
    $table->text('body');
    $table->text('meta_description');
    $table->text('meta_keywords');
    $table->boolean('active')->default(false);
    $table->string('image')->nullable();
});
Pour le factory on va prévoir le remplissage automatique de quelques colonnes :
public function definition()
{
    return [
        'meta_description' => $this->faker->sentence($nbWords = 6, $variableNbWords = true),
        'meta_keywords' => implode(',', $this->faker->words($nb = 3, $asText = false)),
        'excerpt' => $this->faker->paragraph($nbSentences = 4, $variableNbSentences = true),
        'body' => $this->faker->paragraphs($nb = 8, $asText = true),
        'active' => true,
    ];
}
On gèrera titre, slug et image directement dans la population. Pour le modèle on va créer la propriété $fillable et aussi prévoir le trait Notifiable :
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Events\ModelCreated;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class Post extends Model
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'title', 
        'slug', 
        'seo_title', 
        'excerpt', 
        'body', 
        'meta_description', 
        'meta_keywords', 
        'active', 
        'image', 
        'user_id',
    ];
}

Les relations

Arrivé à ce stade il est temps de s'intéresser aux relations. On va avoir :
  • un utilisateur peut avoir plusieurs articles (et réciproquement un article appartient à un seul utilisateur)
  • une catégorie peut avoir plusieurs articles et réciproquement un article peut appartenir à plusieurs catégories
  • un article peut avoir plusieurs étiquettes et une étiquette peut appartenir à plusieurs articles

Un utilisateur a plusieurs articles

Il faut mettre une clé étrangère dans la table posts, donc dans la migration :
public function up()
{
    Schema::create('posts', function(Blueprint $table) {
        ...
        $table->foreignId('user_id')
              ->constrained()
              ->onDelete('cascade')
              ->onUpdate('cascade');
    });
}
J'ai opté pour une cascade en cas de suppression ou de modification. Au niveau des modèles on ajoute la relation dans User :
public function posts()
{
    return $this->hasMany(Post::class);
}
Et dans Post :
public function user()
{
    return $this->belongsTo(User::class);
}

Relation entre catégories et articles

Là c'est une relation many to many, il faut donc une table pivot :
php artisan make:migration category_post_table
<?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');
    }
}
On a évidemment dans cette table deux clés étrangères :
  • category_id : pour référencer la catégorie
  • post_id : pour référencer l'article
On ajoute la relation dans Category :
public function posts()
{
    return $this->belongsToMany(Post::class);
}
Et dans Post :
public function categories()
{
    return $this->belongsToMany(Category::class);
}

Relation entre étiquettes et articles

Là c'est encore une relation many to many, il faut donc une table pivot :
php artisan make:migration post_tag_table
<?php

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

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

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('post_tag');
    }
}
  On a évidemment aussi dans cette table deux clés étrangères :
  • tag_id : pour référencer l'étiquette
  • post_id : pour référencer l'article
On ajoute la relation dans Tag :
public function posts()
{
   return $this->belongsToMany(Post::class);
}
Et dans Post :
public function tags()
{
    return $this->belongsToMany(Tag::class);
}

Les commentaires

Pour les commentaires on a quelque chose d'un peu spécial parce on a un système hiérarchique, on a des commentaires de commentaire, et des des commentaires de commentaire de commentaire, et ainsi de suite. Pour gérer ça on va utiliser le package NestedSet. Il faut d'abord l'installer :

composer require kalnoy/nestedset
On crée le modèle avec factory et migration :
php artisan make:model Comment -mfc

Pour la migration on a pas besoin de grand chose, juste une colonne body pour le texte du commentaire. Par contre on va ajouter les colonnes nécessaires pour la gestion des relations entre les commentaires, mais le package va bien nous aider. En plus il faut relier cette table aux utilisateurs et articles :

<?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');
            $table->nestedSet();
            $table->foreignId('user_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
            $table->foreignId('post_id')
                  ->constrained()
                  ->onDelete('cascade')
                  ->onUpdate('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('comments');
    }
}
  Pour le factory on renseigne juste body :
public function definition()
{
    return [
        'body' => $this->faker->paragraph($nbSentences = 4, $variableNbSentences = true),
    ];
}

Pour le modèle Comment on va ajouter le trait NodeTrait pour le package, et aussi Notifiable. D'autre part on crée la propriété $fillable.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Kalnoy\Nestedset\NodeTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class Comment extends Model
{
    use NodeTrait, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'body', 
        'post_id', 
        'user_id', 
    ];
}
Pour les relations on a :
  • un utilisateur a plusieurs commentaires et un commentaire appartient à un utilisateur
  • un article a plusieurs commentaires et un commentaire appartient à un article
Donc dans User :
public function comments()
{
    return $this->hasMany(Comment::class);
}
Dans Post :
public function comments()
{
    return $this->hasMany(Comment::class);
}

Dans Post on va ajouter une relation spécifique pour récupérer les commentaires valides (en fait les commentaires qui ont été rédigés par un utilisateur validé) :

public function validComments()
{
    return $this->comments()->whereHas('user', function ($query) {
        $query->whereValid(true);
    });
}
Et dans Comment on précise l'appartenance à l'utilisateur et à l'article :
public function user()
{
    return $this->belongsTo(User::class);
}

public function post()
{
    return $this->belongsTo(Post::class);
}
Une petite visualisation de la situation :

La population

Maintenant qu'on a créé les migrations essentielles vous pouvez lancer la migration pour voir si tout se passe bien :

php artisan migrate
Vous devez obtenir ces 11 tables :

Vous pouvez aussi vérifier que vous avez les bonnes colonnes dans chacune de ces tables.

On ajoutera utltérieurement d'autres tables pour les contacts, les pages... Mais pour le moment on va s'en passer et on va faire tourner le blog.

Pour nos essais on va avoir besoin de données dans les tables, on va utiliser pour ça la population (seeder) avec ce fichier :

Pour le moment la fonction run est vide (on a juste un peu de code commenté) :
public function run()
{
    // \App\Models\User::factory(10)->create();
}

Les utilisateurs

On va créer un administrateur, 2 rédacteurs et 3 utilisateurs de base :
use App\Models\{ User, Contact, Post, Comment, Page };
use Illuminate\Support\Facades\DB;

...

User::withoutEvents(function () {
    // Create 1 admin
    User::factory()->create([
        'role' => 'admin',
    ]);
    // Create 2 redactors
    User::factory()->count(2)->create([
        'role' => 'redac',
    ]);
    // Create 3 users
    User::factory()->count(3)->create();
});

$nbrUsers = 6;

J'utilise withoutEvents pour inhiber les événements, pour l'instant on n'en a pas mais on en mettra en place plus tard, du coup ça n'aura pas d'impact sur la population.

Les catégories

On crée 3 catégories :
DB::table('categories')->insert([
    [
        'title' => 'Category 1',
        'slug' => 'category-1'
    ],
    [
        'title' => 'Category 2',
        'slug' => 'category-2'
    ],
    [
        'title' => 'Category 3',
        'slug' => 'category-3'
    ],
]);

$nbrCategories = 3;

Les étiquettes

On prévoit 6 étiquettes :
DB::table('tags')->insert([
    ['tag' => 'Tag1', 'slug' => 'tag1'],
    ['tag' => 'Tag2', 'slug' => 'tag2'],
    ['tag' => 'Tag3', 'slug' => 'tag3'],
    ['tag' => 'Tag4', 'slug' => 'tag4'],
    ['tag' => 'Tag5', 'slug' => 'tag5'],
    ['tag' => 'Tag6', 'slug' => 'tag6']
]);

$nbrTags = 6;

Les articles

On crée 9 articles qu'on attribue aux 2 rédacteurs :
Post::withoutEvents(function () {
    foreach (range(1, 2) as $i) {
        Post::factory()->create([
            'title' => 'Post ' . $i,
            'slug' => 'post-' . $i,
            'seo_title' => 'Post ' . $i,
            'user_id' => 2,
            'image' => 'img0' . $i . '.jpg',
        ]);
    }
    foreach (range(3, 9) as $i) {
        Post::factory()->create([
            'title' => 'Post ' . $i,
            'slug' => 'post-' . $i,
            'seo_title' => 'Post ' . $i,
            'user_id' => 3,
            'image' => 'img0' . $i . '.jpg',
        ]);
    }
});

$nbrPosts = 9;
Pour les images je pars du principe qu'on a des images qui s'appellent img00, img01, img02... On va attacher des étiquettes et des catégories aux articles :
// Tags attachment
$posts = Post::all();
foreach ($posts as $post) {
    if ($post->id === 9) {
        $numbers=[1,2,5,6];
        $n = 4;
    } else {
        $numbers = range (1, $nbrTags);
        shuffle ($numbers);
        $n = rand (2, 4);
    }
    for($i = 0; $i < $n; ++$i) {
        $post->tags()->attach($numbers[$i]);
    }
}

// Set categories
foreach ($posts as $post) {
    if ($post->id === 9) {
        DB::table ('category_post')->insert ([
            'category_id' => 1,
            'post_id' => 9,
        ]);
    } else {
        $numbers = range (1, $nbrCategories);
        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

On commence par créer des commentaires de premier niveau :
foreach (range(1, $nbrPosts - 1) as $i) {
    Comment::factory()->create([
        'post_id' => $i,
        'user_id' => rand(1, $nbrUsers),
    ]);
}
Pour les commentaires des autres niveaux le package permet d'utiliser des tableaux, ce qui est bien pratique :
$faker = \Faker\Factory::create();

Comment::create([
    'post_id' => 2,
    'user_id' => 3,
    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

    'children' => [
        [
            'post_id' => 2,
            'user_id' => 4,
            'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

            'children' => [
                [
                    'post_id' => 2,
                    'user_id' => 2,
                    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                ],
            ],
        ],
    ],
]);

Comment::create([
    'post_id' => 2,
    'user_id' => 6,
    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

    'children' => [
        [
            'post_id' => 2,
            'user_id' => 3,
            'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),
        ],
        [
            'post_id' => 2,
            'user_id' => 6,
            'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

            'children' => [
                [
                    'post_id' => 2,
                    'user_id' => 3,
                    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                
                    'children' => [
                        [
                            'post_id' => 2,
                            'user_id' => 6,
                            'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

Comment::create([
    'post_id' => 4,
    'user_id' => 4,
    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

    'children' => [
        [
            'post_id' => 4,
            'user_id' => 5,
            'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),

            'children' => [
                [   'post_id' => 4,
                    'user_id' => 2,
                    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                ],
                [
                    'post_id' => 4,
                    'user_id' => 1,
                    'body' => $faker->paragraph($nbSentences = 4, $variableNbSentences = true),
                ],
            ],
        ],
    ],
]);
Il ne reste plus qu'à lancer la population :
php artisan db:seed
Vérifiez dans vos tables que vous obtenez bien les données attendues.

Conclusion

On dispose maintenant des tables et des données pour commencer à coder notre frontend, ce que nous feraons dans le prochain article. Il faudra aussi s'occuper des medias avec un nouveau package.



Par bestmomo

Nombre de commentaires : 51