From 56e45ac14289ee8e12ac3b307908b228a92b4a34 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 28 Mar 2023 11:23:26 -0700 Subject: [PATCH 1/2] Feature: add methods for replacing and prepending rules (#11) --- src/ValidationRuleSet.php | 254 ++++++++++++++++++--------- tests/unit/ValidationRuleSetTest.php | 112 ++++++++++++ 2 files changed, 287 insertions(+), 79 deletions(-) diff --git a/src/ValidationRuleSet.php b/src/ValidationRuleSet.php index 049c0cb..eeeadac 100644 --- a/src/ValidationRuleSet.php +++ b/src/ValidationRuleSet.php @@ -45,126 +45,96 @@ public function __construct(ValidationRulesRegistrar $register) public function rules(...$rules): self { foreach ($rules as $rule) { - if ($rule instanceof Closure) { - $this->validateClosureRule($rule); - $this->rules[] = $rule; - } elseif ($rule instanceof ValidationRule) { - $this->rules[] = $rule; - } elseif (is_string($rule)) { - $this->rules[] = $this->getRuleFromString($rule); - } else { - Config::throwInvalidArgumentException( - sprintf( - 'Validation rule must be a string, instance of %s, or a closure', - ValidationRule::class - ) - ); - } + $this->rules[] = $this->sanitizeRule($rule); } return $this; } /** - * Validates that a closure rule has the proper parameters to be used as a validation rule. + * Prepends a given rule to the start of the rules array. * - * @since 1.0.0 + * @unreleased * - * @return void + * @param string|ValidationRule|Closure $rule */ - private function validateClosureRule(Closure $closure) + public function prependRule($rule): self { - try { - $reflection = new ReflectionFunction($closure); - } catch (ReflectionException $e) { - Config::throwInvalidArgumentException( - 'Unable to validate closure parameters. Please ensure that the closure is valid.' - ); - } + array_unshift($this->rules, $this->sanitizeRule($rule)); - $parameters = $reflection->getParameters(); - $parameterCount = count($parameters); + return $this; + } - if ($parameterCount < 2 || $parameterCount > 4) { - Config::throwInvalidArgumentException( - "Validation rule closure must accept between 2 and 4 parameters, $parameterCount given." - ); - } + /** + * Replaces the given rule at the same index position or appends it if it doesn't exist. + * + * @unreleased + * + * @param string|ValidationRule|Closure $rule + * + * @return bool True if the rule was replaced, false if it was appended. + */ + public function replaceOrAppendRule(string $ruleId, $rule): bool + { + $replaced = $this->replaceRule($ruleId, $rule); - $parameterType = $this->getParameterTypeName($parameters[1]); - if ($parameterType !== null && $parameterType !== 'Closure') { - Config::throwInvalidArgumentException( - "Validation rule closure must accept a Closure as the second parameter, {$parameterType} given." - ); - } + if (!$replaced) { + $this->rules($rule); - $parameterType = $parameterCount > 2 ? $this->getParameterTypeName($parameters[2]) : null; - if ($parameterType !== null && $parameterType !== 'string') { - Config::throwInvalidArgumentException( - "Validation rule closure must accept a string as the third parameter, {$parameterType} given." - ); + return false; } - $parameterType = $parameterCount > 3 ? $this->getParameterTypeName($parameters[3]) : null; - if ($parameterType !== null && $parameterType !== 'array') { - Config::throwInvalidArgumentException( - "Validation rule closure must accept a array as the fourth parameter, {$parameterType} given." - ); - } + return true; } /** - * Retrieves the parameter type with PHP 7.0 compatibility. + * Replaces the given rule at the same index position or prepends it if it doesn't exist. * - * @since 1.0.0 + * @unreleased * - * @return string|null + * @param string|ValidationRule|Closure $rule + * + * @return bool True if the rule was replaced, false if it was prepended. */ - private function getParameterTypeName(ReflectionParameter $parameter) + public function replaceOrPrependRule(string $ruleId, $rule): bool { - $type = $parameter->getType(); + $replaced = $this->replaceRule($ruleId, $rule); - if ($type === null) { - return null; - } + if (!$replaced) { + $this->prependRule($rule); - // Check if the method exists for PHP 7.0 compatibility (it exits as of PHP 7.1) - if (method_exists($type, 'getName')) { - return $type->getName(); + return false; } - return (string)$type; + return true; } /** - * Takes a validation rule string and returns the corresponding rule instance. + * Replace a rule with the given id with the given rule at the same index position. Returns true if the rule was + * replaced, false otherwise. * - * @since 1.0.0 + * @unreleased + * + * @param string|ValidationRule|Closure $rule */ - private function getRuleFromString(string $rule): ValidationRule + public function replaceRule(string $ruleId, $rule): bool { - list($ruleId, $ruleOptions) = array_pad(explode(':', $rule, 2), 2, null); - - /** - * @var ValidationRule $ruleClass - */ - $ruleClass = $this->register->getRule($ruleId); + foreach ($this->rules as $index => $validationRule) { + if ($validationRule instanceof ValidationRule && $validationRule::id() === $ruleId) { + $this->rules[$index] = $this->sanitizeRule($rule); - if (!$ruleClass) { - Config::throwInvalidArgumentException( - sprintf( - 'Validation rule with id %s has not been registered.', - $ruleId - ) - ); + return true; + } } - return $ruleClass::fromString($ruleOptions); + return false; } /** * Finds and returns the validation rule by id. Does not work for Closure rules. * + * @since 1.0.0 + * * @return ValidationRule|null */ public function getRule(string $rule) @@ -268,4 +238,130 @@ public function jsonSerialize() return $rules; } + + /** + * Sanitizes a given rule by validating the rule and making sure it's safe to use. + * + * @unreleased + * + * @param mixed $rule + * + * @return Closure|ValidationRule + */ + private function sanitizeRule($rule) + { + if ($rule instanceof Closure) { + $this->validateClosureRule($rule); + + return $rule; + } elseif ($rule instanceof ValidationRule) { + return $rule; + } elseif (is_string($rule)) { + return $this->getRuleFromString($rule); + } else { + Config::throwInvalidArgumentException( + sprintf( + 'Validation rule must be a string, instance of %s, or a closure', + ValidationRule::class + ) + ); + } + } + + /** + * Validates that a closure rule has the proper parameters to be used as a validation rule. + * + * @since 1.0.0 + * + * @return void + */ + private function validateClosureRule(Closure $closure) + { + try { + $reflection = new ReflectionFunction($closure); + } catch (ReflectionException $e) { + Config::throwInvalidArgumentException( + 'Unable to validate closure parameters. Please ensure that the closure is valid.' + ); + } + + $parameters = $reflection->getParameters(); + $parameterCount = count($parameters); + + if ($parameterCount < 2 || $parameterCount > 4) { + Config::throwInvalidArgumentException( + "Validation rule closure must accept between 2 and 4 parameters, $parameterCount given." + ); + } + + $parameterType = $this->getParameterTypeName($parameters[1]); + if ($parameterType !== null && $parameterType !== 'Closure') { + Config::throwInvalidArgumentException( + "Validation rule closure must accept a Closure as the second parameter, {$parameterType} given." + ); + } + + $parameterType = $parameterCount > 2 ? $this->getParameterTypeName($parameters[2]) : null; + if ($parameterType !== null && $parameterType !== 'string') { + Config::throwInvalidArgumentException( + "Validation rule closure must accept a string as the third parameter, {$parameterType} given." + ); + } + + $parameterType = $parameterCount > 3 ? $this->getParameterTypeName($parameters[3]) : null; + if ($parameterType !== null && $parameterType !== 'array') { + Config::throwInvalidArgumentException( + "Validation rule closure must accept a array as the fourth parameter, {$parameterType} given." + ); + } + } + + /** + * Retrieves the parameter type with PHP 7.0 compatibility. + * + * @since 1.0.0 + * + * @return string|null + */ + private function getParameterTypeName(ReflectionParameter $parameter) + { + $type = $parameter->getType(); + + if ($type === null) { + return null; + } + + // Check if the method exists for PHP 7.0 compatibility (it exits as of PHP 7.1) + if (method_exists($type, 'getName')) { + return $type->getName(); + } + + return (string)$type; + } + + /** + * Takes a validation rule string and returns the corresponding rule instance. + * + * @since 1.0.0 + */ + private function getRuleFromString(string $rule): ValidationRule + { + [$ruleId, $ruleOptions] = array_pad(explode(':', $rule, 2), 2, null); + + /** + * @var ValidationRule $ruleClass + */ + $ruleClass = $this->register->getRule($ruleId); + + if (!$ruleClass) { + Config::throwInvalidArgumentException( + sprintf( + 'Validation rule with id %s has not been registered.', + $ruleId + ) + ); + } + + return $ruleClass::fromString($ruleOptions); + } } diff --git a/tests/unit/ValidationRuleSetTest.php b/tests/unit/ValidationRuleSetTest.php index 5ef4a4f..b561fa2 100644 --- a/tests/unit/ValidationRuleSetTest.php +++ b/tests/unit/ValidationRuleSetTest.php @@ -52,6 +52,26 @@ public function testRulesCanBePassedAsClosures() self::assertCount(1, $rules); } + /** + * @unreleased + */ + public function testPrependingARule() + { + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('size:5'); + $rules->prependRule(new Required()); + + self::assertCount(2, $rules); + self::assertInstanceOf(Required::class, $rules->getRule('required')); + self::assertJsonStringEqualsJsonString( + json_encode([ + 'required' => true, + 'size' => 5, + ]), + json_encode($rules) + ); + } + /** * @since 1.1.0 */ @@ -65,6 +85,22 @@ public function testCheckingHasRule() self::assertFalse($rules->hasRule('email')); } + /** + * @unreleased + */ + public function testCheckingHasAnyRules() + { + // True if it has rules + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required', 'size:5'); + + self::assertTrue($rules->hasRules()); + + // False if it has no rules + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + self::assertFalse($rules->hasRules()); + } + /** * @since 1.1.0 */ @@ -102,6 +138,82 @@ public function testForgettingARule() self::assertFalse($rules->hasRule('required')); } + /** + * @unreleased + */ + public function testReplacingARule() + { + // Replace if rule exists + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required', 'size:5'); + + self::assertTrue($rules->replaceRule('size', new Size(10))); + self::assertCount(2, $rules); + self::assertTrue($rules->hasRule('required')); + self::assertTrue($rules->hasRule('size')); + self::assertEquals(10, $rules->getRule('size')->getSize()); + + // Do not replace if rule does not exist + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required'); + + self::assertFalse($rules->replaceRule('size', new Size(10))); + self::assertCount(1, $rules); + } + + /** + * @unreleased + */ + public function testConditionallyReplacingOrAppendingARule() + { + // Replace if rule exists + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required', 'size:5'); + + + self::assertTrue($rules->replaceOrAppendRule('size', new Size(10))); + self::assertCount(2, $rules); + self::assertTrue($rules->hasRule('required')); + self::assertTrue($rules->hasRule('size')); + self::assertEquals(10, $rules->getRule('size')->getSize()); + + // Append if rule does not exist + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required'); + + self::assertFalse($rules->replaceOrAppendRule('size', new Size(10))); + self::assertCount(2, $rules); + self::assertTrue($rules->hasRule('required')); + self::assertTrue($rules->hasRule('size')); + self::assertEquals(10, $rules->getRule('size')->getSize()); + } + + /** + * @unreleased + */ + public function testConditionallyReplacingOrPrependingRules() + { + // Replace if rule exists + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required', 'size:5'); + + self::AssertTrue($rules->replaceOrPrependRule('size', new Size(10))); + self::assertCount(2, $rules); + self::assertTrue($rules->hasRule('required')); + self::assertTrue($rules->hasRule('size')); + self::assertEquals(10, $rules->getRule('size')->getSize()); + + // Prepend if rule does not exist + $rules = new ValidationRuleSet($this->getMockRulesRegister()); + $rules->rules('required'); + + self::assertFalse($rules->replaceOrPrependRule('size', new Size(10))); + self::assertCount(2, $rules); + self::assertTrue($rules->hasRule('required')); + self::assertTrue($rules->hasRule('size')); + self::assertEquals(10, $rules->getRule('size')->getSize()); + } + /** * @since 1.1.0 */ From c5e7fccd63851a942de764adfd1e39c2039722ef Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 28 Mar 2023 12:08:38 -0700 Subject: [PATCH 2/2] chore: prepare for 1.3.0 release --- src/ValidationRuleSet.php | 10 +++++----- tests/unit/ValidationRuleSetTest.php | 10 +++++----- tests/unit/ValidatorTest.php | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ValidationRuleSet.php b/src/ValidationRuleSet.php index eeeadac..6af4359 100644 --- a/src/ValidationRuleSet.php +++ b/src/ValidationRuleSet.php @@ -54,7 +54,7 @@ public function rules(...$rules): self /** * Prepends a given rule to the start of the rules array. * - * @unreleased + * @since 1.3.0 * * @param string|ValidationRule|Closure $rule */ @@ -68,7 +68,7 @@ public function prependRule($rule): self /** * Replaces the given rule at the same index position or appends it if it doesn't exist. * - * @unreleased + * @since 1.3.0 * * @param string|ValidationRule|Closure $rule * @@ -90,7 +90,7 @@ public function replaceOrAppendRule(string $ruleId, $rule): bool /** * Replaces the given rule at the same index position or prepends it if it doesn't exist. * - * @unreleased + * @since 1.3.0 * * @param string|ValidationRule|Closure $rule * @@ -113,7 +113,7 @@ public function replaceOrPrependRule(string $ruleId, $rule): bool * Replace a rule with the given id with the given rule at the same index position. Returns true if the rule was * replaced, false otherwise. * - * @unreleased + * @since 1.3.0 * * @param string|ValidationRule|Closure $rule */ @@ -242,7 +242,7 @@ public function jsonSerialize() /** * Sanitizes a given rule by validating the rule and making sure it's safe to use. * - * @unreleased + * @since 1.3.0 * * @param mixed $rule * diff --git a/tests/unit/ValidationRuleSetTest.php b/tests/unit/ValidationRuleSetTest.php index b561fa2..f8ca7f7 100644 --- a/tests/unit/ValidationRuleSetTest.php +++ b/tests/unit/ValidationRuleSetTest.php @@ -53,7 +53,7 @@ public function testRulesCanBePassedAsClosures() } /** - * @unreleased + * @since 1.3.0 */ public function testPrependingARule() { @@ -86,7 +86,7 @@ public function testCheckingHasRule() } /** - * @unreleased + * @since 1.3.0 */ public function testCheckingHasAnyRules() { @@ -139,7 +139,7 @@ public function testForgettingARule() } /** - * @unreleased + * @since 1.3.0 */ public function testReplacingARule() { @@ -162,7 +162,7 @@ public function testReplacingARule() } /** - * @unreleased + * @since 1.3.0 */ public function testConditionallyReplacingOrAppendingARule() { @@ -189,7 +189,7 @@ public function testConditionallyReplacingOrAppendingARule() } /** - * @unreleased + * @since 1.3.0 */ public function testConditionallyReplacingOrPrependingRules() { diff --git a/tests/unit/ValidatorTest.php b/tests/unit/ValidatorTest.php index 9830ac3..5b914f1 100644 --- a/tests/unit/ValidatorTest.php +++ b/tests/unit/ValidatorTest.php @@ -178,7 +178,7 @@ public function testRuleArraysWithoutRulesAreConsideredOptional() } /** - * @unreleased + * @since 1.3.0 */ public function testRulesThatReturnExcludeValuePreventValidation() {