Réalisation d’une galerie photo avec Laravel 12 – Les bases de données avec Eloquent (Partie 3)

Dans les deux premières parties (Template Blade, Routes), nous avons mis en place notre projet Laravel et créé une structure de page solide avec les composants Blade. Maintenant, il est temps de donner vie à notre galerie en la connectant à une base de données. Laravel rend cette tâche incroyablement simple et élégante grâce à Eloquent, son ORM (Object-Relational Mapper).

  • Concept Clé : Qu’est-ce qu’un ORM ? Un ORM, comme Eloquent, permet de dialoguer avec des bases de données en utilisant des objets et des méthodes PHP simples et intuitives. Au lieu d’écrire SELECT * FROM galleries, nous écrirons quelque chose de bien plus lisible comme Gallery::all(). Eloquent se charge de la traduction en SQL. C’est propre, sûr et agréable !
Structure de base de données
Structure de base de données

1. Les Migrations : L’architecture de ta base de données

Une migration est un fichier PHP qui décrit une modification à apporter à notre base de données : créer une table, ajouter une colonne, la supprimer, etc.

Pour créer ces fichier nous utilisons Artisan comme d’habitude avec Laravel. En général, nous utiliserons également un Model et un Controller pour communiquer avec la base de données et nos vues (nous reviendrons sur ce concept plus tard). Artisan offre une commande toute prête pour créer la migration, le modèle et le controller en même temps (nous n’aurons pas besoin de controller pour les Photos et les Tags) :

php artisan make:model Gallery --controller --migration
php artisan make:model Photos --migration
php artisan make:model Articles --controller --migration
php artisan make:model Tags --migration
php artisan make:migration create_table_article_tag

Les Model sont nommés au singulier sous la forme CamelCase, les base de données au pluriel et sous la forme snak_case. Avec les commandes ci-dessus, Laravel va automatiquement créer les fichiers nécessaires. A savoir :

  • Les Model dans le dossier app/Models
  • Les Controller dans le dossier app/Http/Controller
  • Les Migrations dans le dosier databases/migrations

Les fichiers migrations contiennent 2 fonctions :

  • up() : avec les instructions nécessaires à la création ou la modification des tables
  • down() : avec les instructions nécessaires pour revenir en arrière, c’est à dire supprimer la table si elle vient d’être créé ou les modifications contraires.

Voici le contenu de tout nos fichiers :

// === dans database/migrations/xxxx_xx_xx_xxxxxx_create_galleries_table.php

    public function up(): void
    {
        //
        Schema::create("galleries", function (Blueprint $table) {
            $table->id();
            $table->string("name");
            $table->string("slug")->unique();
            $table->text("description")->nullable();
            $table->timestamps();
        });

    }

    public function down(): void
    {
        //
        Schema::dropIfExists("galleries");
    }

// === dans database/migrations/xxxx_xx_xx_xxxxxx_create_photos_table.php

    public function up(): void
    {
        //
        Schema::create("photos", function (Blueprint $table) {
            $table->id();
            $table->foreignId('gallery_id')->constrained()->cascadeOnDelete();
            $table->string("title");
            $table->string("slug")->unique();
            $table->text("description")->nullable();
            $table->string("filename");
            $table->timestamps();
        });
    }

    public function down(): void
    {
        //
        Schema::dropIfExists("photos");
    }

// === dans database/migrations/xxxx_xx_xx_xxxxxx_create_articles_table.php

    public function up(): void
    {
        //
        Schema::create("articles", function (Blueprint $table) {
            $table->id();
            $table->string("title");
            $table->string("slug")->unique();
            $table->text("content");
            $table->string("featured_image")->nullable();
            $table->string("status", 15)->default("draft");
            $table->timestamps();
        });

    }

    public function down(): void
    {
        //
        Schema::dropIfExists("articles");
    }

// === dans database/migrations/xxxx_xx_xx_xxxxxx_create_tags_table.php

    public function up(): void
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->string('slug');
            $table->timestamps();
        });
    }

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

// === dans database/migrations/xxxx_xx_xx_xxxxxx_create_article_tag_table.php

    public function up(): void
    {
        Schema::create('article_tag', function (Blueprint $table) {
            $table->foreignId("article_id")->constrained()->cascadeOnDelete();
            $table->foreignId("tag_id")->constrained()->cascadeOnDelete();
            $table->primary(["article_id","tag_id"]);
        });
    }

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

Explications :

  • Schema::create : pour créer une table, si nous devions faire une mise à jour nous utiliserions Schema::table
  • ->id() : crée une colonne id avec auto_increment et primary index
  • ->unique() : crée un index unique
  • ->nullable() : cette colonne peut être null
  • ->foreignId()->constrained() : référence à une clé étangère. En respectant les normes de nommage de Laravel, Eloquent sait qu’il s’agit de la table galleries et de la colonne id
  • ->cascadeOnDelete() : si l’élément id de galleries est supprimé, cet enregistrement l’est aussi. On pourrait aussi avoir cascadeOnUpdate, nullOndelete ou nullOnUpdate (la colonne doit être nullable dans les 2 derniers cas)
  • ->timestamps() : cré les 2 colonnes updated_at et created_at
  • ->primary() : clé primaire pour cette ou ces colonnes
  • Schema::dropIfExists('article_tag') : action de retour, supprime la table si elle existe

Lançons la migration !

Maintenant que nos plans sont prêts, il est temps de construire. Exécute cette commande pour que Laravel crée les tables dans ta base de données selon les fichiers de migration que nous venons de créer :

php artisan migrate

Ta base de données contient maintenant les tables galleries et photos ! Si tu fais une erreur, tu peux annuler la dernière migration avec php artisan migrate:rollback (fonction down) on peut aussi préciser le nombre de fichier à exécuter pour revenir en arrière avec --step=X.

2. Les Relations

Les tables peuvent avoir plusieurs types de relations entre elles. Cela peut-être :

  • 1 … 1 (one-to-one) : pas utilisé dans notre site, mais par exemple une personne a une adresse et une adresse n’appartient qu’à une personne.
  • 1 … n (one-to-many) : une galerie a plusieurs photos.
  • n … n (many-to-many) : un article peut avoir plusieurs tags et un tag peut être sur plusieurs articles.

Pour mettre en place ces relations, nous devons les spécifier dans les Model de nos tables :

  • Les galeries ont plusieurs photo (hasMany) et les photos appartiennent à une galerie (belongsTo)
  • Les articles ont plusieurs tags (belongsToMany) et les tags appartiennent à plusieurs articles (belongsToMany).
// ===  app/Models/Gallery.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Gallery extends Model
{
    use HasFactory;
    protected $fillable = ['name', 'slug', 'description'];

    /**
     * Une galerie possède plusieurs photos.
     */
    public function photos(): HasMany
    {
        return $this->hasMany(Photo::class);
    }
}

// === Dans app/Models/Photo.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Photo extends Model
{
    protected $fillable = [
        'gallery_id', 'title', 'slug', 'description', 'filename',
    ];
    public function gallery(): BelongsTo
    {
        return $this->belongsTo('Gallery::class');
    }
}

// === Dans app/Models/Tag.php

namespace App\Models;

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

class Tag extends Model
{
    protected $fillable = ['name', 'slug', 'color'];

    public function articles() : BelongsToMany
    {
        return $this->belongsToMany('Article::class');
    }
}

// === Dans app/Models/Article.php

namespace App\Models;

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

class Article extends Model
{
    protected $fillable = [
        'title', 'slug', 'content', 'featured_image', 'status',
    ];
    
    public function tags() : BelongsToMany
    {
        return $this->belongsToMany('Tag::class');
    }
}

3. Données de test

Notre base de données est vide. Il est possible de la remplir avec des données factices afin de développer notre interface. Nous pouvons les faire avec les Factories de Laravel.

php artisan make:factory GalleryFactory --model=Gallery
php artisan make:factory PhotoFactory --model=Photo
php artisan make:factory ArticleFactory --model=Article
php artisan make:factory TagFactory --model=Tag

Puis dans chaque fichiers crées dans app/factories :

// === database/factories/TagFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class TagFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $name = $this->faker->unique()->word;
        return [
            'name' => $name,
            'slug' => Str::slug($name),
        ];
    }
}

// === database/factories/GalleryFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class GalleryFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $name = $this->faker->sentence(3);
        return [
            'name' => $name,
            'slug' => Str::slug($name),
            'description' => $this->faker->paragraph,
        ];
    }
}

// === database/factories/ArticleFactory.php

namespace Database\Factories;

use App\Models\Article; // Importez Article
use App\Models\Tag;     // Importez Tag
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class ArticleFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $title = $this->faker->sentence(6);
        return [
            'title' => $title,
            'slug' => Str::slug($title),
            'content' => $this->faker->paragraphs(3, true),
            'featured_image' => "https://picsum.photos/640/480?random=" . rand(1, 1000),
            'status' => $this->faker->randomElement(['published', 'draft', 'pending']),
        ];
    }

    /**
     * Configure the model factory.
     *
     * @return $this
     */
    public function configure()
    {
        return $this->afterCreating(function (Article $article) {
            // Récupère 1 à 3 tags au hasard qui existent déjà en BDD
            $tags = Tag::inRandomOrder()->take(rand(1, 3))->pluck('id');
            
            // Attache ces tags à l'article (remplit la table pivot article_tag)
            $article->tags()->attach($tags);
        });
    }
}

// === database/factories/PhotoFactory.php

namespace Database\Factories;

use App\Models\Gallery; // N'oubliez pas d'importer le modèle Gallery
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class PhotoFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $title = $this->faker->sentence(4);
        return [
            'gallery_id' => Gallery::factory(), // Relation par défaut
            'title' => $title,
            'slug' => Str::slug($title),
            'description' => $this->faker->sentence,
            'filename' => "https://picsum.photos/640/480?random=" . rand(1, 1000), // Utilise une URL d'image fictive
        ];
    }
}

Maintenant il faut éditer le fichier principal DatabaseSeeder dans database/seeders et donner l’ordre d’exécution des factories (l’ordre est important car ArticleFactory a besoin de TagFactory).

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
// Importez vos modèles
use App\Models\Gallery;
use App\Models\Photo;
use App\Models\Article;
use App\Models\Tag;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // 1. Créer 10 tags
        Tag::factory(10)->create();

        // 2. Créer 5 galeries. Pour chaque galerie, créer 8 photos associées.
        // La méthode has() s'assure que le 'galerie_id' des photos
        // est correctement défini sur la galerie qui vient d'être créée.
        Gallery::factory(5)
            ->has(Photo::factory()->count(8))
            ->create();

        // 3. Créer 30 articles.
        // La factory (via la méthode configure/afterCreating)
        // se chargera d'attacher des tags (créés à l'étape 1) à chaque article.
        Article::factory(30)->create();
    }
}

Une dernière chose avant de semer nos graines. Il faut spécifier dans nos Model qu’ils disposent d’une factory. Dans chacun des fichiers Tag.php, Article.php, Photo.php et Gallery.php dans app/Model il faut faire ces 2 modifications :

namespace App\Models;

// === 1 === Ajouter HasFactory
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Article extends Model
{
    use HasFactory;  // === 2 === Utilisr HasFactory.
    /*
    Reste du code inchangé
    */

Maintenant, pour lancer toutes les migrations et exécuter les seeders en une seule commande :

php artisan migrate:fresh --seed

Voilà c’est parti ! Un site fonctionnel avec des données de test. Tu peux maintenant finaliser ton design. La commande php artisan migrate:fresh permettra de supprimer toutes les tables et de réinstaller les migrations depuis le début.

Dans le prochain article, nous mettrons tout cela en musique : nous allons utiliser nos Model et Controller pour récupérer ces données avec Eloquent et les afficher dans nos vues Blade.

Publié le 13 octobre 2025

Laisser une réponse

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *