Laravel 11

Albums – Les albums 2/2

Dans le précédent article, on a mis en place la gestion complète des albums. Un utilisateur enregistré peut créer autant d’albums qu’il veut, les supprimer, afficher la liste pour savoir où il en est et aussi modifier le nom d’un album si nécessaire.

Mais il nous reste encore du travail. On doit pouvoir affecter des photos aux différents albums. On sait qu’une photo peut appartenir à plusieurs albums, à un seul, ou à aucun. D’autre part, il faut aussi pouvoir afficher les photos d’un album particulier.

Affecter les photos à un album

On a vu dans un article précédent qu’on peut modifier les éléments d’une photo avec ce formulaire :

L’idéal serait d’ajouter à ce formulaire la possibilité de choisir les albums pour la photo en question. Quand on regarde ce que nous propose MaryUI, on trouve en particulier le composant Choices. Particulièrement dans sa version choix multiples qui va parfaitement nous convenir :

Notre formulaire est prévu dans une page modale dans le composant index.

Actuellement dans le repository (ImageRepository), on a une fonction qui renvoie une photo à partir de son identifiant :

public function getImage(int $id): Image
{
    return Image::find($id);
}

On va avoir besoin de connaître les albums associés à cette image. Donc :

public function getImageWithAlbums(int $id): Image
{
    return Image::with('albums')->find($id);
}

Dans le composant index, on va compléter la fonction editImage pour ajouter les albums et prévoir deux nouvelles propriétés. Il nous faut savoir si l’utilisateur connecté a des albums, sinon ce n’est pas la peine de prévoir quelque chose dans le formulaire :

...

new class extends Component {

    ...

    public $albums;
    public array $albums_multi_ids = [];

    ...

    public function mount($category,  $param = ''): void
    {
        ...
        $this->albums = Auth::user()? Auth::user()->albums()->get() : null;
    }

    public function editImage(ImageRepository $imageRepository, int $id): void
    {
        $this->image = $imageRepository->getImageWithAlbums($id);

        $this->description = $this->image->description;
        $this->category_id = $this->image->category_id;
        $this->adult = $this->image->adult;
        $this->albums_multi_ids = $this->image->albums->pluck('id')->toArray();
        
        $this->imageModal = true;
    }

Et on complète la page modale :

<x-modal wire:model="imageModal" title="{{__('Manage Photo')}}" separator>
    <x-form wire:submit="saveImage"> 
        <x-input label="{{__('Description')}}" value="{{ $description }}" wire:model="description" hint="{{__('Describre your image here')}}" />
        <x-select label="{{__('Category')}}" icon="o-tag" :options="$categories" wire:model="category_id" hint="{{__('Choose a pertinent category')}}"/>
        @if($albums)
            <x-choices label="{{__('Albums')}}" wire:model="albums_multi_ids" :options="$albums" />
        @endif
        <x-checkbox label="{{ __('Adult content') }}" wire:model="adult"/>
        <x-slot:actions>
            <x-button label="{{__('Cancel')}}" icon="o-x-mark" class="btn-ghost" @click="$wire.imageModal = false" />
            <x-button label="{{__('Save')}}" type="submit" icon="o-check" class="btn-primary" />
        </x-slot:actions>
    </x-form>
</x-modal>

Maintenant dans le formulaire apparaît la liste de choix :

Et on voit qu’on peut en choisir plusieurs :

On doit aussi compléter le code pour enregistrer dans la base les albums sélectionnés, et aussi les éventuels qui ont été désélectionnés. Dans la fonction saveData, on va envoyer le nouveau paramètre albums_multi_ids au repository :

public function saveImage(ImageRepository $imageRepository): void
{
    $data = $this->validate();

    $imageRepository->saveImage($this->image, $data, $this->albums_multi_ids);

    $this->success(__('Photo changed with success.'));

    $this->imageModal = false;
}

Il ne reste plus qu’à compléter dans le repository :

public function saveImage(Image $image, array $data, array $albums_multi_ids): void
{
    $image->update($data);
    $image->albums()->sync($albums_multi_ids);
}

Et ça devrait fonctionner !

Afficher les albums

Nous devons réfléchir aux URL que nous utiliserons pour les albums. Nous disposons déjà d’un système d’URL pour les catégories, qui suit le modèle /{category}/{param?}. Dans ce modèle, le premier paramètre est le slug de la catégorie (« all » pour afficher toutes les catégories), et le second paramètre optionnel est l’identifiant du créateur.

Pour garder les choses simples, nous allons utiliser la même route pour les albums, avec l’URL …/album/{slug}. Ainsi, au lieu du nom de la catégorie, nous utiliserons « album ». Il est peu probable qu’une catégorie soit appelée « album », ce qui réduit les risques de confusion. Le paramètre qui suit servira judicieusement à fournir le slug de l’album.

En suivant cette approche, nous pourrons utiliser une structure d’URL cohérente pour les albums et les catégories, tout en évitant les conflits potentiels. De plus, notre application pourra facilement gérer et afficher les albums de manière efficace en s’appuyant sur la même logique que celle utilisée pour les catégories.

On va déjà modifier le menu latéral :

@auth
    @if($albums = auth()->user()->albums)   
        <x-menu-sub title="{{__('Albums')}}" icon="o-book-open">
            @foreach($albums as $album)
                <x-menu-item 
                    title="{{ $album->name }}" 
                    link="{{ route('home', ['category' => 'album', 'param' => $album->slug]) }}" />
            @endforeach
        </x-menu-sub>
    @endif
    <x-menu-sub title="{{__('Images')}}" icon="o-photo">
        <x-menu-item title="{{__('Add image')}}" icon="o-plus" link="{{ route('images.create') }}" />
        <x-menu-item title="{{__('Manage albums')}}" icon="o-archive-box" link="{{ route('albums.index') }}" />
        <x-menu-item title="{{__('Add album')}}" icon="o-plus" link="{{ route('albums.create') }}" />
    </x-menu-sub>
@endauth

Pour que ça fonctionne, il faut intervenir dans le repository pour tenir compte à présent des albums :

public function getImagesPaginate(string $category, string $param): LengthAwarePaginator
{
    $user = Auth::user();
    
    $query = Image::with('user')->latest();

    if (!$user || !$user->adult) {
        $query->whereAdult(false);
    }

    if ($category == 'album') {
        $query->whereHas('albums', function ($query) use($param) {
            $query->whereSlug($param);
        });
    } else {
        if ($param != '') {
            $query->whereUserId($param);
        }
        if ($category != 'all') {
            $query->whereHas('category', function ($query) use($category) {
                $query->whereSlug($category);
            });
        }
    }

    return $query->paginate($user->pagination ?? config('app.pagination'));
}

Et les albums peuvent maintenant s’afficher correctement.

Un peu de sécurité

À présent, on va s’occuper d’un petit malin éventuel qui aurait l’idée d’aller modifier le nom de l’album d’un autre utilisateur. Avec le code actuel, rien n’empêche un utilisateur connecté d’entrer une URL du genre …/albums/1/edit en mentionnant l’identifiant d’un album qui ne lui appartient pas. On va dire que ça peut être gênant et on va donc prendre quelques précautions pour éviter ça.

Laravel est équipé d’autorisations pour ce genre de situation. On crée une police :

php artisan make:policy AlbumPolicy

On ajoute le code :

<?php

namespace App\Policies;

use App\Models\{ User, Album };

class AlbumPolicy
{
    public function edit(User $user, Album $album): bool
    {
        return $user->id === $album->user_id;
    }
}

Il ne reste plus qu’à appliquer cette police sur la route en question :

Volt::route('albums/{album}/edit', 'albums.edit')->name('albums.edit')->can('edit', 'album');

C’est du moins la façon classique de le faire avec Laravel, mais je suis tombé sur une erreur qui m’a un peu perturbé :

Too few arguments to function App\Policies\AlbumPolicy::edit(), 1 passed in E:\laragon\www\albums\vendor\laravel\framework\src\Illuminate\Auth\Access\Gate.php on line 811 and exactly 2 expected

Il semblerait qu’un des deux paramètres ne soit pas correctement transmis à la méthode de la Policy. Après un peu de recherche, il s’agit de l’album qui n’est pas transmis.

C’est donc un souci au niveau de la liaison implicite entre le paramètre de l’URL et le modèle qui est en cause. Je pensais que le composant Volt générait cette liaison implicite, mais ça ne semble pas être le cas.

On peut y arriver avec quelques manipulations pas toujours évidentes alors pour simplifier, j’ai abandonné la Policy et je me suis contenté d’un peu de code dans le composant :

public function mount(): void
{
    if (Auth::id() !== $this->album->user_id) {
        abort(403);
    }

    $this->fill($this->album);
}

Maintenant le petit malin va tomber sur une erreur :

Conclusion

Dans cette étape, on a permis :

  • d’affecter des photos aux albums existants avec la possibilité de le faire pour plusieurs albums pour la même photo
  • de compléter le menu pour afficher les photos des albums
  • de mettre en place une sécurité pour éviter qu’un utilisateur modifie les albums d’un autre utilisateur

Pour vous simplifier la vie, vous pouvez charger le projet dans son état à l’issue de ce chapitre.

Print Friendly, PDF & Email

Laisser un commentaire