-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add a collection rule to validate collection data #28
- Loading branch information
Showing
3 changed files
with
266 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace Elie\Validator\Rule; | ||
|
||
/** | ||
* This class validates an array of data (it could be a json array and would be decoded). | ||
*/ | ||
class CollectionRule extends AbstractRule | ||
{ | ||
|
||
/** | ||
* Specific message error code | ||
*/ | ||
public const INVALID_VALUE = 'INVALID_VALUE'; | ||
|
||
/**#@+ | ||
* Specific options for CollectionRule | ||
*/ | ||
public const RULES = 'rules'; | ||
public const JSON = 'json'; | ||
/**#@-*/ | ||
|
||
protected $rules = []; | ||
|
||
protected $decode = false; // for json value | ||
|
||
/** | ||
* Params could have the following structure: | ||
* [ | ||
* 'required' => {bool:optional:false by default}, | ||
* 'messages' => {array:optional:key/value message patterns}, | ||
* 'rules' => {array:optional:list of rules with their params}, | ||
* 'json' => {boolean:optional:false by default}, | ||
* ] | ||
* <code> | ||
* $params = [<br/> | ||
* 'required' => true,<br/> | ||
* 'rules' => [<br/> | ||
* ['code', StringRule::class, 'min' => 1, 'max' => 255],<br/> | ||
* ['email', EmailRule::class],<br/> | ||
* ] | ||
* ] | ||
* </code> | ||
* | ||
* Value is considered valid if 'rules' is empty | ||
*/ | ||
public function __construct($key, $value, array $params = []) | ||
{ | ||
parent::__construct($key, $value, $params); | ||
|
||
if (isset($params[self::RULES])) { | ||
$this->rules = $params[self::RULES]; | ||
} | ||
|
||
if (isset($params[self::JSON])) { | ||
$this->decode = (bool) $params[self::JSON]; | ||
} | ||
|
||
$this->messages = $this->messages + [ | ||
self::INVALID_VALUE => _("%key%: %value% is not in a collection"), | ||
]; | ||
} | ||
|
||
public function validate(): int | ||
{ | ||
$run = parent::validate(); | ||
|
||
if ($run !== self::CHECK) { | ||
return $run; | ||
} | ||
|
||
return $this->rules === [] ? self::VALID : $this->isValid(); | ||
} | ||
|
||
protected function isValid(): int | ||
{ | ||
$this->error = ''; | ||
|
||
$collection = $this->decode ? json_decode($this->value, true) : $this->value; | ||
|
||
if (! is_array($collection)) { | ||
return $this->setAndReturnError(self::INVALID_VALUE); | ||
} | ||
|
||
$validatedContext = []; | ||
foreach ($collection as $data) { | ||
$validatedData = []; | ||
foreach ($this->rules as $rule) { | ||
$class = $this->resolve($rule, $data); | ||
if ($class->validate() === RuleInterface::ERROR) { | ||
$this->error = $class->getError(); | ||
return RuleInterface::ERROR; | ||
} | ||
$validatedData[$class->getKey()] = $class->getValue(); | ||
} | ||
$validatedContext[] = $validatedData; | ||
} | ||
|
||
$this->value = $validatedContext; | ||
|
||
return RuleInterface::VALID; | ||
} | ||
|
||
protected function resolve(array $rule, $data): RuleInterface | ||
{ | ||
// The first element must be the key context | ||
$key = $rule[0]; | ||
// The second element must be the class validator name | ||
$class = $rule[1]; | ||
|
||
return new $class($key, $data[$key] ?? null, $rule); | ||
} | ||
|
||
public function getValue() | ||
{ | ||
// don't change value on error or if it is not empty | ||
if ($this->value || $this->error) { | ||
return $this->value; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
/** | ||
* Empty value is null or [] only. | ||
* | ||
* @return bool | ||
*/ | ||
protected function isEmpty(): bool | ||
{ | ||
return $this->value === null || $this->value === []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace Elie\Validator\Rule; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use function Composer\Autoload\getData; | ||
|
||
class CollectionRuleTest extends TestCase | ||
{ | ||
|
||
public function testArrayValidate() | ||
{ | ||
$rules = [ | ||
['code', NumericRule::class, NumericRule::MAX => 80], | ||
['slug', MatchRule::class, MatchRule::PATTERN => '/^[a-z]{1,5}$/i'], | ||
]; | ||
|
||
$data = [ | ||
['code' => 12, 'slug' => 'one'], | ||
['code' => 13, 'slug' => 'two'], | ||
['code' => 15, 'slug' => 'three'], | ||
]; | ||
|
||
$rule = new CollectionRule('tags', $data, [CollectionRule::RULES => $rules]); | ||
|
||
assertThat($rule->validate(), is(true)); | ||
|
||
$tags = $rule->getValue(); | ||
|
||
assertThat($tags, arrayWithSize(3)); | ||
} | ||
|
||
public function testJsonValidate() | ||
{ | ||
$rules = [ | ||
['code', NumericRule::class, NumericRule::MAX => 80], | ||
['slug', MatchRule::class, MatchRule::PATTERN => '/^[a-z]{1,5}$/i'], | ||
]; | ||
|
||
$data = json_encode([ | ||
['code' => 12, 'slug' => 'one'], | ||
['code' => 13, 'slug' => 'two'], | ||
['code' => 15, 'slug' => 'three'], | ||
]); | ||
|
||
$rule = new CollectionRule('tags', $data, [CollectionRule::RULES => $rules, CollectionRule::JSON => true]); | ||
|
||
assertThat($rule->validate(), is(true)); | ||
|
||
$tags = $rule->getValue(); | ||
|
||
assertThat($tags, arrayWithSize(3)); | ||
} | ||
|
||
public function testValidateEmptyData() | ||
{ | ||
$rules = [ | ||
['code', NumericRule::class, NumericRule::MAX => 80], | ||
]; | ||
|
||
$data = null; | ||
|
||
$rule = new CollectionRule('tags', $data, [CollectionRule::RULES => $rules]); | ||
|
||
assertThat($rule->validate(), is(true)); | ||
assertThat($rule->getValue(), arrayValue()); // value cast to array | ||
} | ||
|
||
public function testValidateOnError() | ||
{ | ||
$rules = [ | ||
['code', NumericRule::class, NumericRule::MAX => 80], | ||
['slug', MatchRule::class, MatchRule::PATTERN => '/^[a-z]{1,3}$/i'], | ||
]; | ||
|
||
$data = [ | ||
['code' => 12, 'slug' => 'one'], | ||
['code' => 13, 'slug' => 'two'], | ||
['code' => 15, 'slug' => 'three'], | ||
]; | ||
|
||
$rule = new CollectionRule('tags', $data, [CollectionRule::RULES => $rules]); | ||
|
||
assertThat($rule->validate(), is(false)); | ||
assertThat($rule->getError(), containsString('slug: three does not match /^[a-z]{1,3}$/i')); | ||
} | ||
|
||
public function testValidateErrorFormat() | ||
{ | ||
$data = '{email: "elie29@gmail.com"}'; | ||
$rules = [ | ||
['email', EmailRule::class] | ||
]; | ||
|
||
$rule = new CollectionRule('tags', $data, [CollectionRule::RULES => $rules]); | ||
|
||
assertThat($rule->validate(), is(false)); | ||
assertThat($rule->getError(), containsString('tags: {email: "elie29@gmail.com"} is not in a collection')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters