Laravel 5

Créer une application : le contact

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 :

img57

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.

Le statut

Le menu s’adapte automatiquement selon qu’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 :

@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) :

<?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 :

event('user.access');

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

protected $listen = [
	...
	'user.access' => ['App\Services\Statut@setStatut']
];

On défini 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 :

protected $listen = [
	'auth.login' => ['App\Services\Statut@setLoginStatut'],
	'auth.logout' => ['App\Services\Statut@setVisitorStatut'],
	...
];

Le formulaire

Affichage

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

/**
 * 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 :

img58

Avec ce code :

@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 :

{!! 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 :

<?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.

Traitement

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

/**
 * 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 qu’on injecte une requête de formulaire pour la validation (App\Http\Requests\ContactRequest). Voici le code de cette requête :

<?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 :

/**
 * 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 :

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

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

img59

L’administration

Le tableau de bord

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

img60

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 :

/**
* 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 :

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) :

/**
 * 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 :

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

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

img61

Voici la partie qui concerne les contacts :

@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 :

img62

Avec ce code :

<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>

Affichage des messages

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

img63

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é d’afficher et renseigner cette vue :

/**
 * 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 :

/**
 * 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. Ensuite le contrôleur génère la vue :

img64

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

@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 :

img65

Tous les renseignements figurent. les deux actions possibles sont :

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

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 :

$(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 :

img66

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

/**
 * 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 :

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

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

/**
 * 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.

Supprimer le message

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

img67

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

/**
 * 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 :

$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) :

/**
 * 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 qu’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 !

Print Friendly, PDF & Email

Laisser un commentaire