Projet : Réalisation d’une galerie photo avec Laravel 12 – Les contrôleurs et les vues (Partie 4)

Dans les articles précèdent nous avons vu :

Maintenant il est temps de donner un peu de vie à tout ça avec des Controller. Nous les avions déjà créé avec la commande tout en un php artisan make:model Nom --controller -- migration. Mais il et toujours possible de le faire avec la commande dédiée php artisan make:controller NomController. Ils sont créés dans le dossier app/Http/Controller et nous devrions avoir ArticleController.php, GalleryController.php, PhotoController.php, TagController.php.

On s’occupe de la galerie

Nous allons devoir modifier 3 fichiers pour afficher notre galerie :

  • routes/web.php : pour définir la route (en principe c’est déjà fait, mais on va vérifier)
  • app/Http/Controllers/GalleryController.php : c’est ici qu’on va gérer la logique et les données
  • resources/views/pages/galleries.blabe.php : la page HTML renvoyée aux visiteurs

Dans notre fichier routes/web.php on doit retrouver les 2 lignes ci-dessous. Elles servent à indiquer à Laravel quel Controller utiliser si on affiche l’index de la galerie ou une galerie spécifique :

Route::get('/galleries', [GalleryController::class,'index'])->name('galleries');
Route::get('/gallery/{slug}', [ GalleryController::class, 'index' ])->name('gallery');

Ensuite notre fichier app/Http/Controller/GalleryController.php va chercher toutes les galeries et les renvois à la vue (avec ajout du titre de la page : $title). Si slug n’est pas définit, j’ai choisi d’afficher la première galerie de la liste, mais tu es libre de coder un autre comportement.

namespace App\Http\Controllers;

use App\Models\Gallery;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class GalleryController extends Controller
{
    /**
     * Affiche la liste des galeries et la galerie active.
     * Si aucun slug n'est fourni, redirige vers la première galerie.
     *
     * @param string|null $slug
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
     */
    public function index($slug = null)
    {
        // On va chercher toutes les galeries triées par date de création décroissante.
        $galleries = Gallery::latest()->get();

        // --- CAS 1 : Aucun slug n'est fourni (URL: /galleries) ---
        if (!$slug) {
            // On prend la 1ère galerie de la liste
            $firstGallery = $galleries->first();

            // S'il n'y a AUCUNE galerie en base de données, on redirige vers l'accueil.
            if (!$firstGallery) {
                return redirect('/');
            }

            // S'il y a des galeries, on redirige vers la route 'gallery/slug'
            return redirect()->route('gallery', ['slug' => $firstGallery->slug]);
        }

        // --- CAS 2 : Un slug EST fourni (URL: /gallery/slug) ---

        // On cherche la galerie active.
        // On utilise 'with('photos')' pour charger la relation en même temps (Eager Loading).
        // 'firstOrFail()' lèvera automatiquement une erreur 404 si le slug est introuvable.
        $currentGallery = Gallery::where('slug', $slug)->with('photos')->firstOrFail();

        // Si on arrive ici, le slug est valide.
        // On retourne la vue en lui passant TOUTES les galeries (pour la navigation)
        // et la galerie COURANTE (pour l'affichage principal).
        return view("pages.galleries", [
            "title" => $currentGallery->name . " - Galeries de photo amateur - [SK]Photo",
            "galleries" => $galleries,          // La liste complète pour la navigation
            "currentGallery" => $currentGallery  // La galerie active à afficher (elle contient maintenant les photos)
        ]);
    }
}

Et finalement l’affiche de la vue :

<x-layouts.app :title="$title">

    <section id="page-galleries" class="bg-[#18181a]">
        <div class="container mx-auto px-6">
            <div class="fixed z-50 w-full bg-[#18181a] pt-32">
                <h1 class="py-4 text-5xl font-medium text-white">Galeries</h1>

                <div id="gallery-filters" class="scrollbar-hide relative mt-4 flex cursor-grab select-none flex-nowrap space-x-6 overflow-x-auto border-b border-neutral-700 pb-2 text-xl font-light text-gray-400">

                    <!-- Conteneur pour les filtres de la galerie -->
                @forelse ($galleries as $gallery)
                    <a href="#" class="gallery-filter-link whitespace-nowrap transition-colors hover:text-white">{{ $gallery['name'] }}</a>
                @empty
                    Il n'y a aucune galerie disponible.
                @endforelse


                    <!-- Bordure animée -->
                    <div id="active-filter-border" class="absolute bottom-[-1px] h-[2px] bg-orange-500"></div>
                </div>
            </div>

            <div id="gallery-grid" class="relative z-40 flex flex-wrap gap-4 pt-[300px] md:gap-8">

                @foreach ($gallery['photos'] as $photo)
                    <a href="{{ $photo['filename'] }}" data-title="{{ $photo['title'] }}"
                        data-description="{{ $photo['alt'] }}"
                        class="group relative block h-[200px] flex-grow overflow-hidden rounded-lg">
                        <img src="{{ $photo['filename'] }}" alt="{{ $photo['alt'] }}"
                            class="h-full w-full transform object-cover transition-transform duration-300 group-hover:scale-105">
                        <div
                            class="absolute inset-x-0 bottom-0 translate-y-full transform bg-black/75 p-4 text-white transition-transform duration-300 ease-in-out group-hover:translate-y-0">
                            <h3 class="text-lg font-semibold">{{ $photo['title'] }}</h3>
                        </div>
                    </a>
                @endforeach

            </div>
        </div>


    </section>

</x-layouts.app>

Pour afficher simplement une photo, tu peux appliquer la même logique en modifiant les fichiers :

  • routes/web.php : toujours, pour indiquer le chemin de la page de ta photo
  • app/Http/Controller/PhotoController.php : pour la logique et les données de la photo
  • resources/views/pages/photo.blade.php : pour le HTML

Moi j’ai choisi d’afficher les images avec https://www.lightgalleryjs.com/.

Au tour des articles et des tags

Et bien pour les articles… c’est la même logique! Simple non ?

Alors à ton tour de bosser en éditant les fichiers :

  • routes/web.php : toujours, pour indiquer le chemin de la page de ta photo
  • app/Http/Controller/ArticleController.php : pour la logique et les données des articles
  • resources/views/pages/blog.blade.php et resources/views/pages/article.blade.php : pour le HTML

Mais comme je suis sympa, je te mets le contenu des fichiers ci-dessous.

Fichier routes/web.php :

Route::get('/blog', [ArticleController::class, 'index'])->name('blog');
Route::get('/blog/{slug}', [ArticleController::class, 'show'])->name('article');

app/Http/Controller/ArticleController.php

namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class ArticleController extends Controller
{
    // 'title' => 'Les articles du blog de photographie - [SK]Photo'

    /*
     * Affiche la liste des articles
     */
    public function index()
    {
        $articles = Article::with("tags")->latest()->paginate(10);

        return view("pages.blog", [
            "articles" => $articles,
            "title" => "Les articles du blog de photographie - [SK]Photo"
        ]);

    }

    /*
     * Affiche un article en particulier
     */
    public function show($slug)
    {
        $article = Article::whereSlug($slug)->with("tags")->firstOrFail();
        return view("pages.article", [
            "article" => $article, 
            "title" => $article->title
        ]);
    }
}

resources/views/pages/blog.blade.php


<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">

                @forelse ($articles as $article)
                    <div
                        class="posts-item flex transform flex-col overflow-hidden rounded-xl bg-neutral-800 shadow-lg transition-transform duration-300 hover:-translate-y-2">
                        <a href="{{ $article->url }}" class="block">
                            <img src="{{ $article->featured_image }}" alt="Image de l'article {{ $article->title }}"
                                class="h-48 w-full object-cover">
                        </a>
                        <div class="flex flex-grow flex-col p-6">
                            <div>
                                <div class="flex space-x-4 pb-4">
                                    @forelse ($article->tags as $tag)
                                        <span
                                            style="color: {{ $tag->color }}; background-color: {{ $tag->color }}33;"
                                            class="flex rounded-md px-3 py-1 text-sm font-semibold">
                                            {{ $tag->name }}
                                        </span>
                                    @empty
                                        <p class="text-sm text-gray-500">Pas de tags pour ce post.</p>
                                    @endforelse
                                </div>
                                <h3 class="mb-2 text-xl font-bold leading-tight text-white">
                                    <a href="{{ $article->url }}"
                                        class="transition-colors hover:text-orange-500">{{ $article->title }}</a>
                                </h3>
                                <p class="flex-grow text-base text-gray-400">
                                    {{ Str::words($article->content, 30, '...') }}</p>
                            </div>
                            <div class="pt-6">
                                <p class="text-xs text-gray-500">{{ $article->created_at->format('d.m.Y') }}</p>
                            </div>
                        </div>
                    </div>
                @empty
                    <p class="text-sm text-gray-500">Il n'y a aucune article disponible.</p>

                @endforelse

            </div>

            <!-- Pagination -->
            <div class="py-16">
                {{ $articles->links() }}
            </div>

Il y a une petite particularité dans ce fichier. L’attribut url du Model Article n’existe pas. Nous devons le créer manuellement grâce à un assesseur.

app/Model/Article.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Article extends Model
{
    use HasFactory;

    //
    public function tags() : BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }

    protected function url(): Attribute
    {
        return Attribute::make(
            // La fonction 'get' est appelée quand on fait $photo->url
            get: function ($value, $attributes) {
                return route('article', ['slug' => $attributes['slug']]);;
            }
        );
    }

}

Si tu souhaite customiser la partie pagination fournie par Laravel, tu peux taper cette commande :

php artisan vendor:publish --tag=laravel-pagination

Grâce à cette commande, les fichiers de template de pagination ont été copié dans le dossier resources/views/vendor/pagination, tu peux ensuite les modifier à ta guise. Celui qui nous intéresse est tailwind.blade.php.

Il y a encore une subtilité. Tu auras remarqué que les quelques textes sont en anglais. Pour traduire en français tu devras créer les fichiers resources/lang/fr.json pour les termes génériques et resources/lang/fr/pagination.php pour les éléments propres au template de pagination.

fr.json :

{
    "Showing": "Affichage de",
    "to": "à",
    "of": "sur",
    "results": "résultats"
}

fr/pagination.php :

<?php
return [
'previous' => '&laquo; Précédent',
'next' => 'Suivant &raquo;',
];

Et finalement dans le fichier resources/views/pages/articles.php (pour l’affichage d’un seul article) :

<x-layouts.app :title="$title">


                <h1>{{ $article->title }}</h1>
				{{-- TODO: image à la une --}}
                <p>Publié le
                    {{ $article->created_at->format('d.m.Y') }}</p>
                
            </div>
            <div class="text-xl font-normal text-white">
                {{ $article->content }}
            </div>
			<div class="flex space-x-4 py-12">
				@forelse ($article->tags as $tag)
					<span
						style="color: {{ $tag->color }}; background-color: {{ $tag->color }}33;"
						class="flex rounded-md px-3 py-1 text-sm font-semibold">
						{{ $tag->name }}
					</span>
				@empty
					<p class="text-sm text-gray-500">Pas de tags pour ce post.</p>
				@endforelse
			</div>
    </section>


</x-layouts.app>

Dans le prochain article, on s’attaquera à un gros morceau : le backend. On verra comment mettre en place une interface avec connexion sécurisée pour ajouter du contenu (photo et article) à notre site.

Publié le 24 octobre 2025

Laisser une réponse

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