Skip to content

Commit 09fed81

Browse files
vjikStyleCIBot
andauthored
Improve validation context (#427)
* Remove `ValidationContext::getParameters()` * Move data set from constructor to `setDataSet()` method * Remove attribute from constructor * Move validator and raw data from constructor to `setValidatorAndRawDataOnce()` method * Apply fixes from StyleCI * Make `setParameter()` fluent * Add test * fix Co-authored-by: StyleCI Bot <[email protected]>
1 parent 3270502 commit 09fed81

8 files changed

Lines changed: 126 additions & 55 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ final class CompanyNameHandler implements Rule\RuleHandlerInterface
828828

829829
$result = new Result();
830830
$dataSet = $context->getDataSet();
831-
$hasCompany = $dataSet !== null && $dataSet->getAttributeValue('hasCompany') === true;
831+
$hasCompany = $dataSet->getAttributeValue('hasCompany') === true;
832832

833833
if ($hasCompany && $this->isCompanyNameValid($value) === false) {
834834
$result->addError('Company name is not valid.');

src/Rule/CompareHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function validate(mixed $value, object $rule, ValidationContext $context)
4343

4444
if ($targetValue === null && $targetAttribute !== null) {
4545
/** @var mixed $targetValue */
46-
$targetValue = $context->getDataSet()?->getAttributeValue($targetAttribute);
46+
$targetValue = $context->getDataSet()->getAttributeValue($targetAttribute);
4747
if (!is_scalar($targetValue)) {
4848
return $result->addError($rule->getIncorrectDataSetTypeMessage(), [
4949
'type' => get_debug_type($targetValue),

src/ValidationContext.php

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
namespace Yiisoft\Validator;
66

7+
use RuntimeException;
78
use Yiisoft\Arrays\ArrayHelper;
8-
use Yiisoft\Validator\Helper\DataSetNormalizer;
99

1010
/**
1111
* Validation context that might be taken into account when performing validation.
@@ -14,42 +14,61 @@
1414
*/
1515
final class ValidationContext
1616
{
17+
private ?ValidatorInterface $validator = null;
18+
19+
/**
20+
* @var mixed The raw validated data.
21+
*/
22+
private mixed $rawData = null;
23+
24+
/**
25+
* @var DataSetInterface|null Data set the attribute belongs to. Null if data set not set.
26+
*/
27+
private ?DataSetInterface $dataSet = null;
28+
29+
/**
30+
* @var string|null Validated attribute name. Null if a single value is validated.
31+
*/
32+
private ?string $attribute = null;
33+
1734
/**
18-
* @param DataSetInterface|null $dataSet Data set the attribute belongs to. Null if a single value is validated.
19-
* @param mixed $rawData The raw validated data.
20-
* @param string|null $attribute Validated attribute name. Null if a single value is validated.
2135
* @param array $parameters Arbitrary parameters.
2236
*/
2337
public function __construct(
24-
private ValidatorInterface $validator,
25-
private mixed $rawData,
26-
private ?DataSetInterface $dataSet = null,
27-
private ?string $attribute = null,
2838
private array $parameters = []
2939
) {
3040
}
3141

42+
public function setValidatorAndRawDataOnce(ValidatorInterface $validator, mixed $rawData): self
43+
{
44+
if ($this->validator !== null) {
45+
return $this;
46+
}
47+
48+
$this->validator = $validator;
49+
$this->rawData = $rawData;
50+
51+
return $this;
52+
}
53+
3254
/**
3355
* Validate data in current context.
3456
*
35-
* @param DataSetInterface|mixed|RulesProviderInterface $data Data set to validate. If {@see RulesProviderInterface}
36-
* instance provided and rules are not specified explicitly, they are read from the
37-
* {@see RulesProviderInterface::getRules()}.
57+
* @param mixed $data Data set to validate. If {@see RulesProviderInterface} instance provided and rules are
58+
* not specified explicitly, they are read from the {@see RulesProviderInterface::getRules()}.
3859
* @param callable|iterable|object|string|null $rules Rules to apply. If specified, rules are not read from data set
3960
* even if it is an instance of {@see RulesProviderInterface}.
4061
*
4162
* @psalm-param RulesType $rules
4263
*/
4364
public function validate(mixed $data, callable|iterable|object|string|null $rules = null): Result
4465
{
66+
$this->checkValidatorAndRawData();
67+
4568
$currentDataSet = $this->dataSet;
4669
$currentAttribute = $this->attribute;
4770

48-
$dataSet = DataSetNormalizer::normalize($data);
49-
$this->dataSet = $dataSet;
50-
$this->attribute = null;
51-
52-
$result = $this->validator->validate($dataSet, $rules, $this);
71+
$result = $this->validator->validate($data, $rules, $this);
5372

5473
$this->dataSet = $currentDataSet;
5574
$this->attribute = $currentAttribute;
@@ -62,17 +81,31 @@ public function validate(mixed $data, callable|iterable|object|string|null $rule
6281
*/
6382
public function getRawData(): mixed
6483
{
84+
$this->checkValidatorAndRawData();
6585
return $this->rawData;
6686
}
6787

6888
/**
69-
* @return DataSetInterface|null Data set the attribute belongs to. Null if a single value is validated.
89+
* @return DataSetInterface Data set the attribute belongs to.
7090
*/
71-
public function getDataSet(): ?DataSetInterface
91+
public function getDataSet(): DataSetInterface
7292
{
93+
if ($this->dataSet === null) {
94+
throw new RuntimeException('Data set in validation context is not set.');
95+
}
96+
7397
return $this->dataSet;
7498
}
7599

100+
/**
101+
* @param DataSetInterface $dataSet Data set the attribute belongs to.
102+
*/
103+
public function setDataSet(DataSetInterface $dataSet): self
104+
{
105+
$this->dataSet = $dataSet;
106+
return $this;
107+
}
108+
76109
/**
77110
* @return string|null Validated attribute name. Null if a single value is validated.
78111
*/
@@ -90,14 +123,6 @@ public function setAttribute(?string $attribute): self
90123
return $this;
91124
}
92125

93-
/**
94-
* @return array Arbitrary parameters.
95-
*/
96-
public function getParameters(): array
97-
{
98-
return $this->parameters;
99-
}
100-
101126
/**
102127
* Get named parameter.
103128
*
@@ -113,13 +138,24 @@ public function getParameter(string $key, mixed $default = null): mixed
113138
return ArrayHelper::getValue($this->parameters, $key, $default);
114139
}
115140

116-
public function setParameter(string $key, mixed $value): void
141+
public function setParameter(string $key, mixed $value): self
117142
{
118143
$this->parameters[$key] = $value;
144+
return $this;
119145
}
120146

121147
public function isAttributeMissing(): bool
122148
{
123149
return $this->attribute !== null && $this->dataSet !== null && !$this->dataSet->hasAttribute($this->attribute);
124150
}
151+
152+
/**
153+
* @psalm-assert ValidatorInterface $this->validator
154+
*/
155+
private function checkValidatorAndRawData(): void
156+
{
157+
if ($this->validator === null) {
158+
throw new RuntimeException('Validator and raw data in validation context is not set.');
159+
}
160+
}
125161
}

src/Validator.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,19 @@ public function validate(
6868
$this->defaultSkipOnEmptyCriteria
6969
);
7070

71-
$compoundResult = new Result();
72-
$context ??= new ValidationContext($this, $data, $dataSet);
73-
$results = [];
71+
$context ??= new ValidationContext();
72+
$context
73+
->setValidatorAndRawDataOnce($this, $data)
74+
->setDataSet($dataSet);
7475

76+
$results = [];
7577
foreach ($rules as $attribute => $attributeRules) {
7678
$result = new Result();
7779

7880
if (is_int($attribute)) {
7981
/** @psalm-suppress MixedAssignment */
8082
$validatedData = $dataSet->getData();
83+
$context->setAttribute(null);
8184
} else {
8285
/** @psalm-suppress MixedAssignment */
8386
$validatedData = $dataSet->getAttributeValue($attribute);
@@ -93,6 +96,8 @@ public function validate(
9396
$results[] = $result;
9497
}
9598

99+
$compoundResult = new Result();
100+
96101
foreach ($results as $result) {
97102
foreach ($result->getErrors() as $error) {
98103
$compoundResult->addError(

tests/Rule/CallbackTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private static function staticValidateName(
178178
RuleInterface $rule,
179179
ValidationContext $context
180180
): Result {
181-
if ($value !== $context->getDataSet()?->getAttributeValue('age')) {
181+
if ($value !== $context->getDataSet()->getAttributeValue('age')) {
182182
throw new RuntimeException('Method scope was not bound to the object.');
183183
}
184184

tests/Rule/LimitHandlerTraitTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Yiisoft\Validator\RuleHandlerInterface;
1313
use Yiisoft\Validator\RuleInterface;
1414
use Yiisoft\Validator\Tests\Support\Rule\RuleWithoutOptions;
15-
use Yiisoft\Validator\Tests\Support\ValidatorFactory;
1615
use Yiisoft\Validator\ValidationContext;
1716

1817
final class LimitHandlerTraitTest extends TestCase
@@ -81,7 +80,7 @@ public function checkValidateLimits(
8180
$this->validateLimits($rule, $context, $number, $result);
8281
}
8382
};
84-
$context = new ValidationContext(ValidatorFactory::make(), null);
83+
$context = new ValidationContext();
8584

8685
$this->expectException(InvalidArgumentException::class);
8786
$this->expectExceptionMessage('$rule must implement both LimitInterface and RuleInterface.');

tests/ValidationContextTest.php

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,84 @@
55
namespace Yiisoft\Validator\Tests;
66

77
use PHPUnit\Framework\TestCase;
8+
use RuntimeException;
89
use Yiisoft\Validator\DataSet\ArrayDataSet;
9-
use Yiisoft\Validator\Tests\Support\ValidatorFactory;
1010
use Yiisoft\Validator\ValidationContext;
11+
use Yiisoft\Validator\Validator;
1112

1213
final class ValidationContextTest extends TestCase
1314
{
14-
public function testDefault(): void
15+
public function testGetDataSetWithoutDataSet(): void
1516
{
16-
$validator = ValidatorFactory::make();
17+
$context = new ValidationContext();
1718

18-
$context = new ValidationContext($validator, 7);
19-
20-
$this->assertSame(7, $context->getRawData());
21-
$this->assertNull($context->getDataSet());
22-
$this->assertNull($context->getAttribute());
23-
$this->assertSame([], $context->getParameters());
19+
$this->expectException(RuntimeException::class);
20+
$this->expectErrorMessage('Data set in validation context is not set.');
21+
$context->getDataSet();
2422
}
2523

2624
public function testConstructor(): void
2725
{
28-
$data = ['x' => 7];
26+
$context = new ValidationContext(['key' => 42]);
27+
28+
$this->assertSame(42, $context->getParameter('key'));
29+
}
30+
31+
public function testDataSet(): void
32+
{
2933
$dataSet = new ArrayDataSet();
3034

31-
$context = new ValidationContext(ValidatorFactory::make(), $data, $dataSet, 'name', ['key' => 42]);
35+
$context = new ValidationContext();
36+
$context->setDataSet($dataSet);
3237

33-
$this->assertSame($data, $context->getRawData());
3438
$this->assertSame($dataSet, $context->getDataSet());
35-
$this->assertSame('name', $context->getAttribute());
36-
$this->assertSame(['key' => 42], $context->getParameters());
3739
}
3840

3941
public function testSetParameter(): void
4042
{
41-
$context = new ValidationContext(ValidatorFactory::make(), null);
43+
$context = new ValidationContext();
4244
$context->setParameter('key', 42);
4345

44-
$this->assertSame(['key' => 42], $context->getParameters());
46+
$this->assertSame(42, $context->getParameter('key'));
4547
}
4648

4749
public function testGetParameter(): void
4850
{
49-
$context = new ValidationContext(ValidatorFactory::make(), null, parameters: ['key' => 42]);
51+
$context = new ValidationContext(['key' => 42]);
5052

5153
$this->assertSame(42, $context->getParameter('key'));
5254
$this->assertNull($context->getParameter('non-exists'));
5355
$this->assertSame(7, $context->getParameter('non-exists', 7));
5456
}
57+
58+
public function testValidateWithoutValidator(): void
59+
{
60+
$context = new ValidationContext();
61+
62+
$this->expectException(RuntimeException::class);
63+
$this->expectErrorMessage('Validator and raw data in validation context is not set.');
64+
$context->validate(42);
65+
}
66+
67+
public function testGetRawDataWithoutRawData(): void
68+
{
69+
$context = new ValidationContext();
70+
71+
$this->expectException(RuntimeException::class);
72+
$this->expectErrorMessage('Validator and raw data in validation context is not set.');
73+
$context->getRawData();
74+
}
75+
76+
public function testSetValidatorAndRawDataOnce(): void
77+
{
78+
$validator = new Validator();
79+
80+
$context = new ValidationContext();
81+
82+
$context
83+
->setValidatorAndRawDataOnce($validator, 1)
84+
->setValidatorAndRawDataOnce($validator, 2);
85+
86+
$this->assertSame(1, $context->getRawData());
87+
}
5588
}

tests/ValidatorTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Yiisoft\Validator\Error;
1616
use Yiisoft\Validator\Exception\RuleHandlerInterfaceNotImplementedException;
1717
use Yiisoft\Validator\Exception\RuleHandlerNotFoundException;
18-
use Yiisoft\Validator\Helper\DataSetNormalizer;
1918
use Yiisoft\Validator\Result;
2019
use Yiisoft\Validator\Rule\Boolean;
2120
use Yiisoft\Validator\Rule\CompareTo;
@@ -1212,8 +1211,7 @@ public function validate(
12121211
callable|iterable|object|string|null $rules = null,
12131212
?ValidationContext $context = null
12141213
): Result {
1215-
$dataSet = DataSetNormalizer::normalize($data);
1216-
$context ??= new ValidationContext($this, $data, $dataSet);
1214+
$context ??= new ValidationContext();
12171215

12181216
$result = $this->validator->validate($data, $rules, $context);
12191217

0 commit comments

Comments
 (0)