Skip to content
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

Refactor Validator, create ValidatorRule class #20

Merged
merged 49 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
579a61a
introduce ValidatorRule and refactor code and add/fix tests
DarkSide666 Apr 14, 2024
ac38294
add phptype ValidatorCallback
DarkSide666 Apr 14, 2024
142e503
set phpstan-type for valitron rules
DarkSide666 Apr 14, 2024
94fd394
address review comments
DarkSide666 Apr 14, 2024
627f26c
improve doc
mvorisek Apr 14, 2024
48ae9aa
sort validators, rename method to addRule
DarkSide666 Apr 14, 2024
96a3ead
add ValidatorCondition typehint and fix it
DarkSide666 Apr 14, 2024
e57308b
better user Model compare method
DarkSide666 Apr 14, 2024
5135e37
add tests for complex data type (date)
DarkSide666 Apr 14, 2024
09cea81
test
DarkSide666 Apr 14, 2024
582c6b9
change test to show how it fails for empty DateTime
DarkSide666 Apr 15, 2024
5218905
show the problem
DarkSide666 Apr 15, 2024
2126a95
extend Valitron class and fix bugs there
DarkSide666 Apr 15, 2024
4846bfc
proper validation
DarkSide666 Apr 15, 2024
b84cc8d
cs fix
DarkSide666 Apr 15, 2024
7db8c32
add Override attribute
DarkSide666 Apr 15, 2024
8cea4c3
improve tests for coverage and fix dateFormat rule
DarkSide666 Apr 15, 2024
c4f38cb
better test for dateFormat
DarkSide666 Apr 15, 2024
f04f22d
add test to improve coverage
DarkSide666 Apr 15, 2024
f155225
ouch
DarkSide666 Apr 15, 2024
68ce5fc
Update src/Validator.php
DarkSide666 Apr 18, 2024
46a028f
Update src/Validator.php
DarkSide666 Apr 18, 2024
73c3485
Update src/ValidatorRule.php
DarkSide666 Apr 18, 2024
1cac5b9
Update src/ValidatorRule.php
DarkSide666 Apr 18, 2024
49594f3
Update src/ValidatorRule.php
DarkSide666 Apr 18, 2024
365d616
Update tests/BasicTest.php
DarkSide666 Apr 18, 2024
8585e19
Update src/ValitronValidator.php
DarkSide666 Apr 18, 2024
ce6c3fa
Update src/ValitronValidator.php
DarkSide666 Apr 18, 2024
fc779f5
address review
DarkSide666 Apr 18, 2024
fbb007f
start adding tests for fixed valitron validator
DarkSide666 Apr 18, 2024
33dd6ad
tests
DarkSide666 Apr 18, 2024
ee3ef0a
test
DarkSide666 Apr 18, 2024
d4eadd7
simplify tests
DarkSide666 Apr 18, 2024
f592bb3
cs fixes
DarkSide666 Apr 18, 2024
64b609c
test
DarkSide666 Apr 19, 2024
2ff9e0e
test
DarkSide666 Apr 19, 2024
65318ba
test
DarkSide666 Apr 19, 2024
624f63d
catch warnings
DarkSide666 Apr 19, 2024
ed288c6
test
DarkSide666 Apr 19, 2024
d559369
test
DarkSide666 Apr 19, 2024
ddc468e
test
DarkSide666 Apr 19, 2024
d51a941
remove comments
DarkSide666 Apr 19, 2024
2573e64
* reorder tests to put expected exceptions last
DarkSide666 Apr 19, 2024
cfd2a66
we do not create coverage report with PHP 7.4
DarkSide666 Apr 19, 2024
2fbcf7a
cs fix
DarkSide666 Apr 19, 2024
85e88c2
test
DarkSide666 Apr 19, 2024
7733fa2
cs fixes
DarkSide666 Apr 19, 2024
9e06c8c
check for precise message
DarkSide666 Apr 19, 2024
35d4082
variable naming
DarkSide666 Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 49 additions & 62 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,66 @@
* Use https://github.com/vlucas/valitron under the hood.
*
* $v = new \Atk4\Validate\Validator($model);
*
* https://github.com/vlucas/valitron/blob/v1.4.11/src/Valitron/Validator.php#L1251
*
* @phpstan-type ValidatorCallback \Closure(string, mixed, list<mixed>, list<mixed>): bool
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
*
* https://github.com/vlucas/valitron/blob/master/src/Valitron/Validator.php#L1240
mvorisek marked this conversation as resolved.
Show resolved Hide resolved
* @phpstan-type ValitronRuleType 'required'|'equals'|'different'|'accepted'|'array'|'numeric'|'integer'|'length'|'lengthBetween'|'lengthMin'|'lengthMax'|'min'|'max'|'between'|'in'|'listContains'|'notIn'|'contains'|'subset'|'containsUnique'|'ip'|'ipv4'|'ipv6'|'email'|'ascii'|'emailDNS'|'url'|'urlActive'|'alpha'|'alphaNum'|'slug'|'regex'|'date'|'dateFormat'|'dateBefore'|'dateAfter'|'boolean'|'creditCard'|'instanceOf'|'requiredWith'|'requiredWithout'|'optional'|'arrayHasKeys'
* @phpstan-type ValitronRule array<int|'message', ValitronRuleType|int|string|string[]|ValidatorCallback>
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
*/
class Validator
{
use WarnDynamicPropertyTrait;

/**
* Array of rules in following format which is natively supported by Valitron mapFieldsRules():
* [
* 'foo' => [
* ['required'],
* ['integer', 'message'=>'test 1'],
* ],
* 'bar' => [
* ['email'],
* ['lengthBetween', 4, 10, 'message'=>'test 2'],
* ],
* ];.
*
* @var array<string, array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>>>
* @var list<ValidatorRule>
*/
public array $rules = [];

public function __construct(Model $model)
{
$model->onHook(Model::HOOK_VALIDATE, \Closure::fromCallable([$this, 'validate']));
}

/**
* Array of conditional rules in following format:
* [
* [
* $conditions, // array of conditions
* $then_rules, // array in $this->rules format which will be used if conditions are met
* $else_rules // array in $this->rules format which will be used if conditions are not met
* ],
* ].
* Set rules of particular field.
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
*
* @var list<
* array{
* array<string, string>,
* array<string, array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>>>,
* array<string, array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>>>
* }
* >
* @param array<string|ValitronRule> $rules
* @param array<string, mixed>|list<array<string, mixed>> $conditions
*
* @return $this
*/
public array $if_rules = [];

public function __construct(Model $model)
public function rule(string $field, array $rules, ?string $activateOn = null, array $conditions = []): self
{
$model->onHook(Model::HOOK_VALIDATE, \Closure::fromCallable([$this, 'validate']));
foreach ($rules as $rule) {
$validatorRule = new ValidatorRule($field, $rule);
if ($activateOn !== null) {
$validatorRule->setActivateOnResult($activateOn, $conditions);
}
$this->addValidatorRule($validatorRule);
}

return $this;
}

/**
* Set one rule.
*
* @param array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>> $rules
*
* @return $this
*/
public function rule(string $field, array $rules): self
public function addValidatorRule(ValidatorRule $validatorRule): self
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
{
$this->rules[$field] = array_merge(
$this->rules[$field] ?? [],
$rules
);
$this->rules[] = $validatorRule;

return $this;
}

/**
* Set multiple rules.
*
* @param array<string, array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>>> $hash array with field name as key and rules as value
* @param array<string, list<string|ValitronRule>> $hash array with field name as key and rules as value
*
* @return $this
*/
Expand All @@ -95,19 +88,21 @@ public function rules(array $hash): self
/**
* Set conditional rules.
*
* @param array<string, int|string> $conditions
* @param array<string, array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>>> $then_hash
* @param array<string, array<string|array<string|int|\Closure(string, mixed, list<mixed>, list<mixed>): bool>>> $else_hash
* @param array<string, mixed>|list<array<string, mixed>> $conditions
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
* @param array<string, string|list<string|ValitronRule>> $then_hash
* @param array<string, string|list<string|ValitronRule>> $else_hash
*
* @return $this
*/
public function if(array $conditions, array $then_hash, array $else_hash = []): self
{
$this->if_rules[] = [
$conditions,
$then_hash,
$else_hash,
];
foreach ($then_hash as $field => $rules) {
$this->rule($field, $rules, ValidatorRule::ON_SUCCESS, $conditions);
}

foreach ($else_hash as $field => $rules) {
$this->rule($field, $rules, ValidatorRule::ON_FAIL, $conditions);
}

return $this;
}
Expand All @@ -117,28 +112,20 @@ public function if(array $conditions, array $then_hash, array $else_hash = []):
*
* @return array<string, string> array of errors in format: [field_name => error_message]
*/
public function validate(Model $model, ?string $intent = null): array
public function validate(Model $model): array
{
// initialize Validator, set data
$v = new \Valitron\Validator($model->get());

// prepare array of all rules we have to validate
// this should also include respective rules from $this->if_rules.
$all_rules = $this->rules;

foreach ($this->if_rules as $row) {
[$conditions, $then_hash, $else_hash] = $row;

$test = true;
foreach ($conditions as $field => $value) {
$test = $test && ($model->get($field) === $value);
$rules = [];
foreach ($this->rules as $rule) {
if ($rule->isActivated($model) === true) {
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
$rules[$rule->field][] = $rule->getValitronRule();
}

$all_rules = array_merge_recursive($all_rules, $test ? $then_hash : $else_hash);
}

// set up Valitron rules
$v->mapFieldsRules($all_rules);
$v->mapFieldsRules($rules);

// validate and if errors then format them to fit Atk4 error format
if ($v->validate() === true) {
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
141 changes: 141 additions & 0 deletions src/ValidatorRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Atk4\Validate;

use Atk4\Data\Exception;
use Atk4\Data\Model;

/**
* https://github.com/vlucas/valitron/blob/v1.4.11/src/Valitron/Validator.php#L1251.
*
* @phpstan-type ValidatorCallback \Closure(string, mixed, list<mixed>, list<mixed>): bool
*
* https://github.com/vlucas/valitron/blob/master/src/Valitron/Validator.php#L1240
* @phpstan-type ValitronRuleType 'required'|'equals'|'different'|'accepted'|'array'|'numeric'|'integer'|'length'|'lengthBetween'|'lengthMin'|'lengthMax'|'min'|'max'|'between'|'in'|'listContains'|'notIn'|'contains'|'subset'|'containsUnique'|'ip'|'ipv4'|'ipv6'|'email'|'ascii'|'emailDNS'|'url'|'urlActive'|'alpha'|'alphaNum'|'slug'|'regex'|'date'|'dateFormat'|'dateBefore'|'dateAfter'|'boolean'|'creditCard'|'instanceOf'|'requiredWith'|'requiredWithout'|'optional'|'arrayHasKeys'
* @phpstan-type ValitronRule array<int|'message', ValitronRuleType|int|string|string[]|ValidatorCallback>
*/
class ValidatorRule
{
public const ON_SUCCESS = 'success';
public const ON_FAIL = 'fail';
public string $field;
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @var array<string, mixed>
*/
public array $activateConditions = [];
public ?string $activateOn = null;

/**
* @var list<ValitronRule>
*/
private array $rule = [];
private ?string $message = null;
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param ValitronRuleType|ValitronRule|ValidatorCallback $rule
*/
public function __construct(string $field, $rule)
{
if (!is_array($rule)) {
$rule = [$rule];
}

$this->field = $field;

$message = $rule['message'] ?? null;
if (isset($rule['message'])) {
unset($rule['message']);
}

$this->setRule($rule);
$this->setMessage($message);
}

/**
* @param array<string, mixed> $activationConditions [field_name => value]
*/
public function setActivateOnSuccess(array $activationConditions): void
{
$this->setActivateOnResult(self::ON_SUCCESS, $activationConditions);
}

/**
* @param array<string, mixed> $activationConditions [field_name => value]
*/
public function setActivateOnFail(array $activationConditions): void
{
$this->setActivateOnResult(self::ON_FAIL, $activationConditions);
}

/**
* @param array<string, mixed> $activationConditions [field_name => value]
*/
public function setActivateOnResult(string $activateOn, array $activationConditions): void
{
if ($this->activateOn !== null) {
throw new Exception('Activation condition already set');
}

$this->activateOn = $activateOn;
$this->activateConditions = $activationConditions;
}

public function isActivated(Model $model): bool
{
$this->activateOn ??= self::ON_SUCCESS;

foreach ($this->activateConditions as $conditionField => $conditionValue) {
if ($this->activateOn === self::ON_SUCCESS && $model->get($conditionField) !== $conditionValue) {
return false;
}

if ($this->activateOn === self::ON_FAIL && $model->get($conditionField) === $conditionValue) {
return false;
}
}

return true;
}

/**
* @param list<ValitronRule> $rule
*/
private function setRule(array $rule): void
{
$this->rule = $rule;
}

private function setMessage(?string $message = null): void
{
$this->message = $message;
}

/**
* @return list<ValitronRule>
*/
public function getRule(): array
{
return $this->rule;
}

public function getMessage(): ?string
{
return $this->message;
}

/**
* @return list<ValitronRule>
*/
public function getValitronRule(): array
{
$rule = $this->getRule();
if ($this->getMessage() !== null) {
$rule['message'] = $this->getMessage();
}

return $rule;
}
}
Loading
Loading