Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,33 @@ See [rules-aliasing](https://github.com/koorchik/LIVR#rules-aliasing) in LIVR sp
## $validator->getRules()
returns array containing all ruleBuilders for the validator. You can register new rule or update existing one with "registerRules" method.

## $validator->getFieldRule($fieldName)
returns all rules of the field as one rule: `function ($value, $allValues, &$output)`, which returns an error code. It runs the rules like "validate" does for the field: in order, each rule gets the output of the previous one. `$output` gets the output of the rules, or null if the field has no rules.

Use it in meta rules that validate many values with the same rules: it skips the result and errors arrays that "validate" makes for every call. Built-in "list\_of" and "or" work this way.

```php
Validator\LIVR::registerDefaultRules([ 'my_list_of' => function ($rules, $ruleBuilders) {
$validator = new Validator\LIVR([ 'item' => $rules ]);
$validateItem = $validator->registerRules($ruleBuilders)->getFieldRule('item');

return function ($values, $params, &$outputArr) use ($validateItem) {
$output = [];
foreach ($values as $value) {
$itemOutput = null;
$errorCode = $validateItem($value, [ 'item' => $value ], $itemOutput);
if ($errorCode) {
return $errorCode;
}
$output[] = $itemOutput;
}
$outputArr = $output;
};
} ]);

$validator = new Validator\LIVR([ 'tags' => [ 'my_list_of' => [ [ 'required', 'to_lc' ] ] ] ]);
```

# AUTHORS
* antonfin (Anton Morozov)
* k0stik (Konstantin Dvornik)
Expand Down
134 changes: 100 additions & 34 deletions lib/Validator/LIVR.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class LIVR
private $isAutoTrim = false;

private static $IS_DEFAULT_AUTO_TRIM = 0;
private static $BUILDER_CLOSURES = array(); // rule builders given as strings => closures

private static $DEFAULT_RULES = array(
'required' => 'Validator\LIVR\Rules\Common::required',
'not_empty' => 'Validator\LIVR\Rules\Common::notEmpty',
Expand Down Expand Up @@ -99,7 +101,7 @@ public function __construct($livrRules, $isAutoTrim = false)
}

$this->livrRules = $livrRules;
$this->registerRules(self::$DEFAULT_RULES);
$this->validatorBuilders = self::$DEFAULT_RULES;
}

public function prepare()
Expand All @@ -119,7 +121,7 @@ public function prepare()
foreach ($fieldRules as $rule) {
list($name, $args) = $this->parseRule($rule);

array_push($validators, $this->buildValidator($name, $args));
$validators[] = $this->buildValidator($name, $args);
}

$this->validators[$field] = $validators;
Expand Down Expand Up @@ -148,34 +150,35 @@ public function validate($data)
$result = array();

foreach ($this->validators as $fieldName => $validators) {
if (count($validators) == 0) {
continue;
}

$value = isset($data[$fieldName]) ? $data[$fieldName] : null;

$isOk = true;
// The output of the previous rules, the same as $result[$fieldName]
$hasOutput = false;
$output = null;

foreach ($validators as $vCb) {
$fieldResult = array_key_exists($fieldName, $result) ? $result[$fieldName] : $value;
$input = $hasOutput ? $output : $value;
$fieldResult = $input;

$errCode = $vCb(
( array_key_exists($fieldName, $result) ? $result[$fieldName] : $value ),
$data,
$fieldResult
);
$errCode = $vCb($input, $data, $fieldResult);

if ($errCode) {
$errors[$fieldName] = $errCode;
$isOk = false;
$hasOutput = false;

break;
} elseif ($fieldResult !== null) {
$result[$fieldName] = $fieldResult;
$hasOutput = true;
$output = $fieldResult;
} elseif (array_key_exists($fieldName, $data)) {
$result[$fieldName] = $value;
$hasOutput = true;
$output = $value;
}
}

if ($hasOutput) {
$result[$fieldName] = $output;
}
}

if (count($errors) > 0) {
Expand All @@ -195,8 +198,10 @@ public function getErrors()

public function registerRules($rules)
{

$this->validatorBuilders = array_merge($this->validatorBuilders, $rules);
// Meta rules register the rules of their validator, usually the same as these ones
if ($rules !== $this->validatorBuilders) {
$this->validatorBuilders = array_merge($this->validatorBuilders, $rules);
}

return $this;
}
Expand All @@ -215,13 +220,63 @@ public function getRules()
return $this->validatorBuilders;
}

/**
* Returns the rules of the field as one rule: function ($value, $allValues, &$output),
* which returns an error code. It runs the rules like validate() does for the field:
* in order, each rule gets the output of the previous one. $output gets the output
* of the rules, or null if the field has no rules.
*
* Meta rules use it to validate many values without the arrays that validate() makes.
*/
public function getFieldRule($fieldName)
{
if (!$this->isPrepared) {
$this->prepare();
}

$validators = isset($this->validators[$fieldName]) ? $this->validators[$fieldName] : array();

return function ($value, $allValues, &$output) use ($validators) {
// A field without rules is not in the output of validate()
if (!$validators) {
$output = null;
return null;
}

if ($this->isAutoTrim) {
$value = $this->autoTrim($value);
$allValues = $this->autoTrim($allValues);
}

$output = $value;

foreach ($validators as $vCb) {
$input = $output;
$fieldResult = $input;

$errCode = $vCb($input, $allValues, $fieldResult);

if ($errCode) {
return $errCode;
}

// Like in validate(), where the field is in the data
$output = $fieldResult !== null ? $fieldResult : $value;
}

return null;
};
}

private function parseRule($livrRule)
{
if (\Validator\LIVR\Util::isAssocArray($livrRule)) {
reset($livrRule);
$name = key($livrRule);

$args = $livrRule[$name];
// The first key and value, without reset(), which copies the array
$name = null;
$args = null;
foreach ($livrRule as $name => $args) {
break;
}

if (!is_array($args) || \Validator\LIVR\Util::isAssocArray($args)) {
$args = array($args);
Expand All @@ -241,10 +296,20 @@ private function buildValidator($name, $args)
throw new \Exception("Rule [$name] not registered");
}

$funcArgs = $args;
array_push($funcArgs, $this->validatorBuilders);
$builder = $this->validatorBuilders[$name];

// A "Class::method" string is resolved on every call, a closure is not
if (is_string($builder)) {
if (!isset(self::$BUILDER_CLOSURES[$builder])) {
self::$BUILDER_CLOSURES[$builder] = \Closure::fromCallable($builder);
}

$builder = self::$BUILDER_CLOSURES[$builder];
}

$args[] = $this->validatorBuilders;

return call_user_func_array($this->validatorBuilders[$name], $funcArgs);
return $builder(...$args);
}

private static function buildAliasedRule($alias)
Expand All @@ -258,18 +323,19 @@ private static function buildAliasedRule($alias)

return function ($ruleBuilders) use ($alias) {
$validator = new \Validator\LIVR(array('value' => $alias['rules']));
$validator->registerRules($ruleBuilders)->prepare();
$validateValue = $validator->registerRules($ruleBuilders)->getFieldRule('value');
$aliasError = isset($alias['error']) ? $alias['error'] : null;

return function ($value, $params, &$outputArr) use ($validator, $alias) {
$result = $validator->validate(array('value' => $value));
return function ($value, $params, &$outputArr) use ($validateValue, $aliasError) {
$output = null;
$errCode = $validateValue($value, array('value' => $value), $output);

if ($result) {
$outputArr = $result['value'];
return;
} else {
$errors = $validator->getErrors();
return isset($alias['error']) ? $alias['error'] : $errors['value'];
if ($errCode) {
return $aliasError !== null ? $aliasError : $errCode;
}

$outputArr = $output;
return;
};
};
}
Expand Down
43 changes: 22 additions & 21 deletions lib/Validator/LIVR/Rules/Meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public static function listOf()
}

$validator = new \Validator\LIVR(array('field' => $livr));
$validator->registerRules($ruleBuilders)->prepare();
$validateItem = $validator->registerRules($ruleBuilders)->getFieldRule('field');

return function ($values, $params, &$outputArr) use ($validator) {
return function ($values, $params, &$outputArr) use ($validateItem) {
if (!isset($values) || $values === '') {
return;
}
Expand All @@ -58,17 +58,17 @@ public static function listOf()
$hasErrors = false;

foreach ($values as $value) {
$result = $validator->validate(array('field' => $value));
// Rules of an item see it as the only field of the data, like in validate()
$output = null;
$errCode = $validateItem($value, array('field' => $value), $output);

// The result is empty without rules
if ($result !== false && $result !== null) {
$results[] = isset($result['field']) ? $result['field'] : null;
$errors[] = null;
} else {
if ($errCode) {
$results[] = null;
$validatorErrors = $validator->getErrors();
$errors[] = $validatorErrors['field'];
$errors[] = $errCode;
$hasErrors = true;
} else {
$results[] = $output;
$errors[] = null;
}
}

Expand Down Expand Up @@ -212,27 +212,28 @@ public static function __or()
$livrs = func_get_args();
$ruleBuilders = array_pop($livrs);

$validators = array();
$fieldRules = array();

foreach ($livrs as $livr) {
$validator = new \Validator\LIVR(array('field' => $livr));
$validator->registerRules($ruleBuilders)->prepare();
$validators[] = $validator;
$fieldRules[] = $validator->registerRules($ruleBuilders)->getFieldRule('field');
}

return function ($value, $params, &$outputArr) use ($validators) {
return function ($value, $params, &$outputArr) use ($fieldRules) {
// Empty values go to the rule sets too, so "required" and "not_empty" in them work
$lastError = null;
foreach ($validators as $validator) {
$result = $validator->validate(array('field' => $value));
$allValues = array('field' => $value);

if ($result !== false && $result !== null) {
$outputArr = $result['field'];
foreach ($fieldRules as $fieldRule) {
$output = null;
$errCode = $fieldRule($value, $allValues, $output);

if (!$errCode) {
$outputArr = $output;
return;
} else {
$errors = $validator->getErrors();
$lastError = $errors['field'];
}

$lastError = $errCode;
}

if ($lastError) {
Expand Down
18 changes: 8 additions & 10 deletions lib/Validator/LIVR/Rules/Special.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,14 @@ public static function isoDate()
return 'FORMAT_ERROR';
}

$isoDateReg = '/^(\d{4})-(\d{2})-(\d{2})$/';

if (preg_match($isoDateReg, $value)) {
try {
$date = new \DateTime($value);
if ($date->format("Y-m-d") == $value) {
return;
}
} catch (\Exception $e) {
return "WRONG_DATE";
// checkdate() instead of DateTime, which is several times slower.
// \z, not $: "$" also matches before a trailing newline
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})\z/', $value, $matches)) {
// checkdate() starts with year 1. Year 0 is a leap year, like 2000.
$year = (int) $matches[1] ?: 2000;

if (checkdate((int) $matches[2], (int) $matches[3], $year)) {
return;
}
}

Expand Down
9 changes: 5 additions & 4 deletions lib/Validator/LIVR/Rules/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ public static function oneOf()
array_pop($allowedValues); # pop rule_builders
}

$modifiedAllowedValues = array();
// The allowed values as keys: isset() does not go through the whole list like in_array()
$allowedValuesSet = array();
foreach ($allowedValues as $v) {
$modifiedAllowedValues[] = (string) $v;
$allowedValuesSet[(string) $v] = true;
}

return function ($value) use ($modifiedAllowedValues) {
return function ($value) use ($allowedValuesSet) {
if (!isset($value) or $value === '') {
return;
}
Expand All @@ -29,7 +30,7 @@ public static function oneOf()
return 'FORMAT_ERROR';
}

if (! in_array((string) $value, $modifiedAllowedValues, true)) {
if (!isset($allowedValuesSet[(string) $value])) {
return 'NOT_ALLOWED_VALUE';
}

Expand Down
19 changes: 18 additions & 1 deletion lib/Validator/LIVR/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,24 @@ public static function isAssocArray($arr)
return false;
}

return array_keys($arr) !== range(0, count($arr) - 1);
// An empty array is an object too, as PHP decodes {} and [] to the same array
if (!$arr) {
return true;
}

if (\PHP_VERSION_ID >= 80100) {
return !\array_is_list($arr);
}

// Without array_keys() and range(), which make two arrays
$index = 0;
foreach ($arr as $key => $value) {
if ($key !== $index++) {
return true;
}
}

return false;
}

public static function isList($arr)
Expand Down
Loading
Loading