Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Laravel 4 : chapitre 19 : Un blog : le template et les routes
Lundi 25 février 2013 18:26
J'ai enfin mis à niveau cet article pour Bootstrap 3. Comme il y a une continuité j'ai créé un nouveau fil pour cette version.

L'aspect du blog

En général pour réaliser un site je commence par faire une page HTML de l'aspect qu'il aura pour fixer la mise en page et la feuille de style. Pour ce blog on va faire simple et utiliser Bootstrap de Twitter pour simplifier le code. On va avoir une entête avec le nom du blog, une barre de navigation qui comportera les catégories et un espace pour le contenu et enfin un pied de page sommaire :

img75

Ce n'est pas très coloré mais ça suffira à notre exemple. Récupérez les fichiers de Bootstrap sur le site dédié. Créez ces dossiers et copiez les fichiers indiqués (les fichier accueil.html et main.css ne font pas partie de Bootstrap, leur code figure un peu plus bas):

img76 Voici le code HTML de la page accueil.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Mon joli blog</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" >
<link type="text/css" rel="stylesheet" href="assets/css/bootstrap.min.css" > 
<link type="text/css" rel="stylesheet" href="assets/css/bootstrap-responsive.min.css" >
<link type="text/css" rel="stylesheet" href="assets/css/main.css" >  
<link type="text/css" rel="stylesheet" href="http://fonts.googleapis.com/css?family=Imprima" >
</head>

<body>
<div>
  <header>
    <div id="entete">
      <h1>Mon joli blog !</h1>
    </div>
  </header>
  <nav>
    <div>
      <ul>
        <li><a href="#">Accueil</a></li>
        <li><a href="#">Catégorie 1</a></li>
        <li><a href="#">Catégorie 2</a></li>
        <li><a href="#">Catégorie 3</a></li>
        <li><a href="#">Catégorie 4</a></li>
      </ul>
      <form>
        <input type="text" placeholder="Recherche">
      </form>
    </div>
  </nav>
  <section>
    <div>
      <div>
        <h2>Titre</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a href="#">Lire la suite <i></i></a></p>
      </div>
      <div>
        <h2>Titre</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a href="#">Lire la suite <i></i></a></p>
      </div>
    </div>
    <div>
      <div>
        <h2>Titre</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a href="#">Lire la suite <i></i></a></p>
      </div>
      <div>
        <h2>Titre</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a href="#">Lire la suite <i></i></a></p>
      </div>
    </div>
  </section>
  <footer>
    <div> <em>Copyright 2013</em> </div>
  </footer>
</div>
</div>
</body>
Et la feuille de style main.css :
body {
	font-family: 'Imprima', sans-serif;
	background-color: #F5F5F5;
}
.container {
	margin-top: 20px;
}
#entete {
	height: 130px;
	border-radius: 10px;
	box-shadow: 2px 2px 6px #bbb;
}
.comment {
	border:thin;
	border-style:solid;
	border-color:#bbb;
	padding: 10px;
	margin-bottom: 10px;
	border-radius: 10px;
	background-color:#ddd;
}
textarea {
	width:98%;
}
h1 {
	padding-top:20px;
	padding-left:50px;
}
.navbar {
	margin-top: 10px;
}	
.navbar-inner, .well, .comment {
    box-shadow: 2px 2px 14px #999;
}	
.navbar .nav li a {
	text-align:center;
	font-size:large;
}
.navbar .nav li a:hover {
	background-color:#ccc;
	color:#fff;
	box-shadow: 2px 2px 14px #555;
	-moz-transition: all 1s;
    -webkit-transition: all 1s;
    -o-transition: all 1s;
    transition: all 1s;
}
.navbar .nav li a.actif {
	background-color:#888;
	color:#fff;
	box-shadow: 2px 2px 14px #555;
}
footer {
	min-height: 30px;
	text-align: center;
	padding-top: 10px;
}

Je ne commente pas les éléments HTML et CSS qui sortent du cadre de Laravel. Les options de la barre de navigation seront créées dynamiquement (à part Accueil) en fonction des catégories présentes auxquelles il faudra sans doute adjoindre un lien pour la connexion/déconnexion.

Le template

Maintenant il nous faut transformer notre page HTML en template utilisable. Nous allons évidemment faire appel à Blade dont je vous ai déjà parlé. On va conserver dans la page les informations fixes et créer des sections pour les parties qui doivent changer.

On trouve deux sections, une pour la navigation et une autre pour le contenu. On crée un fichier app/views/template_blog.blade.php :

<!DOCTYPE html>
<html lang="fr">
	<head>
		<meta charset="utf-8">
		<title>
			Mon joli blog
		</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
                {{ HTML::style('assets/css/bootstrap.min.css') }}
                {{ HTML::style('assets/css/bootstrap-responsive.min.css') }}
                {{ HTML::style('assets/css/main.css') }}
                {{ HTML::style('http://fonts.googleapis.com/css?family=Imprima') }}
	</head>

	<body>
		<div class="container">

			<header class="row">
				<div id="entete" class="span12">
					<h1>
						Mon joli blog !
					</h1>
				</div>
			</header>

			<nav class="navbar">
				<div class="navbar-inner">
					<ul class="nav">
						@yield('navigation')
					</ul>
					{{ Form::open(array('url' => 'find', 'method' => 'POST', 'class' => 'navbar-search pull-right')) }}
					{{ Form::text('find', '', array('class' => 'search-query', 'placeholder' => 'Recherche')) }}
					{{ Form::close() }}
				</div>
			</nav>

			<section>
				@yield('content')
			</section>

			<footer class="row">
				<div class="span12">
					<em>
						Copyright 2013
					</em>
				</div>
			</footer>

		</div>
	</body>
</html>

Les liens pour les feuilles de style se font avec la méthode style de la classe HTML (Si ça ne fonctionne pas vérifiez d'avoir la dernière version du framework avec composer update).

Maintenant il nous faut une vue pour exploiter ce template. Mais nous devons envisager deux situations différentes, si on affiche la page d’accueil ou une catégorie on va avoir plusieurs introductions d'articles affichées alors que si on affiche un article nous avons juste le titre et le texte de cet article avec ses commentaires et peut-être d'autres informations que nous pourrons greffer ultérieurement. Il semble donc judicieux de créer une vue spécifique pour la navigation qui sera toujours traitée de la même façon et deux vues à appeler directement : une pour une catégorie ou l'accueil, et une autre pour un article.

Pour la navigation il nous faudra recevoir les informations des catégories dans une variable $categories et l'index de la catégorie active dans $actif, ce qui donne le fichier app/views/navigation.blade.php :

@section('navigation')
  <li>{{ link_to_route('accueil', 'Accueil', null, ($actif == 0)? array('class' => 'actif'): null) }}</li>

  @foreach ($categories as $categorie)
    <li>{{ link_to('cat/'.$categorie->id, $categorie->title, ($actif == $categorie->id)? array('class' => 'actif'): null) }}</li>
  @endforeach

@stop

La vue pour l'accueil et l'affichage des catégories doit mentionner évidemment le layout qu'on a créé et aussi intégrer la vue pour la navigation. On reçoit les informations des articles dans la variable $articles. Ce qui donne le fichier app/views/accueil.blade.php :

@extends('template_blog')

@include('navigation')

@section('content')
    @for ($i = 0; $i < count($articles); $i++)
    	@if ($i%2 == 0)
    		<div class="row">
	    @endif
        <div class="span6">
        <h3>{{$articles[$i]->title}}</h3>
        <p>{{$articles[$i]->intro_text}}</p>
        <p><a href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <i class="icon-play"></i></a></p>
        </div>
    	@if ($i%2 != 0)
    		</div>
	    @endif        
    @endfor
    @if (count($articles)%2 != 0)
        </div>
    @endif 
@stop
Il ne nous manque plus que la vue pour un article app/views/article.blade.php :
@extends('template_blog')

@include('navigation')

@section('content')

<div class="row">
	<div class="span12">
		<div class="well">
			<em class="pull-right">
				Ecrit le {{date('d-m-Y',strtotime($article->created_at))}}
			</em>
			<h3>
				{{$article->title}}
			</h3>
			<p>
				{{$article->full_text}}
			</p>
		</div>
	</div>
</div>

@foreach ($comments as $comment)
<div class="row">
	<div class="span12">
		<div class="comment">
			<em class="pull-right">
				Ecrit par {{$comment->username}} le {{date('d-m-Y',strtotime($comment->created_at))}}
			</em>
			<h5>
				{{$comment->title}}
			</h5>
			<p>
				{{$comment->text}}
			</p>
		</div>
	</div>
</div>
@endforeach

@if ($article->allow_comment and Auth::check())
<hr />
<div class="row">
	<div class="span12">
		<div class="well">
			{{ Form::open(array('url' => 'comment')) }}
			{{ Form::hidden('id_art', $article->id) }}
			{{ Form::hidden('id_cat', $actif) }}
			{{ Form::label('title', 'Titre de votre commentaire :') }}
			{{ Form::text('title') }}
			{{ Form::label('comment', 'Votre commentaire :') }}
			{{ Form::textarea('comment') }}
			{{ Form::submit('Envoyer') }}
			{{ Form::close() }}
		</div>
	</div>
</div>
@endif

@stop

Remarquez encore l'utilisation de la classe form pour le formulaire ainsi que les deux conditions pour afficher ce formulaire de saisie de commentaire : allow_comment doit être vrai et l'utilisateur doit être authentifié, ce qui nous est donné par Auth::check(). Pour tester il nous faudra dans la route correspondant authentifier artificiellement un utilisateur dans un premier temps.

Les routes

On va créer les routes et quelques informations pour tester tout ça. Le code correspondant sera évidemment dans app/routes.php. Il nous faut déjà une première route pour répondre à l'URI http://localhost/blog/public. Au niveau des données nous avons besoin de connaître les catégories existantes pour la barre de navigation ainsi que les titres et introductions des articles qui vont figurer sur la page d'accueil. Comme on a pas prévu de pagination on va se retrouver avec tous les articles listés, il nous faudra évidemment corriger cela plus tard mais pour le moment nous allons nous en contenter. Voici donc la syntaxe de cette route :

Route::get('/', array('as' => 'accueil', function()
{
  $categories = Categorie::all();
  $articles = Article::select('id', 'title', 'intro_text')->orderBy('created_at', 'desc')->get();
  return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => 0));  
}));
On peut déjà tester cette route : img80

On obtient bien le résultat attendu. Continuons avec la route quand on clique sur une catégorie. Cette fois l'URI est du genre http://localhost/blog/public/cat/1. Il nous faut récupérer dans la base tous les articles de cette catégorie (on a pas encore prévu ici de pagination non plus). On va aussi vérifier que l'index de la catégorie correspond à une donnée existante, dans le cas contraire on déclenche une erreur 404 :

Route::get('cat/{id}', function($id)
{
	$categories = Categorie::all();
	$categorie = Categorie::find($id);
	if($categorie) {
		$articles = $categorie->articles()->orderBy('created_at', 'desc')->get();
		return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => $id));
	}
	else App::abort(404);
});
App::missing(function($exception)
{
    return 'Cette page n\'existe pas !';
});

On vérifie aussi que ça fonctionne, par exemple en cliquant sur la première catégorie :

img81

Il ne nous manque plus que la route pour l'affichage d'un article du genre http://localhost/blog/public/art/1/1. On va chercher dans la base toutes les données de cet article. Au passage on vérifie que les index transmis correspondent à des données sinon on déclenche une erreur 404 :

Route::get('art/{cat_id}/{art_id}', function($cat_id, $art_id)
{
	$categories = Categorie::all();
	$article = Article::find($art_id);
	if($article) {
		//////////////////////////////////////////////////////////////
		// Log d'un utilisateur pour tester la saisie des commentaires
		$user = User::where('username', '=', 'admin')->first();
		Auth::login($user);
		//////////////////////////////////////////////////////////////
		$comments = $article->comments()->orderBy('comments.created_at', 'desc')
                                    ->join('users', 'users.id', '=', 'comments.user_id')
                                    ->select('users.username', 'title', 'text', 'comments.created_at')
                                    ->get();
		return View::make('article', array('categories' => $categories, 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));
	}
	else App::abort(404);
});

J'ai aussi prévu l'authentification de l'administrateur pour voir apparaître le formulaire de saisie des commentaires. On supprimera ces lignes quand on aura vérifié que tout fonctionne correctement. Voici le résultat avec l'URI mentionnée ci-dessus :

img82

L'affichage est terminé, il ne nous manque plus que la route pour recevoir le retour de la saisie d'un commentaire, il faut enregistrer  les valeurs dans la tables comments et rediriger sur la page de l'article :

Route::post('comment', array('before' => 'csrf', function() 
{
	Comment::create(
		array(
			'title' => Input::get('title'),
			'user_id' => Auth::user()->id,
			'article_id' => Input::get('id_art'),
			'text' => Input::get('comment')
		)
	);
	return Redirect::to('art/'.Input::get('id_cat').'/'.Input::get('id_art'));
}));

Cette fois on a un type post. Notez que tel qu'est écrit ce code l'utilisateur peut transmettre des balises HTML, pour des raisons de sécurité vous pouvez filtrer les entrées, par exemple avec la fonction strip_tags(). Vous pouvez aussi autoriser certaines balises en filtrant les entrées ou encore mieux utiliser HTML Purifier (il y a un package pour l'intégrer à Laravel 4). On a aussi un contrôle csrf pour la sécurité avec un filtre sur la route.

Les modèles de Laravel prévoient désormais une protection d'assignation de masse, et si vous utilisez le code ci-dessus vous allez rencontrer cette exception :

img38

Pour éviter ça vous devez déclarer les champs autorisés dans le modèle app/model/Comment.php :

class Comment extends Eloquent {

    protected $fillable = array('title', 'user_id', 'article_id', 'text');

    public function article()
    {
        return $this->belongsTo('Article');
    }

    public function user()
    {
        return $this->belongsTo('User');
    }
}

La pagination

Ceux qui ont déjà codé une pagination de pages web savent que ça peut être assez laborieux. Avec Laravel c'est presque trop facile. Je vais vous montrer comment ajouter la pagination à la page d'accueil. Apportez cette modification à la route de l'accueil :

Route::get('/', function()
{
	$categories = Categorie::all();
	$articles = Article::select('id', 'title', 'intro_text')->orderBy('created_at', 'desc')->paginate(4);
	return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => 0));
});

J'ai remplacé get() par paginate(4) pour obtenir 4 articles à la fois. Il faut aussi créer les liens de navigation dans la vue app/views/accueil.blade.php :

@extends('template_blog')

@include('navigation')

@section('content')
    @for ($i = 0; $i < count($articles); $i++)
        @if ($i%2 == 0)
            <div>
        @endif
        <div>
            <h3>{{$articles[$i]->title}}</h3>
            <p>{{$articles[$i]->intro_text}}</p>
            <p><a href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <i></i></a></p>
        </div>
        @if ($i%2 != 0)
            </div>
        @endif        
    @endfor
    @if (count($articles)%2 != 0)
        </div>
    @endif 
    {{$articles->links()}}
@stop
Et voilà le résultat :

img83

Notez que la mise en forme est automatiquement adaptée à l'utilisation de Bootstrap !

Si vous réalisez ce tutoriel avec une version récente de Laravel il faut faire une modification de configuration pour obtenir une pagination correcte avec Bootstrap 2 dans le fichier app/config/views.php :

<?php
'pagination' => 'pagination::slider',

Il nous faut faire la même chose pour la route des catégories :

Route::get('cat/{id}', function($id)
{
	$categories = Categorie::all();
	$categorie = Categorie::find($id);
	if($categorie) {
		$articles = $categorie->articles()->orderBy('created_at', 'desc')->paginate(4);
		return View::make('accueil', array('categories' => $categories, 'articles' => $articles, 'actif' => $id));
	}
	else App::abort(404);
});

Injecter l'instance d'un modèle

Il est possible de lier un modèle à une route, autrement dit de définir le paramètre transmis comme étant l'id d'un enregistrement correspondant au modèle et d'injecter l'objet correspondant. Nous allons donc optimiser nos routes avec cette élégante possibilité. Reprenons dans un premier temps la route pour les catégories :

Route::model('cat', 'Categorie');
Route::get('cat/{cat}', function(Categorie $categorie)
{
	$articles = $categorie->articles()->orderBy('created_at', 'desc')->paginate(4);
	return View::make('accueil', array('categories' => Categorie::all(), 'articles' => $articles, 'actif' => $categorie->id));
});

On n'a même plus besoin de tester si la catégorie existe bien, Laravel se charge lui-même d'envoyer une erreur 404 ! On va utiliser la même possibilité pour les articles :

Route::model('art', 'Article');
Route::get('art/{cat_id}/{art}', function($cat_id, Article $article)
{
	$comments = $article->comments()->orderBy('comments.created_at', 'desc')
                                    ->join('users', 'users.id', '=', 'comments.user_id')
                                    ->select('users.username', 'title', 'text', 'comments.created_at')
                                    ->get();
	return View::make('article', array('categories' => Categorie::all(), 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));
});

Le formulaire de recherche

Il nous faut aussi faire fonctionner le formulaire de recherche. On prévoit une route pour lui :

Route::post('find', array('before' => 'csrf', function() {
	$match = Input::get('find');
	if($match) {
		return Redirect::route('accueil', array('match' => $match))
			->with('flash_notice', 'Résultats pour la recherche du terme '.$match);
	} else {
		return Redirect::route('accueil')
			->with('flash_error', 'Il faudrait entrer un terme pour la recherche !');
	}
}));

Remarquez que j'ai prévu la redirection vers une route nommée, nous allons donc un peu modifier la route de l'accueil en conséquence. J'ai aussi prévu la transmission d'information en variable de session "flash", c'est à dire qui ne servent que pour cette redirection. Voilà déjà la route de l'accueil modifiée :

Route::get('/{match?}', array('as' => 'accueil', function ($match = NULL) {
	if($match) {
		$articles = Article::select('id', 'title', 'intro_text')
							->orderBy('created_at', 'desc')
							->where('intro_text', 'like', '%'.$match.'%')
							->orwhere('full_text', 'like', '%'.$match.'%')
							->get();	
	} else {
		$articles = Article::select('id', 'title', 'intro_text')
							->orderBy('created_at', 'desc')
							->paginate(4);
	}
    return View::make('accueil',  array(
        'categories' => Categorie::all(),
        'articles' => $articles,
        'actif' => 0
    )); 
}));

J'ai ajouté un paramètre optionnel pour recevoir le terme de recherche (attention toutefois à ce type de route qui va intercepter toutes les URI comportant un seul paramètre ! Dans le cas de ce blog le problème ne se présente pas sinon il faudrait placer cette route en dernier), j'ai aussi ajouté la requête correspondante. Il ne nous reste plus qu'à adapter la vue de l'accueil pour recevoir les informations de session et aussi pour faire un test pour les liens de pagination. En effet il serait délicat ici de faire une pagination pour le résultat de la recherche :

@extends('template_blog')

@include('navigation')

@section('content')

    @if (Session::has('flash_notice'))
        <div class="alert alert-success span7">
            {{ Session::get('flash_notice') }}
        </div>
    @endif

    @if (Session::has('flash_error'))
        <div class="alert alert-error span6">
            {{ Session::get('flash_error') }}
        </div>
    @endif

    @for ($i = 0; $i < count($articles); $i++)
        @if ($i%2 == 0)
            <div>
        @endif
        <div>
            <h3>{{$articles[$i]->title}}</h3>
            <p>{{$articles[$i]->intro_text}}</p>
            <p><a href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <i></i></a></p>
        </div>
        @if ($i%2 != 0)
            </div>
        @endif        
    @endfor

    @if (count($articles)%2 != 0)
        </div>
    @endif 

    @if (method_exists($articles,'links'))
   		{{$articles->links()}}
    @endif

@stop

Il ne nous reste plus qu'à tester ça :

img84

Et si on ne rentre aucun terme de recherche :img85

Le groupement des filtres

Une dernière touche d'optimisation. On va grouper deux routes qui utilisent le même filtre :

Route::group(array('before' => 'csrf'), function () {

	Route::post('comment', function() {
		Comment::create(
			array(
				'title' => Input::get('title'),
				'user_id' => Auth::user()->id,
				'article_id' => Input::get('id_art'),
				'text' => Input::get('comment')
			)
		);
		return Redirect::to('art/'.Input::get('id_cat').'/'.Input::get('id_art'));
	});

	Route::post('find', function() {
		$match = Input::get('find');
		if($match) {
			return Redirect::route('accueil', array('match' => $match))
				->with('flash_notice', 'Résultats pour la recherche du terme '.$match);
		} else {
			return Redirect::route('accueil')
				->with('flash_error', 'Il faudrait entrer un terme pour la recherche !');
		}
	});

});

Et voilà le travail Laughing. Notre blog commence a avoir un peu d'allure mais il nous reste du travail. Si vous êtes un peu perdu dans le code vous pouvez télécharger les fichiers correspondant à cet article et au précédent. La prochaine étape sera l'authentification des utilisateurs.



Par bestmomo

Nombre de commentaires : 17