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 all 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
118 changes: 53 additions & 65 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,67 @@
* 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 https://github.com/phpstan/phpstan/issues/10874
*
* @phpstan-type ValidatorCallback \Closure(string, mixed, list<mixed>, list<mixed>): bool
* @phpstan-type ValidatorCondition array<string, mixed>
*
* https://github.com/vlucas/valitron/blob/master/src/Valitron/Validator.php#L1240 https://github.com/phpstan/phpstan/issues/10874
* @phpstan-type ValitronRuleType 'accepted'|'alpha'|'alphaNum'|'array'|'arrayHasKeys'|'ascii'|'between'|'boolean'|'contains'|'containsUnique'|'creditCard'|'date'|'dateAfter'|'dateBefore'|'dateFormat'|'different'|'email'|'emailDNS'|'equals'|'in'|'instanceOf'|'integer'|'ip'|'ipv4'|'ipv6'|'length'|'lengthBetween'|'lengthMax'|'lengthMin'|'listContains'|'max'|'min'|'notIn'|'numeric'|'optional'|'regex'|'required'|'requiredWith'|'requiredWithout'|'slug'|'subset'|'url'|'urlActive'
* @phpstan-type ValitronRule array<int|'message', ValitronRuleType|int|string|list<string>|ValidatorCallback>
*/
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
* ],
* ].
* Add rule/rules for given field.
*
* @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 ValidatorCondition $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->addRule($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 addRule(ValidatorRule $validatorRule): self
{
$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 +89,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 ValidatorCondition $conditions
* @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,36 +113,28 @@ 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());
$validator = new ValitronValidator($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)) {
$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);
$validator->mapFieldsRules($rules);

// validate and if errors then format them to fit Atk4 error format
if ($v->validate() === true) {
if ($validator->validate()) {
return [];
}

$errors = [];
foreach ($v->errors() as $key => $e) {
foreach ($validator->errors() as $key => $e) {
if (!isset($errors[$key])) {
$errors[$key] = array_pop($e);
}
Expand Down
143 changes: 143 additions & 0 deletions src/ValidatorRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?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
* @phpstan-type ValidatorCondition array<string, mixed>
*
* https://github.com/vlucas/valitron/blob/master/src/Valitron/Validator.php#L1240
* @phpstan-type ValitronRuleType 'accepted'|'alpha'|'alphaNum'|'array'|'arrayHasKeys'|'ascii'|'between'|'boolean'|'contains'|'containsUnique'|'creditCard'|'date'|'dateAfter'|'dateBefore'|'dateFormat'|'different'|'email'|'emailDNS'|'equals'|'in'|'instanceOf'|'integer'|'ip'|'ipv4'|'ipv6'|'length'|'lengthBetween'|'lengthMax'|'lengthMin'|'listContains'|'max'|'min'|'notIn'|'numeric'|'optional'|'regex'|'required'|'requiredWith'|'requiredWithout'|'slug'|'subset'|'url'|'urlActive'
* @phpstan-type ValitronRule array<int|'message', ValitronRuleType|int|string|list<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 ValidatorCondition
*/
public array $activateConditions = [];
public ?string $activateOn = null;

/**
* @var list<ValitronRule>
*/
private array $rule;
private ?string $message;

/**
* @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 ValidatorCondition $activationConditions [field_name => value]
*/
public function setActivateOnSuccess(array $activationConditions): void
{
$this->setActivateOnResult(self::ON_SUCCESS, $activationConditions);
}

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

/**
* @param ValidatorCondition $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->compare($conditionField, $conditionValue)) {
return false;
}

if ($this->activateOn === self::ON_FAIL && $model->compare($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;
}
}
71 changes: 71 additions & 0 deletions src/ValitronValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Atk4\Validate;

use Valitron\Validator as OriginalValidator;

class ValitronValidator extends OriginalValidator
{
#[\Override]
protected function validateDate($field, $value)
DarkSide666 marked this conversation as resolved.
Show resolved Hide resolved
{
if ($value === null || (is_string($value) && trim($value) === '')) {
return false;
}

return parent::validateDate($field, $value);
}

/**
* @param array<mixed> $params
*/
#[\Override]
protected function validateDateFormat($field, $value, $params)
{
if ($value === null || (is_string($value) && trim($value) === '')) {
return false;
}

if ($value instanceof \DateTime) {
return true;
}

return parent::validateDateFormat($field, $value, $params);
}

/**
* @param array<mixed> $params
*/
#[\Override]
protected function validateDateBefore($field, $value, $params)
{
if ($value === null || (is_string($value) && trim($value) === '')) {
return false;
}

if (!isset($params[0]) || (is_string($params[0]) && trim($params[0]) === '')) {
return false;
}

return parent::validateDateBefore($field, $value, $params);
}

/**
* @param array<mixed> $params
*/
#[\Override]
protected function validateDateAfter($field, $value, $params)
{
if ($value === null || (is_string($value) && trim($value) === '')) {
return false;
}

if (!isset($params[0]) || (is_string($params[0]) && trim($params[0]) === '')) {
return false;
}

return parent::validateDateAfter($field, $value, $params);
}
}