Tutoriel pour apprendre à créer une application Laravel

Le contact

Ce cours fait partie d'une série de tutoriels qui se charge de vous apprendre à créer une application avec Laravel. À travers ce cours, vous allez apprendre à gérer le formulaire de contact.

6 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Pour comprendre comment est organisée l'application je vais prendre quelque chose de simple : le formulaire de contact auquel le visiteur accède à partir du menu :

Image non disponible

Nous allons suivre le cheminement de la requête à partir du clic sur l'option du menu, jusqu'à l'enregistrement du message et nous verrons ensuite la gestion de ce message par l'administrateur. Cela donnera une vision globale de l'application.

II. Le statut

Le menu s'adapte automatiquement selon que l'on a un simple visiteur, un utilisateur connecté, un rédacteur ou un administrateur. On ne va évidemment pas proposer un formulaire de contact à ces deux dernières catégories. Comment cela est-il géré ? Regardons le code de cet item dans la vue resources/views/front/template.blade.php :

 
Sélectionnez
1.
2.
3.
4.
5.
@if(session('statut') == 'visitor' || session('statut') == 'user')
    <li {!! Request::is('contact/create') ? 'class="active"' : '' !!}>
        {!! link_to('contact/create', trans('front/site.contact')) !!}
    </li>
@endif

On a dans la session une clé statut qui informe sur le statut. Ici, on teste si on a un simple visiteur ou un utilisateur de base. Dans les deux cas on fait apparaître l'option. D'autre part on lui ajoute la classe active si la requête correspond à cet item, pour le distinguer dans le menu.

Pour gérer le statut on a un service (app/Services/Statut.php) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
<?php namespace App\Services;
 
use Auth;
 
class Statut  {
 
    /**
     * Set the login user statut
     * 
     * @param  App\Models\User $user
     * @return void
     */
    public function setLoginStatut($user)
    {
        session()->put('statut', $user->role->slug);
    }
 
    /**
     * Set the visitor user statut
     * 
     * @return void
     */
    public function setVisitorStatut()
    {
        session()->put('statut', 'visitor');
    }
 
    /**
     * Set the statut
     * 
     * @return void
     */
    public function setStatut()
    {
        if(!session()->has('statut')) 
        {
            session()->put('statut', Auth::check() ?  Auth::user()->role->slug : 'visitor');
        }
    }
 
}

Lorsqu'une requête arrive le middleware App est activé. On y trouve le déclenchement d'un événement :

 
Sélectionnez
event('user.access');

Or la méthode setStatut du service écoute cet événement :

 
Sélectionnez
1.
2.
3.
4.
protected $listen = [
    ...
    'user.access' => ['App\Services\Statut@setStatut']
];

On définit alors le statut dans cette méthode.

On a aussi besoin de définir le statut lorsque quelqu'un se connecte, ce qui est réalisé dans la méthode setLoginStatut.

Et finalement on doit aussi fixer le statut de visiteur en cas de déconnexion, ce qui est réalisé par la méthode setVisitorStatut.

Ces deux méthodes écoutent les événements correspondants :

 
Sélectionnez
1.
2.
3.
4.
5.
protected $listen = [
    'auth.login' => ['App\Services\Statut@setLoginStatut'],
    'auth.logout' => ['App\Services\Statut@setVisitorStatut'],
    ...
];

III. Le formulaire

III-A. Affichage

La route contact/create aboutit à la fonction create du contrôleur ContactController :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
/**
 * Show the form for creating a new resource.
 *
 * @return Response
 */
public function create()
{
    return view('front.contact');
}

Ici on se contente de retourner la vue du formulaire. Notez qu'aucun middleware ne protège cette fonction, ce qui serait vraiment superflu.

La vue est bien rangée dans le dossier du front-end :

Image non disponible

Avec ce code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
@extends('front.template')
 
@section('main')
    <div class="row">
        <div class="box">
            <div class="col-lg-12">
                <hr>  
                <h2 class="intro-text text-center">{{ trans('front/contact.title') }}</h2>
                <hr>
                <p>{{ trans('front/contact.text') }}</p>                
                 
                {!! Form::open(['url' => 'contact', 'method' => 'post', 'role' => 'form']) !!} 
                 
                    <div class="row">
 
                        {!! Form::control('text', 6, 'name', $errors, trans('front/contact.name')) !!}
                        {!! Form::control('email', 6, 'email', $errors, trans('front/contact.email')) !!}
                        {!! Form::control('textarea', 12, 'message', $errors, trans('front/contact.message')) !!}
                        {!! Form::text('address', '', ['class' => 'hpet']) !!}       
 
                        {!! Form::submit(trans('front/form.send'), ['col-lg-12']) !!}
 
                    </div>
                     
                {!! Form::close() !!}
 
            </div>
        </div>
    </div>
@stop

Le formulaire est simplifié grâce à l'utilisation de quelques extensions du FormBuilder (ce que nous verrons dans un article ultérieur).

Notez la présence d'un champ qui peut vous sembler inutile :

 
Sélectionnez
{!! Form::text('address', '', ['class' => 'hpet']) !!}

C'est en fait un pot de miel masqué par la classe hpet pour piéger les robots. En effet ceux-ci ont tendance à remplir tous les champs. On va donc vérifier à la soumission si ce champ a été complété. Si c'est le cas, on ne va pas aller plus loin dans le traitement du formulaire. Ceci s'effectue dans la classe de base App\Http\Requests\Request :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?php namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
abstract class Request extends FormRequest {
 
    public function authorize()
    {
        // Honeypot 
        return  $this->input('address') == '';
    }
 
}

Comme toutes les requêtes de formulaire héritent de cette classe on va donc appliquer le pot de miel à l'ensemble du site.

III-B. Traitement

Lorsque le formulaire est soumis on aboutit à la méthode store du contrôleur ContactController :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/**
 * Store a newly created resource in storage.
 *
 * @param  App\Repositories\ContactRepository $contact_gestion
 * @param  ContactRequest $request
 * @return Response
 */
public function store(
    ContactRepository $contact_gestion,
    ContactRequest $request)
{
    $contact_gestion->store($request->all());
 
    return redirect('/')->with('ok', trans('front/contact.ok'));
}

On voit que l'on injecte une requête de formulaire pour la validation (App\Http\Requests\ContactRequest). Voici le code de cette requête :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<?php namespace App\Http\Requests;
 
class ContactRequest extends Request {
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:100',
            'email' => 'required|email',
            'message' => 'required|max:1000'
        ];
    }
 
}

On réclame tous les champs et on impose quelques contraintes.

On injecte aussi dans la méthode un repository (App\Repositories\ContactRepository). C'est la méthode store qui est chargée d'enregistrer le contact dans la base :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
/**
 * Store a contact.
 *
 * @param  array $inputs
 * @return void
 */
public function store($inputs)
{
    $contact = new $this->model;
 
    $contact->name = $inputs['name'];
    $contact->email = $inputs['email'];
    $contact->text = $inputs['message'];
 
    $contact->save();
}

J'aurais pu utiliser la méthode create pour alléger le code, mais j'aime bien détailler les entrées.

Quand le message a été mémorisé, le contrôleur renvoie sur la page d'accueil avec un message flashé dans la session :

 
Sélectionnez
return redirect('/')->with('ok', trans('front/contact.ok'));

Ainsi le visiteur a une confirmation que son message est bien arrivé à destination :

Image non disponible

IV. L'administration

IV-A. Le tableau de bord

Lorsqu'un administrateur va se connecter sur le tableau de bord il va voir qu'un nouveau message est arrivé :

Image non disponible

Comment cela fonctionne-t-il ?

C'est la méthode admin du contrôleur AdminController qui est chargée de gérer le tableau de bord :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
/**
* Show the admin panel.
*
* @param  App\Repositories\ContactRepository $contact_gestion
* @param  App\Repositories\BlogRepository $blog_gestion
* @param  App\Repositories\CommentRepository $comment_gestion
* @return Response
*/
public function admin(
    ContactRepository $contact_gestion, 
    BlogRepository $blog_gestion,
    CommentRepository $comment_gestion)
{   
    $nbrMessages = $contact_gestion->getNumber();
    $nbrUsers = $this->user_gestion->getNumber();
    $nbrPosts = $blog_gestion->getNumber();
    $nbrComments = $comment_gestion->getNumber();
 
    return view('back.index', compact('nbrMessages', 'nbrUsers', 'nbrPosts', 'nbrComments'));
}

Évidemment l'accès est réservé aux administrateurs :

 
Sélectionnez
1.
2.
3.
4.
5.
Route::get('admin', [
    'uses' => 'AdminController@admin',
    'as' => 'admin',
    'middleware' => 'admin'
]);

On voit que plusieurs repositories sont injectés dans la méthode. En effet, il faut aller vérifier le nombre de nouveaux articles et commentaires en plus des messages. D'autre part, on affiche aussi le nombre total de chacune des catégories. Dans tous les cas c'est la méthode getNumber des repositories qui est appelée.

Comme la méthode est commune à plusieurs repositories elle est placée dans la classe mère (App\Repositories\BaseRepository) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/**
 * Get number of records.
 *
 * @return array
 */
public function getNumber()
{
    $total = $this->model->count();
 
    $new = $this->model->whereSeen(0)->count();
 
    return compact('total', 'new');
}

On va chercher ici le nombre total et les nouveautés (repérés par le champ seen à 0) et on les retourne au contrôleur qui envoie tout dans la vue :

 
Sélectionnez
return view('back.index', compact('nbrMessages', 'nbrUsers', 'nbrPosts', 'nbrComments'));

Cette vue est rangée dans le dossier du back-end :

Image non disponible

Voici la partie qui concerne les contacts :

 
Sélectionnez
@include('back/partials/pannel', ['color' => 'red', 'icone' => 'comment', 'nbr' => $nbrComments, 'name' => trans('back/admin.new-comments'), 'url' => 'comment', 'total' => trans('back/admin.comments')])

Comme le code est commun à toutes les catégories, on fait appel à une vue partielle :

Image non disponible

avec ce code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<div class="col-lg-4 col-md-6">
    <div class="panel panel-{{ $color }}">
        <div class="panel-heading">
            <div class="row">
                <div class="col-xs-3">
                    <span class="fa fa-{{ $icone }} fa-5x"></span>
                </div>
                <div class="col-xs-9 text-right">
                <div class="huge">{{ $nbr['new'] }}</div>
                <div>{{ $name }}</div>
                </div>
            </div>
        </div>
        <a href="{{ $url }}">
        <div class="panel-footer">
            <span class="pull-left">{{ $nbr['total'] . ' ' . $total }}</span>
            <span class="pull-right fa fa-arrow-circle-right"></span>
            <div class="clearfix"></div>
        </div>
        </a>
    </div>
</div>

IV-B. Affichage des messages

L'administrateur peut accéder à la gestion des messages :

Image non disponible

Là, il trouve le nouveau message avec un fond jauni et la case à cocher « Vu » non activée.

C'est la méthode index du contrôleur ContactController qui est chargée d'afficher et renseigner cette vue :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/**
 * Display a listing of the resource.
 *
 * @param  ContactRepository $contact_gestion
 * @return Response
 */
public function index(
    ContactRepository $contact_gestion)
{
    $messages = $contact_gestion->index();
 
    return view('back.messages.index', compact('messages'));
}

On retrouve un appel au repository (ContactRepository), cette fois la méthode index :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
/**
 * Get contacts collection.
 *
 * @return Illuminate\Support\Collection
 */
public function index()
{
    return $this->model
    ->oldest('seen')
    ->latest()
    ->get();
}

On va chercher tous les messages, classés prioritairement avec ceux qui n'ont pas été vus et ensuite en partant des plus récents. Puis le contrôleur génère la vue :

Image non disponible

Les messages sont générés dans une boucle :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
@foreach ($messages as $message)
    <div class="panel {!! $message->seen? 'panel-default' : 'panel-warning' !!}">
        <div class="panel-heading">
            <table class="table">
                <thead>
                    <tr>
                        <th class="col-lg-1">{{ trans('back/messages.name') }}</th>
                        <th class="col-lg-1">{{ trans('back/messages.email') }}</th>
                        <th class="col-lg-1">{{ trans('back/messages.date') }}</th>
                        <th class="col-lg-1">{{ trans('back/messages.seen') }}</th>
                        <th class="col-lg-1"></th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="text-primary"><strong>{{ $message->name }}</strong></td>
                        <td>{!! HTML::mailto($message->email, $message->email) !!}</a></td>
                        <td>{{ $message->created_at }}</td>
                        <td>{!! Form::checkbox('vu', $message->id, $message->seen) !!}</td>
                        <td>
                        {!! Form::open(['method' => 'DELETE', 'route' => ['contact.destroy', $message->id]]) !!}
                            {!! Form::destroy(trans('back/messages.destroy'), trans('back/messages.destroy-warning'), 'btn-xs') !!}
                        {!! Form::close() !!}
                        </td>
                    </tr>
                </tbody>
            </table>  
        </div>
        <div class="panel-body">
            {{ $message->text }}
        </div>
    </div>
@endforeach

On retrouve en particulier le dernier message :

Image non disponible

Tous les renseignements figurent et les deux actions possibles sont :

  • marquer le message comme vu en cochant la case ;
  • supprimer le message en utilisant le bouton.

IV-C. Marquer le message

Le marquage du message se fait en Ajax pour éviter de régénérer toute la vue. Cela est effectué en JavaScript sur la page avec l'utilisation de jQuery pour faciliter le codage :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
$(function() {
    $(':checkbox').change(function() {     
        $(this).parents('.panel').toggleClass('panel-warning').toggleClass('panel-default');
        $(this).hide().parent().append('<i class="fa fa-refresh fa-spin"></i>');
        var token = $('input[name="_token"]').val();
        $.ajax({
            url: 'contact/' + this.value,
            type: 'PUT',
            data: "seen=" + this.checked + "&_token=" + token
        })
        .done(function() {
            $('.fa-spin').remove();
            $('input[type="checkbox"]:hidden').show();
        })
        .fail(function() {
            $('.fa-spin').remove();
            var chk = $('input[type="checkbox"]:hidden');
            chk.parents('.panel').toggleClass('panel-warning').toggleClass('panel-default');
            chk.show().prop('checked', chk.is(':checked') ? null:'checked');
            alert('{{ trans('back/messages.fail') }}');
        });
    });
});

Comme la procédure peut prendre un petit moment on fait apparaître une petite animation à la place de la case à cocher :

Image non disponible

La requête arrive à la méthode update du contrôleur ContactController :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
/**
 * Update the specified resource in storage.
 *
 * @param  App\Repositories\ContactRepository $contact_gestion
 * @param  Illuminate\Http\Request $request
 * @param  int  $id
 * @return Response
 */
public function update(
    ContactRepository $contact_gestion,
    Request $request,        
    $id)
{
    $contact_gestion->update($request->input('seen'), $id);
 
    return response()->json(['statut' => 'ok']);
}

On applique le middleware ajax pour cette méthode au niveau du constructeur :

 
Sélectionnez
$this->middleware('ajax', ['only' => 'update']);

La mise à jour s'effectue dans la méthode update du repository (ContactRepository) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/**
 * Update a contact.
 *
 * @param  bool  $vu
 * @param  int   $id
 * @return void
 */
public function update($seen, $id)
{
    $contact = $this->getById($id);
 
    $contact->seen = $seen == 'true';
 
    $contact->save();
}

Ensuite on envoie une réponse JSON au navigateur. Si la procédure a réussi on réaffiche la case en changeant son aspect. En cas d'échec on affiche un message.

IV-D. Supprimer le message

Pour supprimer le message on clique sur le bouton « Supprimer » et on affiche une fenêtre de confirmation :

Image non disponible

En cas de réponse positive on arrive à la méthode destroy du contrôleur (ContactController) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/**
 * Remove the specified resource from storage.
 *
 * @param  App\Repositories\ContactRepository $contact_gestion
 * @param  int  $id
 * @return Response
 */
public function destroy(
    ContactRepository $contact_gestion, 
    $id)
{
    $contact_gestion->destroy($id);
     
    return redirect('contact');
}

Cette méthode est protégée par le middleware admin :

 
Sélectionnez
$this->middleware('admin', ['except' => ['create', 'store']]);

Elle fait appel à la méthode destroy du repository (ContactRepository). Comme cette méthode est commune aux repositories, on la trouve dans le repository de base (Baserepository) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
/**
 * Destroy a model.
 *
 * @param  int $id
 * @return void
 */
public function destroy($id)
{
    $this->getById($id)->delete();
}
 
/**
 * Get Model by id.
 *
 * @param  int  $id
 * @return App\Models\Model
 */
public function getById($id)
{
    return $this->model->findOrFail($id);
}

Remarquez que l'on utilise la méthode getById qui est, elle aussi, commune.

Lorsque le message a été supprimé le contrôleur renvoie la vue d'affichage des messages actualisée.

On a ainsi fait un peu le tour de l'application avec cette gestion des messages !

V. Remerciements

Nous remercions Maurice Chavelli qui nous autorise à publier ce tutoriel.

Nous tenons également à remercier Winjerome pour la gabarisation et Jacques_jean pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2017 Laravel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.