Créer une application : les articles (front-end)

Article mis à jour le 28/10/2015

Les éléments essentiels d’un blog sont assurément les articles. Nous allons donc voir cet aspect. Les articles sont accessibles à tous les visiteurs. Seuls les rédacteurs et les administrateurs peuvent en rédiger.

Nous allons voir comment sont gérés les articles au niveau du front-end. Avec ces considérations :

  • affichage des articles avec pagination
  • affichage des articles par tag
  • recherche dans les articles
  • affichage des commentaires
  • création et modification des commentaires

Les sommaires

Les articles sont accessibles à tous les visiteurs en cliquant sur BLOG dans le menu avec cet aspect :

img68

On a alors les sommaires des articles qui apparaissent avec une pagination qui limite la génération à deux articles à la fois.

C’est la méthode indexFront du contrôleur BlogController qui est chargée de cet affichage :

/**
 * Display a listing of the resource.
 *
 * @return Response
 */
public function indexFront()
{
	$posts = $this->blog_gestion->indexFront($this->nbrPages);
	$links = $posts->setPath('')->render();

	return view('front.blog.index', compact('posts', 'links'));
}

Le repository est injecté dans le constructeur :

/**
 * Create a new BlogController instance.
 *
 * @param  App\Repositories\BlogRepository $blog_gestion
...
 * @return void
*/
public function __construct(
	BlogRepository $blog_gestion,
	...)
{
	...
	$this->blog_gestion = $blog_gestion;
	$this->nbrPages = 2;

	...
}

C’est la méthode indexFront du repository BlogRepository qui est appelée :

/**
 * Get post collection.
 *
 * @param  int  $n
 * @return Illuminate\Support\Collection
 */
public function indexFront($n)
{
	$query = $this->queryActiveWithUserOrderByDate();

	return $query->paginate($n);
}

Le nombre de pages à afficher est passé en paramètre.

On fait appel dans le repository à une fonction privée :

/**
* Create a query for Post.
*
* @return Illuminate\Database\Eloquent\Builder
*/
private function queryActiveWithUserOrderByDate()
{	
	return $this->model
	->select('id', 'created_at', 'updated_at', 'title', 'slug', 'user_id', 'summary')
	->whereActive(true)
	->with('user')
	->latest();
}

On récupère ici les articles actifs classés par dates et leurs auteurs. Le contrôleur envoie ensuite tout ça, accompagné des liens de pagination à la vue :

img69

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

@foreach($posts as $post)
    <div class="box">
        <div class="col-lg-12 text-center">
            <h2>{{ $post->title }}
            <br>
            <small>{{ $post->user->username }} {{ trans('front/blog.on') }} {!! $post->created_at . ($post->created_at != $post->updated_at ? trans('front/blog.updated') . $post->updated_at : '') !!}</small>
            </h2>
        </div>
        <div class="col-lg-12">
            <p>{!! $post->summary !!}</p>
        </div>
        <div class="col-lg-12 text-center">
            {!! link_to('blog/' . $post->slug, trans('front/blog.button'), ['class' => 'btn btn-default btn-lg']) !!}
            <hr>
        </div>
    </div>
@endforeach

Affichage d’un article

Lorsqu’on clique sur le bouton En lire plus on affiche l’article :

img42

C’est la méthode show du contrôleur BlogController qui est chargée de cet affichage :

/**
 * Display the specified resource.
 *
 * @param  Illuminate\Contracts\Auth\Guard $auth	 
 * @param  string $slug
 * @return Response
 */
public function show(
	Guard $auth, 
	$slug)
{
	$user = $auth->user();

	return view('front.blog.show',  array_merge($this->blog_gestion->show($slug), compact('user')));
}

On a besoin de connaître l’utilisateur actuel pour une bonne gestion des commentaires.

C’est la méthode show du repository BlogRepository qui est appelée avec transmission du slug de l’article :

/**
 * Get post collection.
 *
 * @param  string  $slug
 * @return array
 */
public function show($slug)
{
	$post = $this->model->with('user', 'tags')->whereSlug($slug)->firstOrFail();

	$comments = $this->comment
	->wherePost_id($post->id)
	->with('user')
	->whereHas('user', function($q) { $q->whereValid(true); })
	->get();

	return compact('post', 'comments');
}

Ici on récupère l’article, accompagné de son auteur et des tags. On charge aussi les commentaires correspondants et on envoie tout ça au contrôleur qui lui les envoie dans la vue :

img71

Voici le code de l’affichage hors commentaires  :

<div class="row">
	<div class="box">
		<div class="col-lg-12">
			<hr>
			<h2 class="text-center">{{ $post->title }}
			<br>
			<small>{{ $post->user->username }} {{ trans('front/blog.on') }} {!! $post->created_at . ($post->created_at != $post->updated_at ? trans('front/blog.updated') . $post->updated_at : '') !!}</small>
			</h2>
			<hr>
			{!! $post->summary !!}<br>
			{!! $post->content !!}
			<hr>
			@if($post->tags->count())
				<div class="text-center">
					@if($post->tags->count() > 0)
						<small>{{ trans('front/blog.tags') }}</small> 
						@foreach($post->tags as $tag)
							{!! link_to('blog/tag?tag=' . $tag->id, $tag->tag, ['class' => 'btn btn-default btn-xs']) !!}
						@endforeach
					@endif
				</div>
			@endif
		</div>
	</div>
</div>

Les tags

Lorsqu’on clique sur un bouton de tag on doit afficher tous les articles qui correspondent à ce tag :

img43

C’est la méthode tag du contrôleur BlogController qui est chargée de cet affichage :

/**
 * Get tagged posts
 * 
 * @param  Illuminate\Http\Request $request
 * @return Response
 */
public function tag(Request $request)
{
	$tag = $request->input('tag');
	$posts = $this->blog_gestion->indexTag($this->nbrPages, $tag);
	$links = $posts->setPath('')->appends(compact('tag'))->render();
	$info = trans('front/blog.info-tag') . '<strong>' . $this->blog_gestion->getTagById($tag) . '</strong>';
	
	return view('front.blog.index', compact('posts', 'links', 'info'));
}

L’index du tag est récupéré dans la requête qu’on injecte dans la méthode. Pour récupérer les articles on utilise la méthode indexTag du repository BlogRepository :

/**
 * Get post collection.
 *
 * @param  int  $n
 * @param  int  $id
 * @return Illuminate\Support\Collection
 */
public function indexTag($n, $id)
{
	$query = $this->queryActiveWithUserOrderByDate();

	return $query->whereHas('tags', function($q) use($id) { $q->where('tags.id', $id); })
				->paginate($n);
}

Cette méthode ressemble beaucoup à celle qu’on a vue pour l’affichage des articles (indexFront) puisqu’on fait appel à la même fonction privée queryActiveWithUserOrderByDate. La différence réside dans le fait qu’on va limiter les articles à ceux qui possèdent le tag.

On a également besoin dans la vue du nom du tag pour la barre d’information :

img73

C’est la méthode getTagById du repository BlogRepository qui nous le donne :

/**
 * Get tag name by id.
 *
 * @param  int  $tag_id
 * @return string
 */
public function getTagById($tag_id)
{
	return $this->tag->findOrFail($tag_id)->tag;
}

Le contrôleur envoie toutes les informations à la même vue que celle que nous avons vue ci-dessus pour l’affichage classique des articles mais en transmettant en plus l’information pour la barre :

return view('front.blog.index', compact('posts', 'links', 'info'));

C’est au niveau du template (resources/views/front/template.blade.php) que s’effectue cet affichage :

<main role="main" class="container">
	@if(session()->has('ok'))
		@include('partials/error', ['type' => 'success', 'message' => session('ok')])
	@endif	
	@if(isset($info))
		@include('partials/error', ['type' => 'info', 'message' => $info])
	@endif
	@yield('main')
</main>

La recherche

Il est prévu un champ de saisie pour la recherche dans les articles :

img74

C’est la méthode search du contrôleur BlogController qui est chargée de cet affichage :

/**
 * Find search in blog
 *
 * @param  App\Http\Requests\SearchRequest $request
 * @return Response
 */
public function search(SearchRequest $request)
{
	$search = $request->input('search');
	$posts = $this->blog_gestion->search($this->nbrPages, $search);
	$links = $posts->setPath('')->appends(compact('search'))->render();
	$info = trans('front/blog.info-search') . '<strong>' . $search . '</strong>';
	
	return view('front.blog.index', compact('posts', 'links', 'info'));
}

Elle ressemble évidemment beaucoup à la méthode tag vue ci-dessus.

La validation est assurée par la requête de formulaire SearchRequest avec des règles simples :

<?php namespace App\Http\Requests;

class SearchRequest extends Request {

	/**
	 * Get the validation rules that apply to the request.
	 *
	 * @return array
	 */
	public function rules()
	{
		return [
			'search' => 'required|max:100',
		];
	}

}

C’est la méthode search du repository BlogRepository qui est appelée par le contrôleur :

/**
 * Get search collection.
 *
 * @param  int  $n
 * @param  string  $search
 * @return Illuminate\Support\Collection
 */
public function search($n, $search)
{
	$query = $this->queryActiveWithUserOrderByDate();

	return $query->where(function($q) use ($search) {
		$q->where('summary', 'like', "%$search%")
			->orWhere('content', 'like', "%$search%");
	})->paginate($n);
}

On utilise à nouveau la fonction privée queryActiveWithUserOrderByDate. Ensuite on sélectionne les articles avec la recherche passée en paramètre. On applique enfin la pagination.

Le retour par le contrôleur est exactement le même que pour les tags :

return view('front.blog.index', compact('posts', 'links', 'info'));

En effet c’est la même vue et on envoie un message pour la barre :

img75

Les commentaires

Affichage

Les articles sont affichés avec une zone de commentaire dans leur partie inférieure :

img76

On a vu ci-dessus que c’est la méthode show du repository BlogRepository qui récupère les commentaires dans la base pour l’article.

Au niveau de la vue (resources/views/front/blog/show.blade.php) ils sont générés dans une boucle :

@if($comments->count())
    @foreach($comments as $comment)
        <div class="commentitem">
            <h3>
                <small>{{ $comment->user->username . ' ' . trans('front/blog.on') . ' ' . $comment->created_at }}</small>
                @if($user && $user->username == $comment->user->username) 
                    <a id="deletecomment{!! $comment->id !!}" href="#" class="deletecomment"><span class="fa fa-fw fa-trash pull-right" data-toggle="tooltip" data-placement="left" title="{{ trans('front/blog.delete') }}"></span></a>
                    <a id="comment{!! $comment->id !!}" href="#" class="editcomment"><span class="fa fa-fw fa-pencil pull-right" data-toggle="tooltip" data-placement="left" title="{{ trans('front/blog.edit') }}"></span></a>
                @endif
            </h3>
            <div id="contenu{!! $comment->id !!}">{!! $comment->content !!}</div>
            <hr>
        </div>
    @endforeach
@endif

D’autre part si l’utilisateur est authentifié on génère aussi un formulaire :

<div class="row" id="formcreate"> 
    @if(session()->has('warning'))
        @include('partials/error', ['type' => 'warning', 'message' => session('warning')])
    @endif    
    @if(session('statut') != 'visitor')
        {!! Form::open(['url' => 'comment']) !!}    
            {!! Form::hidden('post_id', $post->id) !!}
            {!! Form::control('textarea', 12, 'comments', $errors, trans('front/blog.comment')) !!}
            {!! Form::submit(trans('front/form.send'), ['col-lg-12']) !!}
        {!! Form::close() !!}
    @else
        <div class="text-center"><i class="text-center">{{ trans('front/blog.info-comment') }}</i></div>
    @endif
</div>

S’il n’est pas authentifié il a à la place un petit message :

img77

Soumission

C’est la méthode store du contrôleur CommentController qui est chargée de gérer la soumission d’un commentaire :

/**
 * Store a newly created resource in storage.
 *
 * @param  App\requests\CommentRequest $request
 * @return Response
 */
public function store(
	CommentRequest $request)
{
	$this->comment_gestion->store($request->all(), $request->user()->id);

	if($request->user()->valid)
	{
		return redirect()->back();
	}

	return redirect()->back()->with('warning', trans('front/blog.warning'));
}

La validation est assurée par la requête de formulaire CommentRequest :

<?php namespace App\Http\Requests;

class CommentRequest extends Request {

	/**
	 * Get the validation rules that apply to the request.
	 *
	 * @return array
	 */
	public function rules()
	{
		$id = $this->comment;
		return [
			'comments' . $id => 'required|max:65000',
		];
	}

}

Le contrôleur appelle la méthode store du repository CommentRepository :

/**
 * Store a comment.
 *
 * @param  array $inputs
 * @param  int   $user_id
 * @return void
 */
	public function store($inputs, $user_id)
{
	$comment = new $this->model;	

	$comment->content = $inputs['comments'];
	$comment->post_id = $inputs['post_id'];
	$comment->user_id = $user_id;

	$comment->save();
}

Le contrôleur renvoie alors la même page, avec le commentaire créé et un message s’il l’utilisateur n’a pas été encore validé pour les commentaires :

img78

C’est cette partie de la vue (resources/views/front/blog/show.blade.php) qui gère ce commentaire :

@if(session()->has('warning'))
	@include('partials/error', ['type' => 'warning', 'message' => session('warning')])
@endif

Et évidemment en cas de problème de validation c’est signalé à l’utilisateur :

img79

Suppression

Un utilisateur peut supprimer son commentaire. Il dispose pour cela d’une icône représentant une poubelle :

img80

Évidemment ces icônes n’apparaissent que si l’utilisateur connecté est l’auteur du commentaire :

@if($user && $user->username == $comment->user->username) 
    <a id="deletecomment{!! $comment->id !!}" href="#" class="deletecomment"><span class="fa fa-fw fa-trash pull-right" data-toggle="tooltip" data-placement="left" title="{{ trans('front/blog.delete') }}"></span></a>
    <a id="comment{!! $comment->id !!}" href="#" class="editcomment"><span class="fa fa-fw fa-pencil pull-right" data-toggle="tooltip" data-placement="left" title="{{ trans('front/blog.edit') }}"></span></a>
@endif

La requête est envoyée en Ajax avec ce code :

// Delete comment
$('a.deletecomment').click(function(e) {   
	e.preventDefault();		
	if (!confirm('{{ trans('front/blog.confirm') }}')) return;	
	var i = $(this).attr('id').substring(13);
	var token = $('input[name="_token"]').val();
	$(this).replaceWith('<i class="fa fa-refresh fa-spin pull-right"></i>');
	$.ajax({
		method: 'delete',
		url: '{!! url('comment') !!}' + '/' + i,
		data: '_token=' + token
	})
	.done(function(data) {
		$('#comment' + data.id).parents('.commentitem').remove();
	})
	.fail(function() {
		alert('{{ trans('front/blog.fail-delete') }}');
	});					
});

C’est la méthode destroy du contrôleur CommentController qui est chargée de gérer la suppression d’un commentaire :

/**
 * Remove the specified resource from storage.
 *
 * @param  Illuminate\Http\Request $request
 * @param  int  $id
 * @return Response
 */
public function destroy(
	Request $request, 
	$id)
{
	$this->comment_gestion->destroy($id);

	if($request->ajax())
	{
		return response()->json(['id' => $id]);
	}

	return redirect('comment');
}

On verra que cette méthode est aussi utilisée par le back-end sans Ajax. D’où la présence d’un test de la requête.

Cette méthode est protégée par le middleware auth au niveau du constructeur :

$this->middleware('auth', ['only' => ['store', 'update', 'destroy']]);

Par contre je n’ai jugé utile de vérifier qu’il s’agit réellement de l’auteur du commentaire.

C’est la méthode destroy du repository de base BaseRepository qui effectue la suppression dans la base :

/**
 * Destroy a model.
 *
 * @param  int $id
 * @return void
 */
public function destroy($id)
{
	$this->getById($id)->delete();
}

Modification

Voyons maintenant la partie la plus délicate qui concerne la modification d’un commentaire. Délicate parce qu’il faut :

  • générer dynamiquement un formulaire
  • donner la possibilité d’annuler l’action
  • gérer les erreurs de saisie avec forcément une requête en Ajax
  • ne pas mélanger les formulaires de la page

Pour modifier un article l’utilisateur a un bouton a disposition :

img80

Il obtient ainsi un formulaire :

img81

Il conserve la possibilité de supprimer son commentaire avec la petite poubelle. Il peut aussi annuler l’action avec le bouton « Annuler ». Il peut enfin soumettre le formulaire avec le bouton « Valider ».

Tout cela est évidemment géré côté client en Javascript :

// Set comment edition
$('a.editcomment').click(function(e) {   
	e.preventDefault();
	$(this).hide();
	var i = $(this).attr('id').substring(7);
	var existing = $('#contenu' + i).html();
	var url = $('#formcreate').find('form').attr('action');
	jQuery.data(document.body, 'comment' + i, existing);
	var html = "<div class='row'><form id='form" + i + "' method='POST' action='" + url + '/' + i + "' accept-charset='UTF-8' class='formajax'><input name='_token' type='hidden' value='" + $('input[name="_token"]').val() + "'><div class='form-group col-lg-12 '><label for='comments' class='control-label'>{{ trans('front/blog.change') }}</label><textarea id='cont" + i +"' class='form-control' name='comments" + i + "' cols='50' rows='10' id='comments" + i + "'>" + existing + "</textarea><small class='help-block'></small></div><div class='form-group col-lg-12'>" + buttons(i) + "</div>";
	$('#contenu' + i).html(html);
	CKEDITOR.replace('comments' + i, {
		language: '{{ config('app.locale') }}',
		height: 200,
		toolbarGroups: [
			{ name: 'basicstyles', groups: [ 'basicstyles'] }, 
			{ name: 'links' },
			{ name: 'insert' }
		],
		removeButtons: 'Table,SpecialChar,HorizontalRule,Anchor'
	});
});

Je ne vais pas détailler le fonctionnement puisqu’il n’a rien à voir avec Laravel à ce niveau et pourrait être traité de manière différente, par exemple avec AngularJS. Par contre on va s’intéresser à la partie soumission en Ajax parce qu’ici Laravel intervient.

C’est la méthode update du contrôleur CommentController qui est chargée de gérer la modification d’un commentaire :

/**
 * Update the specified resource in storage.
 *
 * @param  App\requests\CommentRequest $request
 * @param  int  $id
 * @return Response
 */
public function update(
	CommentRequest $request, 
	$id)
{
	$id = $request->segment(2);
	$content = $request->input('comments' . $id);
	$this->comment_gestion->updateContent($content, $id);

	return response()->json(['id' => $id, 'content' => $content]);	
}

On voit que la validation est réalisée par la requête de formulaire CommentRequest que nous avons déjà vue ci-dessus pour la création.

La soumission côté client est gérée par ce code Javascript :

// Validation 
$(document).on('submit', '.formajax', function(e) {  
	e.preventDefault();
	var i = $(this).attr('id').substring(4);
	$('#val' + i).parent().html('<i class="fa fa-refresh fa-spin fa-2x"></i>').addClass('text-center');
	$.ajax({
		method: 'put',
		url: $(this).attr('action'),
		data: $(this).serialize()
	})
	.done(function(data) {
		$('#comment' + data.id).show();
		$('#contenu' + data.id).html(data.content);	
	})
	.fail(function(data) {
		var errors = data.responseJSON;
		$.each(errors, function(index, value) {
			$('textarea[name="' + index + '"]' + ' ~ small').text(value);
			$('textarea[name="' + index + '"]').parent().addClass('has-error');
			$('.fa-spin').parent().html(buttons(index.slice(-1))).removeClass('text-center');
		});
	});
});

Si la validation est correcte le contrôleur renvoie une réponse JSON en transmettant l’identifiant du commentaire et son contenu. On régénère le commentaire avec le nouveau contenu.

C’est la méthode updateContent du repository CommentRepository qui fait la mise à jour dans la base :

/**
 * Update a comment.
 *
 * @param  string $commentaire
 * @param  int    $id
 * @return void
 */
	public function updateContent($content, $id)
{
	$comment = $this->getById($id);	
	$comment->content = $content;
	$comment->save();
}

Si la validation n’est pas correcte il faut le signaler à l’utilisateur. Donc on récupère les erreurs (ici en fait une seule peut arriver) et on adapte le DOM en conséquence.

Laisser un commentaire