Les bases 1/6 : Création du modèle

Tutoriels
Publié par William

Contexte

La fonctionnalité que nous allons développer dans cette suite d’articles est la gestion de restaurants.

Un utilisateur authentifié pourra créer ses restaurants, les éditer et les supprimer. Il sera le seul, avec les admins, à pouvoir le faire sur les siens.

Nous allons passer en revue aussi bien l’élaboration du modèle de données, que la validation des entrées du formulaire, les droits, le contrôleur, les vues et en conclusion les tests.

Sommaire

  1. Création du modèle
  2. Création du contrôleur
  3. Validation des données
  4. Contrôle d’accès
  5. Création des vues
  6. Tests

Le diagramme

L’élaboration d’un diagramme, aussi basique soit-il, a plusieurs vertus.

Il permet de se poser et de réfléchir à la manière dont nos données vont être stocké et des liens qui vont exister entre nos tables. Aussi, il permet à une équipe de se documenter et d’avoir une représentation claire de la base, pourvu qu'elle soit à jour !

Dans notre exemple, nous allons partir sur un cas simple :

  • Un utilisateur a un rôle en plus des propriétés de base que propose Laravel
  • Un utilisateur dispose de 0 ou plus restaurants
  • Un restaurant a un nom, une adresse et un type

Voici le diagramme résultant, créé via l’outil dbdiagram.io :

Untitled

Les migrations

Maintenant qu’on a définit, à travers le diagramme, la représentation de nos tables et de leur relation, nous allons créer les migrations résultantes.

Pour rappel, les migrations dans Laravel permettent de définir une instruction qui va modifier la structure de notre base via la méthode up et son inverse via la méthode down.

C’est donc depuis ces classes que nous allons définir la structure de nos tables.

Nous allons commencer par la migration de la table restaurants dont nous allons créer le fichier via la commande :

php artisan make:migration --create restaurants CreateRestaurantsTable 

Cela va nous pré-remplir notre migration, il nous reste plus qu’à compléter avec les champs particuliers de notre table :

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('restaurants', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('address');
            $table->string('type');
            $table->foreignIdFor(\App\Models\User::class);
            $table->timestamps();
        });
    }

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

La grande nouveauté réside dans l’utilisation de la méthode foreignIdFor qui permet de déclarer une clé étrangère sur un modèle. Laravel va donc résoudre depuis le modèle la clé primaire à utiliser, très pratique !

Pour ce qui est de la table users, la migration est livrée à l’installation du projet, or on veut lui ajouter le champ role. Comment faire ? On va partir du postulat qu’elle peut avoir été jouée dans un environnement et que la modifier aurait aucune incidence.

Une migration jouée dans un environnement est marquée en tant que telle et ne peut être rejouée à moins de refresh la base. Ce qui est à prohiber en production !

L’occasion de créer une migration sur une table existante, la table users, via la commande :

php artisan make:migration --table users AddColumnRoleOnUsersTable

Cela nous crée notre fichier de base que l’on complète comme ceci :

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('role');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('role');
        });
    }
};

Les modèles

Maintenant que nous avons nos migrations, nous allons concevoir les modèles liés à ces dernières.

A noter que Laravel nous facilite grandement la vie en respectant ces standards comme le nom des tables au pluriel, le modèle au singulier, la clé primaire id, etc.

Commençons par la création du modèle Restaurant via la commande suivante :

php artisan make:model Restaurant

Encore une fois une commande qui nous fait gagner du temps car il nous reste qu'à compléter le corps de notre classe, ce qui donne :

<?php

namespace App\Models;

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

class Restaurant extends Model
{
    use HasFactory;
    
    protected $fillable = [
        'name', 'address', 'type',    
    ];
    
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Ici rien de particulier ci ce n’est la relation avec le modèle User qui est un belongsTo.

On aurait pu améliorer la gestion du type à travers un Enum : nouveauté de Laravel 9 !

Pour ce qui est du modèle User, il existe déjà, on n’a juste à le compléter comme ceci :

<?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;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

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

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

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

    public function restaurants()
    {
        return $this->hasMany(Restaurant::class);
    }
}

Les factories

A quoi sert une Factory ? Elle permet de définir la nature de la valeur d’une propriété d’un modèle dans le cadre d’un test ou en environnement de développement.

C’est à dire, qu’on va y définir des instructions via l’outil Faker, comme :

  • Le nom d’un utilisateur doit avoir la forme M. Dupond
  • Le contenu de mon article est un Lorem Ipsum de 300 caractères maximum

Voici la commande qui nous permet de créer la factory de notre modèle Restaurant :

php artisan make:factory --model Restaurant RestaurantFactory

Suivi de son contenu pour illustrer le propos :

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Restaurant>
 */
class RestaurantFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'name' => $this->faker->randomElement([
                'Au fin gourmet', "A l'ombre du Sakura", "Slow food", "Pizza Yolo",
            ]),
            'address' => $this->faker->address,
            'type' => $this->faker->randomElement(['Asiatique', 'Tradi', 'Italien'])
        ];
    }
}

Il existe de nombreuses méthodes disponibles via Faker et il est même possible de créer ses propres providers !

L’utilisation de données réalistes permet, lors d’une démo par exemple, de rendre concrètes les interfaces et de faciliter la compréhension et le dialogue avec son publique.

Pour la factory du modèle User, on définit qu'un utilisateur aura le rôle user par défaut et nous allons utiliser les states afin de pouvoir lui donner le rôle admin au besoin comme ceci :

<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
            'role' => 'user',
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     *
     * @return static
     */
    public function unverified()
    {
        return $this->state(function (array $attributes) {
            return [
                'email_verified_at' => null,
            ];
        });
    }

    public function admin()
    {
        return $this->state(function (array $attributes) {
            return [
                'role' => 'admin',
            ];
        });
    }
}

Les seeders

On attaque la dernière étape de ce premier chapitre, les Seeders.

La fonction du seeder est de créer un jeu de données, souvent pour l’environnement de développement ou pour les tests.

Ils se basent sur la définition des factories pour créer ses objets et remplir notre base de données.

Dans notre exemple, on va lui donner l’instruction de créer les données suivantes :

  • Un utilisateur avec le rôle admin et l’email admin@example.net
  • 5 utilisateurs avec le rôle user
  • Pour chacun des utilisateurs, 2 restaurants

Nous allons placer ces instructions dans la classe Database\Seeders\DatabaseSeeder ainsi :

<?php

namespace Database\Seeders;

use App\Models\Restaurant;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        User::factory()->count(1)->admin()->create([
            'email' => 'admin@example.net',
        ]);

        User::factory()->count(5)
            ->has(Restaurant::factory()->count(2))
            ->create();
    }
}

Les sources de ce tutoriel sont disponibles sur ce dépôt.

Dans le prochain épisode…

Le premier chapitre est maintenant terminé, nous y avons mis en place les briques essentielles à la gestion de données pour notre application et aussi pour ses tests.

Le prochain sera axé sur l’utilisation de ces modèles dans un contrôleur !