Un site d’annonces – annonce et message
Dans cet article on va voir l’affichage des détails d’une annonce (titre, texte, photos…). On va aussi prévoir qu’un visiteur puisse envoyer un message à l’émetteur de l’annonce pour se mettre en relation avec lui.
Pour vous simplifier la vie vous pouvez télécharger le dossier complet pour le code de cet article.
La liste des annonces
On en est restés avec la possibilité d’obtenir la liste des annonces pour une localisation (région, département, commune) et une catégorie :
Mais je ne suis pas très satisfait de l’aspect actuel avce le lien en bleu et le soulignement au survol, j’aimerais quelque chose de plus esthétique. On va ajouter quelques règles dans resources\sass\app.scss :
a.blockAd { text-decoration: none; color: inherit; &:hover h4 { color: #e47517; } &:hover .card { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } }
On lance une compilation…
Maintenant le texte de l’annonce est noir :
Et au survol le titre devient orange et on ajoute un peu d’ombre autour de la carte :
Voilà qui est mieux !
On a déjà mis en place la route pour l’affichage d’une annonce :
Route::prefix('annonces')->group(function () { Route::get('voir/{ad}', 'AdController@show')->name('annonces.show');
Quand je survole une annonce je vois le lien de la forme annonces/voir/{id}. On doit diriger la requête vers la méthode show du contrôleur qu’on n’a pas encore codée.
Contrôleur, repository et autorisation
Dans le contrôleur AdController on va prévoir ce code pour la méthode show :
use App\Models\ { Category, Region, Ad }; ... public function show(Ad $ad) { $this->authorize('show', $ad); $photos = $this->adRepository->getPhotos($ad); return view('ad', compact('ad', 'photos')); }
Avec la liaison de données au niveau du paramètre de la route on récupère directement une annonce dans la variable $ad.
Une autorisation
On sait qu’on a des annonces valides, qui ont été modérées par un administrateur et des non valides. il ne faudrait pas qu’un petit malin change l’identifiant dans l’url pour aller lire une annonce pas encore valide. On va donc mettre en place une autorisation pour accéder à une annonce : si elle est valide pas de souci, si elle ne l’est pas on laisse l’accès uniquement à celui qui l’a créée et qui a un compte et aussi aux administrateurs.
On utilise Artisan pour créer cette autorisation (policy) :
php artisan make:policy AdPolicy --model=Ad
On a un squelette de code, on va le compléter ainsi :
<?php namespace App\Policies; use App\Models\ { User, Ad }; use Illuminate\Auth\Access\HandlesAuthorization; class AdPolicy { use HandlesAuthorization; /** * Grant all abilities to administrator. * * @param \App\Models\User $user * @return bool */ public function before(User $user) { if($user->admin) { return true; } } /** * Determine whether the user can view the ad. * * @param \App\Models\User $user * @param \App\Models\Ad $ad * @return mixed */ public function show(?User $user, Ad $ad) { if($user && $user->id == $ad->user_id) { return true; } return $ad->active; } }
Avec la méthode before on prévoit d’autoriser les administrateur de façon systématique. Avec la méthode show on autorise le visiteur, connecté ou pas, à accéder si l’annonce est valide ou si le visiteur connecté est l’auteur de l’annonce.
On enregistre cette autorisation dans AuthServiceProvider :
... use App\Policies\AdPolicy; use App\Models\Ad; class AuthServiceProvider extends ServiceProvider { protected $policies = [ Ad::class => AdPolicy::class ];
Maintenant on peut l’utiliser dans le contrôleur :
$this->authorize('show', $ad);
Le repository
On peut avoir des photos associées à l’annonce. Dans le repository (AdRepository) on va prévoir une méthode pour aller récupérer ces photos :
public function getPhotos($ad) { return $ad->photos()->get(); }
On se contente d’utiliser la relation qu’on a déjà mis en place.
On peut maintenant appeler cette méthode à partir du contrôleur :
$photos = $this->adRepository->getPhotos($ad);
Et pour finir on appelle la vue en transmettant les données :
return view('ad', compact('ad', 'photos'));
La vue pour afficher une annonce
On crée la vue :
@extends('layouts.app') @section('content') <div class="container"> <div class="card bg-light"> <h5 class="card-header">{{ $ad->title }}</h5> @if($photos->isNotEmpty()) @if($photos->count() > 1) <div id="ctrl" class="carousel slide" data-ride="carousel"> <ol class="carousel-indicators"> @foreach ($photos as $photo) <li data-target="#ctrl" data-slide-to="{{ $loop->index }}" @if($loop->first) class="active" @endif></li> @endforeach </ol> <div class="carousel-inner"> @foreach ($photos as $photo) <div class="carousel-item @if($loop->first) active @endif"> <img class="d-block w-100" src="{{ asset('images/' . $photo->filename) }}" alt="First slide"> </div> @endforeach </div> <a class="carousel-control-prev" href="#ctrl" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Précédent</span> </a> <a class="carousel-control-next" href="#ctrl" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Suivant</span> </a> </div> @else <img class="card-img-top" src="{{ asset('images/' . $ad->photos->first()->filename) }}"> @endif @endif <div class="card-body"> <hr> <p><u>Description :</u></p> <p class="card-text">{{ $ad->texte }}</p> <hr> <p class="card-text"><u>Catégorie</u> : {{ $ad->category->name }}</p> <p class="card-text"> <u>Ville</u> : {{ $ad->commune_name . ' (' . $ad->commune_postal . ')'}}<br> <u>Publication</u> : {{ $ad->created_at->calendar() }} </p> <hr> <p class="card-text"><u>Pseudo</u> : {{ $ad->pseudo }}</p> </div> </div> </div> @endsection
On affiche en premier le titre de l’annonce :
<h5 class="card-header">{{ $ad->title }}</h5>
Ensuite s’il y a des photos :
@if($photos->isNotEmpty())
On a deux cas :plusieurs photos alors on prévoir un caroussel :
@if($photos->count() > 1) <div id="ctrl" class="carousel slide" data-ride="carousel"> ... </div> @else
Ou une seule photo, alors on l’affiche de façon classique :
@else <img class="card-img-top" src="{{ asset('images/' . $ad->photos->first()->filename) }}"> @endif
On renseigne ensuite dans le corps de la carte :
- la description
- la catégorie
- la ville
- la date de publication
- le pseudo du rédacteur
On a par exemple cet aspect :
Les messages
On affiche les annonces mais on n’a rien pour permettre au visiteur de contacter l’auteur de l’annonce. On va donc ajouter un bouton pour l’envoi d’un message. On va prévoir deux cas :
- le visiteur est un utilisateur authentifié, dans ce cas on expédie directement le message
- le visiteur est un inconnu, dans ce cas on mémorise le message qui sera modéré par un administrateur
Route et contrôleur
On va prévoir une route :
Route::middleware('ajax')->group(function () { Route::post('message', 'UserController@message')->name('message'); });
On crée un groupe ajax parce qu’on aura d’autre routes à mettre là. On voit qu’on va utiliser un nouveau contrôleur UserController. On commence par le créer :
php artisan make:controller UserController
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Notifications\AdMessage; use App\Http\Requests\MessageAd; use App\Repositories\ { AdRepository, MessageRepository }; class UserController extends Controller { protected $adRepository; protected $messagerepository; public function __construct(AdRepository $adRepository, Messagerepository $messagerepository) { $this->adRepository = $adRepository; $this->messagerepository = $messagerepository; } public function message(MessageAd $request) { $ad = $this->adRepository->getById($request->id); if(auth()->check()) { $ad->notify(new AdMessage($ad, $request->message, auth()->user()->email)); return response()->json(['info' => 'Votre message va être rapidement transmis.']); } $this->messagerepository->create([ 'texte' => $request->message, 'email' => $request->email, 'ad_id' => $ad->id, ]); return response()->json(['info' => 'Votre message a été mémorisé et sera transmis après modération.']); } }
Pour la validation j’ai prévu une form request MessageAd.
La form request
On la crée avec Artisan :
php artisan make:request MessageAd
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class MessageAd extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'message' => ['required', 'string', 'max:500'], 'email' => ['sometimes', 'required', 'string', 'email', 'max:255'], ]; } }
On a deux paramètres :
- message : c’est le texte du message (on limite la longueur à 500 caractères)
- email : c’est l’email de celui qui veut envoyer le message
Il y a sometimes pour la validation de l’email parce que si c’est un utilisateur connecté qui laisse un message on ne va pas lui demander d’entrer son email parce qu’on le connait déjà.
Le repository AdRepository
Dans adRepository on ajoute une méthode pour aller chercher une annonce à partir de son identifiant :
public function getById($id) { return Ad::findOrFail($id); }
On peut ainsi l’utiliser dans le contrôleur :
$ad = $this->adRepository->getById($request->id);
Il ne faudra pas oublier d’envoyer cet identifiant dans le formulaire, dans un champ caché.
La notification
Pour l’envoi du message on va utiliser le système de notification de Laravel. En général il est associé au modèle User mais dans notre cas on l’associe au modèle Ad. d’ailleurs lorsqu’on a créé ce modèle on a déjà ajouté le trait :
class Ad extends Model { use Notifiable;
On crée la notification :
php artisan make:notification AdMessage
On y colle ce code :
<?php namespace App\Notifications; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; class AdMessage extends Notification { use Queueable; protected $ad; protected $message; protected $email; public function __construct($ad, $message, $email) { $this->ad = $ad; $this->message = $message; $this->email = $email; } public function via($notifiable) { return ['mail']; } public function toMail($notifiable) { return (new MailMessage) ->line('Vous avez reçu un message concernant une annonce que vous avez déposée :') ->line('--------------------------------------') ->line($this->message) ->line('--------------------------------------') ->action('Voir votre annonce', route('annonces.show', $this->ad->id)) ->line("L'email de l'expéditeur est : " . $this->email) ->line("Merci d'utiliser notre site pour vos annonces !"); } public function toArray($notifiable) { return [ // ]; } }
Maintenant dans le contrôleur on peut envoyer la notification pour un utilisateur connecté :
if(auth()->check()) { $ad->notify(new AdMessage($ad, $request->message, auth()->user()->email)); return response()->json(['info' => 'Votre message va être rapidement transmis.']); }
Le repository MessageRepository
Pour un simple visiteur on doit mémoriser le message dans la table messages en attente de validation par un administrateur. pour la gestion des données on crée un repository :
<?php namespace App\Repositories; use App\Models\Message; class MessageRepository { public function create($data) { return Message::create($data); } }
On peut alors l’utiliser dans le contrôleur :
$this->messagerepository->create([ 'texte' => $request->message, 'email' => $request->email, 'ad_id' => $ad->id, ]);
On complète la vue ad
Maintenant que notre intendance est en place on va compléter la vue avec un bouton qui va ouvrir une fenêtre modale avec le formulaire pour le message. Je mets le code complet de la vue ad :
@extends('layouts.app') @section('content') <div class="container"> <div id="massageOk" class="alert alert-success" role="alert" style="display: none"> Votre message a été pris en compte et sera envoyé rapidement </div> @include('partials.message', ['url' => route('message')]) <div class="card bg-light"> <h5 class="card-header">{{ $ad->title }}</h5> @if($photos->isNotEmpty()) @if($photos->count() > 1) <div id="ctrl" class="carousel slide" data-ride="carousel"> <ol class="carousel-indicators"> @foreach ($photos as $photo) <li data-target="#ctrl" data-slide-to="{{ $loop->index }}" @if($loop->first) class="active" @endif></li> @endforeach </ol> <div class="carousel-inner"> @foreach ($photos as $photo) <div class="carousel-item @if($loop->first) active @endif"> <img class="d-block w-100" src="{{ asset('images/' . $photo->filename) }}" alt="First slide"> </div> @endforeach </div> <a class="carousel-control-prev" href="#ctrl" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Précédent</span> </a> <a class="carousel-control-next" href="#ctrl" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Suivant</span> </a> </div> @else <img class="card-img-top" src="{{ asset('images/' . $ad->photos->first()->filename) }}" alt="Card image cap"> @endif @endif <div class="card-body"> <hr> <p><u>Description :</u></p> <p class="card-text">{{ $ad->texte }}</p> <hr> <p class="card-text"><u>Catégorie</u> : {{ $ad->category->name }}</p> <p class="card-text"> <u>Ville</u> : {{ $ad->commune_name . ' (' . $ad->commune_postal . ')'}}<br> <u>Publication</u> : {{ $ad->created_at->calendar() }} </p> <hr> <p class="card-text"><u>Pseudo</u> : {{ $ad->pseudo }}</p> <button id="openModal" type="button" class="btn btn-primary">Envoyer un message</button> </div> </div> </div> @endsection @section('script') <script> $(() => { const toggleButtons = () => { $('#icon').toggle(); $('#buttons').toggle(); } $('#openModal').click(() => { $('#messageModal').modal(); }); $('#messageForm').submit((e) => { let that = e.currentTarget; e.preventDefault(); $('#message').removeClass('is-invalid'); $('.invalid-feedback').html(''); toggleButtons(); $.ajax({ method: $(that).attr('method'), url: $(that).attr('action'), data: $(that).serialize() }) .done((data) => { toggleButtons(); $('#messageModal').modal('hide'); $('#massageOk').text(data.info).show(); }) .fail((data) => { toggleButtons(); $.each(data.responseJSON.errors, function (i, error) { $(document) .find('[name="' + i + '"]') .addClass('is-invalid') .next() .append(error[0]); }); }); }); }) </script> @endsection
On voit que la page modale est incluse dans cette vue à partir d’une vue partielle (on s’en reservira plus tard) :
La vue partielle
Voici le code de la vue partielle message :
<div class="modal fade" id="messageModal" tabindex="-1" role="dialog" aria-labelledby="message" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-lg" role="document"> <div class="modal-content"> <form id="messageForm" method="POST" action="{{ $url }}"> @csrf <div class="modal-body"> @guest <div class="alert alert-warning alert-dismissible fade show" role="alert"> Vous n'êtes pas connecté. Votre message sera modéré avant expédition. <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> @endguest <input id="id" name="id" type="hidden" value="{{ isset($ad) ? $ad->id : '' }}"> <div class="form-group"> <label for="texte">Entrez ici votre message</label> <textarea class="form-control" id="message" name="message" rows="3" required>{{ old('texte', isset($value) ? $value : '') }}</textarea> <div id="messageError" class="invalid-feedback"></div> </div> @guest <div class="form-group"> <label for="email">Votre email pour vous contacter</label> <input type="email" class="form-control" name=email id="email" required> <div id="emailError" class="invalid-feedback"></div> </div> @endguest </div> <div class="modal-footer"> <div id="buttons"> <button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button> <button type="submit" class="btn btn-primary">Envoyer</button> </div> <i id="icon" class="fas fa-spinner fa-pulse fa-2x" style="display: none"></i> </div> </form> </div> </div> </div>
Maintenant on a un bouton en bas de la page :
Quand on clique ça ouvre la page modale :
Pour un utilisateur connecté au aura l’alerte en moins et pas de champ email :
La gestion de la soumission se fait en Javascript avec JQuery.
Comme on utilise une icône de Font Awesome on va ajouter le chargement de cette librairie dans notre layout (resources\layouts\app) :
<!-- Styles --> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"> <link href="{{ asset('css/app.css') }}" rel="stylesheet">
L’icône animée remplace les deux boutons le temps que se fasse la communication avec le serveur.
Vous pouvez vérifier que la validation se passe bien :
Pour un simple visiteur il y a affichage d’un message au retour du serveur :
Et vous pourrez vérifier que le message se mémorise bien dans la base.
Sinon le message part en email :
Là encore ça mérite une francisation. On va publier le template :
php artisan vendor:publish --tag=laravel-notifications
En adaptant un peu les textes :
@component('mail::message') {{-- Greeting --}} @if (! empty($greeting)) # {{ $greeting }} @else @if ($level === 'error') # @lang('Whoops!') @else # Bonjour ! @endif @endif {{-- Intro Lines --}} @foreach ($introLines as $line) {{ $line }} @endforeach {{-- Action Button --}} @isset($actionText) <?php switch ($level) { case 'success': case 'error': $color = $level; break; default: $color = 'primary'; } ?> @component('mail::button', ['url' => $actionUrl, 'color' => $color]) {{ $actionText }} @endcomponent @endisset {{-- Outro Lines --}} @foreach ($outroLines as $line) {{ $line }} @endforeach {{-- Salutation --}} @if (! empty($salutation)) {{ $salutation }} @else Cordialement,<br>{{ config('app.name') }} @endif {{-- Subcopy --}} @isset($actionText) @slot('subcopy') @lang( "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\n". 'into your web browser: [:actionURL](:actionURL)', [ 'actionText' => $actionText, 'actionURL' => $actionUrl, ] ) @endslot @endisset @endcomponent
Maintenant c’est mieux :
Il ne reste plus qu’un détail, dans le titre de l’email on a « Laravel », on va mettre a jour le fichier .env :
APP_NAME=Annonces
Conclusion
On a maintenant toutes les fonctionnalités côté client pour la recherche des annonces, leur affichage et l’envoi de message au rédacteur. On aura encore du travail côté client pour proposer une interface de gestion du profil. Dans le prochain article on mettra en place l’intendance pour la création d’une annonce.
27 commentaires
zouboulba
actuellement je suis configuré comme ça :
MAIL_MAILER=sendmail
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=28****31c11e
MAIL_PASSWORD=bf***********3e01
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@gmail.com
MAIL_FROM_NAME= »${APP_NAME} »
celà fonctionne très bien pour faire des test avec mailtrap.
cependant je ne vois pas du tout comment configurer pour la production ?
pourrais tu m’aider ?
bestmomo
Si tu utilises la fonction de base de PHP la configuration se résume à ça :
MAIL_MAILER=sendmail
MAIL_FROM_ADDRESS=monadresse@chezmoi.com
MAIL_FROM_NAME=Administrateur
En général ça fonctionne.
zouboulba
non ça ne fonctionne pas =/
bestmomo
C’est quoi comme hébergement ?
zouboulba
désolé pour la réponse très tardive.
l’ébergement c’est IONOS
bestmomo
Bonjour,
J’ai regardé la documentation de chez Ionos, c’est vraiment pas clair du tout. Peut-être partir sur le paramétrage SMTP préconisé pour les boîtes d’email. Mais je suis surpris que le sendmail de base de PHP ne fonctionne pas.
zouboulba
merci beaucoup d’y avoir regardé.
je vais essayer avec SMTP
zouboulba
c’est bon j’ai réussi, je me suis servi du serveur smtp de google :
MAIL_MAILER=smtp
MAIL_HOST=smtp.googlemail.com
MAIL_PORT=465
MAIL_USERNAME=n********@gmail.com
MAIL_PASSWORD=fb************dev
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=ad*********r@********.com
MAIL_FROM_NAME= »${APP_NAME} »
ce site m’a bien aidé
https://www.akilischool.com/cours/laravel-envoyer-un-mail-via-le-serveur-smtp-google
zouboulba
Dans le cas ou le projet est en production,
comment configurer le fichier .env pour les emails ? il y a t’il d’autre fichier à modifier ?
bestmomo
En général le plus simple est d’utiliser MAIL_MAILER=sendmail
zouboulba
et bien quand je soumet le formulaire :
etat : 419
méthode : POST
Domaine : 127.0.0.1:8000
fichier : message
initiateur : main.js:2678(xhr)
type: json
transfert : 12,60 ko
taille : 11,93 ko
zouboulba
Bonjour
quand je valide le message, le toggle tourne, puis rien ne se passe.
bestmomo
Bonjour,
Il faudrait voir ce qui passe en ligne avec les outils du navigateur (F12) dans l’onglet réseaux.
zouboulba
et bien quand je soumet le formulaire :
etat : 419
méthode : POST
Domaine : 127.0.0.1:8000
fichier : message
initiateur : main.js:2678(xhr)
type: json
transfert : 12,60 ko
taille : 11,93 ko
bestmomo
Ca ressemble à un problème de protection CSRF.
zouboulba
j’ai assayez pas mal de truc au niveau CSRF, mais pourquoi toi ça fonctionnerai mais pas moi…
j’ai bien mis le CSRF en balise meta dans le head.
après je vois pas ce que je dois faire de plus
bestmomo
Dans le HEAD ça sert à rien parce qu’il y a déjà un input caché dans le formulaire généré avec @csrf.
zouboulba
malgrés plusieurs essai avec le CSRF, rien ne fonctionne.
as tu rajouter des choses toi concernant le CSRF ?
bestmomo
Non, du moment qu’il y a le champ créé avec @csrf dans le formulaire il est pris en compte dans le Javascript avec data: $(that).serialize() et si le middleware est bien présent côté Laravel, et il doit y être si rien n’a été changé par rapport à l’installation par défaut, ça devrait marcher. Il faudrait suivre la requête côté Laravel pour voir où ça coince…
zouboulba
rien n’a été modifié dans le middleware.
comment je pourrai faire pour suivre la requête coté Laravel ?
zouboulba
quand je vais dans la console, puis dans requête, je récupère bien les données du formulaire
zouboulba
et par contre dans réponse j’ai :
message: « Call to a member function getById() on null »
exception: « Error »
file: « C:\\Users\\njoub\\Desktop\\server_wamp\\www\\projet-travaux\\app\\Http\\Controllers\\UserController.php »
line: 27
bestmomo
Ah voilà une erreur intéressante, apparemment le repository adRepository est mal déclaré dans le contrôleur.
zouboulba
je l’appel comme ça
use App\Repositories\ { AdRepository, MessageRepository };
bestmomo
Et dans le constructeur ?
zouboulba
wouhou ça fonctionne !!!!!!!!!
mais que quand je suis guest.
quand je suis connecté, rien ne se passe =/
zouboulba
finalement j’ai réussi en utilisant mailtrap ! en suivant ton cours : les bases – envoyer un email
merci beaucoup à toi, pour le temps que tu m’as accordé !!! vraiment c’est super sympa !!
je vais pouvoir avancer !