Laravel 4

Laravel 4 : chapitre 30 : Le filtrage

On a vu dans l’article précédent qu’on peut contraindre les paramètres d’une route mais on est rapidement assez limités. Si on désire quelque chose de plus complet et efficace on a à notre disposition le filtrage. Les filtres sont des règles d’action que l’on applique sur les routes, en amont (before) ou en aval (after). Ils se situent physiquement dans le dossier app/filters.php :

img35

Les filtres par défaut

Par défaut on y trouve déjà les filtres before, after, auth, auth.basic, guest et csrf :

App::before(function($request)
{
	//
});

App::after(function($request, $response)
{
	//
});

Route::filter('auth', function()
{
	if (Auth::guest()) return Redirect::guest('login');
});

Route::filter('auth.basic', function()
{
	return Auth::basic();
});

Route::filter('guest', function()
{
	if (Auth::check()) return Redirect::to('/');
});

Route::filter('csrf', function()
{
	if (Session::token() != Input::get('_token'))
	{
		throw new Illuminate\Session\TokenMismatchException;
	}
});

Les filtres globaux

Les filtres before et after sont particuliers parce qu’ils s’appliquent à toutes les routes. Autrement dit le filtrage que vous effectuez à ce niveau concernera toutes les URL qui arrivent. Imaginez par exemple que vous voulez appliquer le filtre CSRF à toutes les requêtes de type « post ». Vous pouvez évidemment le prévoir dans vos routes ou vos contrôleurs mais vous pouvez aussi anticiper dans le filtre before, par exemple ainsi :

App::before(function($request)
{
	if ($request->getMethod() === 'POST') Route::callRouteFilter('csrf', [], '', $request);
});

De la même façon vous pouvez intervenir sur la réponse avec le filtre after. Supposons que vous voulez ajouter quelque chose systématiquement, vous pouvez le faire facilement :

App::after(
	function ($request, $response) {
		$content = $response->getContent() . '<br>Mon petit ajout';
		$response->setContent($content);
	}
);

Maintenant avec la route :

Route::get('/', function() { return 'Coucou'; });

et l’URL http://localhost/laravel/public/ vous obtenez :

Coucou
Mon petit ajout

A l’usage on utilise essentiellement les filtres before. En effet il est intéressant d’effectuer un tri avant la gestion des routes, la faire après présente bien moins d’intérêt et ne correspond qu’à des cas très particuliers.

Les filtres d’authentification

Il y a 3 filtres prévus de base pour l’authentification :

  • auth pour vérifier si l’utilisateur est authentifié
  • auth.basic pour vérifier l’authentification « basique » qui utilise l’email
  • guest qui est l’inverse de auth

Pour utiliser ces filtres sur une route la syntaxe est la suivante :

Route::get('messages', array('before' => 'auth', function()
{
	echo 'Je peux lire les messages !';
}));

Ici on applique le filtre auth sur la route en effectuant ce filtrage avant d’accéder à la route (before), donc si l’utilisateur n’est pas authentifié il n’aura pas accès aux messages. Si vous regardez l’URL généré dans ce cas vous trouvez http://localhost/laravel/public/login. Pourquoi ? Parce que dans le filtre auth on a une redirection en cas d’échec :

if (Auth::guest()) return Redirect::guest('login');

Il faut donc gérer cette redirection dans le cadre d’une application comme je l’ai montré dans cet article. Vous pouvez évidemment modifier la cible si « login » ne vous plaît pas. pour tester l’efficacité du filtrage dans sa version positive vous pouvez authentifier artificiellement un utilisateur :

Auth::login(User::find(1));

Il faut évidemment avoir une table active dans votre base de données et le modèle correspondant. Et cette fois vous devez obtenir :

Je peux lire les messages !

Si vous voulez activer une route non plus pour les utilisateurs authentifiés mais pour les autres alors il faut utiliser le filtre guest :

Route::get('messages', array('before' => 'guest', function()
{
	echo 'Je peux lire les messages !';
}));

Si vous voulez tester tout ça pensez à supprimer les cookies entre deux tests Tongue Out.

Le filtre auth.basic s’utilise exactement de la même façon. Il est basé sur la vérification de l’email par défaut mais vous pouvez changer ça avec ce code :

Auth::basic('username');

Maintenant c’est le nom de l’utilisateur qui va servir de référence et on obtient une fenêtre de connexion :

img37

Le filtre CSRF

Ce filtre sert à se protéger contre les attaques de type CSRF. Il est appliqué systématiquement pour les formulaires mais vous devez préciser que vous l’utilisez au niveau de la requête de retour. Il y a plusieurs façons de localiser ce filtre. On a déjà vu ci dessus une façon de l’appliquer systématiquement à toutes les requête « post » qui arrivent dans le filtre global before. ce n’est pas toujours un comportement judicieux. On peut l’appliquer directement à une route :

Route::post('ajout', array('before' => 'csrf', function()
{
	return 'Le token est le bon !'
}));

Si vous avez plusieurs routes qui nécessitent ce filtre vous pouvez les grouper :

Route::group(array('before' => 'csrf'), function()
{
    Route::post('ajout', function()
    {
        ...
    });

    Route::post('edition', function()
    {
        ...
    });
});

Une autre façon de faire est de prévoir le filtrage au niveau d’un contrôleur :

public function __construct()
{
	$this->beforeFilter('csrf', array('on' => 'post'));
}

Si vous le faites dans le BaseController il concernera évidemment tous les contrôleurs !

Créer un filtre

On peut aussi ajouter ses propres filtres dans ce fichier. C’est ce que nous allons faire maintenant. Créer un filtre est tout simple. Admettons que l’on veuille limiter des noms à une liste, voilà le filtre :

Route::filter('noms', function()
{
	$noms = array('Alfred','Gaston');
	if (!in_array(Route::input('nom'), $noms)) return Redirect::to('/');
});

On utilise la méthode filter pour créer un filtre.Comme les filtres vu précédemment, il a un nom, ici noms. Il a également une fonction anonyme pour le traitement. Ici on a un tableau avec les noms autorisés. On teste que le nom est dans le tableau et, si ce n’est pas le cas, on redirige vers la page d’accueil. Par contre si c’est le cas on traite la route de façon classique en continuant l’exécution normale. Voici les routes :

Route::get('/', function()
{
	return 'Accueil';
});
Route::get('{nom}', array('before' => 'noms', function($nom)
{
	return "Salut $nom";
}));

La syntaxe pour intégrer un filtre à une route est simple comme vous pouvez le voir. Au niveau du fonctionnement on a :

Vous pouvez utiliser after au lieu de before à ce niveau, mais alors ce n’est plus vraiment un filtre de route et je ne vois pas trop l’utilité Undecided.

Plusieurs filtres

On peut appliquer autant de filtres que l’on veut. par exemple complétons le cas précédent en prévoyant aussi un âge limite :

Route::filter('ages', function()
{
	if (Route::input('age') < 20) return Redirect::to('/');
});

On applique les deux filtres à la route :

Route::get('/', function()
{
	return 'Accueil';
});
Route::get('{nom}/{age}', array('before' => 'noms|ages', function($nom, $age)
{
	return "Salut $nom, vous avez $age ans";
}));

On a mis les noms des deux filtres séparés par le signe « | ». Les filtres sont exécutés l’un après l’autre en commençant par la gauche. Cet ordre d’exécution peut avoir une incidence dans des cas particuliers mais n’est en général pas important comme dans notre exemple. Le fonctionnement est le suivant :

Rien ne vous empêche de coupler un filtre « before » avec un filtre « after ». La syntaxe est alors la suivante :

Route::get('{nom}/{age}', array(
	'before' => 'noms', 
	'after' => 'ages', 
	function($nom, $age) {
		return "Salut $nom, vous avez $age ans";
}));

Paramètres dans les filtres

Un paramètre

Il peut être utile de transmettre des paramètres à un filtre. Supposons que notre filtre pour l’âge doive être utilisé plusieurs fois avec des âges limites différents. A ce moment ce serait bien que la route envoie cette limite au filtre. Voici notre exemple modifié :

Route::get('/', function()
{
	return 'Accueil';
});
Route::get('cas1/{age}', array('before' => 'ages:20', function($age)
{
	return "Salut vous avez $age ans";
}));
Route::get('cas2/{age}', array('before' => 'ages:40', function($age)
{
	return "Salut vous avez $age ans";
}));

Et voici le filtre :

Route::filter('ages', function($route, $request, $age)
{
	if (Route::input('age') < $age) return Redirect::to('/');
});

Remarquez les paramètres du filtre : $route qui est la route actuelle, $request qui est la requête en cours, puis notre paramètre. Voyons si ça fonctionne :

Aurait-on la même syntaxe pour un filtre « after » ? Pas tout à fait parce qu’un filtre « after » est appliqué après traitement de la route, on a donc généré une réponse et il est sans doute utile de disposer de cette réponse dans le code du filtre. C’est pour cette raison que dans le cas d’un filtre « after » on a un paramètre supplémentaire pour pouvoir agir sur la réponse :

Route::filter('ages', function($route, $request, $response, $mon_paramètre)
{
	....
});

Plusieurs paramètres

Complétons notre exemple avec un deuxième paramètre :

Route::get('membres', function()
{
	return 'Accueil des membres';
});
Route::get('actifs', function()
{
	return 'Accueil des actifs';
});
Route::get('cas1/{age}', array('before' => 'ages:membres,20', function($age)
{
	return "Salut vous avez $age ans";
}));
Route::get('cas2/{age}', array('before' => 'ages:actifs,40', function($age)
{
	return "Salut vous avez $age ans";
}));

Et voici le filtre :

Route::filter('ages', function($route, $request, $statut, $age)
{
	if (Route::input('age') < $age) return Redirect::to($statut);
});

Un patron de route

Il peut arriver que vous deviez faire agir un filtre sur plusieurs routes qui on la même structure d’URL. Par exemple imaginez une gestion d’articles avec toute sles URL qui commencent par « articles/ ». Et vous désirez que ces URL ne soient accessibles que pour les utilisateurs authentifiés. Vous pouvez alors utiliser cette syntaxe :

Route::when('articles/*', 'articles');
Route::get('articles/ajout', function()
{
	return "Ajout d'un article";
});
Route::get('articles/suppression/{id}', function($id)
{
	return "Suppression de l'article d'id $id";
});

Et voici un exemple rudimentaire pour le filtre :

Route::filter('articles', function()
{
	if(!Auth::check()) return 'Vous devez être connecté pour agir sur les articles !';
});

Si vous voulez authentifié de façon artificielle un utilisateur pour tester ce genre de code vous pouvez utiliser ce code :

Auth::login(User::find(1));

Mais ça ne fonctionnera que si vous avez une base renseignée et accessible Tongue Out.

Les classes de filtres

Mettre le code d’un filtre dans une fonction anonyme est une stratégie efficace et en général largement suffisante. Mais il se peut que cela ne vous suffise pas. Par principe une fonction anonyme est localisée, c’est même son principal intérêt mais qui peut se retourner contre vous selon vos besoins. Si vous voulez une approche plus souple alors vous pouvez créer une classe pour votre filtre et instancier cette classe où e quand vous voulez.

Ajouter une classe à Laravel implique d’informer L’IoC sinon elles resteront inconnues et donc inutilisables. Pour gérer convenablement votre code il faut bien le classer et le localiser. Si vous avez des classes de filtres autant créer un dossier pour mettre ces classes, par exemple app/filters. Vous devez alors informer Composer :

"autoload": {
	"classmap": [
		"app/filters",
		"app/commands",
		"app/controllers",
		"app/models",
		"app/database/migrations",
		"app/database/seeds",
		"app/tests/TestCase.php"
	]
},

Et évidemment faire un dumpautoload. Vous avez ainsi un endroit judicieux pour placer vos classes de filtres.

Reprenons notre exemple de filtre paramétré sur l’âge vu précédemment et créons une classe pour ce filtre (app/filters/AgeFilter.php) :

class AgeFilter
{
	public function filter($route, $request, $age)
	{
		if (Route::input('age') < $age) return 'Vous êtes trop jeune !';
	}
}

Il faut ensuite agir sur la route :

Route::filter('ages', 'AgeFilter');
Route::get('{age}', array('before' => 'ages:20', function($age) {
		return "Vous avez $age ans";
}));

Mis à part l’organisation du code, tout le reste est conforme à ce que nous avons vu pour les fonctions anonymes.

Filtres sur les contrôleurs

Pour le moment nous avons vu les filtres appliqués sur les routes, voyons à présent comment faire avec des contrôleurs.

Filtre déclaré dans la route

On peut évidemment encore spécifier le filtre au niveau d’une route. Voilà un contrôleur :

class TestController extends BaseController {
	public function index($nom)	{
		return "Bonjour $nom";
	}
}

On reprend le filtre pour les noms vu précédemment :

Route::filter('noms', function()
{
	$noms = array('Alfred','Gaston');
	if (!in_array(Route::input('nom'), $noms)) return Redirect::to('/');
});

On spécifie le filtre au niveau de la route :

Route::get('{nom}', array(
	'before' => 'noms',
	'uses' => 'testController@index'
));

On a le fonctionnement :

Filtre déclaré dans le contrôleur

Mais c’est plus élégant de spécifier le filtre dans le contrôleur :

class TestController extends BaseController {
	public function __construct()	{
		$this->beforeFilter('noms');
	}
	public function index($nom)	{
		return "Bonjour $nom";
	}
}

A ce moment là la route se contente de pointer la méthode du contrôleur :

Route::get('{nom}', array('uses' => 'testController@index'));

Tout fonctionne bien mais… si on déclare ainsi ce filtre il sera appliqué à toutes les méthodes du contrôleur. Ce n’est pas toujours ce qu’on désire. Heureusement on a la possibilité de spécifier les méthodes pour lesquelles le filtre doit agir. Complétons notre contrôleur :

class TestController extends BaseController {
	public function __construct()	{
		$this->beforeFilter('noms', array('only' => 'index'));
	}
	public function index($nom)	{
		return "Bonjour $nom";
	}
	public function login($nom)	{
		return "Login de $nom";
	}
	public function logout($nom)	{
		return "Logout de $nom";
	}
}

J’ai ajouté deux méthodes et aussi complété la déclaration du filtre pour cibler uniquement la méthode index. Voici les routes associées :

Route::get('index/{nom}', array('uses' => 'testController@index'));
Route::get('login/{nom}', array('uses' => 'testController@login'));
Route::get('logout/{nom}', array('uses' => 'testController@logout'));

Et le fonctionnement :

On se rend compte que le filtre n’est effectivement appliqué qu’à la méthode index. On peut coder avec la logique inverse en excluant les méthodes non concernées :

$this->beforeFilter('noms', array('except' => array('login', 'logout')));

A vous de choisir Tongue Out.

Filtre dans le contrôleur

Vous pouvez aussi mettre le code du filtre directement dans le contrôleur :

public function __construct()	{
	$this->beforeFilter(function() {
		$noms = array('Alfred','Gaston');
		if (!in_array(Route::input('nom'), $noms)) return "Le nom n'est pas correct";
	}, array('only' => 'index'));
}

C’est une autre façon d’organiser son code si le filtre ne concerne effectivement que ce contrôleur. Ici le code est placé dans le constructeur. Vous pouvez également le mettre dans une méthode du contrôleur :

public function __construct()	
{
	$this->beforeFilter('@filterAges', array('only' => 'index'));
}
public function filterAges($route, $request)
{
	$noms = array('Alfred','Gaston');
	if (!in_array(Route::input('nom'), $noms)) return "Le nom n'est pas correct";		
}

Il suffit de mettre le nom de la méthode précédé du signe « @ » comme premier paramètre de la méthode beforeFilter.

Filtres paramétrés

On peut maintenant se poser une question : comment transmettre un paramètre à un filtre déclaré ou codé dans un contrôleur ? Prenons encore notre filtre pour l’âge :

Route::filter('ages', function($route, $request, $statut, $age)
{
	if (Route::input('age') < $age) return 'Vous êtes trop jeune !';
});

Ce filtre attend un paramètre comme limite d’âge. Au niveau de la route on appelle de façon classique la méthode du contrôleur :

Route::get('{age}', array('uses' => 'testController@index'));

Et dans le contrôleur on déclare de filtre avec son paramètre :

public function __construct()	
{
	$this->beforeFilter('ages:20', array('only' => 'index'));
}
public function index($age)	{
	return "Vous avez $age ans.";
}

Tout cela est simple mais imaginez maintenant que vous avez un contrôleur de ressource, par exemple pour des livres que vous référencez ainsi au niveau de la route :

Route::resource('livre', 'LivreController');

Vous avez dans le contrôleur une méthode show($id). Si vous voulez filtrer cet id comment faire ? Vous pouvez récupérer le paramètre dans le filtre avec la méthode getParameter :

Route::filter('livres', function($route, $request)
{
	$id = $route->getParameter('livre');
	if($id > 10) return "Cet id de livre n'est pas correct";
});

Le codage dans le contrôleur est classique :

public function __construct() {
	$this->beforeFilter('livres', array('only' => array('show')));
}
public function show($id)
{
	return "Affichage du livre avec l'id $id";
}

Je pense que mon tour d’horizon du filtrage est relativement complet, si vous voyez des lacunes dites-le moi Wink.

Print Friendly, PDF & Email

Laisser un commentaire