Laravel 4

Laravel 4 : chapitre 32 : Organiser son code

Les 3 façons d’ajouter du code

Quand on veut organiser son code personnel avec Laravel on est de prime abord un peu décontenancé. Tout est si bien organisé qu’on a du mal à décider comment s’y prendre pour ajouter quelque chose. Surtout quand on nous dit qu’on peut le mettre où on veut !

Mais où qu’on le place il faut qu’il soit accessible. On peut résumer la situation à 3 cas :

Ajout de fonctions

Je veux juste ajouter des fonctions personnelles, par exemple pour ajouter des helpers. Dans ce cas je crée un fichier app/helpers.php avec mes fonctions. Pour que mes fonctions soient accessibles il faut que je renseigne Composer en créant une rubrique files si elle n’existe pas encore :

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

Et évidemment je fais un dumpautoload ensuite pour que ça fonctionne Smile.

Une autre solution équivalente consiste à déclarer le fichier dans app/start/global.php :

require_once __DIR__.'/../helpers.php';

C’est d’ailleurs la solution donnée par Taylor Otwell dans son bouquin. Mais je préfère quand même ne pas toucher au code de base de Laravel et me contenter d’intervenir uniquement dans Composer, ce qui me paraît plus logique. Mais vous avez le choix !

Ajout de classes

Supposons maintenant que je veuille ajouter quelques classes. Je crée un dossier app/lib pour les ranger. Ensuite j’ajoute mon dossier dans la rubrique classmap :

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

Avec un petit dumpautoload toutes mes classes dans ce dossier seront reconnues.

PSR-0

Maintenant prenons le cas le plus riche. J’organise mon code dans des dossiers hiérarchisés en suivant les préconisations psr-0. C’est-à-dire que j’utilise des espaces de noms qui correspondent à l’architecture de mes dossiers. Voici la syntaxe dans ce cas :

	"autoload": {
		"classmap": [
			"app/commands",
			"app/controllers",
			"app/models",
			"app/database/migrations",
			"app/database/seeds",
			"app/tests/TestCase.php"
		],
		"psr-0": {
			"MesClasses\\": "app"
		}
	},

Après un dumpautoload mes classes organisées dans mon espace de nom MesClasses et situées dans le dossier app seront reconnues.

Un cas d’exemple

Pour voir ces 3 possibilités en action je vais prendre un exemple simple d’application. A partir d’une installation neuve de Laravel et après avoir configuré une base de données on crée une table livres avec cette migration :

class CreateLivresTable extends Migration {

	public function up()
	{
		Schema::create('livres', function(Blueprint $table) {
			$table->increments('id');
			$table->timestamps();
			$table->string('titre', 100);
			$table->string('auteur', 100);
			$table->string('editeur', 100);
		});
	}

	public function down()
	{
		Schema::drop('livres');
	}
}

Donc une simple table avec un id, 3 champs de données et les timestamps. Pas vraiment réaliste évidemment mais le but est juste de montrer l’organisation du code, pas de créer une application réelle. Pour les besoins des tests je crée aussi quelques enregistrements :

class DatabaseSeeder extends Seeder {

	public function run()
	{

		Eloquent::unguard();

		for ($i = 1; $i < 21; $i++) {
	  	DB::table('livres')->insert(
	  		array(
	  			'titre' => 'Titre ' . $i,
	  			'auteur' => 'Auteur ' . $i,
	  			'editeur' => 'Editeur ' . $i
	  		)
	  	);
		}

	}

}

J’ai maintenant 20 enregistrements à disposition. Il ne me reste plus qu’à créer le code pour gérer tout ça. Il me faut un modèle Livre :

class Livre extends Eloquent {

    protected $table = 'livres';
    public $timestamps = true;
    protected $softDelete = false;
    protected $guarded = array('id');

}

Un contrôleur qui va se contenter d’afficher les livres avec une pagination et de permettre l’ajout de livres avec une validation :

class LivreController extends BaseController {

    public function getIndex()
    {
            $livres = Livre::paginate(5);
            return View::make('hello')->with('livres', $livres);
    }

    public function getCreate()
    {
            return View::make('saisie');
    }

    public function postCreate()
    {

            $rules = array(
                'titre' => 'required|alpha|unique:livres',
                'auteur' => 'required|alpha',
                'editeur' => 'required|alpha'
            );

            $validator = Validator::make(Input::all(), $rules);

            if ($validator->passes()) {
                $livre = new Livre;
                $livre->create(Input::all());
                return 'Livre enregistré'; 
            } else {
                return Redirect::back()->withInput()->withErrors($validator->messages());
            }

    }

}

Comme j’ai prévu un contrôleur RESTful la route est élémentaire :

Route::controller('livres', 'LivreController');

Il ne manque plus que la vue hello :

<!doctype html>
<html lang="fr">
<head>
	<meta charset="UTF-8">
	<title>Ou placer mon code</title>
</head>
<body>
{{ link_to_action('LivreController@getCreate', 'Ajouter un livre') }}
@foreach ($livres as $livre)
  <h1>Titre du livre : {{ $livre->titre }}</h1>
  <p>Auteur : {{ $livre->auteur }} </p>
  <p>Editeur :  {{ $livre->editeur }}</p>
@endforeach
{{ $livres->links(); }}
</body>
</html>

Et la vue saisie :

<!doctype html>
<html lang="fr">
<head>
	<meta charset="UTF-8">
	<title>Ou placer mon code</title>
</head>
<body>
@if ($errors->count())
  <p>Les erreurs suivantes sont survenues :</p>
  <ul>
    @foreach($errors->all() as $message)
      <li>{{ $message }}</li>
    @endforeach
  </ul>
@endif
{{ Form::open(array('url' => 'livres/create')) }}
{{ Form::label('titre', 'Titre :')}}
{{ Form::text('titre') }}
{{ Form::label('auteur', 'Auteur :')}}
{{ Form::text('auteur') }}
{{ Form::label('auteur', 'Editeur :')}}
{{ Form::text('editeur') }}
{{ Form::submit('Envoyer') }}
{{ Form::close() }}
</body>
</html>

Tout ça est rapide à mettre en place et fonctionne à merveille. A partir de cette situation je vais envisager plusieurs cas d’organisation du codage pour mettre en lumière ce que j’ai cité au départ de ce fil.

La validation dans des fonctions

Si j’ai plusieurs formulaires dans une application je risque de multiplier les cas de validation. Au bout d’un moment je me dis que ça serait bien de rassembler toutes ces validations quelque part. Alors je crée un fichier app/validation.php pour le faire. Dans mon cas évidemment je n’en ai qu’une, alors je construis mon code dans ce fichier :

function validateLivre($inputs)
{
    $rules = array(
        'titre' => 'required|alpha|unique:livres',
        'auteur' => 'required|alpha',
        'editeur' => 'required|alpha'
    );

    return validate($inputs, $rules);

}

function validate($inputs, $rules)
{

    $validator = Validator::make($inputs, $rules);

    if ($validator->passes()) {
        return true; 
    } else {
        return $validator->messages();
    }

}

J’ai prévu une fonction validateLivre qui a comme unique paramètre les entrées du formulaire et qui contient les règles. J’ai ensuite une fonction validate qui accomplit la validation et qui renvoie l’information. Pour que mes fonctions soient connues de l’application j’informe Composer :

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

Après un dumpautoload je peux alléger la méthode postCreate de mon contrôleur LivreController :

public function postCreate()
{

    $validate = validateLivre(Input::all());

    if($validate === true) 
    {
        $livre = new Livre;
        $livre->create(Input::all());
        return 'Livre enregistré';                 
    } else {
        return Redirect::back()->withInput()->withErrors($validate);
    }

}

Maintenant tout ce qui concerne la validation est rassemblé dans un seul fichier et j’ai pu factoriser quelques actions. Ce n’est pas forcément très joli mais on va améliorer tout ça bientôt…

La validation dans des classes

Supposons maintenant que la solution vu plus haut ne me convienne pas. Je peux Placer mon code dans des classes. Pour localiser ces classes je crée un dossier app/validation. J’explique à Composer qu’il doit charger mes classes :

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

Ensuite je réfléchis un peu. Dans mon application je vais avoir sans doute plusieurs formulaires et je vais donc avoir du code en commun. Je vais donc créer une classe abstraite app/validation/ValidationBase avec ce code commun :

abstract class ValidationBase
{

	protected $rules;

	public function validate($inputs)
	{
		$validator = Validator::make($inputs, $this->rules);

		if ($validator->passes()) {
			return true; 
		} else {
			return $validator->messages();
		}
	}

}

Je prévoie donc dans cette classe la méthode pour la validation qui prend comme paramètre les entrées du formulaire. J’ai aussi une propriété $rules qui sera différente selon le formulaire. Pour mon cas je crée donc une classe app/validation/ValidationLivre qui hérite de la précédente :

class ValidationLivre extends ValidationBase
{

	protected $rules = array(
		'titre' => 'required|alpha|unique:livres',
		'auteur' => 'required|alpha',
		'editeur' => 'required|alpha'
	);

}

Je me retrouve pour cette classe avec seulement la propriété $rules renseignée.

Je dois ensuite modifier mon contrôleur pour tenir compte de ces changements :

public function postCreate()
{

	$val = new ValidationLivre();

	$validate = $val->validate(Input::all());

	if($validate === true) 
	{
		$livre = new Livre;
		$livre->create(Input::all());
		return 'Livre enregistré'; 				
	} else {
		return Redirect::back()->withInput(Input::all())->withErrors($validate);
	}

}

Dans la méthode postCreate je crée une instance de la classe ValidationLivre, puis j’utilise la méthode validate en transmettant les entrées. Tout ça fonctionne très bien. Mais ce n’est pas très propre. Il serait plus élégant d’utiliser le conteneur de Laravel pour pourvoir injecter la classe ValidationLivre dans le contrôleur plutôt que de créer une instance dans le code. Le conteneur de Laravel est suffisamment intelligent pour résoudre des liaisons même si on ne les a pas déclarées. Je vais donc injecter ma classe dans le contrôleur et voir si ça fonctionne :

class LivreController extends BaseController {

    protected $val;

    public function __construct(ValidationLivre $val)
    {
        $this->val = $val;
    }

    public function getIndex()
    {
        $livres = Livre::paginate(5);
        return View::make('hello')->with('livres', $livres);
    }

    public function getCreate()
    {
        return View::make('saisie');
    }

    public function postCreate()
    {

        $validate = $this->val->validate(Input::all());

        if($validate === true) 
        {
            $livre = new Livre;
            $livre->create(Input::all());
            return 'Livre enregistré';                 
        } else {
            return Redirect::back()->withInput(Input::all())->withErrors($validate);
        }

    }

}

J’ai ajouté un constructeur avec un paramètre de type ValidationLivre. Le conteneur de Laravel cherche la classe et la trouve ! En effet les contrôleurs sont toujours résolus dans le conteneur, ce qui permet d’injecter un type. Il me crée une instance et je peux l’utiliser Smile. Mon code devient plus propre et plus facile à tester.

Les espaces de noms

psr-o kesaco ?

Il n’est pas très prudent de créer des classes sans les placer dans des espaces de noms. D’autre part il est conseillé d’utiliser les préconisations du psr-0. Mais c’est quelque chose de finalement assez récent pour les développeurs PHP. C’est une préconisation qui a été établie par les principaux acteurs du PHP pour unifier la présentation des librairies. En gros ça permet de pouvoir assurer le chargement automatique des classes grâce à une convention de nommage et ainsi de constituer facilement une application avec des « briques » issues de différentes provenances.

Pour lire cette norme c’est ici. Le plus important est de comprendre l’organisation des espaces de noms :

\<Nom du Vendor>\(<Espace de noms>\)*<Nom de la Classe>

Si vous regardez le code de Laravel vous allez voir que la norme a été parfaitement appliquée.

Utilisation de psr-0 pour l’exemple

Je vais poursuivre mon exemple en utilisant cette possibilité. Je vais crée l’architecture app/lib/validation et je vais indiquer à Composer que je respecte le psr-0 :

"autoload": {
	"classmap": [
		"app/commands",
		"app/controllers",
		"app/models",
		"app/database/migrations",
		"app/database/seeds",
		"app/tests/TestCase.php"
	],
	"psr-0": {
		"Lib\\": "app"
	}
},

Mon espace de nom est donc Lib\Validation. Comme je n’ai pas grand chose à mettre dedans je n’aurai pas de sous-espace.

Pour faire les choses bien je commence par créer une interface app/lib/validation/ValidationInterface :

<?php namespace Lib\Validation;

interface ValidationInterface {

	public function validate(array $inputs);

}

Le contrat est établi. Je veux une méthode de validation. Je crée ma classe abstraite app/lib/validation/ValidationBase comme précédemment mais évidemment modifiée :

<?php namespace Lib\Validation;

use Illuminate\Validation\Factory as Validator;

abstract class ValidationBase implements ValidationInterface
{

	protected $rules;

	protected $validator;

	public function __construct(Validator $validator)
	{
		$this->validator = $validator;
	}

	public function validate(array $inputs)
	{
		$v = $this->validator->make($inputs, $this->rules);

		if ($v->passes()) {
			return true; 
		} else {
			return $v->messages();
		}
	}

}

Cette fois j’injecte le validateur de Laravel dans le constructeur. Je crée ensuite ma classe de validation concrète app/lib/validation/ValidationLivre pour les livres :

<?php namespace Lib\Validation;

class ValidationLivre extends ValidationBase
{

	protected $rules = array(
		'titre' => 'required|alpha|unique:livres',
		'auteur' => 'required|alpha',
		'editeur' => 'required|alpha'
	);

}

Je me retrouve donc avec cette architecture :

img56

Et évidemment dans le contrôleur je dois tenir compte de l’espace de nom :

<?php

use Lib\Validation\ValidationLivre;
class LivreController extends BaseController {

	protected $val;

	public function __construct(ValidationLivre $val)
	{
		$this->val = $val;
	}

	public function getIndex()
	{
		$livres = Livre::paginate(5);
		return View::make('hello')->with('livres', $livres);
	}

	public function getCreate()
	{
		return View::make('saisie');
	}

	public function postCreate()
	{
		$validate = $this->val->validate(Input::all());

		if($validate === true) 
		{
			$livre = new Livre;
			$livre->create(Input::all());
			return 'Livre enregistré'; 				
		} else {
			return Redirect::back()->withInput(Input::all())->withErrors($validate);
		}

	}

}

Comme j’injecte une classe concrète c’est résolu automatiquement. Bon ça commence à avoir un peu plus d’allure comme ça Smile.

Délocaliser la gestion du modèle

On peut pousser un peu plus la réflexion à partir de notre exemple. Un contrôleur sert à quoi ? Essentiellement à récupérer les informations de l’utilisateur et à générer une réponse. Tout le traitement intermédiaire ne devrait pas le concerner. Nous avons déjà extrait la validation. Par contre on voit que l’action sur le modèle est encore inclus dans le contrôleur, ce qui ne devrait pas être sa tâche. Je ne devrais pas avoir à créer une instance du modèle Livre ici. Dans l’idéal je veux un contrôleur comme ça :

<?php

use Lib\Validation\ValidationLivre;
use Lib\Gestion\GestionLivre;

class LivreController extends BaseController {

	protected $val;
	protected $gestion;

	public function __construct(ValidationLivre $val, GestionLivre $gestion)
	{
		$this->val = $val;
		$this->gestion = $gestion;
	}

	public function getIndex()
	{
		$livres = $this->gestion->affiche(5);
		return View::make('hello')->with('livres', $livres);
	}

	public function getCreate()
	{
		return View::make('saisie');
	}

	public function postCreate()
	{

		$validate = $this->val->validate(Input::all());

		if($validate === true) 
		{
			$this->gestion->create(Input::all());
			return 'Livre enregistré'; 				
		} else {
			return Redirect::back()->withInput(Input::all())->withErrors($validate);
		}

	}

}

Vous remarquez que j’ai ajouté l’espace de nom Lib\Gestion\GestionLivre. C’est ici que je vais assurer toute l’intendance du modèle. Je me contente ensuite d’une injection dans le contrôleur :

public function __construct(ValidationLivre $val, GestionLivre $gestion)

Pour afficher les livres je fais simplement appel à la méthode affiche :

$livres = $this->gestion->affiche(5);

Et ensuite pour créer le livre j’appelle la méthode create et c’est bouclé :

$this->gestion->create(Input::all());

C’est propre, efficace et beaucoup plus simple à tester ! Mais bon pour que ça marche je dois créer quelques classes. D’abord j’enrichis mon espace de noms avec Lib\Gestion\GestionLivre. Comme je veux faire quelque chose de propre je commence par une interface app/lib/gestion/GestionInterface  :

<?php namespace Lib\Gestion;

interface GestionInterface {

    public function affiche($pages);
    public function create(array $inputs);

}

Ensuite une classe abstraite app/lib/gestion/GestionBase qui sera commune à tous les modèles :

<?php namespace Lib\Gestion;

abstract class GestionBase implements GestionInterface
{

    protected $model;

    public function affiche($pages)
    {
        return $this->model->paginate($pages);
    }

    public function create(array $inputs)
    {
        $this->model->create($inputs);
    }

}

Et enfin ma classe spécifique aux livres app/lib/gestion/GestionLivre :

<?php namespace Lib\Gestion;

class GestionLivre extends GestionBase
{

	public function __construct(\Livre $livre)
	{
		$this->model = $livre;
	}

}

Je me retrouve donc maintenant avec cette architecture :

img57

Mon contrôleur est propre et fait uniquement ce qu’il doit faire sans se soucier de l’intendance intermédiaire. Le code est bien organisé et facile à comprendre et modifier. En plus il permet une extension facile. Si je dois créer un autre formulaire avec un autre modèle j’ai déjà tout ce qu’il faut pour le faire Smile.

Un service de validation

Faisons un pas de plus dans l’organisation du code. Regardons le constructeur de notre contrôleur :

public function __construct(ValidationLivre $val, GestionLivre $gestion)

On se rend compte qu’on injecte des classes concrètes. Elle sont résolues automatiquement mais posons-nous une question : puisque nous avons une interface il serait sans doute plus judicieux d’injecter celle-ci. Quel intérêt ? Une interface représente un contrat, c’est une façon de dire au contrôleur : tu as la possibilité d’effectuer une validation de telle façon. Dans notre cas très simple on lui dit juste qu’il existe une méthode validate. Il n’a pas à savoir comment la validation est réalisée. On pourrait imaginer une toute autre organisation du code, si on injecte l’interface, le contrôleur fonctionnera toujours. Voici le contrôleur modifié en conséquence :

use Lib\Validation\ValidationInterface;
use Lib\Gestion\GestionLivre;

class LivreController extends BaseController {

    protected $val;
    protected $gestion;

    public function __construct(ValidationInterface $val, GestionLivre $gestion)
    {
        $this->val = $val;
        $this->gestion = $gestion;
    }

    public function getIndex()
    {
        $livres = $this->gestion->affiche(5);
        return View::make('hello')->with('livres', $livres);
    }

    public function getCreate()
    {
        return View::make('saisie');
    }

    public function postCreate()
    {

        $validate = $this->val->validate(Input::all());

        if($validate === true) 
        {
            $this->gestion->create(Input::all());
            return 'Livre enregistré';              
        } else {
            return Redirect::back()->withInput(Input::all())->withErrors($validate);
        }

    }

}

On injecte l’interface ValidationInterface. Si on teste le code on va recevoir une erreur parce que le conteneur ne sait pas quelle classe concrète correspond à cette interface. Il faut donc l’en informer. Pour cela il faut créer un ServiceProvider :

<?php namespace Lib\Validation;

use Illuminate\Support\ServiceProvider;

class ValidationServiceProvider extends ServiceProvider {

    public function register()
    {
        $this->app->bind('Lib\Validation\ValidationInterface', function($app)
        {
            return new ValidationLivre($app->make('Illuminate\Validation\Factory'));
        });
    }

}

Ici on crée un lien entre l’interface ValidationInterface et la classe concrète ValidationLivre. Il faut aussi renseigner le fichier app/config/app.php :

		'Illuminate\Workbench\WorkbenchServiceProvider',
		'Lib\Validation\ValidationServiceProvider'
	),

On fait un dumpautoload pour que le lien soit enregistré et ça devrait fonctionner.

Print Friendly, PDF & Email

5 commentaires

  • John Smith

    Bonjour Momo,

    J’ai un problème très simple. Je souhaite définir un tableau qui contient l’ensemble des images de mon site. Ainsi mes images seront définies comme suit :
    $image[‘logo’] = ‘assets/img/logo.png’;
    $image[‘home’] = ‘assets/img/home.png’;

    et ainsi de suite. Ensuite dans mon code pour placer une image, je voudrais faire quelque chose comme :
    HTML::image($image[‘logo’], ‘Logo’);

    Alors, je me suis dis : « Je vais créer un fichier qui va contenir mon tableau… » Ainsi j’ai mon fichier images.php :

    Et j’ai suivi ta procédure d’ajout du fichier dans composer :

    « autoload »: {
    « classmap »: [
    « app/commands »,
    « app/controllers »,
    « app/models »,
    « app/database/migrations »,
    « app/database/seeds »,
    « app/tests/TestCase.php »
    ],
    « files »: [
    « app/includes/images.php »
    ]
    },

    Et ai lancé la commande « composer dumpautoload »

    Le soucis, c’est que ma variable $images, n’est pas reconnu dans le reste du code.

    Alors je me suis dis, je vais créer une fonction dans ce même fichier qui va me retourner cette variable pour faire un essai.

    J’ai créé la fonction :
    function image($image)
    {
    globale $images;
    return $images[$image];
    }
    Que j’appelle comme suit :
    HTML::image(image(‘logo’), ‘Logo’);

    Et là je me rends compte que la variable globale n’est toujours pas reconnue. Bref pour que ça marche, il m’a fallu inclure mon tableau dans ma fonction. Ce que je trouve moche.

    Bref en conclusion, voici mes questions :
    1. Comment faire pour que mon tableau ou tout autre variable déclaré dans mon fichier soit visible par l’ensemble du code?
    2. Pourquoi la directive « globale » dans une fonction ne marche pas ?

    Je te serais infiniment reconnaissant si tu pouvais m’apporter des réponses.

    • bestmomo

      Salut John !

      Te complique pas la vie, dans ton cas le plus simple est de créer un fichier de configuration app/config/images.php :


      'assets/img/logo.png';
      'home' => 'assets/img/home.png';
      ...
      );

      Ensuite tu vas chercher tes infos en toute simplicité :

      Config::get('images.logo');

      • John Smith

        Merci Momo ! A vrai dire je me doutais bien qu’il devait y avoir une astuce.
        (Le soucis, c’est que je suis en train de porter tout mon site sur Laravel et je risque d’être confronté au problème de cette histoire de « globale » dans des fonctions…)

        Sinon, comment faire pour créer un alias du style :
        image(‘images.logo’) qui soit l’équivalent de Config::get(‘images.logo’) ?

        • bestmomo

          Tu peux créer un fichier app/helpers.php que tu déclares dans composer.json. Dans ce fichier tu prévoies ta fonction :


          if ( ! function_exists('image'))
          {
          function image($element)
          {
          return Config::get('images.' . $element);
          }
          }

          Dans ton code tu n’a plus qu’à appeler ainsi :

          image('logo');

          Par contre évite les variables globales, on peut toujours s’en sortir autrement 😉

Laisser un commentaire