Créer une application avec Laravel 5.5 – La galerie 2/2

Dans le précédent chapitre on a affiché la galerie en prévoyant la possibilité d’une présentation par catégorie. On va dans ce chapitre compléter avec une présentation par utilisateur. D’autre part on va mettre en place le code pour la suppression des photos. On va également créer les pages pour les erreurs les plus classiques. On finira avec la suppression des images orphelines.

Les photos d’un utilisateur

Dans la galerie pour chaque photo on a le nom de celui qui l’a envoyée. C’est un lien avec un popup au survol :

Pour le moment on a pas référencé l’url correspondante (views/home) :

<a href="#" data-toggle="tooltip" title="{{ __('Voir les photos de ') . $image->user->name }}">{{ $image->user->name }}</a>

On commence par ajouter une route :

Route::name('user')->get('user/{user}', 'ImageController@user');

On crée la fonction dans ImageController :

use App\Models\ { Category, User };

...

public function user(User $user)
{
    $images = $this->repository->getImagesForUser($user->id);

    return view('home', compact('user', 'images'));
}

La fonction dans ImageRepository :

public function getImagesForUser($id)
{
    return Image::latestWithUser()->whereHas('user', function ($query) use ($id) {
        $query->whereId($id);
    })->paginate(config('app.pagination'));
}

Et on complète la vue (views/home) :

<a href="{{ route('user', $image->user->id) }}" data-toggle="tooltip" title="{{ __('Voir les photos de ') . $image->user->name }}">{{ $image->user->name }}</a>

Et maintenant on peut cliquer sur le nom :

Supprimer une photo

Quand un utilisateur est connecté on lui permet de supprimer ses photos (et s’il est administrateur il peut toutes les supprimer) :

Dans la vue (views/home) c’est cette partie du code qui est concernée :

@adminOrOwner($image->user_id)
    <a class="form-delete" href="{{ route('image.destroy', $image->id) }}" data-toggle="tooltip" title="@lang('Supprimer cette photo')"><i class="fa fa-trash"></i></a>
    <form action="{{ route('image.destroy', $image->id) }}" method="POST" class="hide">
        {{ csrf_field() }}
        {{ method_field('DELETE') }}
    </form>
@endadminOrOwner

...

$('a.form-delete').click(function(e) {
    e.preventDefault();
    let href = $(this).attr('href')
    $("form[action='" + href + "'").submit()
})

On utilise la directive Blade qu’on a créée au précédent chapitre (@adminOrOwner) pour faire apparaître l’icône pour les utilisateurs concernés.

On a un formulaire et une soumission par Javascript.

On a déjà créé la route précédemment dans cette ressource :

Route::resource('image', 'ImageController', [
    'only' => ['create', 'store', 'destroy']
]);

On met ce code dans ImageController :

use App\Models\ { Category, User, Image };

...

public function destroy(Image $image)
{
    $image->delete();

    return back();
}

Et maintenant si on clique sur la petite poubelle l’image disparaît.

Mais on a quand même un petit souci de sécurité. ce n’est pas parce qu’on n’affiche pas une icône aux autres utilisateurs qu’ils ne sont pas capable de générer une requête pour supprimer une photo, même si ça demande quelques connaissances…

On va donc ajouter une autorisation pour verrouiller cette possibilité :

php artisan make:policy ImagePolicy

Avec ce code :

<?php

namespace App\Policies;

use App\Models\ { User, Image };
use Illuminate\Auth\Access\HandlesAuthorization;

class ImagePolicy
{
    use HandlesAuthorization;

    /**
     * Grant all abilities to administrator.
     *
     * @param  \App\Models\User  $user
     * @return bool
     */
    public function before(User $user)
    {
        if ($user->role === 'admin') {
            return true;
        }
    }

    /**
     * Determine whether the user can delete the image.
     *
     * @param \App\Models\User $user
     * @param \App\Models\Image $image
     * @return mixed
     */
    public function delete(User $user, Image $image)
    {
        return $user->id === $image->user_id;
    }
}

Dans la fonction before on autorise les administrateurs et dans la fonction delete l’owner.

On l’enregistre dans AuthServiceProvider :

use App\Policies\ImagePolicy;
use App\Models\Image;

...

protected $policies = [
    Image::class => ImagePolicy::class,
];

Et on l’ajoute dans ImageController :

public function destroy(Image $image)
{
    $this->authorize('delete', $image);

    $image->delete();

    return back();
}

Maintenant on est sûrs qu’une petit malin ne pourra pas supprimer une photo qui ne lui appartient pas !

Pour vérifier que ça fonctionne supprimez la directive @adminOrOwner dans la vue home, connectez-vous avec Dupont tentez de supprimer une photo de Durand :

Ce n’est pas élégant mais efficace !

Les pages d’erreur

On va en profiter pour améliorer l’affichage des erreurs comme celle vue ci-dessus.

On créer un dossier spécifique et un layout :

Avec ce code (inspiré de cet exemple de Bootstrap 4) :

@extends('layouts.app')

@section('css')

  <style>

    html,
    body {
      height: 100%;
    }
    body {
      color: white;
      text-align: center;
    }
    .site-wrapper {
      display: table;
      width: 100%;
      height: 100%;
      min-height: 100%;
    }
    .site-wrapper-inner {
      display: table-cell;
      vertical-align: middle;
      margin-right: auto;
      margin-left: auto;
      width: 100%;
      padding: 0 1.5rem;
    }

  </style>

@endsection

@section('content')

  <div class="site-wrapper">
    <main role="main" class="site-wrapper-inner">
      <h1>@yield('title')</h1>
      <p class="lead">@yield('text')</p>
    </main>
  </div>

@endsection

On ajoute une vue 403 :

Avec ce code :

@extends('errors.base')

@section('title')
  @lang('Erreur 403')
@endsection

@section('text')
  @lang("Vos droits d'accès ne pous permettent pas d'accéder à cette ressource")
@endsection

On a maintenant quelque chose de plus joli :

On va ajouter aussi 404 :

@extends('errors.base')

@section('title')
  @lang('Erreur 404')
@endsection

@section('text')
  @lang("Cette page n'existe pas")
@endsection

Et 503 :

@extends('errors.base')

@section('title')
  @lang('Erreur 503')
@endsection

@section('text')
  @lang("Service temporairement indisponible ou en maintenance")
@endsection

Les images orphelines

Lorsqu’on supprime une image tel qu’on l’a fait ci-dessus ça a pour effet de supprimer la ligne dans la table images mais les deux versions de la photo (haute et basse résolution) restent sur le disque. Ce n’est pas vraiment gênant mais ça pourrait le devenir en cas de nombreuses suppression et puis ça serait quand même plus élégant de s’en occuper.

On pourrait ajouter cette action systématiquement quand on supprime une photo mais j’ai préféré créer une partie maintenance réservée à l’administrateur.

On va créer deux nouvelles routes :

Route::middleware('admin')->group(function () {

    ...

    Route::name('maintenance.index')->get('maintenance', 'AdminController@index');
    Route::name('maintenance.destroy')->delete('maintenance', 'AdminController@destroy');

});

On ajoute un item au menu de l’administration (views/layouts/app) :

@admin
    <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle{{ currentRoute(
                        route('category.create'), 
                        route('category.index'),
                        route('category.edit', request()->category),
                        route('maintenance.index')
                    )}}" href="#" id="navbarDropdownGestCat" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            @lang('Administration')
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdownGestCat">
            <a class="dropdown-item" href="{{ route('category.create') }}">
                <i class="fa fa-plus fa-lg"></i> @lang('Ajouter une catégorie')
            </a>
            <a class="dropdown-item" href="{{ route('category.index') }}">
                <i class="fa fa-wrench fa-lg"></i> @lang('Gérer les catégories')
            </a>
            <a class="dropdown-item" href="{{ route('maintenance.index') }}">
                <i class="fa fa-gears fa-lg"></i> @lang('Maintenance')
            </a>
        </div>
    </li>
@endadmin

Et on a le nouvel item :

On crée un nouveau contrôleur :

php artisan make:controller AdminController --resource

On va utiliser ImageRepository et conserver les fonctions index et destroy :

<?php

namespace App\Http\Controllers;

use App\Repositories\ImageRepository;

class AdminController extends Controller
{
    protected $repository;

    public function __construct(ImageRepository $repository)
    {
        $this->repository = $repository;
    }

    public function index()
    {
        //
    }

    public function destroy($id)
    {
        //
    }
}

Affichage des orphelines

Pour l’affichage des orphelines on code la méthode index :

public function index()
{
    $orphans = $this->repository->getOrphans ();
    $countOrphans = count($orphans);

    return view('maintenance.index', compact ('orphans', 'countOrphans'));
}

Et dans ImageRepository :

public function getOrphans()
{
    $files = collect(Storage::disk('images')->files());
    $images = Image::select('name')->get()->pluck('name');
    return $files->diff($images);
}

On en profite pour voir la puissance des collections de Laravel !

On crée la vue :

Avec ce code :

@extends('layouts.app')

@section('content')

    <main class="container-fluid">
        <h1>
            {{ $countOrphans }} {{ trans_choice(__('image orpheline|images orphelines'), $countOrphans) }}
            @if($countOrphans)
                <a class="btn btn-danger pull-right" href="{{ route('maintenance.destroy') }}" role="button">@lang('Supprimer')</a>
            @endif
        </h1>

        <div class="card-columns">
            @foreach($orphans as $orphan)
                <div class="card">
                    <img class="img-fluid" src="{{ url('thumbs/' . $orphan) }}" alt="image">
                </div>
            @endforeach
        </div>
    </main>

@endsection

@section('script')

    <script>
        $(function() {

            $.ajaxSetup({
                headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }
            })

            $('a.btn-danger').click(function(e) {
                let that = $(this)
                e.preventDefault()
                swal({
                    title: '@lang('Vraiment supprimer toutes les photos orphelines ?')',
                    type: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#DD6B55',
                    confirmButtonText: '@lang('Oui')',
                    cancelButtonText: '@lang('Non')'
                }).then(function () {
                    $.ajax({
                        url: that.attr('href'),
                        type: 'DELETE'
                    })
                        .done(function () {
                            location.reload();
                        })
                        .fail(function () {
                            swal({
                                title: '@lang('Il semble y avoir une erreur sur le serveur, veuillez réessayer plus tard...')',
                                type: 'warning'
                            })
                        }
                    )
                })
            })
        })
    </script>

@endsection

Suppression des orphelines

On a un bouton pour la suppression :

On code AdminController :

public function destroy()
{
    $this->repository->destroyOrphans ();

    return response()->json();
}

Et ImageRepository :

public function destroyOrphans()
{
    $orphans = $this->getOrphans ();

    foreach($orphans as $orphan) {
        Storage::disk('images')->delete($orphan);
        Storage::disk('thumbs')->delete($orphan);
    }
}

Dans la vue on a une alerte :

Si on supprime on se retrouve avec ça :

Remarquez la gestion du pluriel au niveau de la vue :

__('image orpheline|images orphelines')

Conclusion

Dans ce chapitre on a :

  • affiché les photos par utilisateur
  • codé la suppression des photos en prévoyant une autorisation
  • ajouté les pages d’erreurs les plus usuelles
  • ajouté la gestion des images orpheline dans l’administration

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

12 réflexions sur “Créer une application avec Laravel 5.5 – La galerie 2/2

  1. bestmomo dit :

    Une réponse détaillée ça représente en fait un article complet parce qu’il faut :

    • imaginer l’aspect frontend : une icône sur la photo réservée à l’owner et les administrateurs ? Comment pratiquement on propose ce changement ?
    • ajouter en backend : les routes, les méthodes du contrôleur et les vues.

Laisser un commentaire