From be336de2cb89f5df6aae737e506002ec9f5140ab Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 15 Jul 2017 08:44:31 -0500 Subject: [PATCH 01/16] Modifies ValidatorInterface to return ValidatorResults Instead of `isValid() : bool`, `isValid()` now returns a `ValidatorResult`, which will contain the value validated, results of validation, and, in the case of an invalid result, validation error messages. Additionally, `isValid()` now accepts an additional, optional array argument, `$context`; this allows validators to check against other values in a validation set in order to do their work. --- src/ValidatorInterface.php | 24 +++--------- src/ValidatorResult.php | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 src/ValidatorResult.php diff --git a/src/ValidatorInterface.php b/src/ValidatorInterface.php index 6424ef7a6..e08a3bf4f 100644 --- a/src/ValidatorInterface.php +++ b/src/ValidatorInterface.php @@ -12,27 +12,15 @@ interface ValidatorInterface { /** - * Returns true if and only if $value meets the validation requirements + * Validate a value. * - * If $value fails validation, then this method returns false, and - * getMessages() will return an array of messages that explain why the - * validation failed. + * Returns a ValidatorResult, containing the results of validation. * * @param mixed $value - * @return bool + * @param array $context Optional; additional context for validation, such + * as other form values. + * @return ValidatorResult * @throws Exception\RuntimeException If validation of $value is impossible */ - public function isValid($value); - - /** - * Returns an array of messages that explain why the most recent isValid() - * call returned false. The array keys are validation failure message identifiers, - * and the array values are the corresponding human-readable message strings. - * - * If isValid() was never called or if the most recent isValid() call - * returned true, then this method returns an empty array. - * - * @return array - */ - public function getMessages(); + public function isValid($value, array $context = []) : ValidatorResult; } diff --git a/src/ValidatorResult.php b/src/ValidatorResult.php new file mode 100644 index 000000000..7f3199060 --- /dev/null +++ b/src/ValidatorResult.php @@ -0,0 +1,76 @@ +value = $value; + $this->isValid = $isValid; + $this->messages = $messages; + } + + /** + * @param mixed $value + */ + public static function createValidResult($value) : self + { + return new self($value, true); + } + + /** + * @param mixed $value + * @param string[] $messages + */ + public static function createInvalidResult($value, array $messages) : self + { + return new self($value, false, $messages); + } + + public function isValid() : bool + { + return $this->isValid; + } + + public function getMessages() : array + { + return $this->messages; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } +} From aa288a7eabeeae26d0341bfdd916f48ff7842abf Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 15 Jul 2017 09:25:27 -0500 Subject: [PATCH 02/16] Implements i18n features for ValidatorResult `ValidatorResult` now composes message templates and message variables instead of messages; this will allow the various validators to omit translation features. A new class, `ValidatorResultTranslator`, composes a translator and text domain, and accepts a `ValidatorResult` to a method `translateMessages()`; it will then return an array of translated message strings. `ValidatorResult::getMessages()` will return the verbatim message templates with variable interpolations, with no translation. --- src/ValidatorResult.php | 67 +++++++++++++++++++--- src/ValidatorResultMessageInterpolator.php | 38 ++++++++++++ src/ValidatorResultTranslator.php | 50 ++++++++++++++++ 3 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 src/ValidatorResultMessageInterpolator.php create mode 100644 src/ValidatorResultTranslator.php diff --git a/src/ValidatorResult.php b/src/ValidatorResult.php index 7f3199060..315879fd4 100644 --- a/src/ValidatorResult.php +++ b/src/ValidatorResult.php @@ -12,15 +12,28 @@ */ class ValidatorResult { + use ValidatorResultMessageInterpolator; + /** * @var bool */ private $isValid; /** + * Message templates. + * + * Each may be a self-contained message, or contain placeholders of the form + * "%name%" for variables to interpolate into the string. + * + * @var string[] + */ + private $messageTemplates = []; + + /** + * Map of message variable names to the values to interpolate. * @var string[] */ - private $messages = []; + private $messageVariables = []; /** * @var mixed @@ -30,13 +43,19 @@ class ValidatorResult /** * @param mixed $value * @param bool $isValid - * @param string[] $messages + * @param string[] $messageTemplates + * @param string[] $messageVariables */ - public function __construct($value, bool $isValid, array $messages = []) - { + public function __construct( + $value, + bool $isValid, + array $messageTemplates = [], + array $messageVariables = [] + ) { $this->value = $value; $this->isValid = $isValid; - $this->messages = $messages; + $this->messageTemplates = $messageTemplates; + $this->messageVariables = $messageVariables; } /** @@ -49,11 +68,15 @@ public static function createValidResult($value) : self /** * @param mixed $value - * @param string[] $messages + * @param string[] $messageTemplates + * @param string[] $messageVariables */ - public static function createInvalidResult($value, array $messages) : self - { - return new self($value, false, $messages); + public static function createInvalidResult( + $value, + array $messageTemplates, + array $messageVariables = [] + ) : self { + return new self($value, false, $messageTemplates, $messageVariables); } public function isValid() : bool @@ -61,11 +84,37 @@ public function isValid() : bool return $this->isValid; } + /** + * Retrieve validation error messages. + * + * If you are not using i18n features, you may use this method to get an + * array of validation error messages. The method loops through each + * message template and interpolates any message variables discovered in + * the string. + * + * If you are using i18n features, you should create a ValidatorResultTranslator + * instance, and pass this instance to its `translateMessages()` method in + * order to get localized messages. + */ public function getMessages() : array + { + $messages = []; + foreach ($this->getMessageTemplates() as $template) { + $messages[] = $this->interpolateMessageVariables($template, $this); + } + return $messages; + } + + public function getMessageTemplates() : array { return $this->messages; } + public function getMessageVariables() : array + { + return $this->messageVariables; + } + /** * @return mixed */ diff --git a/src/ValidatorResultMessageInterpolator.php b/src/ValidatorResultMessageInterpolator.php new file mode 100644 index 000000000..ca96129a2 --- /dev/null +++ b/src/ValidatorResultMessageInterpolator.php @@ -0,0 +1,38 @@ +getMessageVariables(), ['value' => $result->getValue()]); + foreach ($messageVariables as $variable => $substitution) { + $message = $this->interpolateMessageVariable($message, $variable, $substitution); + } + return $message; + } + + /** + * @param mixed $substitution + */ + private function interpolateMessageVariable(string $message, string $variable, $substitution) : string + { + if (is_object($substitution)) { + $substitution = method_exists($substitution, '__toString') + ? (string) $substitution + : sprintf('%s object', get_class($substitution)); + } + + $substitution = is_array($substitution) + ? sprintf('[%s]', implode(', ', $substitution)) + : $substitution; + + return str_replace("%$variable%", (string) $substitution, $message); + } +} diff --git a/src/ValidatorResultTranslator.php b/src/ValidatorResultTranslator.php new file mode 100644 index 000000000..9a7a6b9b5 --- /dev/null +++ b/src/ValidatorResultTranslator.php @@ -0,0 +1,50 @@ +translator = $translator; + $this->textDomain = $textDomain; + } + + /** + * Create translated messages from a ValidatorResult + * + * Loops through each message template from the ValidatorResult and returns + * translated messages. Each message will have interpolated the composed + * message variables from the result. + * + * Additionally, if a `%value%` placeholder is found, the ValidatorResult + * value will be interpolated. + */ + public function translateMessages(ValidatorResult $result) : array + { + $value = $result->getValue(); + $messages = []; + + foreach ($result->getMessageTemplates() as $template) { + $messages[] = $this->interpolateMessageVariables( + $this->translator->translate($template, $this->textDomain), + $result + ); + } + + return $messages; + } +} From a99bde39d175c63b5e7049b38227a3e05fd94a66 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sat, 15 Jul 2017 09:42:05 -0500 Subject: [PATCH 03/16] Updates AbstractValidtor to remove message, i18n awareness Removes all translator awareness from the `AbstractValidator`. Consolidates `error` and `createMessage` to only do a lookup for the given message key; `error` is removed. Removes `__get()` and `__invoke()` as being superfluous. Removes memoization of error messages and the validated value. --- src/AbstractValidator.php | 353 +------------------------------------- 1 file changed, 4 insertions(+), 349 deletions(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 28496d64f..fb13da26f 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -12,29 +12,8 @@ use Traversable; use Zend\Stdlib\ArrayUtils; -abstract class AbstractValidator implements - Translator\TranslatorAwareInterface, - ValidatorInterface +abstract class AbstractValidator implements ValidatorInterface { - /** - * The value to be validated - * - * @var mixed - */ - protected $value; - - /** - * Default translation object for all validate objects - * @var Translator\TranslatorInterface - */ - protected static $defaultTranslator; - - /** - * Default text domain to be used with translator - * @var string - */ - protected static $defaultTranslatorTextDomain = 'default'; - /** * Limits the maximum returned length of an error message * @@ -46,9 +25,6 @@ abstract class AbstractValidator implements 'messages' => [], // Array of validation failure messages 'messageTemplates' => [], // Array of validation failure message templates 'messageVariables' => [], // Array of additional variables available for validation failure messages - 'translator' => null, // Translation object to used -> Translator\TranslatorInterface - 'translatorTextDomain' => null, // Translation text domain - 'translatorEnabled' => true, // Is translation enabled? 'valueObscured' => false, // Flag indicating whether or not value should be obfuscated // in error messages ]; @@ -149,27 +125,6 @@ public function setOptions($options = []) return $this; } - /** - * Returns array of validation failure messages - * - * @return array - */ - public function getMessages() - { - return array_unique($this->abstractOptions['messages'], SORT_REGULAR); - } - - /** - * Invoke as command - * - * @param mixed $value - * @return bool - */ - public function __invoke($value) - { - return $this->isValid($value); - } - /** * Returns an array of the names of variables that are used in constructing validation failure messages * @@ -217,149 +172,16 @@ public function setMessage($messageString, $messageKey = null) } /** - * Sets validation failure message templates given as an array, where the array keys are the message keys, - * and the array values are the message template strings. - * - * @param array $messages - * @return AbstractValidator - */ - public function setMessages(array $messages) - { - foreach ($messages as $key => $message) { - $this->setMessage($message, $key); - } - return $this; - } - - /** - * Magic function returns the value of the requested property, if and only if it is the value or a - * message variable. - * - * @param string $property - * @return mixed - * @throws Exception\InvalidArgumentException - */ - public function __get($property) - { - if ($property == 'value') { - return $this->value; - } - - if (array_key_exists($property, $this->abstractOptions['messageVariables'])) { - $result = $this->abstractOptions['messageVariables'][$property]; - if (is_array($result)) { - return $this->{key($result)}[current($result)]; - } - return $this->{$result}; - } - - if (isset($this->messageVariables) && array_key_exists($property, $this->messageVariables)) { - $result = $this->{$this->messageVariables[$property]}; - if (is_array($result)) { - return $this->{key($result)}[current($result)]; - } - return $this->{$result}; - } - - throw new Exception\InvalidArgumentException("No property exists by the name '$property'"); - } - - /** - * Constructs and returns a validation failure message with the given message key and value. - * - * Returns null if and only if $messageKey does not correspond to an existing template. - * - * If a translator is available and a translation exists for $messageKey, - * the translation will be used. - * - * @param string $messageKey - * @param string|array|object $value - * @return string - */ - protected function createMessage($messageKey, $value) - { - if (! isset($this->abstractOptions['messageTemplates'][$messageKey])) { - return; - } - - $message = $this->abstractOptions['messageTemplates'][$messageKey]; - - $message = $this->translateMessage($messageKey, $message); - - if (is_object($value) && - ! in_array('__toString', get_class_methods($value)) - ) { - $value = get_class($value) . ' object'; - } elseif (is_array($value)) { - $value = var_export($value, 1); - } else { - $value = (string) $value; - } - - if ($this->isValueObscured()) { - $value = str_repeat('*', strlen($value)); - } - - $message = str_replace('%value%', (string) $value, $message); - foreach ($this->abstractOptions['messageVariables'] as $ident => $property) { - if (is_array($property)) { - $value = $this->{key($property)}[current($property)]; - if (is_array($value)) { - $value = '[' . implode(', ', $value) . ']'; - } - } else { - $value = $this->$property; - } - $message = str_replace("%$ident%", (string) $value, $message); - } - - $length = self::getMessageLength(); - if (($length > -1) && (strlen($message) > $length)) { - $message = substr($message, 0, ($length - 3)) . '...'; - } - - return $message; - } - - /** - * @param string $messageKey - * @param string $value OPTIONAL - * @return void + * Constructs and returns a validation failure message template associated with the given message key. */ - protected function error($messageKey, $value = null) + protected function createMessage(string $messageKey) : string { if ($messageKey === null) { $keys = array_keys($this->abstractOptions['messageTemplates']); $messageKey = current($keys); } - if ($value === null) { - $value = $this->value; - } - - $this->abstractOptions['messages'][$messageKey] = $this->createMessage($messageKey, $value); - } - - /** - * Returns the validation value - * - * @return mixed Value to be validated - */ - protected function getValue() - { - return $this->value; - } - - /** - * Sets the value to be validated and clears the messages and errors arrays - * - * @param mixed $value - * @return void - */ - protected function setValue($value) - { - $this->value = $value; - $this->abstractOptions['messages'] = []; + return $this->abstractOptions['messageTemplates'][$messageKey] ?? ''; } /** @@ -385,156 +207,6 @@ public function isValueObscured() return $this->abstractOptions['valueObscured']; } - /** - * Set translation object - * - * @param Translator\TranslatorInterface|null $translator - * @param string $textDomain (optional) - * @return AbstractValidator - * @throws Exception\InvalidArgumentException - */ - public function setTranslator(Translator\TranslatorInterface $translator = null, $textDomain = null) - { - $this->abstractOptions['translator'] = $translator; - if (null !== $textDomain) { - $this->setTranslatorTextDomain($textDomain); - } - return $this; - } - - /** - * Return translation object - * - * @return Translator\TranslatorInterface|null - */ - public function getTranslator() - { - if (! $this->isTranslatorEnabled()) { - return; - } - - if (null === $this->abstractOptions['translator']) { - $this->abstractOptions['translator'] = self::getDefaultTranslator(); - } - - return $this->abstractOptions['translator']; - } - - /** - * Does this validator have its own specific translator? - * - * @return bool - */ - public function hasTranslator() - { - return (bool) $this->abstractOptions['translator']; - } - - /** - * Set translation text domain - * - * @param string $textDomain - * @return AbstractValidator - */ - public function setTranslatorTextDomain($textDomain = 'default') - { - $this->abstractOptions['translatorTextDomain'] = $textDomain; - return $this; - } - - /** - * Return the translation text domain - * - * @return string - */ - public function getTranslatorTextDomain() - { - if (null === $this->abstractOptions['translatorTextDomain']) { - $this->abstractOptions['translatorTextDomain'] = - self::getDefaultTranslatorTextDomain(); - } - return $this->abstractOptions['translatorTextDomain']; - } - - /** - * Set default translation object for all validate objects - * - * @param Translator\TranslatorInterface|null $translator - * @param string $textDomain (optional) - * @return void - * @throws Exception\InvalidArgumentException - */ - public static function setDefaultTranslator(Translator\TranslatorInterface $translator = null, $textDomain = null) - { - static::$defaultTranslator = $translator; - if (null !== $textDomain) { - self::setDefaultTranslatorTextDomain($textDomain); - } - } - - /** - * Get default translation object for all validate objects - * - * @return Translator\TranslatorInterface|null - */ - public static function getDefaultTranslator() - { - return static::$defaultTranslator; - } - - /** - * Is there a default translation object set? - * - * @return bool - */ - public static function hasDefaultTranslator() - { - return (bool) static::$defaultTranslator; - } - - /** - * Set default translation text domain for all validate objects - * - * @param string $textDomain - * @return void - */ - public static function setDefaultTranslatorTextDomain($textDomain = 'default') - { - static::$defaultTranslatorTextDomain = $textDomain; - } - - /** - * Get default translation text domain for all validate objects - * - * @return string - */ - public static function getDefaultTranslatorTextDomain() - { - return static::$defaultTranslatorTextDomain; - } - - /** - * Indicate whether or not translation should be enabled - * - * @param bool $flag - * @return AbstractValidator - */ - public function setTranslatorEnabled($flag = true) - { - $this->abstractOptions['translatorEnabled'] = (bool) $flag; - return $this; - } - - /** - * Is translation enabled? - * - * @return bool - */ - public function isTranslatorEnabled() - { - return $this->abstractOptions['translatorEnabled']; - } - /** * Returns the maximum allowed message length * @@ -554,21 +226,4 @@ public static function setMessageLength($length = -1) { static::$messageLength = $length; } - - /** - * Translate a validation message - * - * @param string $messageKey - * @param string $message - * @return string - */ - protected function translateMessage($messageKey, $message) - { - $translator = $this->getTranslator(); - if (! $translator) { - return $message; - } - - return $translator->translate($message, $this->getTranslatorTextDomain()); - } } From 9fc8eee9fa4049a393f8a2dfc80fe3175faf4482 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 16 Jul 2017 13:15:22 -0500 Subject: [PATCH 04/16] Refactors validator results to allow decorators Realized that decoration is likely the best path for altering the values returned via `getMessages()`. To enable that, added a new interface, `Result`, describing the capabilities of a validation result, and modified `ValidatorResult` to implement it. Renamed `ValidatorResultTranslator` to `TranslatableValidatorResult`; it now accepts a `Result` to its constructor and implements `Result`. For all methods other than `getMessages()`, it proxies to the underlying `Result` instance (these are implemented by a new trait, `ValidatorResultDecorator`). Also provides a new result type, `ObscuredValueValidatorResult`; it overrides the `getValue()` method to obscure the value, and the `getMessages()` method to ensure that its own `getValue()` is called when performing message template substitutions. A new method was extracted within `ValidatorResultMessageInterpolator`, `castValueToString()`; this was done to allow the `ObscuredValueValidatorResult` to work with the string value when performing obfuscation. --- src/ObscuredValueValidatorResult.php | 48 +++++++++++++++++++ src/Result.php | 24 ++++++++++ ...or.php => TranslatableValidatorResult.php} | 30 +++++++----- src/ValidatorResult.php | 16 +++---- src/ValidatorResultDecorator.php | 48 +++++++++++++++++++ src/ValidatorResultMessageInterpolator.php | 24 ++++++---- 6 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 src/ObscuredValueValidatorResult.php create mode 100644 src/Result.php rename src/{ValidatorResultTranslator.php => TranslatableValidatorResult.php} (56%) create mode 100644 src/ValidatorResultDecorator.php diff --git a/src/ObscuredValueValidatorResult.php b/src/ObscuredValueValidatorResult.php new file mode 100644 index 000000000..1f0b8d32f --- /dev/null +++ b/src/ObscuredValueValidatorResult.php @@ -0,0 +1,48 @@ +result = $result; + } + + /** + * Override `getMessages()` to ensure value is obscured. + * + * Recreates the logic of ValidatorResult::getMessages in order to ensure + * that the decorator's getValue() is called when substituting the value + * into message templates. + */ + public function getMessages() : array + { + $messages = []; + foreach ($this->result->getMessageTemplates() as $template) { + $messages[] = $this->interpolateMessageVariables($template, $this); + } + return $messages; + } + + /** + * Returns an obscured version of the value. + * + * Casts the value to a string, and then replaces all characters with '*'. + * + * @return string + */ + public function getValue() + { + $value = $this->castValueToString($this->result->getValue()); + return str_repeat('*', strlen($value)); + } +} diff --git a/src/Result.php b/src/Result.php new file mode 100644 index 000000000..f7b9c4ab1 --- /dev/null +++ b/src/Result.php @@ -0,0 +1,24 @@ +result = $result; $this->translator = $translator; $this->textDomain = $textDomain; } /** - * Create translated messages from a ValidatorResult + * Returns translated error message strings from the decorated result instance. * - * Loops through each message template from the ValidatorResult and returns + * Loops through each message template from the composed Result and returns * translated messages. Each message will have interpolated the composed * message variables from the result. * - * Additionally, if a `%value%` placeholder is found, the ValidatorResult - * value will be interpolated. + * Additionally, if a `%value%` placeholder is found, the Result value will + * be interpolated. */ - public function translateMessages(ValidatorResult $result) : array + public function getMessages() : array { - $value = $result->getValue(); + $value = $this->result->getValue(); $messages = []; - foreach ($result->getMessageTemplates() as $template) { + foreach ($this->result->getMessageTemplates() as $template) { $messages[] = $this->interpolateMessageVariables( $this->translator->translate($template, $this->textDomain), - $result + $this->result ); } diff --git a/src/ValidatorResult.php b/src/ValidatorResult.php index 315879fd4..89b7bb49f 100644 --- a/src/ValidatorResult.php +++ b/src/ValidatorResult.php @@ -10,7 +10,7 @@ /** * Value object representing results of validation. */ -class ValidatorResult +class ValidatorResult implements Result { use ValidatorResultMessageInterpolator; @@ -87,14 +87,14 @@ public function isValid() : bool /** * Retrieve validation error messages. * - * If you are not using i18n features, you may use this method to get an - * array of validation error messages. The method loops through each - * message template and interpolates any message variables discovered in - * the string. + * This method loops through each message template and interpolates any + * message variables discovered in the string. * - * If you are using i18n features, you should create a ValidatorResultTranslator - * instance, and pass this instance to its `translateMessages()` method in - * order to get localized messages. + * If you are using i18n features, decorate this instance with a + * TranslatableValidatorResult. + * + * If you wish to osbcure the value, decorate this instance with an + * ObscuredValueValidatorResult. */ public function getMessages() : array { diff --git a/src/ValidatorResultDecorator.php b/src/ValidatorResultDecorator.php new file mode 100644 index 000000000..13cf62f86 --- /dev/null +++ b/src/ValidatorResultDecorator.php @@ -0,0 +1,48 @@ +result->isValid(); + } + + /** + * Proxies to decorated Result instance. + */ + public function getMessageTemplates() : array + { + return $this->result->getMessageTemplates(); + } + + /** + * Proxies to decorated Result instance. + */ + public function getMessageVariables() : array + { + return $this->result->getMessageVariables(); + } + + /** + * Proxies to decorated Result instance. + * + * @return mixed + */ + public function getValue() + { + return $this->result->getValue(); + } +} diff --git a/src/ValidatorResultMessageInterpolator.php b/src/ValidatorResultMessageInterpolator.php index ca96129a2..5936f815c 100644 --- a/src/ValidatorResultMessageInterpolator.php +++ b/src/ValidatorResultMessageInterpolator.php @@ -23,16 +23,24 @@ private function interpolateMessageVariables(string $message, ValidatorResult $r */ private function interpolateMessageVariable(string $message, string $variable, $substitution) : string { - if (is_object($substitution)) { - $substitution = method_exists($substitution, '__toString') - ? (string) $substitution - : sprintf('%s object', get_class($substitution)); + return str_replace("%$variable%", $this->castValueToString($substitution), $message); + } + + /** + * @param mixed $value + */ + private function castValueToString($value) : string + { + if (is_object($value)) { + $value = method_exists($value, '__toString') + ? (string) $value + : sprintf('%s object', get_class($value)); } - $substitution = is_array($substitution) - ? sprintf('[%s]', implode(', ', $substitution)) - : $substitution; + $value = is_array($value) + ? sprintf('[%s]', implode(', ', $value)) + : $value; - return str_replace("%$variable%", (string) $substitution, $message); + return (string) $value; } } From 88d2be075cf35dd584e6df19886f13a2cd9d140d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 16 Jul 2017 13:24:23 -0500 Subject: [PATCH 05/16] Renames ValidatorInterface to Validator - Removing "Interface", "Trait" suffixes as part of PHP 7.1 initiative. --- src/AbstractValidator.php | 46 ++----------------- src/Barcode.php | 4 +- src/EmailAddress.php | 5 +- src/Explode.php | 12 ++--- src/File/MimeType.php | 8 ++-- src/{ValidatorInterface.php => Validator.php} | 8 ++-- src/ValidatorChain.php | 30 ++++++------ src/ValidatorPluginManager.php | 4 +- 8 files changed, 35 insertions(+), 82 deletions(-) rename src/{ValidatorInterface.php => Validator.php} (74%) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index fb13da26f..51f7a823a 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -12,7 +12,7 @@ use Traversable; use Zend\Stdlib\ArrayUtils; -abstract class AbstractValidator implements ValidatorInterface +abstract class AbstractValidator implements Validator { /** * Limits the maximum returned length of an error message @@ -147,34 +147,16 @@ public function getMessageTemplates() /** * Sets the validation failure message template for a particular key - * - * @param string $messageString - * @param string $messageKey OPTIONAL - * @return AbstractValidator Provides a fluent interface - * @throws Exception\InvalidArgumentException */ - public function setMessage($messageString, $messageKey = null) + public function setMessageTemplate(string $messageKey, string $messageString) : void { - if ($messageKey === null) { - $keys = array_keys($this->abstractOptions['messageTemplates']); - foreach ($keys as $key) { - $this->setMessage($messageString, $key); - } - return $this; - } - - if (! isset($this->abstractOptions['messageTemplates'][$messageKey])) { - throw new Exception\InvalidArgumentException("No message template exists for key '$messageKey'"); - } - $this->abstractOptions['messageTemplates'][$messageKey] = $messageString; - return $this; } /** - * Constructs and returns a validation failure message template associated with the given message key. + * Finds and returns the message template associated with the given message key. */ - protected function createMessage(string $messageKey) : string + protected function getMessageTemplate(string $messageKey) : string { if ($messageKey === null) { $keys = array_keys($this->abstractOptions['messageTemplates']); @@ -206,24 +188,4 @@ public function isValueObscured() { return $this->abstractOptions['valueObscured']; } - - /** - * Returns the maximum allowed message length - * - * @return int - */ - public static function getMessageLength() - { - return static::$messageLength; - } - - /** - * Sets the maximum allowed message length - * - * @param int $length - */ - public static function setMessageLength($length = -1) - { - static::$messageLength = $length; - } } diff --git a/src/Barcode.php b/src/Barcode.php index e2f0f9bf3..2428d6b8b 100644 --- a/src/Barcode.php +++ b/src/Barcode.php @@ -134,9 +134,7 @@ public function useChecksum($checksum = null) } /** - * Defined by Zend\Validator\ValidatorInterface - * - * Returns true if and only if $value contains a valid barcode + * Determine if the given $value contains a valid barcode * * @param string $value * @return bool diff --git a/src/EmailAddress.php b/src/EmailAddress.php index d089f84c0..76b89ad50 100644 --- a/src/EmailAddress.php +++ b/src/EmailAddress.php @@ -479,10 +479,7 @@ protected function splitEmailParts($value) } /** - * Defined by Zend\Validator\ValidatorInterface - * - * Returns true if and only if $value is a valid email address - * according to RFC2822 + * Determine if the given $value is a valid email address per RFC 2822. * * @link http://www.ietf.org/rfc/rfc2822.txt RFC2822 * @link http://www.columbia.edu/kermit/ascii.html US-ASCII characters diff --git a/src/Explode.php b/src/Explode.php index 6ff3b58ec..e0507745a 100644 --- a/src/Explode.php +++ b/src/Explode.php @@ -37,7 +37,7 @@ class Explode extends AbstractValidator implements ValidatorPluginManagerAwareIn protected $valueDelimiter = ','; /** - * @var ValidatorInterface + * @var Validator */ protected $validator; @@ -95,7 +95,7 @@ public function getValidatorPluginManager() /** * Sets the Validator for validating each value * - * @param ValidatorInterface|array $validator + * @param Validator|array $validator * @throws Exception\RuntimeException * @return Explode */ @@ -112,7 +112,7 @@ public function setValidator($validator) $validator = $this->getValidatorPluginManager()->get($name, $options); } - if (! $validator instanceof ValidatorInterface) { + if (! $validator instanceof Validator) { throw new Exception\RuntimeException( 'Invalid validator given' ); @@ -125,7 +125,7 @@ public function setValidator($validator) /** * Gets the Validator for validating each value * - * @return ValidatorInterface + * @return Validator */ public function getValidator() { @@ -155,9 +155,7 @@ public function isBreakOnFirstFailure() } /** - * Defined by Zend\Validator\ValidatorInterface - * - * Returns true if all values validate true + * Returns true if all values validate true. * * @param mixed $value * @param mixed $context Extra "context" to provide the composed validator diff --git a/src/File/MimeType.php b/src/File/MimeType.php index 68e7dd8e3..94ed616c4 100644 --- a/src/File/MimeType.php +++ b/src/File/MimeType.php @@ -329,11 +329,11 @@ public function addMimeType($mimetype) } /** - * Defined by Zend\Validator\ValidatorInterface + * Determine if the file matches the accepted mimetypes. * - * Returns true if the mimetype of the file matches the given ones. Also parts - * of mimetypes can be checked. If you give for example "image" all image - * mime types will be accepted like "image/gif", "image/jpeg" and so on. + * Also, parts of mimetypes can be checked. If you give for example "image" + * all image mime types will be accepted like "image/gif", "image/jpeg" and + * so on. * * @param string|array $value Real file to check for mimetype * @param array $file File data from \Zend\File\Transfer\Transfer (optional) diff --git a/src/ValidatorInterface.php b/src/Validator.php similarity index 74% rename from src/ValidatorInterface.php rename to src/Validator.php index e08a3bf4f..48302ba11 100644 --- a/src/ValidatorInterface.php +++ b/src/Validator.php @@ -9,18 +9,18 @@ namespace Zend\Validator; -interface ValidatorInterface +interface Validator { /** * Validate a value. * - * Returns a ValidatorResult, containing the results of validation. + * Returns a Result, containing the results of validation. * * @param mixed $value * @param array $context Optional; additional context for validation, such * as other form values. - * @return ValidatorResult + * @return Result * @throws Exception\RuntimeException If validation of $value is impossible */ - public function isValid($value, array $context = []) : ValidatorResult; + public function isValid($value, array $context = []) : Result; } diff --git a/src/ValidatorChain.php b/src/ValidatorChain.php index 3c72c7aca..5b3503c39 100644 --- a/src/ValidatorChain.php +++ b/src/ValidatorChain.php @@ -15,7 +15,7 @@ class ValidatorChain implements Countable, - ValidatorInterface + Validator { /** * Default priority at which validators are added @@ -89,7 +89,7 @@ public function setPluginManager(ValidatorPluginManager $plugins) * * @param string $name Name of validator to return * @param null|array $options Options to pass to validator constructor (if not already instantiated) - * @return ValidatorInterface + * @return Validator */ public function plugin($name, array $options = null) { @@ -103,17 +103,15 @@ public function plugin($name, array $options = null) * If $breakChainOnFailure is true, then if the validator fails, the next validator in the chain, * if one exists, will not be executed. * - * @param ValidatorInterface $validator - * @param bool $breakChainOnFailure - * @param int $priority Priority at which to enqueue validator; defaults to - * 1 (higher executes earlier) - * + * @param Validator $validator + * @param bool $breakChainOnFailure + * @param int $priority Priority at which to enqueue validator; defaults + * to 1 (higher executes earlier) * @throws Exception\InvalidArgumentException - * * @return self */ public function attach( - ValidatorInterface $validator, + Validator $validator, $breakChainOnFailure = false, $priority = self::DEFAULT_PRIORITY ) { @@ -132,13 +130,13 @@ public function attach( * Proxy to attach() to keep BC * * @deprecated Please use attach() - * @param ValidatorInterface $validator - * @param bool $breakChainOnFailure - * @param int $priority + * @param Validator $validator + * @param bool $breakChainOnFailure + * @param int $priority * @return ValidatorChain Provides a fluent interface */ public function addValidator( - ValidatorInterface $validator, + Validator $validator, $breakChainOnFailure = false, $priority = self::DEFAULT_PRIORITY ) { @@ -151,11 +149,11 @@ public function addValidator( * If $breakChainOnFailure is true, then if the validator fails, the next validator in the chain, * if one exists, will not be executed. * - * @param ValidatorInterface $validator - * @param bool $breakChainOnFailure + * @param Validator $validator + * @param bool $breakChainOnFailure * @return ValidatorChain Provides a fluent interface */ - public function prependValidator(ValidatorInterface $validator, $breakChainOnFailure = false) + public function prependValidator(Validator $validator, $breakChainOnFailure = false) { $priority = self::DEFAULT_PRIORITY; diff --git a/src/ValidatorPluginManager.php b/src/ValidatorPluginManager.php index ef0d2fa0e..6e0a2196d 100644 --- a/src/ValidatorPluginManager.php +++ b/src/ValidatorPluginManager.php @@ -367,7 +367,7 @@ class ValidatorPluginManager extends AbstractPluginManager * * @var string */ - protected $instanceOf = ValidatorInterface::class; + protected $instanceOf = Validator::class; /** * Constructor @@ -418,7 +418,7 @@ public function validatePlugin($plugin) throw new Exception\RuntimeException(sprintf( 'Plugin of type %s is invalid; must implement %s', (is_object($plugin) ? get_class($plugin) : gettype($plugin)), - ValidatorInterface::class + Validator::class ), $e->getCode(), $e); } } From 35baa012362642ae53988b6389e2dad21de7135c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 16 Jul 2017 13:40:20 -0500 Subject: [PATCH 06/16] Begins refactoring validators, starting with Between Adds `AbstractValidator::createInvalidResult()`, for automating creation of the `Result` instance returned by validators; that method retrieves method templates based on the message keys passed to it, and pulls the message variables from the instance, passing them along with the value to the `ValidatorResult::createInvalidResult()` constructor. If the "value obscured" flag is enabled, decorates the result with `ObscuredValueValidatorResult` before returning it. The `Between` validator was refactored to use this approach. --- src/AbstractValidator.php | 30 +++++++++++++++++++++++++++++- src/Between.php | 33 +++++++++++++++++---------------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 51f7a823a..38632358f 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -59,6 +59,35 @@ public function __construct($options = null) } } + /** + * Create and return a result indicating validation failure. + * + * Use this within validators to create the validation result when a failure + * condition occurs. Pass it the value, and an array of message keys. + * + * If the "value obscured" flag is set, this method will decorate the result + * using ObscuredValueValidatorResult before returning it. + */ + protected function createInvalidResult($value, array $messageKeys) : Result + { + $messageTemplates = array_map(function ($key) { + return $this->getMessageTemplate($key); + }, $messageKeys); + + $result = ValidatorResult::createInvalidResult( + $value, + $messageTemplates, + $this->getMessageVariables() + ); + + if ($this->isValueObscured()) { + $result = new ObscuredValueValidatorResult($result); + } + + return $result; + } + + /** * Returns an option * @@ -165,7 +194,6 @@ protected function getMessageTemplate(string $messageKey) : string return $this->abstractOptions['messageTemplates'][$messageKey] ?? ''; } - /** * Set flag indicating whether or not value should be obfuscated in messages * diff --git a/src/Between.php b/src/Between.php index 386393dbd..fa7915004 100644 --- a/src/Between.php +++ b/src/Between.php @@ -156,26 +156,27 @@ public function setInclusive($inclusive) /** * Returns true if and only if $value is between min and max options, inclusively * if inclusive option is true. - * - * @param mixed $value - * @return bool */ - public function isValid($value) + public function isValid($value, array $context = []) : Result { - $this->setValue($value); + return $this->getInclusive() + ? $this->validateInclusive($value, $context) + : $this->validate($value, $context); + } - if ($this->getInclusive()) { - if ($this->getMin() > $value || $value > $this->getMax()) { - $this->error(self::NOT_BETWEEN); - return false; - } - } else { - if ($this->getMin() >= $value || $value >= $this->getMax()) { - $this->error(self::NOT_BETWEEN_STRICT); - return false; - } + private function validateInclusive($value, array $context) : Result + { + if ($this->getMin() > $value || $value > $this->getMax()) { + return $this->createInvalidResult($value, [self::NOT_BETWEEN]); } + return ValidatorResult::createValidResult($value); + } - return true; + private function validate($value, array $context) : Result + { + if ($this->getMin() >= $value || $value >= $this->getMax()) { + return $this->createInvalidResult($value, [self::NOT_BETWEEN_STRICT]); + } + return ValidatorResult::createValidResult($value); } } From 6678759f5510c26b2d951f6f4f0ea62a0edb6889 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 16 Jul 2017 13:51:26 -0500 Subject: [PATCH 07/16] Refactors Bitwise validator to return ValidatorResult instances. --- src/Bitwise.php | 72 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/Bitwise.php b/src/Bitwise.php index 273fd6657..b8244ecfd 100644 --- a/src/Bitwise.php +++ b/src/Bitwise.php @@ -120,35 +120,27 @@ public function getStrict() } /** - * Returns true if and only if $value is between min and max options, inclusively - * if inclusive option is true. + * Validates successfully if and only if $value is between min and max + * options, inclusively if inclusive option is true. * - * @param mixed $value - * @return bool + * @throws Exception\RuntimeException for unrecognized operators. */ - public function isValid($value) + public function isValid($value, array $context = []) : Result { - $this->setValue($value); - - if (self::OP_AND === $this->operator) { - if ($this->strict) { - // All the bits set in value must be set in control - $this->error(self::NOT_AND_STRICT); - - return (bool) (($this->control & $value) == $value); - } else { - // At least one of the bits must be common between value and control - $this->error(self::NOT_AND); - - return (bool) ($this->control & $value); - } - } elseif (self::OP_XOR === $this->operator) { - $this->error(self::NOT_XOR); - - return (bool) (($this->control ^ $value) === ($this->control | $value)); + switch ($this->operator) { + case (self::OP_AND): + return $this->validateAndOperation($value); + case (self::OP_OR): + return $this->validateOrOperation($value); + default: + throw Exception\RuntimeException(sprintf( + '%s instance has unrecognized operator "%s"; must be one of "%s" or "%s"', + get_class($this), + var_export($this->operator, true), + self::OP_AND, + self::OP_OR + )); } - - return false; } /** @@ -189,4 +181,34 @@ public function setStrict($strict) return $this; } + + /** + * @param mixed $value + */ + private function validateAndOperation($value) : Result + { + if ($this->strict) { + // All the bits set in value must be set in control + $this->error(self::NOT_AND_STRICT); + + return ($this->control & $value) == $value + ? ValidatorResult::createValidResult($value) + : $this->createInvalidResult($value, [self::NOT_AND_STRICT]); + } + + // At least one of the bits must be common between value and control + return (bool) ($this->control & $value) + ? ValidatorResult::createValidResult($value) + : $this->createInvalidResult($value, [self::NOT_AND]); + } + + /** + * @param mixed $value + */ + private function validateOrOperation($value) : Result + { + return ($this->control ^ $value) === ($this->control | $value) + ? ValidatorResult::createValidResult($value) + : $this->createInvalidResult($value, [self::NOT_XOR]); + } } From 74ad3649975e9f149e411392a6e1c17b7dee7275 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 16 Jul 2017 13:58:32 -0500 Subject: [PATCH 08/16] Refactors Callback validator to return Result --- src/Callback.php | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Callback.php b/src/Callback.php index fb84b97fb..626ca528b 100644 --- a/src/Callback.php +++ b/src/Callback.php @@ -9,6 +9,8 @@ namespace Zend\Validator; +use Throwable; + class Callback extends AbstractValidator { /** @@ -108,20 +110,19 @@ public function setCallbackOptions($options) * Returns true if and only if the set callback returns * for the provided $value * - * @param mixed $value - * @param mixed $context Additional context to provide to the callback - * @return bool - * @throws Exception\InvalidArgumentException + * @throws Exception\InvalidArgumentException if no callback present + * @throws Exception\InvalidArgumentException if callback is not callable */ - public function isValid($value, $context = null) + public function isValid($value, $context = null) : Result { - $this->setValue($value); - $options = $this->getCallbackOptions(); $callback = $this->getCallback(); if (empty($callback)) { throw new Exception\InvalidArgumentException('No callback given'); } + if (! is_callable($callback)) { + throw new Exception\InvalidArgumentException('Invalid callback given; not callable'); + } $args = [$value]; if (empty($options) && ! empty($context)) { @@ -136,15 +137,11 @@ public function isValid($value, $context = null) } try { - if (! call_user_func_array($callback, $args)) { - $this->error(self::INVALID_VALUE); - return false; - } - } catch (\Exception $e) { - $this->error(self::INVALID_CALLBACK); - return false; + return (bool) $callback(...$args) + ? ValidatorResult::createValidResult($value) + : $this->createInvalidResult($value, [self::INVALID_VALUE]); + } catch (Throwable $e) { + return $this->createInvalidResult($value, [self::INVALID_CALLBACK]); } - - return true; } } From e9556bee1628e9c7f1c347c5dcf6a3903e28c321 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 16 Jul 2017 14:08:06 -0500 Subject: [PATCH 09/16] Performs initial test refactor, for Between validator Updates the `Between` validator tests to reflect the refactor to return Result instances. Discovers a bug in the ValidatorResult::getMessageTemplates method, and corrects it. --- src/ValidatorResult.php | 2 +- test/BetweenTest.php | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/ValidatorResult.php b/src/ValidatorResult.php index 89b7bb49f..a5e0aecc3 100644 --- a/src/ValidatorResult.php +++ b/src/ValidatorResult.php @@ -107,7 +107,7 @@ public function getMessages() : array public function getMessageTemplates() : array { - return $this->messages; + return $this->messageTemplates; } public function getMessageVariables() : array diff --git a/test/BetweenTest.php b/test/BetweenTest.php index bf86e4e5f..806417957 100644 --- a/test/BetweenTest.php +++ b/test/BetweenTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase; use Zend\Validator\Between; use Zend\Validator\Exception\InvalidArgumentException; +use Zend\Validator\Result; /** * @group Zend_Validator @@ -43,26 +44,17 @@ public function testBasic() foreach ($valuesExpected as $element) { $validator = new Between(['min' => $element[0], 'max' => $element[1], 'inclusive' => $element[2]]); foreach ($element[4] as $input) { - $this->assertEquals( + $result = $validator->isValid($input); + $this->assertInstanceOf(Result::class, $result); + $this->assertSame( $element[3], - $validator->isValid($input), - 'Failed values: ' . $input . ":" . implode("\n", $validator->getMessages()) + $result->isValid(), + 'Failed values: ' . $input . ":" . implode("\n", $result->getMessages()) ); } } } - /** - * Ensures that getMessages() returns expected default value - * - * @return void - */ - public function testGetMessages() - { - $validator = new Between(['min' => 1, 'max' => 10]); - $this->assertEquals([], $validator->getMessages()); - } - /** * Ensures that getMin() returns expected value * @@ -140,12 +132,17 @@ public function testConstructorCanAcceptInclusiveParameter() $this->assertFalse($validator->getInclusive()); } - public function testConstructWithTravesableOptions() + public function testConstructWithTraversableOptions() { $options = new \ArrayObject(['min' => 1, 'max' => 10, 'inclusive' => false]); $validator = new Between($options); - $this->assertTrue($validator->isValid(5)); - $this->assertFalse($validator->isValid(10)); + $result = $validator->isValid(5); + $this->assertInstanceOf(Result::class, $result); + $this->assertTrue($result->isValid()); + + $result = $validator->isValid(10); + $this->assertInstanceOf(Result::class, $result); + $this->assertFalse($result->isValid()); } } From ecea71abcb45a25757fff66f113333580f162bce Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 17 Jul 2017 08:35:21 -0500 Subject: [PATCH 10/16] Renames `Validator::isValid()` to `Validator::validate()` Done for two reasons. First, the method no longer returns a boolean, so calling it `isValid()` did not indicate that there might be a non-boolean result. Second, having the name different will allow us to add the `Validator` interface to v2 releases, and thus provide a forwards-compat path for developers. --- src/Between.php | 6 +++--- src/Bitwise.php | 2 +- src/Callback.php | 2 +- src/Validator.php | 2 +- test/BetweenTest.php | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Between.php b/src/Between.php index fa7915004..380fce6d2 100644 --- a/src/Between.php +++ b/src/Between.php @@ -157,11 +157,11 @@ public function setInclusive($inclusive) * Returns true if and only if $value is between min and max options, inclusively * if inclusive option is true. */ - public function isValid($value, array $context = []) : Result + public function validate($value, array $context = []) : Result { return $this->getInclusive() ? $this->validateInclusive($value, $context) - : $this->validate($value, $context); + : $this->validateExclusive($value, $context); } private function validateInclusive($value, array $context) : Result @@ -172,7 +172,7 @@ private function validateInclusive($value, array $context) : Result return ValidatorResult::createValidResult($value); } - private function validate($value, array $context) : Result + private function validateExclusive($value, array $context) : Result { if ($this->getMin() >= $value || $value >= $this->getMax()) { return $this->createInvalidResult($value, [self::NOT_BETWEEN_STRICT]); diff --git a/src/Bitwise.php b/src/Bitwise.php index b8244ecfd..05b0dbb23 100644 --- a/src/Bitwise.php +++ b/src/Bitwise.php @@ -125,7 +125,7 @@ public function getStrict() * * @throws Exception\RuntimeException for unrecognized operators. */ - public function isValid($value, array $context = []) : Result + public function validate($value, array $context = []) : Result { switch ($this->operator) { case (self::OP_AND): diff --git a/src/Callback.php b/src/Callback.php index 626ca528b..235340891 100644 --- a/src/Callback.php +++ b/src/Callback.php @@ -113,7 +113,7 @@ public function setCallbackOptions($options) * @throws Exception\InvalidArgumentException if no callback present * @throws Exception\InvalidArgumentException if callback is not callable */ - public function isValid($value, $context = null) : Result + public function validate($value, $context = null) : Result { $options = $this->getCallbackOptions(); $callback = $this->getCallback(); diff --git a/src/Validator.php b/src/Validator.php index 48302ba11..cc611828c 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -22,5 +22,5 @@ interface Validator * @return Result * @throws Exception\RuntimeException If validation of $value is impossible */ - public function isValid($value, array $context = []) : Result; + public function validate($value, array $context = []) : Result; } diff --git a/test/BetweenTest.php b/test/BetweenTest.php index 806417957..f90d34d84 100644 --- a/test/BetweenTest.php +++ b/test/BetweenTest.php @@ -44,7 +44,7 @@ public function testBasic() foreach ($valuesExpected as $element) { $validator = new Between(['min' => $element[0], 'max' => $element[1], 'inclusive' => $element[2]]); foreach ($element[4] as $input) { - $result = $validator->isValid($input); + $result = $validator->validate($input); $this->assertInstanceOf(Result::class, $result); $this->assertSame( $element[3], @@ -137,11 +137,11 @@ public function testConstructWithTraversableOptions() $options = new \ArrayObject(['min' => 1, 'max' => 10, 'inclusive' => false]); $validator = new Between($options); - $result = $validator->isValid(5); + $result = $validator->validate(5); $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->isValid()); - $result = $validator->isValid(10); + $result = $validator->validate(10); $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isValid()); } From ae76729eafe6034f1af0ca86216421f2c77f09f4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 18 Jul 2017 08:38:16 -0500 Subject: [PATCH 11/16] Implements aggregate results for validation chains. Validator chains need to aggregate results... but still return a `Result`. This commit does the following: - Adds a `ResultAggregate` interface, which extends `Result`, as well as `IteratorAggregate` and `Countable`. It defines one additional method, `push()`, allowing pushing validation results onto the aggregate. - Adds `ValidatorResultAggregate` as the only concrete implementation of a `ResultAggregate`. - `ValidatorChain` now implements `Validator`, and its `validate()` method creates a `ValidatorResultAggregate` and pushes the result of each validator in the chain to it. - `TranslatableValidatorResult` and `ObscuredValueValidatorResult` now check if the underlying result is an aggregate when `getMessages()` is called; if so, they loop through each result in order to aggregate messages to return. This allows them to act strictly as decorators. --- src/ObscuredValueValidatorResult.php | 44 ++++++++- src/ResultAggregate.php | 19 ++++ src/TranslatableValidatorResult.php | 31 +++++-- src/ValidatorChain.php | 46 ++-------- src/ValidatorResultAggregate.php | 101 +++++++++++++++++++++ src/ValidatorResultMessageInterpolator.php | 2 +- 6 files changed, 192 insertions(+), 51 deletions(-) create mode 100644 src/ResultAggregate.php create mode 100644 src/ValidatorResultAggregate.php diff --git a/src/ObscuredValueValidatorResult.php b/src/ObscuredValueValidatorResult.php index 1f0b8d32f..410295897 100644 --- a/src/ObscuredValueValidatorResult.php +++ b/src/ObscuredValueValidatorResult.php @@ -26,11 +26,9 @@ public function __construct(Result $result) */ public function getMessages() : array { - $messages = []; - foreach ($this->result->getMessageTemplates() as $template) { - $messages[] = $this->interpolateMessageVariables($template, $this); - } - return $messages; + return $this->result instanceof ResultAggregate + ? $this->getMessagesForResultAggregate($this->result, $this->getValue()) + : $this->getMessagesForResult($this->result, $this->getValue()); } /** @@ -45,4 +43,40 @@ public function getValue() $value = $this->castValueToString($this->result->getValue()); return str_repeat('*', strlen($value)); } + + private function getMessagesForResult(Result $result, string $value) : array + { + return array_reduce( + $result->getMessageTemplates(), + function (array $messages, string $template) use ($result, $value) { + array_push( + $messages, + $this->interpolateMessageVariablesWithValue($template, $result, $value) + ); + return $messages; + }, + [] + ); + } + + private function getMessagesForResultAggregate(ResultAggregate $aggregate, string $value) : array + { + $messages = []; + foreach ($aggregate as $result) { + array_merge($messages, $this->getMessagesForResult($result, $value)); + } + return $messages; + } + + /** + * Ensure that the value is obscured when interpolating messages for an aggregate. + */ + private function interpolateMessageVariablesWithValue(string $message, Result $result, string $value) : string + { + $messageVariables = array_merge($result->getMessageVariables(), ['value' => $value]); + foreach ($messageVariables as $variable => $substitution) { + $message = $this->interpolateMessageVariable($message, $variable, $substitution); + } + return $message; + } } diff --git a/src/ResultAggregate.php b/src/ResultAggregate.php new file mode 100644 index 000000000..a543bc2f6 --- /dev/null +++ b/src/ResultAggregate.php @@ -0,0 +1,19 @@ +result->getValue(); - $messages = []; + return $this->result instanceof ResultAggregate + ? $this->getMessagesForResultAggregate($this->result) + : $this->getMessagesForResult($this->result); + } - foreach ($this->result->getMessageTemplates() as $template) { - $messages[] = $this->interpolateMessageVariables( - $this->translator->translate($template, $this->textDomain), - $this->result - ); - } + private function getMessagesForResult(Result $result) : array + { + return array_reduce( + $result->getMessageTemplates(), + function (array $messages, string $template) use ($result) { + array_push($messages, $this->interpolateMessageVariables( + $this->translator->translate($template, $this->textDomain), + $result + )); + }, + [] + ); + } + private function getMessagesForResultAggregate(ResultAggregate $aggregate) : array + { + $messages = []; + foreach ($aggregate as $result) { + array_merge($messages, $this->getMessagesForResult($result)); + } return $messages; } } diff --git a/src/ValidatorChain.php b/src/ValidatorChain.php index 5b3503c39..4fad17407 100644 --- a/src/ValidatorChain.php +++ b/src/ValidatorChain.php @@ -34,13 +34,6 @@ class ValidatorChain implements */ protected $validators; - /** - * Array of validation failure messages - * - * @var array - */ - protected $messages = []; - /** * Initialize validator chain */ @@ -235,23 +228,23 @@ public function prependByName($name, $options = [], $breakChainOnFailure = false * @param mixed $context Extra "context" to provide the validator * @return bool */ - public function isValid($value, $context = null) + public function validate($value, $context = null) : Result { - $this->messages = []; - $result = true; + $results = new ValidatorResultAggregate($value); foreach ($this->validators as $element) { $validator = $element['instance']; - if ($validator->isValid($value, $context)) { + $result = $validator->validate($value, $context); + $results->push($result); + if ($result->isValid()) { continue; } - $result = false; - $messages = $validator->getMessages(); - $this->messages = array_replace_recursive($this->messages, $messages); + if ($element['breakChainOnFailure']) { break; } } - return $result; + + return $results; } /** @@ -269,16 +262,6 @@ public function merge(ValidatorChain $validatorChain) return $this; } - /** - * Returns array of validation failure messages - * - * @return array - */ - public function getMessages() - { - return $this->messages; - } - /** * Get all the validators * @@ -289,17 +272,6 @@ public function getValidators() return $this->validators->toArray(PriorityQueue::EXTR_DATA); } - /** - * Invoke chain as command - * - * @param mixed $value - * @return bool - */ - public function __invoke($value) - { - return $this->isValid($value); - } - /** * Deep clone handling */ @@ -320,6 +292,6 @@ public function __clone() */ public function __sleep() { - return ['validators', 'messages']; + return ['validators']; } } diff --git a/src/ValidatorResultAggregate.php b/src/ValidatorResultAggregate.php new file mode 100644 index 000000000..8451b582c --- /dev/null +++ b/src/ValidatorResultAggregate.php @@ -0,0 +1,101 @@ +value = $value; + } + + public function push(Result $result) : void + { + $this->results[] = $result; + } + + /** + * @return int + */ + public function count() + { + return count($this->results); + } + + /** + * @return iterable + */ + public function getIterator() + { + foreach ($this->results as $result) { + yield $result; + } + } + + public function isValid() : bool + { + return array_reduce($this->results, function (bool $isValid, Result $result) { + return $isValid && $result->isValid(); + }, true); + } + + /** + * Returns a shallow list of all messages, with variables interpolated. + */ + public function getMessages() : array + { + return array_reduce($this->results, function (array $messages, Result $result) { + return array_merge($messages, $result->getMessages()); + }, []); + } + + /** + * Returns a list with message templates from each validator. + * + * Instead of a shallow list, this contains an array of arrays, with the + * second level being the full list of templates for a single validator. + */ + public function getMessageTemplates() : array + { + return array_reduce($this->results, function (array $templates, Result $result) { + $templates[] = $result->getMessageTemplates(); + return $templates; + }, []); + } + + /** + * Returns a list with message variables from each validator. + * + * Instead of a shallow list, this contains an array of arrays, with the + * second level being the full map of variables for a single validator. + */ + public function getMessageVariables() : array + { + return array_reduce($this->results, function (array $variables, Result $result) { + $variables[] = $result->getMessageVariables(); + return $variables; + }, []); + } + + /** + * {@inheritDoc} + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/ValidatorResultMessageInterpolator.php b/src/ValidatorResultMessageInterpolator.php index 5936f815c..520ce74d0 100644 --- a/src/ValidatorResultMessageInterpolator.php +++ b/src/ValidatorResultMessageInterpolator.php @@ -9,7 +9,7 @@ trait ValidatorResultMessageInterpolator { - private function interpolateMessageVariables(string $message, ValidatorResult $result) : string + private function interpolateMessageVariables(string $message, Result $result) : string { $messageVariables = array_merge($result->getMessageVariables(), ['value' => $result->getValue()]); foreach ($messageVariables as $variable => $substitution) { From 59a71839eca952bca9a4e03cf1ed8e6f9f47f433 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 18 Jul 2017 08:44:30 -0500 Subject: [PATCH 12/16] Removes value obscuration awareness from the `AbstractValidator` Like translation, value obscuration is a presentation issue; leave it to the decorators. --- src/AbstractValidator.php | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 38632358f..386985cda 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -25,8 +25,6 @@ abstract class AbstractValidator implements Validator 'messages' => [], // Array of validation failure messages 'messageTemplates' => [], // Array of validation failure message templates 'messageVariables' => [], // Array of additional variables available for validation failure messages - 'valueObscured' => false, // Flag indicating whether or not value should be obfuscated - // in error messages ]; /** @@ -64,9 +62,6 @@ public function __construct($options = null) * * Use this within validators to create the validation result when a failure * condition occurs. Pass it the value, and an array of message keys. - * - * If the "value obscured" flag is set, this method will decorate the result - * using ObscuredValueValidatorResult before returning it. */ protected function createInvalidResult($value, array $messageKeys) : Result { @@ -74,20 +69,13 @@ protected function createInvalidResult($value, array $messageKeys) : Result return $this->getMessageTemplate($key); }, $messageKeys); - $result = ValidatorResult::createInvalidResult( + return ValidatorResult::createInvalidResult( $value, $messageTemplates, $this->getMessageVariables() ); - - if ($this->isValueObscured()) { - $result = new ObscuredValueValidatorResult($result); - } - - return $result; } - /** * Returns an option * @@ -194,26 +182,4 @@ protected function getMessageTemplate(string $messageKey) : string return $this->abstractOptions['messageTemplates'][$messageKey] ?? ''; } - /** - * Set flag indicating whether or not value should be obfuscated in messages - * - * @param bool $flag - * @return AbstractValidator - */ - public function setValueObscured($flag) - { - $this->abstractOptions['valueObscured'] = (bool) $flag; - return $this; - } - - /** - * Retrieve flag indicating whether or not value should be obfuscated in - * messages - * - * @return bool - */ - public function isValueObscured() - { - return $this->abstractOptions['valueObscured']; - } } From c401620b3b151a3d9bd7c5d086d68f42d3dd1b6a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 18 Jul 2017 08:45:18 -0500 Subject: [PATCH 13/16] Removes message length awareness from the `AbstractValidator` Like translation and value obfuscation, message truncation is a presentation issue. --- src/AbstractValidator.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 386985cda..66031f392 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -14,13 +14,6 @@ abstract class AbstractValidator implements Validator { - /** - * Limits the maximum returned length of an error message - * - * @var int - */ - protected static $messageLength = -1; - protected $abstractOptions = [ 'messages' => [], // Array of validation failure messages 'messageTemplates' => [], // Array of validation failure message templates From 90d652e0239ecdef0ed349d129c27e93de0b624e Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 18 Jul 2017 08:46:23 -0500 Subject: [PATCH 14/16] Removes `messages` abstract option from `AbstractValidator` Actual failure messages are no longer stored in the validator, only message templates and variables. --- src/AbstractValidator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 66031f392..b1e7a29bf 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -15,7 +15,6 @@ abstract class AbstractValidator implements Validator { protected $abstractOptions = [ - 'messages' => [], // Array of validation failure messages 'messageTemplates' => [], // Array of validation failure message templates 'messageVariables' => [], // Array of additional variables available for validation failure messages ]; From 60e0d25c5aab9c3d39d43d2f4175b386ea63d1b3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 18 Jul 2017 08:54:53 -0500 Subject: [PATCH 15/16] Removes options processing from AbstractValidator, and promotes abstract options to instance variables Removes the constructor, `getOptions()`, `setOptions()`, and `setOption()` from the `AbstractValidator`. Additionally, it promotes the two remaining members of `$abstractOptions` to instance variables, and modifies the various methods that reference them to reference the new variables. --- src/AbstractValidator.php | 135 +++++++------------------------------- 1 file changed, 22 insertions(+), 113 deletions(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index b1e7a29bf..121f3cf52 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -9,45 +9,25 @@ namespace Zend\Validator; -use Traversable; -use Zend\Stdlib\ArrayUtils; - abstract class AbstractValidator implements Validator { - protected $abstractOptions = [ - 'messageTemplates' => [], // Array of validation failure message templates - 'messageVariables' => [], // Array of additional variables available for validation failure messages - ]; - /** - * Abstract constructor for all validators - * A validator should accept following parameters: - * - nothing f.e. Validator() - * - one or multiple scalar values f.e. Validator($first, $second, $third) - * - an array f.e. Validator(array($first => 'first', $second => 'second', $third => 'third')) - * - an instance of Traversable f.e. Validator($config_instance) + * Array of validation failure message templates. Should be an array of + * key value pairs, to allow both lookup of templates by key, as well as + * overriding the message template string. * - * @param array|Traversable $options + * @var string[] */ - public function __construct($options = null) - { - // The abstract constructor allows no scalar values - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } - - if (isset($this->messageTemplates)) { - $this->abstractOptions['messageTemplates'] = $this->messageTemplates; - } + protected $messageTemplates = []; - if (isset($this->messageVariables)) { - $this->abstractOptions['messageVariables'] = $this->messageVariables; - } - - if (is_array($options)) { - $this->setOptions($options); - } - } + /** + * Array of variable subsitutions to make in message templates. Typically, + * these will be validator constraint values. The message templates will + * refer to them as `%name%`. + * + * @var array + */ + protected $messageVariables = []; /** * Create and return a result indicating validation failure. @@ -69,89 +49,23 @@ protected function createInvalidResult($value, array $messageKeys) : Result } /** - * Returns an option - * - * @param string $option Option to be returned - * @return mixed Returned option - * @throws Exception\InvalidArgumentException - */ - public function getOption($option) - { - if (array_key_exists($option, $this->abstractOptions)) { - return $this->abstractOptions[$option]; - } - - if (isset($this->options) && array_key_exists($option, $this->options)) { - return $this->options[$option]; - } - - throw new Exception\InvalidArgumentException("Invalid option '$option'"); - } - - /** - * Returns all available options - * - * @return array Array with all available options - */ - public function getOptions() - { - $result = $this->abstractOptions; - if (isset($this->options)) { - $result += $this->options; - } - return $result; - } - - /** - * Sets one or multiple options - * - * @param array|Traversable $options Options to set - * @throws Exception\InvalidArgumentException If $options is not an array or Traversable - * @return AbstractValidator Provides fluid interface - */ - public function setOptions($options = []) - { - if (! is_array($options) && ! $options instanceof Traversable) { - throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable'); - } - - foreach ($options as $name => $option) { - $fname = 'set' . ucfirst($name); - $fname2 = 'is' . ucfirst($name); - if (($name != 'setOptions') && method_exists($this, $name)) { - $this->{$name}($option); - } elseif (($fname != 'setOptions') && method_exists($this, $fname)) { - $this->{$fname}($option); - } elseif (method_exists($this, $fname2)) { - $this->{$fname2}($option); - } elseif (isset($this->options)) { - $this->options[$name] = $option; - } else { - $this->abstractOptions[$name] = $option; - } - } - - return $this; - } - - /** - * Returns an array of the names of variables that are used in constructing validation failure messages + * Returns an array of variable names used in constructing validation failure messages. * - * @return array + * @return string[] */ - public function getMessageVariables() + public function getMessageVariables() : array { - return array_keys($this->abstractOptions['messageVariables']); + return array_keys($this->messageVariables); } /** * Returns the message templates from the validator * - * @return array + * @return string[] */ - public function getMessageTemplates() + public function getMessageTemplates() : array { - return $this->abstractOptions['messageTemplates']; + return $this->messageTemplates; } /** @@ -159,7 +73,7 @@ public function getMessageTemplates() */ public function setMessageTemplate(string $messageKey, string $messageString) : void { - $this->abstractOptions['messageTemplates'][$messageKey] = $messageString; + $this->messageTemplates[$messageKey] = $messageString; } /** @@ -167,11 +81,6 @@ public function setMessageTemplate(string $messageKey, string $messageString) : */ protected function getMessageTemplate(string $messageKey) : string { - if ($messageKey === null) { - $keys = array_keys($this->abstractOptions['messageTemplates']); - $messageKey = current($keys); - } - - return $this->abstractOptions['messageTemplates'][$messageKey] ?? ''; + return $this->messageTemplates[$messageKey] ?? ''; } } From a07f1cd84f3c54a4bc85bbc0453322680563ce6e Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 18 Jul 2017 09:37:45 -0500 Subject: [PATCH 16/16] Updates Between validator to have concrete constructor arguments Updates the `Between` validator to no longer accept an array of options, but instead three discrete arguments. It also sets the `$messageVariables` property during instantiation. Tests were refactored to use data providers, and to test for both valid arguments and the expected behavior, as well as invalid constructor arguments and expected exceptions. --- src/AbstractValidator.php | 2 +- src/Between.php | 129 +++++++++------------------- test/BetweenTest.php | 175 ++++++++++++++++++-------------------- 3 files changed, 125 insertions(+), 181 deletions(-) diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 121f3cf52..d12e371a4 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -44,7 +44,7 @@ protected function createInvalidResult($value, array $messageKeys) : Result return ValidatorResult::createInvalidResult( $value, $messageTemplates, - $this->getMessageVariables() + $this->messageVariables ); } diff --git a/src/Between.php b/src/Between.php index 380fce6d2..f4299e130 100644 --- a/src/Between.php +++ b/src/Between.php @@ -28,25 +28,19 @@ class Between extends AbstractValidator ]; /** - * Additional variables available for validation failure messages - * - * @var array + * @var bool */ - protected $messageVariables = [ - 'min' => ['options' => 'min'], - 'max' => ['options' => 'max'], - ]; + private $inclusive; /** - * Options for the between validator - * - * @var array + * @var int|float */ - protected $options = [ - 'inclusive' => true, // Whether to do inclusive comparisons, allowing equivalence to min and/or max - 'min' => 0, - 'max' => PHP_INT_MAX, - ]; + private $max; + + /** + * @var int|float + */ + private $min; /** * Sets validator options @@ -55,102 +49,59 @@ class Between extends AbstractValidator * 'max' => scalar, maximum border * 'inclusive' => boolean, inclusive border values * - * @param array|Traversable $options - * - * @throws Exception\InvalidArgumentException + * @param int|float $min + * @param int|float $max + * @throws Exception\InvalidArgumentException if $min is not numeric + * @throws Exception\InvalidArgumentException if $max is not numeric */ - public function __construct($options = null) + public function __construct($min = 0, $max = PHP_INT_MAX, bool $inclusive = true) { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); + if (! is_numeric($min)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid value for "min"; must be numeric, received %s', + is_object($min) ? get_class($min) : gettype($min) + )); } - if (! is_array($options)) { - $options = func_get_args(); - $temp['min'] = array_shift($options); - if (! empty($options)) { - $temp['max'] = array_shift($options); - } - - if (! empty($options)) { - $temp['inclusive'] = array_shift($options); - } - - $options = $temp; + if (! is_numeric($max)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid value for "max"; must be numeric, received %s', + is_object($max) ? get_class($max) : gettype($max) + )); } - if (count($options) !== 2 - && (! array_key_exists('min', $options) || ! array_key_exists('max', $options)) - ) { - throw new Exception\InvalidArgumentException("Missing option. 'min' and 'max' have to be given"); - } + $this->min = $min; + $this->max = $max; + $this->inclusive = $inclusive; - parent::__construct($options); + $this->messageVariables = [ + 'min' => $min, + 'max' => $max, + ]; } /** * Returns the min option * - * @return mixed + * @return int|float */ public function getMin() { - return $this->options['min']; - } - - /** - * Sets the min option - * - * @param mixed $min - * @return Between Provides a fluent interface - */ - public function setMin($min) - { - $this->options['min'] = $min; - return $this; + return $this->min; } /** * Returns the max option * - * @return mixed + * @return int|float */ public function getMax() { - return $this->options['max']; - } - - /** - * Sets the max option - * - * @param mixed $max - * @return Between Provides a fluent interface - */ - public function setMax($max) - { - $this->options['max'] = $max; - return $this; - } - - /** - * Returns the inclusive option - * - * @return bool - */ - public function getInclusive() - { - return $this->options['inclusive']; + return $this->max; } - /** - * Sets the inclusive option - * - * @param bool $inclusive - * @return Between Provides a fluent interface - */ - public function setInclusive($inclusive) + public function isInclusive() : bool { - $this->options['inclusive'] = $inclusive; - return $this; + return $this->inclusive; } /** @@ -159,14 +110,14 @@ public function setInclusive($inclusive) */ public function validate($value, array $context = []) : Result { - return $this->getInclusive() + return $this->isInclusive() ? $this->validateInclusive($value, $context) : $this->validateExclusive($value, $context); } private function validateInclusive($value, array $context) : Result { - if ($this->getMin() > $value || $value > $this->getMax()) { + if ($value < $this->getMin() || $value > $this->getMax()) { return $this->createInvalidResult($value, [self::NOT_BETWEEN]); } return ValidatorResult::createValidResult($value); @@ -174,7 +125,7 @@ private function validateInclusive($value, array $context) : Result private function validateExclusive($value, array $context) : Result { - if ($this->getMin() >= $value || $value >= $this->getMax()) { + if ($value <= $this->getMin() || $value >= $this->getMax()) { return $this->createInvalidResult($value, [self::NOT_BETWEEN_STRICT]); } return ValidatorResult::createValidResult($value); diff --git a/test/BetweenTest.php b/test/BetweenTest.php index f90d34d84..0c0cf7fcf 100644 --- a/test/BetweenTest.php +++ b/test/BetweenTest.php @@ -10,49 +10,88 @@ namespace ZendTest\Validator; use PHPUnit\Framework\TestCase; +use stdClass; use Zend\Validator\Between; use Zend\Validator\Exception\InvalidArgumentException; use Zend\Validator\Result; -/** - * @group Zend_Validator - */ class BetweenTest extends TestCase { + public function validationProvider() + { + return [ + 'inclusive-int-lower-valid' => [1, 100, true, 1, true], + 'inclusive-int-between-valid' => [1, 100, true, 10, true], + 'inclusive-int-upper-valid' => [1, 100, true, 100, true], + 'inclusive-int-lower-invalid' => [1, 100, true, 0, false], + 'inclusive-int-upper-invalid' => [1, 100, true, 101, false], + 'inclusive-float-lower-valid' => [0.01, 0.99, true, 0.02, true], + 'inclusive-float-between-valid' => [0.01, 0.99, true, 0.51, true], + 'inclusive-float-upper-valid' => [0.01, 0.99, true, 0.98, true], + 'inclusive-float-lower-invalid' => [0.01, 0.99, true, 0.009, false], + 'inclusive-float-upper-invalid' => [0.01, 0.99, true, 1.0, false], + 'exclusive-int-lower-valid' => [1, 100, false, 2, true], + 'exclusive-int-between-valid' => [1, 100, false, 10, true], + 'exclusive-int-upper-valid' => [1, 100, false, 99, true], + 'exclusive-int-lower-invalid' => [1, 100, false, 1, false], + 'exclusive-int-upper-invalid' => [1, 100, false, 100, false], + 'exclusive-float-lower-valid' => [0.01, 0.99, false, 0.02, true], + 'exclusive-float-between-valid' => [0.01, 0.99, false, 0.51, true], + 'exclusive-float-upper-valid' => [0.01, 0.99, false, 0.98, true], + 'exclusive-float-lower-invalid' => [0.01, 0.99, false, 0.01, false], + 'exclusive-float-upper-invalid' => [0.01, 0.99, false, 0.99, false], + ]; + } + /** - * Ensures that the validator follows expected behavior - * - * @return void + * @dataProvider validationProvider */ - public function testBasic() + public function testValidateReturnsExpectedResults( + $min, + $max, + bool $inclusive, + $input, + bool $expectedResult + ) { + $validator = new Between($min, $max, $inclusive); + $result = $validator->validate($input); + $this->assertInstanceOf(Result::class, $result); + $this->assertSame( + $expectedResult, + $result->isValid(), + 'Failed value: ' . $input . ":" . implode("\n", $result->getMessages()) + ); + } + + public function invalidConstructorValues() { - /** - * The elements of each array are, in order: - * - minimum - * - maximum - * - inclusive - * - expected validation result - * - array of test input values - */ - $valuesExpected = [ - [1, 100, true, true, [1, 10, 100]], - [1, 100, true, false, [0, 0.99, 100.01, 101]], - [1, 100, false, false, [0, 1, 100, 101]], - ['a', 'z', true, true, ['a', 'b', 'y', 'z']], - ['a', 'z', false, false, ['!', 'a', 'z']] - ]; - foreach ($valuesExpected as $element) { - $validator = new Between(['min' => $element[0], 'max' => $element[1], 'inclusive' => $element[2]]); - foreach ($element[4] as $input) { - $result = $validator->validate($input); - $this->assertInstanceOf(Result::class, $result); - $this->assertSame( - $element[3], - $result->isValid(), - 'Failed values: ' . $input . ":" . implode("\n", $result->getMessages()) - ); - } - } + return [ + 'invalid-min-null' => [null, 1, '"min"'], + 'invalid-min-false' => [false, 1, '"min"'], + 'invalid-min-true' => [true, 1, '"min"'], + 'invalid-min-string' => ['invalid', 1, '"min"'], + 'invalid-min-array' => [[], 1, '"min"'], + 'invalid-min-object' => [new stdClass(), 1, '"min"'], + 'invalid-max-null' => [1, null, '"max"'], + 'invalid-max-false' => [1, false, '"max"'], + 'invalid-max-true' => [1, true, '"max"'], + 'invalid-max-string' => [1, 'invalid', '"max"'], + 'invalid-max-array' => [1, [], '"max"'], + 'invalid-max-object' => [1, new stdClass(), '"max"'], + ]; + } + + /** + * @dataProvider invalidConstructorValues + */ + public function testRaisesExceptionForInvalidMinAndMaxValues( + $min, + $max, + string $expectedExceptionMessage + ) { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + new Between($min, $max); } /** @@ -62,7 +101,7 @@ public function testBasic() */ public function testGetMin() { - $validator = new Between(['min' => 1, 'max' => 10]); + $validator = new Between(1, 10); $this->assertEquals(1, $validator->getMin()); } @@ -73,76 +112,30 @@ public function testGetMin() */ public function testGetMax() { - $validator = new Between(['min' => 1, 'max' => 10]); + $validator = new Between(1, 10); $this->assertEquals(10, $validator->getMax()); } /** - * Ensures that getInclusive() returns expected default value + * Ensures that isInclusive() returns expected default value * * @return void */ - public function testGetInclusive() - { - $validator = new Between(['min' => 1, 'max' => 10]); - $this->assertEquals(true, $validator->getInclusive()); - } - - public function testEqualsMessageTemplates() - { - $validator = new Between(['min' => 1, 'max' => 10]); - $this->assertAttributeEquals($validator->getOption('messageTemplates'), 'messageTemplates', $validator); - } - - public function testEqualsMessageVariables() - { - $validator = new Between(['min' => 1, 'max' => 10]); - $this->assertAttributeEquals($validator->getOption('messageVariables'), 'messageVariables', $validator); - } - - /** - * @covers Zend\Validator\Between::__construct() - * @dataProvider constructBetweenValidatorInvalidDataProvider - * - * @param array $args - */ - public function testMissingMinOrMax(array $args) - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Missing option. 'min' and 'max' have to be given"); - - new Between($args); - } - - public function constructBetweenValidatorInvalidDataProvider() + public function testDefaultInclusiveFlagIsTrue() { - return [ - [ - ['min' => 1], - ], - [ - ['max' => 5], - ], - ]; + $validator = new Between(1, 10); + $this->assertTrue($validator->isInclusive()); } - public function testConstructorCanAcceptInclusiveParameter() + public function testCanPassInclusiveFlagToConstructor() { $validator = new Between(1, 10, false); - $this->assertFalse($validator->getInclusive()); + $this->assertFalse($validator->isInclusive()); } - public function testConstructWithTraversableOptions() + public function testEqualsMessageVariables() { - $options = new \ArrayObject(['min' => 1, 'max' => 10, 'inclusive' => false]); - $validator = new Between($options); - - $result = $validator->validate(5); - $this->assertInstanceOf(Result::class, $result); - $this->assertTrue($result->isValid()); - - $result = $validator->validate(10); - $this->assertInstanceOf(Result::class, $result); - $this->assertFalse($result->isValid()); + $validator = new Between(1, 10); + $this->assertAttributeEquals(['min' => 1, 'max' => 10], 'messageVariables', $validator); } }