diff --git a/src/AbstractValidator.php b/src/AbstractValidator.php index 28496d64f..d12e371a4 100644 --- a/src/AbstractValidator.php +++ b/src/AbstractValidator.php @@ -9,566 +9,78 @@ namespace Zend\Validator; -use Traversable; -use Zend\Stdlib\ArrayUtils; - -abstract class AbstractValidator implements - Translator\TranslatorAwareInterface, - ValidatorInterface +abstract class AbstractValidator implements Validator { /** - * The value to be validated + * 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. * - * @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 + * @var string[] */ - protected static $defaultTranslatorTextDomain = 'default'; + protected $messageTemplates = []; /** - * Limits the maximum returned length of an error message + * 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 int + * @var array */ - protected static $messageLength = -1; - - 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 - '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 - ]; + protected $messageVariables = []; /** - * 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) + * Create and return a result indicating validation failure. * - * @param array|Traversable $options + * Use this within validators to create the validation result when a failure + * condition occurs. Pass it the value, and an array of message keys. */ - public function __construct($options = null) + protected function createInvalidResult($value, array $messageKeys) : Result { - // The abstract constructor allows no scalar values - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } - - if (isset($this->messageTemplates)) { - $this->abstractOptions['messageTemplates'] = $this->messageTemplates; - } - - if (isset($this->messageVariables)) { - $this->abstractOptions['messageVariables'] = $this->messageVariables; - } + $messageTemplates = array_map(function ($key) { + return $this->getMessageTemplate($key); + }, $messageKeys); - if (is_array($options)) { - $this->setOptions($options); - } + return ValidatorResult::createInvalidResult( + $value, + $messageTemplates, + $this->messageVariables + ); } /** - * Returns an option + * Returns an array of variable names used in constructing validation failure messages. * - * @param string $option Option to be returned - * @return mixed Returned option - * @throws Exception\InvalidArgumentException + * @return string[] */ - public function getOption($option) + public function getMessageVariables() : array { - 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 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 - * - * @return array - */ - public function getMessageVariables() - { - 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; } /** * 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) - { - 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; - } - - /** - * 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 - */ - protected function error($messageKey, $value = null) - { - 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'] = []; - } - - /** - * 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']; - } - - /** - * 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() + public function setMessageTemplate(string $messageKey, string $messageString) : void { - return static::$defaultTranslator; + $this->messageTemplates[$messageKey] = $messageString; } /** - * Is there a default translation object set? - * - * @return bool + * Finds and returns the message template associated with the given message key. */ - public static function hasDefaultTranslator() + protected function getMessageTemplate(string $messageKey) : string { - 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 - * - * @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; - } - - /** - * 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()); + return $this->messageTemplates[$messageKey] ?? ''; } } 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/Between.php b/src/Between.php index 386393dbd..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,127 +49,85 @@ 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']; + return $this->max; } - /** - * Sets the max option - * - * @param mixed $max - * @return Between Provides a fluent interface - */ - public function setMax($max) + public function isInclusive() : bool { - $this->options['max'] = $max; - return $this; + return $this->inclusive; } /** - * Returns the inclusive option - * - * @return bool + * Returns true if and only if $value is between min and max options, inclusively + * if inclusive option is true. */ - public function getInclusive() + public function validate($value, array $context = []) : Result { - return $this->options['inclusive']; + return $this->isInclusive() + ? $this->validateInclusive($value, $context) + : $this->validateExclusive($value, $context); } - /** - * Sets the inclusive option - * - * @param bool $inclusive - * @return Between Provides a fluent interface - */ - public function setInclusive($inclusive) + private function validateInclusive($value, array $context) : Result { - $this->options['inclusive'] = $inclusive; - return $this; + if ($value < $this->getMin() || $value > $this->getMax()) { + return $this->createInvalidResult($value, [self::NOT_BETWEEN]); + } + return ValidatorResult::createValidResult($value); } - /** - * 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) + private function validateExclusive($value, array $context) : Result { - $this->setValue($value); - - 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; - } + if ($value <= $this->getMin() || $value >= $this->getMax()) { + return $this->createInvalidResult($value, [self::NOT_BETWEEN_STRICT]); } - - return true; + return ValidatorResult::createValidResult($value); } } diff --git a/src/Bitwise.php b/src/Bitwise.php index 273fd6657..05b0dbb23 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 validate($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]); + } } diff --git a/src/Callback.php b/src/Callback.php index fb84b97fb..235340891 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 validate($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; } } 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/ObscuredValueValidatorResult.php b/src/ObscuredValueValidatorResult.php new file mode 100644 index 000000000..410295897 --- /dev/null +++ b/src/ObscuredValueValidatorResult.php @@ -0,0 +1,82 @@ +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 + { + return $this->result instanceof ResultAggregate + ? $this->getMessagesForResultAggregate($this->result, $this->getValue()) + : $this->getMessagesForResult($this->result, $this->getValue()); + } + + /** + * 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)); + } + + 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/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; + } + + /** + * Returns translated error message strings from the decorated result instance. + * + * 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 Result value will + * be interpolated. + */ + public function getMessages() : array + { + return $this->result instanceof ResultAggregate + ? $this->getMessagesForResultAggregate($this->result) + : $this->getMessagesForResult($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/Validator.php b/src/Validator.php new file mode 100644 index 000000000..cc611828c --- /dev/null +++ b/src/Validator.php @@ -0,0 +1,26 @@ +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; } /** @@ -271,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 * @@ -291,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 */ @@ -322,6 +292,6 @@ public function __clone() */ public function __sleep() { - return ['validators', 'messages']; + return ['validators']; } } diff --git a/src/ValidatorInterface.php b/src/ValidatorInterface.php deleted file mode 100644 index 6424ef7a6..000000000 --- a/src/ValidatorInterface.php +++ /dev/null @@ -1,38 +0,0 @@ -getCode(), $e); } } diff --git a/src/ValidatorResult.php b/src/ValidatorResult.php new file mode 100644 index 000000000..a5e0aecc3 --- /dev/null +++ b/src/ValidatorResult.php @@ -0,0 +1,125 @@ +value = $value; + $this->isValid = $isValid; + $this->messageTemplates = $messageTemplates; + $this->messageVariables = $messageVariables; + } + + /** + * @param mixed $value + */ + public static function createValidResult($value) : self + { + return new self($value, true); + } + + /** + * @param mixed $value + * @param string[] $messageTemplates + * @param string[] $messageVariables + */ + public static function createInvalidResult( + $value, + array $messageTemplates, + array $messageVariables = [] + ) : self { + return new self($value, false, $messageTemplates, $messageVariables); + } + + public function isValid() : bool + { + return $this->isValid; + } + + /** + * Retrieve validation error messages. + * + * This method loops through each message template and interpolates any + * message variables discovered in the string. + * + * 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 + { + $messages = []; + foreach ($this->getMessageTemplates() as $template) { + $messages[] = $this->interpolateMessageVariables($template, $this); + } + return $messages; + } + + public function getMessageTemplates() : array + { + return $this->messageTemplates; + } + + public function getMessageVariables() : array + { + return $this->messageVariables; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } +} 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/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 new file mode 100644 index 000000000..520ce74d0 --- /dev/null +++ b/src/ValidatorResultMessageInterpolator.php @@ -0,0 +1,46 @@ +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 + { + 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)); + } + + $value = is_array($value) + ? sprintf('[%s]', implode(', ', $value)) + : $value; + + return (string) $value; + } +} diff --git a/test/BetweenTest.php b/test/BetweenTest.php index bf86e4e5f..0c0cf7fcf 100644 --- a/test/BetweenTest.php +++ b/test/BetweenTest.php @@ -10,57 +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) { - $this->assertEquals( - $element[3], - $validator->isValid($input), - 'Failed values: ' . $input . ":" . implode("\n", $validator->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"'], + ]; } /** - * Ensures that getMessages() returns expected default value - * - * @return void + * @dataProvider invalidConstructorValues */ - public function testGetMessages() - { - $validator = new Between(['min' => 1, 'max' => 10]); - $this->assertEquals([], $validator->getMessages()); + public function testRaisesExceptionForInvalidMinAndMaxValues( + $min, + $max, + string $expectedExceptionMessage + ) { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + new Between($min, $max); } /** @@ -70,7 +101,7 @@ public function testGetMessages() */ public function testGetMin() { - $validator = new Between(['min' => 1, 'max' => 10]); + $validator = new Between(1, 10); $this->assertEquals(1, $validator->getMin()); } @@ -81,71 +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() + public function testDefaultInclusiveFlagIsTrue() { - $validator = new Between(['min' => 1, 'max' => 10]); - $this->assertEquals(true, $validator->getInclusive()); + $validator = new Between(1, 10); + $this->assertTrue($validator->isInclusive()); } - 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() - { - return [ - [ - ['min' => 1], - ], - [ - ['max' => 5], - ], - ]; - } - - public function testConstructorCanAcceptInclusiveParameter() + public function testCanPassInclusiveFlagToConstructor() { $validator = new Between(1, 10, false); - $this->assertFalse($validator->getInclusive()); + $this->assertFalse($validator->isInclusive()); } - public function testConstructWithTravesableOptions() + public function testEqualsMessageVariables() { - $options = new \ArrayObject(['min' => 1, 'max' => 10, 'inclusive' => false]); - $validator = new Between($options); - - $this->assertTrue($validator->isValid(5)); - $this->assertFalse($validator->isValid(10)); + $validator = new Between(1, 10); + $this->assertAttributeEquals(['min' => 1, 'max' => 10], 'messageVariables', $validator); } }