New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validator requires one attribute and cannot see the whole model #28
Comments
Even better, if public function validate(?string $attribute, DataSetInterface $dataSet, ResultSet $resultSet): ResultSet /* return for override? */ |
How would that work for cases described in readme? |
class CountryValidator extends Validator
{
public function validateAttribute($model, $attribute)
{
if($model->type === 'any') {
$this->addError($model, 'fieldA', '....');
} else {
$this->addError($model, 'fieldA', '....');
$this->addError($model, 'fieldB', '....');
}
}
} |
And the rest of the cases? |
I have access to everything in the example code method:
|
There are multiple examples:
|
class CountryValidator extends Validator
{
// check multiple attribute at one context
public function validateAttribute($model, $attribute)
{
if($model->fieldA && $model->fieldB) {
// together not allowed
$this->addError($model, 'fieldA', 'together not allowed');
$this->addError($model, 'fieldB', 'together not allowed');
}
}
} |
Isn't it a special case and should happened somewhere else? |
That's just the rule. It is normal for a form to contain attributes that are related to one another. For example in sign up form with password and password re, or email and email re. |
More problem in code complexity for example delivery address, with more sub fields. Here, too, the problem is with an alternative delivery address option:
In addition, the code complexity issue is: // If the attributes of the dataset cannot be accessed in the rule
[
'anyattribute' => new Any()->...,
'delivery_country' => new HasLength()->..
'delivery_postal_code' => new HasLength()...
'delivery_...' => ...
]
// A billing address and a shipping address must be provided.
[
'anyattribute' => new Any()->...,
'delivery_country' => new HasLength()->..
'billing_country' => new HasLength()->..
'delivery_postal_code' => new HasLength()...
'billing_postal_code' => new HasLength()...
'delivery_...' => ...
'billing_...' => ...
] by issue: [
'anyattribute' => new Any()->...,
/* attribute is not relevant */ (new Address())->attributePrefix('delivery_'),
/* attribute is not relevant */ (new Address())->attributePrefix('billing_'),
]
class Address {
...
public function attributePrefix(..);
...
public function validateValue(?string $attribute, DataSetInterface $dataSet, ResultSet $resultSet): ResultSet {
if($this->optional && empty($dataSet->getValue($this->prefix.'country')) && ....) {
return $resultSet;
}
$countryResult = (new HasLength())->min...->validateValue($dataSet->getValue($this->prefix.'country'));
$resultSet->addResult($this->prefix.'country', $countryResult);
...
return $resultSet;
}
} |
This seems like a use-case for scenarios :) Edit: After thinking about your last example, i have another example i could fixed easier. |
Correct, no screnarios are planned. |
@samdark what is your opinion for this issue? |
No final opinion yet. That's fairly typical use case for validation in Yii 2 and, no doubt, there will be similar problem to solve in Yii 3 projects:
etc. For these we, indeed, need to:
This part is clear. What's not clear is where/how to do this type of comopound validation. |
I'm agree that in practical we often need to make validation against attributes that depends on values of another attributes. It often happens when we check data from complex forms by api
In yii2 we have "when" condition and can create rules like return [
[['ip'], 'ip', 'ipv6'=>false, 'when'=>function($model){return $model->allowIpv6===false;}],
[['ip'], 'ip', 'ipv6'=>true, 'when'=>function($model){return $model->allowIpv6===true && !$model->ipv6Only;}],
[['ip'], 'ip', 'ipv6'=>true, 'ipv4'=>false, 'when'=>function($model){return $model->allowIpv6===true && $model->ipv6Only===true;}]
] Why not do something similar $validator = new Validator([
//...
'allowIpv6'=>[new Boolean()],
'onlyIpv6'=>[new When(function(DataSetInterface $data){
if($data->getValue('allowIpv6') == false) {
return (new CompareTo(false)->asBoolean());
}
return new Boolean(),
})],
'ip' => [
new When(function(DataSetInterface $data){
return (new Ip())->allowIpv6($data->getValue('allowIpv6'))
->allowIpv4(!$data->getValue('onlyIpv6'));
});
],
]); |
And for previous case something like $validator = new Validator([
//...
'fieldA' =>[new InRange(['X', 'Y', 'Z'])],
new ConditionalRules(function(DatasetInterface $data){
if($data->getValue('fieldA') == 'X'){
return RuleSetForX::get();
}
//..
return [
'anyattribute' => new Any()->...,
'delivery_country' => new HasLength()->..
'delivery_postal_code' => new HasLength()...
]
})
]) |
I prefer classes instead of in line functions. For small validations it's fine, aber you geht some functions, it will bloat up the code. When we will use classes for this, then it would be similar with the solution from @kamarton |
In present state Rule and Rules aren't bound to data set. They work with individual value. |
There are two use cases for validation:
Yii 2 validation was data set oriented and it wasn't convenient to use validation with individual values via fake dynamic model. |
In my cases i dont't boud dataset in rule directly. It can be bounded by wrapper and resolved inside
I look on validation from point of applicable context also. In yii2 we have models with set of rules and different scenarios. At now, especially with orm, we probably will validate request before fill model. So we can do it in controller or with separated validation classes like AuthRequestValidation, RegistrationRequestValidation, CreatePostValidation, EditPostValidation... And probably we will need some business-rule-scoped validators/or rule/or rulesets that should be reusable in different requests |
How would |
As i see in https://github.com/yiisoft/validator/blob/master/src/Validator.php#L31 Draft public function validate(DataSetInterface $dataSet): ResultSet
{
$results = new ResultSet();
foreach ($this->attributeRules as $attribute => $rules)
{
if ($rules instanse of Conditional){
$rules = $rules->resolve($dataSet);
}
$results->addResult($attribute, $rules->validate($dataSet->getValue($attribute), $dataSet));
}
return $results;
}
class Conditional
{
private $rule;
public function __construct(callable $rule){ $this->rule = $rule;}
public function resolve(DataSetInterface $data): Rules{
return $this->rule($data);
}
} And for When, seems we should pass dataset in Rules and apply in similar manner class When
{
private $rule;
public function __construct(callable $rule){ $this->rule = $rule;}
public function resolve($value, DataSetInterface $data): Rule{
return $this->rule($value, $data);
}
} https://github.com/yiisoft/validator/blob/master/src/Rules.php#L35 public function validate($value, ?DatasetInterface $dataSet = null): Result
{
$compoundResult = new Result();
foreach ($this->getRules($value) as $rule) {
if ($rule instance of When && $dataSet !== null){
$rule = $rule->resolve($value, $dataSet);
}else{
continue or throw Exception
}
$ruleResult = $rule->validateValue($value);
if ($ruleResult->isValid() === false) {
foreach ($ruleResult->getErrors() as $error) {
$compoundResult->addError($error);
}
}
}
return $compoundResult;
} |
I think it would be enough for the ResultSet to get to the |
@Insolita that brings two possible use-cases into Rule class. If we're going to do that, Yii 2 concept fits a bit better.
Applying errors to any attribute is not the only thing that is missing. That's only point 3 from #28 (comment) |
The return value must be changed to $resultset = (new Integer())->max(100)->validate(100);
$resultset->isValid();
$resultset->getErrors();
$resultset = (new Complex())->...->validate($dataset);
$resultset->isValid();
$resultset->getErrors(); // all erros
$resultset->getErrors($attribute); // errors by attribute |
@kamarton That's OK but how do you plan getting data set into Rule? |
Lines 31 to 39 in 4d504bc
With the class Address extends Rules
{
public function isSupportDataSet() { <---- maybe put it here ??
return true;
}
} This solution implies that Lines 8 to 13 in 4d504bc
|
After thinking more of it, I came to conclusion that it doesn't make sense to have validator that works for a single value. That is the job for assertions. So having an interface like the one that was present in Yii 2 is the way to go. |
A validator for a single value is basically assertion. If we can have both that would be good but I suspect that it may complicate library too much. |
I think this is mandatory rather than optional. For example, if I make my own validator that uses additional validator, I will usually validate single values. |
@samdark needed for the alpha version? |
Yes. |
@samdark when will be the freeze for alpha? |
By the end of the month or around it. |
|
kamarton commentedOct 31, 2019
What steps will reproduce the problem?
In yii2, it was easy to access any of the model's attributes. In yii3, In Yii3 this option seems to me not feasible.
https://www.yiiframework.com/doc/guide/2.0/en/input-validation#standalone-validators
validator/src/Validator.php
Line 36 in 725fdfc
This issue also affects the
Result
class because it is not suitable for adding errors to an attribute.What is the expected result?
I get the whole dataset in an extra parameter, and change return type to
ResultSet
validator/src/Rule.php
Line 10 in 725fdfc
validator/src/Result.php
Line 19 in 725fdfc
validator/src/ResultSet.php
Line 12 in 725fdfc
usage:
What do you get instead?
But in this case, I have no way of returning an arbitrary attribute error of the model.
Additional info
Also, to keep the benefits of the current solution, the
Rule
class should be somehow allow theResult
class to be used internally in simpler cases.Use case
I need to check for additional attributes depending on a type attribute.
The text was updated successfully, but these errors were encountered: