胖胖 Model 的減重方法:Form

Web application常常需要對使用者輸入的參數做驗證。

這些驗證(validation)logic該放哪裡呢?

現代框架通常會提供工具協助這件事,

以Laravel來說,使用內建的Validator可以在controller內這麼做:

$validation = Validator::make(
    array(
        'name' => Input::get( 'name' ),
        'email' => Input::get( 'email' ),
    ),
    array(
        'name' => array( 'required', 'alpha_dash' ),
        'email' => array( 'required', 'email' ),
    )
);
 
if ( $validation->fails() ) {
    $errors = $validation->messages();
}

同樣的validation logic只出現一次還好,出現在不同controller就會導致這段code duplicate。

該怎麼辦呢?

試試看放進model:

class Ball extends Eloquent
{
    private $rules = array(
        'color' => 'required|alpha|min:3',
        'size'  => 'required',
        // .. more rules here ..
    );

    public function validate($data)
    {
        // make a new validator object
        $v = Validator::make($data, $this->rules);
        // return the result
        return $v->passes();
    }
}

controller內的code就會簡化成這樣:

$b = new Ball();

if ($b->validate(Input::all())){
    // success code
}else{
    // failure code
}

還算OK的作法…如果你能接受將validation logic視為business logic的話。

這類的code有比controller和model更適合的地方。寫一個form class即可:

class ArticleForm
{
    protected $validationRules = [
        'title' => 'required',
        'content' => 'required',
    ];
    protected $inputData;
    protected $validator;

    public function __construct($input)
    {
        $this->inputData = $input;
    }

    public function isValid()
    {
        $this->validator = Validator::make($this->input, $this->validationRules);
        return $this->validator->passes();
    }
    
    public function getErrors()
    {
        return $this->validator->errors();
    }

}

controller內直接呼叫即可,看起來readable許多:

$form = new ArticleForm( Input::all() );
 
if ( ! $form->isValid() ){
    return Redirect::back()->with( [ 'errors' => $form->getErrors() ] );    
}
 
$article = new Article( Input::only('title', 'content', 'status') );
 
$article->save();

如果多個model都有form classs的話,可以將共用的code抽出來成為抽象類別:

abstract class FormModel
{
    protected $validationRules;
    protected $inputData;
    protected $validator;

    public function __construct($input)
    {
        $this->inputData = $input;
    }

    public function isValid()
    {
        $this->validator = Validator::make($this->input, $this->validationRules);
        return $this->validator->passes();
    }
    
    public function getErrors()
    {
        return $this->validator->errors();
    }

}

這樣原本的form class可以簡化到只剩下validation rules:

class ArticleForm extends FormModel
{
    protected $validationRules = [
        'title' => 'required',
        'content' => 'required',
    ];
}

甚至能寫多種validation rules給不同controller:

   protected $happyArticleValidationRules;
    protected $angryArticleValidationRules;
    protected $funnyArticleValidationRules;

    public function isValidHappy()
    {
        $this->validator = Validator::make($this->input, $this->happyArticleValidationRules);
        return $this->validator->passes();
    }
    
    public function isValidAngry()
    {
        $this->validator = Validator::make($this->input, $this->angryArticleValidationRules);
        return $this->validator->passes();
    } 
   
    public function isValidFunny()
    {
        $this->validator = Validator::make($this->input, $this->funnyArticleValidationRules);
        return $this->validator->passes();
    }

實作上可以參考Laravel官方論壇原始碼

(注意:若您是用Laravel 5,Request的封裝可以取代這件事。)

下次你的application包含大量validation logic時,不妨試試看這個作法。

(Photo via Susanne Nilsson, CC licensed)