Laravel 11

Albums – Les images

Notre galerie avance bien. On a désormais des catégories pour classer les photos et des utilisateurs qui peuvent s’authentifier. On a aussi une base de données bien organisée. On va maintenant voir comment ajouter des photos pour remplir notre galerie. Pour le faire, un utilisateur doit être enregistré ou bien administrateur. On va donc créer de nouveaux composants avec Volt et encore utiliser les superbes composants de MaryUI. On conservera les images en deux formats : les dimensions d’origine pour l’affichage en plein écran et une version plus petite pour afficher dans la galerie. Pour créer la version réduite, on va utiliser le superbe plugin Intervention Image. J’ai eu des soucis avec la version 3 au niveau du décodage des images, alors je me suis limité à la version 2 qui fonctionne très bien.

La gestion des fichiers de Laravel

Laravel offre de puissantes fonctionnalités pour la gestion des fichiers, facilitant ainsi la création d’applications web. Il fournit un système de fichiers abstrait qui permet aux développeurs de gérer de manière simple et élégante des fichiers sur différents systèmes de stockage, tels que le stockage local, Amazon S3, FTP, SFTP et Rackspace.

La configuration du système de fichiers Laravel est stockée dans le fichier config/filesystems.php. Vous pouvez y définir vos disques (disks), qui représentent des systèmes de stockage spécifiques, en configurant des options telles que le nom du disque, le type de système de fichiers et les informations d’identification, si nécessaire.

Laravel utilise le package PHP Flysystem pour fournir une interface uniforme pour différents systèmes de stockage. Le système de fichiers Laravel vous permet de réaliser diverses opérations de gestion de fichiers, telles que la création, la suppression, la copie, le déplacement et la lecture de fichiers. Vous pouvez également demander une URL temporaire pour un fichier afin de le partager avec d’autres utilisateurs.

Laravel prend en charge le stockage de fichiers sur différents disques. Pour stocker un fichier, vous pouvez utiliser la méthode store du système de fichiers Laravel. En fournissant une instance de Illuminate\Http\UploadedFile ou Illuminate\Http\File, vous pouvez stocker des fichiers téléchargés par les utilisateurs ou créés par votre application.

Le dossier storage dans Laravel sert principalement de répertoire de stockage pour les fichiers temporaires, les fichiers téléchargés par les utilisateurs, les fichiers compilés et d’autres données générées par votre application. En raison de son rôle dans la gestion de fichiers sensibles, le dossier storage n’est pas accessible publiquement par défaut.

Le dossier storage est créé automatiquement lorsque vous installez Laravel. Vous pouvez configurer différents sous-dossiers dans storage pour différents types de fichiers. Certains sous-dossiers courants sont:

  • app: pour les fichiers générés par votre application, tels que les fichiers de cache et les fichiers de configuration.
  • framework: pour les fichiers générés par le framework Laravel lui-même.
  • logs: pour les fichiers de journalisation de votre application.

Les liens symboliques, également appelés symlinks, peuvent être utilisés pour fournir un accès sécurisé aux fichiers dans le dossier storage depuis votre racine de projet. Cela vous permet de servir des fichiers sensibles tout en conservant une sécurité renforcée pour vos données.

Pour créer un symlink entre le dossier storage/app/public et le dossier public/storage, on va utiliser cette commande :

php artisan storage:link

Cette commande crée un lien symbolique permettant d’accéder aux fichiers dans storage/app/public à travers public/storage. On va créer avec notre application un dossier images pour les photos originales et thumbs pour les versions réduites.

Une fois le symlink créé, vous pouvez utiliser la méthode asset() dans vos vues Blade pour créer des URL pour les fichiers dans storage/app/public.

Intervention Image

Intervention Image est un package PHP open source populaire pour la manipulation d’images. Il offre une interface simple et intuitive pour travailler avec les images dans Laravel et d’autres frameworks PHP. Pour l’installer c’est facile :

composer require intervention/image "^2"

J’ai précisé la version 2 parce que, comme je le dis au début de cet article, j’ai eu des soucis avec la version 3.

Un composant pour les images

Pour gérer le chargement des images, on va créer un composant Volt :

php artisan make:volt images/create --class

Et on prévoit ce code :

<?php

use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Rule; 
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image as InterventionImage;
use Illuminate\Support\Facades\Auth;
use Mary\Traits\Toast;
use App\Models\Category;

new class extends Component {

    use WithFileUploads, Toast;

    #[Rule('required|image|max:2000')]
    public $photo = '';

    #[Rule('required|exists:categories,id')]
    public string $category_id = '1';

    #[Rule('nullable|string|max:255')]
    public string $description = '';

    #[Rule('boolean')]
    public bool $adult = false;

    public function save()
    {
        // Validation
        $data = $this->validate();

        // Save image
        $path = basename($this->photo->store('images', 'public'));
            
        // Save thumb
        $image = InterventionImage::make($this->photo)->widen(500)->encode ();
        Storage::disk('public')->put('thumbs/' . $path, $image);

        // Save in database
        Auth::user()->images()->create($data + ['name' => $path]);

        $this->success(__('Image added with success.'), redirectTo: '/images/create');
    }

    public function with(): array 
    {
        return [
            'categories' => Category::all()
        ];
    }
}; ?>   

<div>
    <x-card class="h-screen flex items-center justify-center" title="{{__('Add image')}}">
        <x-form wire:submit="save"> 
            <x-file wire:model="photo" label="{{__('Image')}}" hint="{{__('Only image')}}" accept="image/png, image/jpeg">
                <img src="{{ $photo == '' ? '/ask.jpg' : $photo }}" class="h-40" />
            </x-file>
            <x-select label="{{__('Category')}}" icon="o-tag" :options="$categories" wire:model="category_id" hint="{{__('Choose a pertinent category')}}"/>
            <x-input label="{{__('Description')}}" wire:model="description" hint="{{__('Describe your image here')}}" />
            <x-checkbox label="{{ __('Adult content') }}" wire:model="adult"/>
            <x-slot:actions>
                <x-button label="{{__('Save')}}" icon="o-paper-airplane" spinner="save" type="submit" class="btn-primary" />
            </x-slot:actions>
        </x-form>
    </x-card>
</div>

Il faut aussi prévoir l’assignement de masse dans le modèle Image parce qu’on utilise la commande create :

...

class Image extends Model
{
    protected $fillable = [
        'description',
        'category_id',
        'adult',
        'name',
    ];

   ...

Et bien sûr une route associée avec le middleware auth parce que seuls les utilisateurs authentifiés peuvent ajouter des photos à la galerie :

Route::middleware('auth')->group(function () {
    Volt::route('images/create', 'images.create')->name('images.create');
});

Et on ajoute quelque traductions :

"Add image": "Ajouter une image",
"Category": "Catégorie",
"Only image": "Seulement des images",
"Choose a pertinent category": "Choisissez une catégorie pertinente",
"Describe your image here": "Décrivez votre image ici",
"Adult content": "Contenu pour adulte",
"Image added with success.": "L'image a bien été ajoutée"

Vous aurez aussi besoin d’une image de référence à récupérer ici et à ajouter dans votre dossier public.

Normalement avec l’url …/images/create, vous devez obtenir le formulaire pour un utilisateur authentifié :

Quand cliquez sur l’image de référence, vous ouvrez une gestionnaire de fichier et vous pouvez choisir une photo. Elle apparaît alors en remplacement de l’image de référence. Vous pouvez alors choisir la catégorie dans la liste et ajouter éventuellement un commentaire :

Quand vous sauvegardez, les deux versions d’image sont enregistrées ici :

Les dossiers images et thumbs ont été créés à l’occasion de la sauvegarde de la première photo. Et évidemment la base a été renseignée :

Analyse du code

Le chargement d’une image par le formulaire est grandement simplifié par l’utilisation du composant File Upload de MaryUI. Le code dans notre formulaire est :

<x-file wire:model="photo" label="{{__('Image')}}" hint="{{__('Only image')}}" accept="image/png, image/jpeg">
    <img src="{{ $photo == '' ? '/ask.jpg' : $photo }}" class="h-40" />
</x-file>

Avec la validation prévue au niveau de la propriété :

#[Rule('required|image|max:2000')]
public $photo = '';

Les autres éléments du formulaire bénéficient aussi de composants de MaryUI :

<x-select label="{{__('Category')}}" icon="o-tag" :options="$categories" wire:model="category_id" hint="{{__('Choose a pertinent category')}}"/>
<x-input label="{{__('Description')}}" wire:model="description" hint="{{__('Describre your image here')}}" />
<x-checkbox label="{{ __('Adult content') }}" wire:model="adult"/>

Là aussi, les propriétés associées sont équipées de leurs règles de validation :

#[Rule('required|exists:categories,id')]
public string $category_id = '1';

#[Rule('nullable|string|max:255')]
public string $description = '';

#[Rule('boolean')]
public bool $adult = false;

La fonction save assure :

  • la validation
  • l’enregistrement de l’image originale
  • la création de la vignette (thumb) et son enregistrement
  • la sauvegarde des informations dans la base de données
  • l’affichage du message de réussite (toast)
public function save()
{
    // Validation
    $data = $this->validate();

    // Save image
    $path = basename($this->photo->store('images', 'public'));
        
    // Save thumb
    $image = InterventionImage::make($this->photo)->widen(500)->encode ();
    Storage::disk('public')->put('thumbs/' . $path, $image);

    // Save in database
    $image = new Image;
    $image->description = $data['description'];
    $image->category_id = $data['category_id'];
    $image->adult = $data['adult'];
    $image->name = $path;
    Auth::user()->images()->save($image);

    $this->success(__('Image added with success.'), redirectTo: '/images/create');
}

Vous voyez qu’avec Volt et MaryUI on s’en sort simplement et élégamment !

Le menu

Il ne nous reste plus qu’à prévoir l’accès au formulaire à partir du menu latéral. je le remets complet :

<div>
    <x-menu activate-by-route>
        @if($user = auth()->user())
            <x-menu-separator />
                <x-list-item :item="$user" value="name" sub-value="email" no-separator no-hover class="-mx-2 !-my-2 rounded">
                    <x-slot:actions>
                        <x-button icon="o-power" wire:click="logout" class="btn-circle btn-ghost btn-xs" tooltip-left="{{__('Logout')}}" no-wire-navigate />
                    </x-slot:actions>
                </x-list-item>
            <x-menu-separator />
        @else
            <x-menu-item title="{{__('Login')}}" icon="o-user" link="{{ route('login') }}" />                   
        @endif

        <x-menu-sub title="{{__('Images')}}" icon="o-photo">
            <x-menu-item title="{{__('Add image')}}" icon="o-plus" link="{{ route('images.create') }}" />
        </x-menu-sub>
    </x-menu>
</div>

On va aussi ajouter un titre à notre page (le layout) et enlever le Brand :

<body class="min-h-screen font-sans antialiased bg-base-200/50 dark:bg-base-200">

    {{-- NAVBAR mobile only --}}
    <x-nav sticky class="lg:hidden">
        <x-slot:actions>
            <label for="main-drawer" class="lg:hidden mr-3">
                <x-icon name="o-bars-3" class="cursor-pointer" />
            </label>
        </x-slot:actions>
    </x-nav>

    {{-- MAIN --}}
    <x-main full-width>
        {{-- SIDEBAR --}}
        <x-slot:sidebar drawer="main-drawer" collapsible class="bg-base-100 lg:bg-inherit">
            {{-- MENU --}}
            <livewire:navigation />
        </x-slot:sidebar>

        {{-- The `$slot` goes here --}}
        <x-slot:content>
            <div class="font-extrabold flex mb-4 justify-center items-center text-4xl"><a href="/">Albums</a></div>
            {{ $slot }}
        </x-slot:content>
    </x-main>

    {{--  TOAST area --}}
    <x-toast />
</body>
</html>

Pour peaufiner on va changer aussi le nom de la page et l’url de référence dans .env :

APP_NAME=Albums
...
APP_URL=http://albums.oo

Et on en a fini avec l’enregistrement des images !

Conclusion

Dans ce chapitre on a :

  • complété le menu latéral
  • créé route et composant Volt pour la création d’une image
  • installé le package Intervention Image
  • écrit tout le code pour le chargement d’une image

Pour vous simplifier la vie vous pouvez charger le projet dans son état à l’issue de ce chapitre. J’ai ajouté un seeder pour les images ainsi que toute la collection d’images dans le storage. Donc si vous faites une migration avec la population vous aurez toutes les images prêtes, ce qui va nous être utile pour la suite de cette série.

Print Friendly, PDF & Email

Laisser un commentaire