Matrix Cette version bis de ce chapitre possède le même contenu que l’autre mais est mis à jour pour la version 3 de Bootstrap.

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 :

img15 Voici le code HTML de la page :

<!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 rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.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 class="container">
  <header class="jumbotron" id="entete">
    <h1>Mon joli blog !</h1>
  </header>
  <nav class="navbar navbar-default">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">Mon joli blog</a>
    </div>
    <div class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li><a href="#" class="actif">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 class="navbar-form navbar-left pull-right">
        <input type="text" class="form-group form-control" placeholder="Recherche">
      </form>
    </div>
  </nav>
  <section class="row">
    <div class="col-md-6">
      <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>
      <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
    </div>
    <div class="col-md-6">
      <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>
      <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
    </div>
    <div class="col-md-6">
      <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>
      <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
    </div>
    <div class="col-md-6">
      <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>
      <a href="#" class="btn btn-default">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
    </div>
  </section>
  <footer>
    <em>© 2013</em>
  </footer>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>
</body>

J’utilise des CDN pour les librairies nécessaires. Et voici la feuille de style main.css :

body {
    font-family: 'Imprima', sans-serif;
    background-color: #F5F5F5;
}
.container {
   margin-top: 20px;
}
.jumbotron, .navbar, .comment {
    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%;
}
.nav li a {
    font-size:large;
}
.nav li a:not(.actif):hover {
    background-color:#eee !important;
    box-shadow: 2px 2px 14px #555;
}
.nav li a.actif {
    background-color:#888 !important;
    color:#fff !important;
    box-shadow: 2px 2px 14px #555;
}
footer {
    margin-top: 20px;
    text-align: center;
}

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('//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css') }}
		{{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css') }}
		{{ HTML::style('assets/css/main.css') }}
		{{ HTML::style('http://fonts.googleapis.com/css?family=Imprima') }}
	</head>

	<body>
		<div class="container">

		  <header class="jumbotron" id="entete">
		    <h1>Mon joli blog !</h1>
		  </header>

		  <nav class="navbar navbar-default">
		    <div class="navbar-header">
		      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
		        <span class="icon-bar"></span>
		        <span class="icon-bar"></span>
		        <span class="icon-bar"></span>
		      </button>
		      <a class="navbar-brand" href="#">Mon joli blog</a>
		    </div>
		    <div class="collapse navbar-collapse">
		      <ul class="nav navbar-nav">
		        @yield('navigation')
		      </ul>
		      {{ Form::open(array('url' => 'find', 'method' => 'POST', 'class' => 'navbar-form navbar-left pull-right')) }}
					{{ Form::text('find', '', array('class' => 'form-group form-control', 'placeholder' => 'Recherche')) }}
					{{ Form::close() }}
		    </div>
		  </nav>

			<section class="row">
				@yield('content')
			</section>

		  <footer>
		    <em>© 2013</em>
		  </footer>

		</div>
		{{ HTML::script('//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'); }}
		{{ HTML::script('//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js'); }}
	</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++)
        <div class="col-md-6">
            <h3>{{$articles[$i]->title}}</h3>
            <p>{{$articles[$i]->intro_text}}</p>
            <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
        </div>
    @endfor

@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="col-md-12">
		<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>

@foreach ($comments as $comment)
	<div class="col-md-12">
		<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>
@endforeach

@if ($article->allow_comment and Auth::check())
<hr />
	<div class="col-md-12">
		<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', '', $attributes = array('class' => 'form-control')) }}
			{{ Form::label('comment', 'Votre commentaire :') }}
			{{ Form::textarea('comment', '', $attributes = array('class' => 'form-control')) }}
			{{ Form::submit('Envoyer', array('class' => 'btn btn-default')) }}
			{{ Form::close() }}
		</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 : img16

On obtient bien le résultat attendu Smile. 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 :

img17

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 :

img18

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 :

img19

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. Malheureusement la version « automatique » ne connait pour le moment que Bootstrap 2, alors il va falloir un peu besogner pour arriver au résultat. Ouvrez le fichier app/config/view.php et trouvez cette ligne :

'pagination' => 'pagination::slider',

Modifiez la pour obtenir ça :

'pagination' => 'pagination::slider-3',

Maintenant votre pagination est Bootstrap 3 ready ! Je vais vous montrer comment ajouter cette pagination à la page d’accueil. Apportez cette modification à la route de l’accueil :

Route::get('/', array('as' => 'accueil', 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++)
        <div class="col-md-6">
            <h3>{{$articles[$i]->title}}</h3>
            <p>{{$articles[$i]->intro_text}}</p>
            <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
        </div>
    @endfor

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

@stop

Et voilà le résultat :

img20

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

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 !');
	}
}));

J’ai 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">
            {{ Session::get('flash_notice') }}
        </div>
    @endif

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

    @for ($i = 0; $i < count($articles); $i++)
        <div class="col-md-6">
            <h3>{{$articles[$i]->title}}</h3>
            <p>{{$articles[$i]->intro_text}}</p>
            <a class="btn btn-default" href="{{ url('art/'.$actif.'/'.$articles[$i]->id) }}">Lire la suite <span class="glyphicon glyphicon-play"></span></a>
        </div>
    @endfor

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

@stop

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

img21

Et si on ne rentre aucun terme de recherche :

img22

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.

Laravel 4 : chapitre 19 bis : Un blog : le template et les routes

Vous pourrez aussi aimer

Laisser un commentaire