Un model générique pour Laravel

Suite a l’article précédent : Génération de règles de validation pour model nous avons a disposition des règles. maintenant il faut les exploiter au mieux dans nos modèles.
Mon contrat de départ est
1) de pouvoir utiliser ces règles de validation automatiquement a l’enregistrement :)
2) utiliser automatiquement ces règles avec les mutateurs (setters)
3) et pourquoi pas réutiliser notre travail pour la validation de formulaires
4) ce système ne doit pas être obligatoire et rester transparent pour le développeur.

Solution

Nous allons avoir 2 classes :
ValidatorManager
Classe qui chargera les règles de validation et exécutera la validation
BaseModel
Classe abstraite dérivée de Eloquent, le model générique qui déclenchera les validations

ValidatorManager

Nos règles sont dans le dossier :app/config/models/*-rules.php, dans le constructeur nous les chargeons.
Nous avons 2 méthodes passes() et passe(), le première valide les règles pour tout le modele et la seconde valide les règles pour un seul attribut(pour les setters).
De même, getRules() et getRule() sont elles aussi pour tout le model ou un seul attribut.

SetID() est particulière, elle existe pour la règle « unique » :
la valeur doit être unique dans une table,un champ, excepté un id
‘email’=> unique:user,email,$id
Si nous faisons un INSERT , l’id de l’enregistrement est a 0 donc notre règle unique s’applique sur toute la table.
Si nous faisons un UPDATE, l’id est présent, donc notre règle unique s’applique sur toute la table excepté l’id de l’enregistrement.
La méthode ->setID() teste donc la présence ou non de l’id et remplace « $id » par la bonne valeur.

<?php namespace App\Services;

class ValidatorManager
{
    const MODEL = 0;
    const FORM  = 1;
    protected $type;

    private $rules;
    private $errors;

    public function __construct($name,$type= ValidatorManager::MODEL)
    {
        $this->type=$type;
        if (substr($name,-1)!='s') $name.='s';
        $path=(($this->type==ValidatorManager::MODEL)?'models':'forms').'/'.strtolower($name).'-rules';
        $this->rules = \Config::get($path);

        //si pas de fichier config pour form ou model no problem
        if (!is_array($this->rules)) {
                $this->rules= array();
                \Log::notice('pas de règles pour '.(($this->type==ValidatorManager::MODEL)?'model':'form').':'.$name);
        }
    }

    public function passes(Array $attr=array())
    {
         if (($this->type==ValidatorManager::FORM)&&(count($attr)<1))
            $attr= \Input::all();

        $validation = \Validator::make($attr, $this->getRules());
        if ($validation->passes()) return true;

        $this->errors = $validation->messages();
        if ($this->type==ValidatorManager::MODEL)
            throw new \Exception( $validation->messages()->first());
        return false;
    }

    public function passe($key,$attr)
    {
        $validation = \Validator::make(array($key=>$attr), $this->getRule($key));

        if ($validation->passes()) return true;

        $this->errors = $validation->messages();
        return false;
    }    

    public function getErrors()
    {
        return $this->errors;
    }

    public function getRules()
    {
        return $this->rules;
    }

    /**
    *@return Array une seule regle et sa clé
    **/
    public function getRule($key)
    {
        $rules=$this->getRules();
        return (isset($rules[$key])) ? array($key=>$rules[$key]) : array(''=>'') ;
    }

    /**
     * id>0 alors un update : mettre son id a unique
     * si insert alors mettre 0 a unique
     */
    public function setID($id)
    {
        if ((integer)$id<1)  $id=0;
        foreach ($this->rules as $key=>$rule) {
            if (strpos($rule,'$id')>0){
                $this->rules[$key]= str_replace('$id',$id,$rule);
            }
        }
        return $this;
    }
}

BaseModel

Nous insérons bien sur un objet ValidatorManager dans notre classe :) et nous écrivons une méthode ->getValidator($id) puisque les règles(unique) changent en fonction de la presence ou non de l’id.

Dans la méthode statique ::boot() nous créons l’objet ValidatorManager. Mais surtout nous répondons à l’ORM juste avant de sauver l’enregistrement par :

static::saving(function($model) {
     return $model->getValidator($model->id)->passes($model->attributes);
});

Nous déclenchons la validation de tout notre modèle avec notre ValidatorManager. Nous pouvons aussi tester la validité de notre utilisateur, écrire dans le fichier log…

<?php namespace App\Services;

use App\Services\ValidatorManager;

abstract class BaseModel extends \Eloquent
{
	protected static $validator;

	public static function boot()
	{
	  if (!static::$validator){
		parent::boot();
		static::$validator = new ValidatorManager(get_called_class(),ValidatorManager::MODEL);

		static::saving(function($model)
		{
		    if (method_exists($model,'onSaving')){
			  if (true !==$model::onSaving($model)) return false;
		    }
		    return $model->getValidator($model->id)->passes($model->attributes);
		});
	  }
	}

    /**
     * faire une validation automatique a chaque setter
     * déclenché par model->create, model->update et model->item=
     */
    protected function _validate($key,$value)
    {
	$key= strtolower(substr( substr($key,3) ,0,-9)) ;
	$validation= $this->getValidator($this->id);
	if ($validation->passe($key,$value))
	{
		$this->attributes[$key] = $value;
	}
	else
        {
	    throw new \Exception( 'Validation - '.$validation->getErrors()->first($key));
        }
    }

    /**
     *@param integer id si==0 alors un insert
     *@return Validator
     */
    static public function getValidator($id)
    {
	if (!static::$validator) static::boot();
	$validator= static::$validator;
	return $validator->setID($id);
    }
}

Reste la validation optionnelle, des Attributs par les Setters. Cette fois ci, il faut bien écrire dans notre modèle, par exemple :

<?php

class Article extends App\Services\BaseModel {

	protected $guarded = array();

	public function categorie() {
		return $this->belongsTo('Categorie');
	}

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

    /**
     * setter pour validation automatique de l'attribut "intro"
     */
    public function setIntroAttribute($value) {
	$this->_validate(__FUNCTION__,$value );
    }	

}

Nous utilisons juste la méthode ->_validate() après d’éventuelles modifications de $value.

Notre classe Modèle hérite de BaseModel et ici on se rend bien compte que tout notre système de validation est transparent (en particulier sans setter). Il suffit en fait juste de créer un fichier de règles dans app/conf.

	public static function onSaving(Article $model)
	{
		\Log::debug('Article::saving');
		throw new \Exception('utilisateur non autorisé');
		return true;
	}

Il est encore possible d’intercepter le message « saving » dans notre model, pour par exemple tester les permissions de l’utilisateur.

ps: j’ai utilisé des namespaces.


Pour la validation des formulaires :

 
$valid= new ValidatorManager('NomUniqueDeMonFormulaire', ValidatorManager::FORM);
$valid = $valid->setID(  \Input:get('id') );
if ( $valid->passes() ) 
...

Notre méthode ->passes() ne déclenche pas d’exception pour les formulaires.

Share Button

Vous devriez aimer...