Validators API preview / discussion


(Alexander Makarov) #1

Validator package starting to take its shape: https://github.com/yiisoft/validator.

Requirements

  1. Validation rules should be simple to implement.
  2. Validation should be usable for any kind of data.
  3. It should be easy to use it in a form generator package or API package.
  4. It should be possible to validate both individual values and value sets.
  5. Messages should be customizable.

Validating a single value

$rules = new Rules([
    new Required(),
    (new Number())->min(10),
    function ($value): Result {
        $result = new Result();
        if ($value !== 42) {
            $result->addError('Value should be 42!');
        }
        return $result;
    }
]);

$result = $rules->validate(41);
if ($result->isValid() === false) {
    foreach ($result->getErrors() as $error) {
        // ...
    }
}

Validating a set of data

class MoneyTransfer implements \Yiisoft\Validator\DataSet
{
    private $amount;
    
    public function __construct($amount) {
        $this->amount = $amount;
    }
    
    public function getValue(string $key){
        if (!isset($this->$key)) {
            throw new \InvalidArgumentException("There is no \"$key\" in MoneyTransfer.");
        }
        
        return $this->$key;
    }
}

$moneyTransfer = new MoneyTransfer();

$validator = new Validator([    
    'amount' => [
        (new Number())->integer(),
        (new Number())->integer()->max(100),
        function ($value): Result {
            $result = new Result();
            if ($value === 13) {
                $result->addError('Value should not be 13!');
            }
            return $result;
        }
    ],
]);

$results = $validator->validate($moneyTransfer);
foreach ($results as $attribute => $result) {
    if ($result->isValid() === false) {
        foreach ($result->getErrors() as $error) {
            // ...
        }
    }
}

Creating your own validation rules

In order to create your own valdation rule you should extend Rule class:

namespace MyVendor\Rules;

use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule;

class Pi extends Rule
{
    public function validateValue($value): Result
    {
        $result = new Result();
        if ($value != M_PI) {
            $result->addError('Value is not PI!');
        }
        return $result;
    }
}

(Fabrizio Caldarelli) #2

Are we sure that this syntax is needed and useful?

At first glance it is less readable respect previous array values syntax and, from my point of view, custom validation inside rules it is for me almost never useful, because the most time I need to compare an attribute (or attributes) with others, so I need entire model.

For example, one validation rule could be that a specific field is mandatory when a check is set, and I solve it with “afterValidate” method. This should be impossible to achieve with callback.


(Alexander Makarov) #3

It is possible:

class CheckboxForm implements DataSet
{
    private $checked;
    private $mandatoryIfChecked;

    public function __construct(array $data)
    {
        $this->checked = $data['checked'] ?? false;
        $this->mandatoryIfChecked = $data['mandatoryIfChecked'] ?? null;
    }

    public function rules()
    {
        $rules = [];

        if ($this->checked) {
            $rules['mandatoryIfChecked'] = [new Required()];
        }

        return $rules;
    }

    public function getValue(string $key)
    {
        if (!isset($this->$key)) {
            throw new InvalidArgumentException("$key does not exist in CheckboxForm.");
        }

        return $this->$key;
    }
}

class FormController
{
    public function actionSubmit()
    {
        $form = new CheckboxForm($_POST);
        $validator = new Validator($form->rules());
        $results = $validator->validate($form);
        // ..
    }
}

(Fabrizio Caldarelli) #4

Comparing with afterValidate:

class MyModel extends Model {

      public function afterValidate()
      {
                if(($this->checked)&&($this->mandatoryIfChecked == null))
                {
                        $this->addError('mandatoryIfChecked', 'This field is mandatory when check is on');
                }
      }

}

The old syntax is clearer and shorter. Finally, afterValidation is inside the model, so it will be called everytime model is validated/saved, without adding extra code.


(Alexander Makarov) #5

Yes but there’s one huge issue with the old syntax. You need a class extending from Model while often validation is required in different classes: requests, DTOs, Entities etc. that may be inherited from something we do not control.


(Fabrizio Caldarelli) #6

We can subclass Model class to handle different scope where we are validating data, to keep things clean. This new syntax is maybe useful for IDE integration, but not from developer point of view.

Anyway, could we continue to use also old syntax or it will be discontinued? For all other simpler cases in which validation is required only in web environment.


(Viktor) #7
  1. I think it is ok to couple active record library with validators and to use it inside. Lots of developers love this functionality
  2. There was a discussion at GitHub and Slack with the decision to necessarily leave ability to configure validation rules with arrays (I’ll seek for links if you want). I.e. new Number(['min' => 10]);
  3. I think when option should be returned too.

#8

How this is going to work for some more advanced rules, like Unique?


(Tecnologiaterabyte) #9

Why changing the previous syntax is basic, simple and intuitive, example:

return [
    // username rules
    'usernameTrim'     => ['username', 'trim'],
    'usernameLength'   => ['username', 'string', 'min' => 3, 'max' => 255],
    'usernamePattern'  => ['username', 'match', 'pattern' => $this->userModel::$usernameRegexp],
    'usernameRequired' => ['username', 'required'],
    'usernameUnique'   => [
        'username',
        'unique',
        'targetClass' => $this->userModel,
        'message' => $this->app->t('ModuleUser', 'This username has already been taken.')
    ],
    // email rules
    'emailTrim'     => ['email', 'trim'],
    'emailRequired' => ['email', 'required'],
    'emailPattern'  => ['email', 'email'],
    'emailUnique'   => [
        'email',
        'unique',
        'targetClass' => $this->userModel,
        'message' => $this->app->t('ModuleUser', 'This email address has already been taken.')
    ],
    // password rules
    'passwordRequired' => ['password', 'required', 'skipOnEmpty' => $this->module->accountGeneratingPassword],
    'passwordLength'   => ['password', 'string', 'min' => 6, 'max' => 72],
];

(Alexander Makarov) #10

Why?

I was going to discontinue it.

I don’t think so. Yii 1.1 and Yii 2.0 were actively used with Doctrine and in that case validation, forms, data providers and everything was not usable. Now we have multiple alternatives some of which, I believe, are better than Doctrine and potentially work well in AR-style as well. That’s why I want an ability to use Yii with any DB layer possible.

That could be added, yes. Syntax would still require new XRule([ ... ]).

I think that’s not needed. See my code post above. You can compose rules dynamically.

Something like that should be possible:

// $model
$validator = new Validator([
   'title' => [new Unique()->against($model)->field('title')],
]);

(Fabrizio Caldarelli) #11

What is keys purpose? Why cant’ we avoid them?


(Alexander Makarov) #12

I think new syntax isn’t worse and is IDE friendly:

return [
    'username'   => [
       new Required(),
       (new HasLength())
           ->min(3)
           ->max(255),
       new MatchRegularExpression($this->userModel::$usernameRegexp),
       new Unique()
           ->against($this->userModel)
           ->message($this->app->t('ModuleUser', 'This username has already been taken.')),
   ],

    'email' => [
         new Required(),
         new Email(),
         (new Unique())
             ->against($this->userModel)
             ->message($this->app->t('ModuleUser', 'This email address has already been taken.')),
    ],

    'password' => [
        (new Required())
            ->skipOnEmpty($this->module->accountGeneratingPassword),
        (new HasLength())
            ->min(6)
            ->max(72),
     ]
];

More than that, concrete rule syntax could be tuned. It’s not final yet.


(Tecnologiaterabyte) #13

If the change will give us more freedom and more functionality I agree, but must have the written documentation before removing the validators, to be clear in their syntax and form of employment.


(Sizemail) #14

Can we make it more IDE friendly? For example adding an validator\Element

return [
    (new Element('phone'))->set(new Required())->set(/* ... */),
    // For some one who loved this one
    (new Element('email'))->setMulti(new Required(), new Email() /*, ... */),
];

So we can create a multi usable classes like

class Email extends validator\Element
{
    public function __constructor(string $attribute = 'email')
    {
        parent::__construct($attribute);
    }
    
    public function defaultValidators(): array
    {
        return [new Required(), new Email()];
    }
}

and just return it

return [
    new myvalidator\Email(),
    new myvalidator\Phone(),
];

(jomonjohnson) #15

What about client-side validation?


(Alexander Makarov) #16

Currently there’s none.


(Fabrizio Caldarelli) #17

All this logic to make the same that now we do in 3 simple lines (using array) ?


(Mehdi Achour) #18

Here is it: https://github.com/yiisoft/yii-core/pull/108


(Mehdi Achour) #19

A little drawback I see with the new approach. In Yii2, it was possible to apply validations rules to multiple attributes:

[['attr1','attr2'], 'myvalidator'],
[['attr1'], 'anotherValidator'],

This won’t be as straight forward with the new syntax.

As for classes vs array, when I see both syntaxes:

[
    'username'   => [
        new Required(),
        (new HasLength())->min(3)->max(255),
        new MatchRegularExpression($this->userModel::$usernameRegexp),
        (new Unique())->against($this->userModel)->message($this->app->t('ModuleUser', 'This username has already been taken.')),
    ],
    
    // vs
    ['username', 'required'],
    ['username', 'string', 'min' => 3, 'max' => 255],
    ['username', 'matches', 'reg' => $this->userModel::$usernameRegexp],
    ['username', 'unique', 'against' => $this->userModel, 'message' => $this->app->t('ModuleUser', 'This username has already been taken.')],

];

I’m pretty sure that the array syntax can be dynamically interpreted and use the new API under the hood. Can this be explored?


#20

The problem with array syntax is that it should not be AR responsibility to interpret this array and instantiate validators objects. And you can still create helper which will interpret this array and instantiate validators at user code level, without introducing such convention in framework itself.