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 :
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 :
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) :
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 :
event('
user.access
'
);
Or la méthode setStatut du service écoute cet événement :
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 :
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 :
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 :
Avec ce code :
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 :
{!!
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 :
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 :
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 :
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 :
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 :
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é :
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 :
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 :
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) :
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 :
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 :
avec ce code :
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 :
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 :
On retrouve un appel au repository (ContactRepository), cette fois la méthode index :
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 :
Les messages sont générés dans une boucle :
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 :
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 :
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 :
La requête arrive à la méthode update du contrôleur ContactController :
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 :
$this
->
middleware('
ajax
'
,
[
'
only
'
=>
'
update
'
]
);
La mise à jour s'effectue dans la méthode update du repository (ContactRepository) :
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 :
En cas de réponse positive on arrive à la méthode destroy du contrôleur (ContactController) :
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 :
$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) :
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.