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 n’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="fas fa-plus fa-lg"></i> @lang('Ajouter une catégorie') </a> <a class="dropdown-item" href="{{ route('category.index') }}"> <i class="fas fa-wrench fa-lg"></i> @lang('Gérer les catégories') </a> <a class="dropdown-item" href="{{ route('maintenance.index') }}"> <i class="fas fa-cogs 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.
17 commentaires
didier22
Tout d’abord merci pour ce tutoriel très complet ! Je remarque que la suppression d’une photo fonctionne correctement avec le navigateur Firefox et pas du tout avec un navigateur Safari sous Mac OS high-Sierra. as-tu déjà remarqué cela , si oui as-tu une parade et une explication ?
bestmomo
Bonjour,
Je pense que ça doit coincer dans le Javascript mais je n’ai pourtant pas utilisé de syntaxe trop récente. Il me semblait que le code que j’ai utilisé était compréhensible par tous les navigateurs à condition qu’ils soient d’une version récente, par exemple la 11 pour Safari. Je ne sais pas s’il y a des outils de débogage sur ce navigateur sinon il faudrait voir où ça coince…
Lucas
Bonjour, je suis en train de réaliser l’album pour me familiariser avec laravel 5. Premièrement merci pour ce suberbe tutoriel !
J’ai un soucis que je n’arrive pas à régler pour l’affichage des images Orphelines :
J’ai l’erreur suivante quand je veux accéder à la page maintenance : (1/1) FatalThrowableError
Call to a member function getOrphans() on null
J’ai essayer de la résoudre en vain, je pense que cela vient de mon $orphans = $this->repository qui est null quoi que je fasse.
Si jamais vous savez d’ou vient cette erreur, je vous en serai très reconnaissant !
Merci par avance,
Lucas
bestmomo
Salut,
Apparemment le repository est mal déclaré dans le contrôleur. Il faut vérifier que la propriété est présente et si dans le constructeur elle est bien affectée.
Lucas
Réponse tardive mais effectivement, c’était bien ça le soucis, j’avais fais une faute de frappe sur le constructeur « __contruct » au lieu de « __construct » ! Tout marche niquel sinon !
Merci !
Lucas
bestmomo
Une réponse détaillée ça représente en fait un article complet parce qu’il faut :
lauraneblettery
Et cela n’est pas envisageable?
bestmomo
Tout est envisageable bien sûr mais c’est une question de disponibilité, je suis sur plusieurs projets simultanés, pourquoi ne te lancerais-tu pas ? 🙂
lauraneblettery
J’ai essayé mais j’arrive pas du tout
bestmomo
Il faut que je voie si j’ai le temps de caser ça 😉
lauraneblettery
se serait adorable vraiment !!!
bestmomo
C’est fait
lauraneblettery
Peux tu expliquer comment changer la catégorie d’une image ? Cela serait vraiment interessant
bestmomo
Salut,
Ce n’est pas prévu dans l’application, il faut le faire manuellement dans la table images en changeant category_id.
lauraneblettery
peux tu mexpliquer en detail stp ? comment faire ? ce serait adorable
bestmomo
Tu regardes dans la table categories les id et tu peux changer dans la table images en mettant l’id de la catégorie que tu veux. Mais bon c’est quand même la solution de secours ça, si tu veux ajouter cette possibilité à l’application il faudrait le prévoir avec les routes et tout le reste.
lauraneblettery
Oui, et tu ne voudrai pas m’expliquer en detail comment ajouter cette fonctionnalité ? cela serait génial