Laravel 11

Sillo – les données

Dans un article précédent, j’ai présenté mon projet de migration de la plateforme de mon blog actuel. Dans mes projets antérieurs, j’ai toujours démontré l’évolution progressive d’une application. Cette fois-ci, cependant, nous allons procéder différemment. Le code est déjà disponible en entier sur GitHub, mais il est destiné à être modifié et amélioré au fil du temps. Mon objectif est de rendre ce projet interactif en encourageant certains de mes lecteurs à participer activement à sa création. J’ai donc eu l’idée d’écrire une série d’articles sur les principales caractéristiques de ce projet pour inciter au dialogue et à la contribution.

Commençons dès maintenant en examinant les données. Que contiendra notre base de données ? Quels tables avec quelles colonnes nous seront nécessaires ? Quels types de relations devrons-nous créer ? Quelle sera la structure de la base de données pour que notre application fonctionne dès son lancement ? Il est essentiel de réfléchir à ces questions au début d’un projet, même si nous nous attendons à des évolutions au fil du temps.

Les utilisateurs

Voici la migration pour la table users :

public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->string('password');
        $table->enum('role', array('user', 'redac', 'admin'))->default('user');
        $table->boolean('valid')->default(false);
        $table->rememberToken();
        $table->timestamps();
    });

    Schema::create('password_reset_tokens', function (Blueprint $table) {
        $table->string('email')->primary();
        $table->string('token');
        $table->timestamp('created_at')->nullable();
    });

    Schema::create('sessions', function (Blueprint $table) {
        $table->string('id')->primary();
        $table->foreignId('user_id')->nullable()->index();
        $table->string('ip_address', 45)->nullable();
        $table->text('user_agent')->nullable();
        $table->longText('payload');
        $table->integer('last_activity')->index();
    });
}

Au colonnes de base déjà créées par laravel on ajoute 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 dispose déjà d’un factory pour cette table (database/factories/UserFactory.php). On le compléte pour mettre la colonne valid à true pour que notre population génère des utilisateurs déjà validés :

return [
    'name' => fake()->name(),
    'email' => fake()->unique()->safeEmail(),
    'password' => static::$password ??= Hash::make('password'),
    'remember_token' => Str::random(10),
    'valid' => true,
];

On pourra évidemment en dévalider pour tester le code correspondant.

Les catégories

Les articles vont être organisées en catégories, on a donc une table pour mémoriser ces informations. 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();
});

Les séries

Une série est une suite d’articles qui composent un même thème. L’intérêt principal est de générer une navigation au niveau du frontend. Voici la table :

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

    $table->foreignId('category_id')
            ->constrained()
            ->onDelete('cascade')
            ->onUpdate('cascade');

    $table->foreignId('user_id')
            ->constrained()
            ->onDelete('cascade')
            ->onUpdate('cascade');  
});

Une série appartient à un auteur et à une catégorie.

Les articles (posts)

Là c’est le gros morceau évidemment !

On va avoir besoin de pas mal d’informations :

  • title : le titre
  • slug
  • body : le corps de l’article
  • active : pour savoir si l’article est publié
  • image : pour le lien de l’image pour la page d’accueil
  • seo_title : le tire de la page
  • meta_description : description de la page (SEO)
  • meta_keywords : mots clefs de la page (SEO)
Schema::create('posts', function(Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body');
    $table->boolean('active')->default(false);
    $table->string('image')->nullable();
    $table->string('seo_title');
    $table->text('meta_description');
    $table->text('meta_keywords');

    $table->foreignId('user_id')
        ->constrained()
        ->onDelete('cascade')
        ->onUpdate('cascade');

    $table->foreignId('category_id')
        ->constrained()
        ->onDelete('cascade')
        ->onUpdate('cascade');

    $table->unsignedBigInteger('serie_id')->nullable()->default(null);
    $table->foreign('serie_id')
            ->references('id')
            ->on('series')
            ->nullable()
            ->default(null)
            ->onDelete('cascade');

    $table->unsignedBigInteger('parent_id')->nullable()->default(null);
    $table->foreign('parent_id')
            ->references('id')
            ->on('posts')
            ->nullable()
            ->default(null);                   
});

Un article appartient à un auteur, une catégorie et optionnellement à une série. Il peut aussi appartenir à un autre article dans le cas d’une série, en effet il nous faut connaître l’enchainement des articles dans la série.

Les relations

Faisons un petit point des relations jusque-là :

Les pages

Les pages sont du contenu non lié à un auteur, une catégorie ou une série. basiquement c’est pour des pages « à propos », « Politique de confidentialité »…

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

Ce sont donc des articles épurés de toute relation ou appartenance.

Les commentaires

Pour les commentaires, on a quelque chose d’un peu spécial parce que on a un système hiérarchique, on a des commentaires de commentaire, et des commentaires de commentaire de commentaire, et ainsi de suite. Il y a plusieurs façons de gérer cela et il existe des packages pour nous aider. J’ai plutôt opté pour une solution simple sans package. Voici la table :

Schema::create('comments', function(Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->text('body');
    $table->foreignId('user_id')
            ->constrained()
            ->onDelete('cascade');
    $table->foreignId('post_id')
            ->constrained()
            ->onDelete('cascade');
    $table->unsignedBigInteger('parent_id')->nullable()->default(null);
    $table->foreign('parent_id')
            ->references('id')
            ->on('comments')
            ->nullable()
            ->default(null)
            ->onDelete('cascade');
});

Au niveau des relations, on a :

  • un commentaire appartient à un auteur
  • un commentaire appartient à un article
  • un commentaire peut appartenir à un commentaire (c’est son enfant) ou être une racine (c’est un parent d’origine)

Les relations

Un autre petit point des relations :

Les menus

Pour avoir une navigation dynamique, on a une table pour les menus :

Schema::create('menus', function (Blueprint $table) {
    $table->id();
    $table->string('label');
    $table->string('link')->nullable();
    $table->integer('order');
});

On a :

  • un label (le texte visible)
  • un lien optionnel (sans lien ça veut dire que c’est juste un déclencheur pour un sous-menu)
  • un ordre (on doit pouvoir organiser l’ordre des menus)

On a donc aussi une table pour les sous-menus :

Schema::create('submenus', function (Blueprint $table) {
    $table->id();
    $table->string('label');
    $table->integer('order');
    $table->string('link')->default('#');
    $table->foreignId('menu_id')->constrained()->onDelete('cascade');
});

On trouve la même chose que pour la table précédente à par que :

  • on a forcément un lien
  • un sous-menu appartient à un menu

J’ai aussi prévu des liens en bas de page, avec la table footers :

Schema::create('footers', function (Blueprint $table) {
    $table->id();
    $table->string('label');
    $table->string('link');
    $table->integer('order');
});

On a exactement la même chose que pour les menus à part qu’il y a forcément un lien parce qu’on a pas de sous-menu.

Au niveau relations c’est simple :

Les contacts

Les visiteurs peuvent laisser un message à l’aide d’un formulaire, la table contacts est là pour mémoriser l’auteur du message et le message lui-même :

Schema::create('contacts', function(Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->unsignedBigInteger('user_id')->nullable()->default(null);
    $table->string('name');
    $table->string('email');
    $table->text('message');
    $table->boolean('handled')->default(false);
});

On prévoit que le message puisse appartenir à un utilisateur enregistré. On a aussi une colonne booléenne handled qui nous indique si le contact a été pris en compte.

La population

J’ai prévu une population qui remplit toutes les tables avec des informations suffisantes pour que le site soit utilisable facilement.

Conclusion

Je suis évidemment ouvert à toute discussion au sujet de cette organisation des données. Faut-il prévoir autre chose ? Remanier certaines données ?

Print Friendly, PDF & Email

Laisser un commentaire