diff --git a/src/BaseInputFilter.php b/src/BaseInputFilter.php index a99f4a89..c3688b24 100644 --- a/src/BaseInputFilter.php +++ b/src/BaseInputFilter.php @@ -187,8 +187,10 @@ protected function validateInputs(array $inputs) && $input->isRequired() && $input->allowEmpty() ) { - $this->validInputs[$name] = $input; - continue; + if (!$input->allowEmpty()) { + $this->validInputs[$name] = $input; + continue; + } } // key exists, is null, input is not required; valid @@ -223,14 +225,18 @@ protected function validateInputs(array $inputs) } // key exists, empty string, input is required, allows empty; valid + // if continueIfEmpty is false, otherwise validation continues if ($dataExists && '' === $this->data[$name] && $input instanceof InputInterface + && $input instanceof EmptyContextInterface && $input->isRequired() && $input->allowEmpty() ) { - $this->validInputs[$name] = $input; - continue; + if (!$input->continueIfEmpty()) { + $this->validInputs[$name] = $input; + continue; + } } // key exists, is array representing file, no file present, input not diff --git a/src/EmptyContextInterface.php b/src/EmptyContextInterface.php new file mode 100644 index 00000000..72933b18 --- /dev/null +++ b/src/EmptyContextInterface.php @@ -0,0 +1,17 @@ +setRequired(!$value); } break; + case 'continue_if_empty': + $input->setContinueIfEmpty($inputSpecification['continue_if_empty']); + break; case 'error_message': $input->setErrorMessage($value); - break; case 'fallback_value': $input->setFallbackValue($value); break; diff --git a/src/Factory.php.orig b/src/Factory.php.orig new file mode 100644 index 00000000..f1199abe --- /dev/null +++ b/src/Factory.php.orig @@ -0,0 +1,331 @@ +defaultFilterChain = $filterChain; + return $this; + } + + /** + * Get default filter chain, if any + * + * @return null|FilterChain + */ + public function getDefaultFilterChain() + { + return $this->defaultFilterChain; + } + + /** + * Clear the default filter chain (i.e., don't inject one into new inputs) + * + * @return void + */ + public function clearDefaultFilterChain() + { + $this->defaultFilterChain = null; + } + + /** + * Set default validator chain to use + * + * @param ValidatorChain $validatorChain + * @return Factory + */ + public function setDefaultValidatorChain(ValidatorChain $validatorChain) + { + $this->defaultValidatorChain = $validatorChain; + return $this; + } + + /** + * Get default validator chain, if any + * + * @return null|ValidatorChain + */ + public function getDefaultValidatorChain() + { + return $this->defaultValidatorChain; + } + + /** + * Clear the default validator chain (i.e., don't inject one into new inputs) + * + * @return void + */ + public function clearDefaultValidatorChain() + { + $this->defaultValidatorChain = null; + } + + /** + * Factory for input objects + * + * @param array|Traversable $inputSpecification + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + * @return InputInterface|InputFilterInterface + */ + public function createInput($inputSpecification) + { + if (!is_array($inputSpecification) && !$inputSpecification instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or Traversable; received "%s"', + __METHOD__, + (is_object($inputSpecification) ? get_class($inputSpecification) : gettype($inputSpecification)) + )); + } + if ($inputSpecification instanceof Traversable) { + $inputSpecification = ArrayUtils::iteratorToArray($inputSpecification); + } + + $class = 'Zend\InputFilter\Input'; + if (isset($inputSpecification['type'])) { + $class = $inputSpecification['type']; + if (!class_exists($class)) { + throw new Exception\RuntimeException(sprintf( + 'Input factory expects the "type" to be a valid class; received "%s"', + $class + )); + } + } + $input = new $class(); + + if ($input instanceof InputFilterInterface) { + return $this->createInputFilter($inputSpecification); + } + + if (!$input instanceof InputInterface) { + throw new Exception\RuntimeException(sprintf( + 'Input factory expects the "type" to be a class implementing %s; received "%s"', + 'Zend\InputFilter\InputInterface', + $class + )); + } + + if ($this->defaultFilterChain) { + $input->setFilterChain(clone $this->defaultFilterChain); + } + if ($this->defaultValidatorChain) { + $input->setValidatorChain(clone $this->defaultValidatorChain); + } + + foreach ($inputSpecification as $key => $value) { + switch ($key) { + case 'name': + $input->setName($value); + break; + case 'required': + $input->setRequired($value); + if (!isset($inputSpecification['allow_empty'])) { + $input->setAllowEmpty(!$value); + } + break; + case 'allow_empty': + $input->setAllowEmpty($value); + if (!isset($inputSpecification['required'])) { + $input->setRequired(!$value); + } + break; +<<<<<<< HEAD + case 'error_message': + $input->setErrorMessage($value); +======= + case 'continue_if_empty': + $input->setContinueIfEmpty($inputSpecification['continue_if_empty']); +>>>>>>> zburnham/validate_empty_with_context + break; + case 'fallback_value': + $input->setFallbackValue($value); + break; + case 'filters': + if ($value instanceof FilterChain) { + $input->setFilterChain($value); + break; + } + if (!is_array($value) && !$value instanceof Traversable) { + throw new Exception\RuntimeException(sprintf( + '%s expects the value associated with "filters" to be an array/Traversable of filters or filter specifications, or a FilterChain; received "%s"', + __METHOD__, + (is_object($value) ? get_class($value) : gettype($value)) + )); + } + $this->populateFilters($input->getFilterChain(), $value); + break; + case 'validators': + if ($value instanceof ValidatorChain) { + $input->setValidatorChain($value); + break; + } + if (!is_array($value) && !$value instanceof Traversable) { + throw new Exception\RuntimeException(sprintf( + '%s expects the value associated with "validators" to be an array/Traversable of validators or validator specifications, or a ValidatorChain; received "%s"', + __METHOD__, + (is_object($value) ? get_class($value) : gettype($value)) + )); + } + $this->populateValidators($input->getValidatorChain(), $value); + break; + default: + // ignore unknown keys + break; + } + } + + return $input; + } + + /** + * Factory for input filters + * + * @param array|Traversable $inputFilterSpecification + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + * @return InputFilterInterface + */ + public function createInputFilter($inputFilterSpecification) + { + if (!is_array($inputFilterSpecification) && !$inputFilterSpecification instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or Traversable; received "%s"', + __METHOD__, + (is_object($inputFilterSpecification) ? get_class($inputFilterSpecification) : gettype($inputFilterSpecification)) + )); + } + if ($inputFilterSpecification instanceof Traversable) { + $inputFilterSpecification = ArrayUtils::iteratorToArray($inputFilterSpecification); + } + + $class = 'Zend\InputFilter\InputFilter'; + if (isset($inputFilterSpecification['type']) && is_string($inputFilterSpecification['type'])) { + $class = $inputFilterSpecification['type']; + if (!class_exists($class)) { + throw new Exception\RuntimeException(sprintf( + 'Input factory expects the "type" to be a valid class; received "%s"', + $class + )); + } + unset($inputFilterSpecification['type']); + } + $inputFilter = new $class(); + + if (!$inputFilter instanceof InputFilterInterface) { + throw new Exception\RuntimeException(sprintf( + 'InputFilter factory expects the "type" to be a class implementing %s; received "%s"', + 'Zend\InputFilter\InputFilterInterface', $class)); + } + + if ($inputFilter instanceof CollectionInputFilter) { + if (isset($inputFilterSpecification['input_filter'])) { + $inputFilter->setInputFilter($inputFilterSpecification['input_filter']); + } + if (isset($inputFilterSpecification['count'])) { + $inputFilter->setCount($inputFilterSpecification['count']); + } + return $inputFilter; + } + + foreach ($inputFilterSpecification as $key => $value) { + + if (($value instanceof InputInterface) + || ($value instanceof InputFilterInterface) + ) { + $input = $value; + } else { + $input = $this->createInput($value); + } + + $inputFilter->add($input, $key); + } + + return $inputFilter; + } + + protected function populateFilters(FilterChain $chain, $filters) + { + foreach ($filters as $filter) { + if (is_object($filter) || is_callable($filter)) { + $chain->attach($filter); + continue; + } + + if (is_array($filter)) { + if (!isset($filter['name'])) { + throw new Exception\RuntimeException( + 'Invalid filter specification provided; does not include "name" key' + ); + } + $name = $filter['name']; + $options = array(); + if (isset($filter['options'])) { + $options = $filter['options']; + } + $chain->attachByName($name, $options); + continue; + } + + throw new Exception\RuntimeException( + 'Invalid filter specification provided; was neither a filter instance nor an array specification' + ); + } + } + + protected function populateValidators(ValidatorChain $chain, $validators) + { + foreach ($validators as $validator) { + if ($validator instanceof ValidatorInterface) { + $chain->attach($validator); + continue; + } + + if (is_array($validator)) { + if (!isset($validator['name'])) { + throw new Exception\RuntimeException( + 'Invalid validator specification provided; does not include "name" key' + ); + } + $name = $validator['name']; + $options = array(); + if (isset($validator['options'])) { + $options = $validator['options']; + } + $breakChainOnFailure = false; + if (isset($validator['break_chain_on_failure'])) { + $breakChainOnFailure = $validator['break_chain_on_failure']; + } + $chain->attachByName($name, $options, $breakChainOnFailure); + continue; + } + + throw new Exception\RuntimeException( + 'Invalid validator specification provided; was neither a validator instance nor an array specification' + ); + } + } +} diff --git a/src/Input.php b/src/Input.php index 277340be..759e44b1 100644 --- a/src/Input.php +++ b/src/Input.php @@ -13,13 +13,18 @@ use Zend\Validator\ValidatorChain; use Zend\Validator\NotEmpty; -class Input implements InputInterface +class Input implements InputInterface, EmptyContextInterface { /** * @var bool */ protected $allowEmpty = false; + /** + * @var bool + */ + protected $continueIfEmpty = false; + /** * @var bool */ @@ -90,6 +95,16 @@ public function setBreakOnFailure($breakOnFailure) return $this; } + /** + * @param bool $continueIfEmpty + * @return \Zend\InputFilter\Input + */ + public function setContinueIfEmpty($continueIfEmpty) + { + $this->continueIfEmpty = (bool) $continueIfEmpty; + return $this; + } + /** * @param string|null $errorMessage * @return Input @@ -176,6 +191,14 @@ public function breakOnFailure() return $this->breakOnFailure; } + /** + * @return bool + */ + public function continueIfEmpty() + { + return $this->continueIfEmpty; + } + /** * @return string|null */ @@ -274,7 +297,12 @@ public function merge(InputInterface $input) */ public function isValid($context = null) { - $this->injectNotEmptyValidator(); + // Empty value needs further validation if continueIfEmpty is set + // so don't inject NotEmpty validator which would always + // mark that as false + if (!$this->continueIfEmpty()) { + $this->injectNotEmptyValidator(); + } $validator = $this->getValidatorChain(); $value = $this->getValue(); $result = $validator->isValid($value, $context); diff --git a/src/InputInterface.php b/src/InputInterface.php index 6144ac11..cf87198c 100644 --- a/src/InputInterface.php +++ b/src/InputInterface.php @@ -16,6 +16,7 @@ interface InputInterface { public function setAllowEmpty($allowEmpty); public function setBreakOnFailure($breakOnFailure); + public function setContinueIfEmpty($continueIfEmpty); public function setErrorMessage($errorMessage); public function setFilterChain(FilterChain $filterChain); public function setName($name); @@ -26,6 +27,7 @@ public function merge(InputInterface $input); public function allowEmpty(); public function breakOnFailure(); + public function continueIfEmpty(); public function getErrorMessage(); public function getFilterChain(); public function getName(); diff --git a/test/BaseInputFilterTest.php b/test/BaseInputFilterTest.php index 148584ad..4af65579 100644 --- a/test/BaseInputFilterTest.php +++ b/test/BaseInputFilterTest.php @@ -531,7 +531,11 @@ public function testValidationAllowsEmptyValuesToRequiredInputWhenAllowEmptyFlag $filter->add($foo, '') ->add($bar, 'bar'); - $data = array('bar' => 124); + $data = array( + 'bar' => 124, + 'foo' => '', + ); + $filter->setData($data); $this->assertTrue($filter->isValid()); @@ -560,6 +564,49 @@ public function testValidationMarksInputInvalidWhenRequiredAndAllowEmptyFlagIsFa $this->assertFalse($filter->isValid()); } + public static function contextDataProvider() + { + return array( + array('', 'y', true), + array('', 'n', false), + ); + } + + /** + * Idea here is that an empty field may or may not be valid based on + * context. + */ + /** + * @dataProvider contextDataProvider() + */ + public function testValidationMarksInputValidWhenAllowEmptyFlagIsTrueAndContinueIfEmptyIsTrueAndContextValidatesEmptyField($allowEmpty, $blankIsValid, $valid) + { + // $this->markTestSkipped(); + + $filter = new InputFilter(); + + $data = array ( + 'allowEmpty' => $allowEmpty, + 'blankIsValid' => $blankIsValid, + ); + + $allowEmpty = new Input(); + $allowEmpty->setAllowEmpty(true) + ->setContinueIfEmpty(true); + + $blankIsValid = new Input(); + $blankIsValid->getValidatorChain()->attach(new Validator\Callback(function($value, $context) { + return ('y' === $value && empty($context['allowEmpty'])); + })); + + $filter->add($allowEmpty, 'allowEmpty') + ->add($blankIsValid, 'blankIsValid'); + $filter->setData($data); +// die(var_dump($filter->get('blankIsValid'))); + + $this->assertSame($valid, $filter->isValid()); + } + public function testCanRetrieveRawValuesIndividuallyWithoutValidating() { $filter = $this->getInputFilter(); diff --git a/test/FactoryTest.php b/test/FactoryTest.php index 8e5c2708..eebcb71b 100644 --- a/test/FactoryTest.php +++ b/test/FactoryTest.php @@ -240,6 +240,17 @@ public function testFactoryWillCreateInputWithSuggestedName() $this->assertEquals('foo', $input->getName()); } + public function testFactoryWillCreateInputWithContinueIfEmptyFlag() + { + $factory = new Factory(); + $input = $factory->createInput(array( + 'name' => 'foo', + 'continue_if_empty' => true, + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $this->assertTrue($input->continueIfEmpty()); + } + public function testFactoryAcceptsInputInterface() { $factory = new Factory(); @@ -339,11 +350,15 @@ public function testFactoryWillCreateInputFilterAndAllInputObjectsFromGivenConfi 'type' => 'ZendTest\InputFilter\TestAsset\CustomInput', 'name' => 'bat', ), + 'zomg' => array( + 'name' => 'zomg', + 'continue_if_empty' => true, + ), )); $this->assertInstanceOf('Zend\InputFilter\InputFilter', $inputFilter); - $this->assertEquals(4, count($inputFilter)); + $this->assertEquals(5, count($inputFilter)); - foreach (array('foo', 'bar', 'baz', 'bat') as $name) { + foreach (array('foo', 'bar', 'baz', 'bat', 'zomg') as $name) { $input = $inputFilter->get($name); switch ($name) { @@ -373,6 +388,9 @@ public function testFactoryWillCreateInputFilterAndAllInputObjectsFromGivenConfi $this->assertInstanceOf('ZendTest\InputFilter\TestAsset\CustomInput', $input); $this->assertEquals('bat', $input->getName()); break; + case 'zomg': + $this->assertInstanceOf('Zend\InputFilter\Input', $input); + $this->assertTrue($input->continueIfEmpty()); } } } diff --git a/test/InputTest.php b/test/InputTest.php index 11f0a19b..f2c2caa0 100644 --- a/test/InputTest.php +++ b/test/InputTest.php @@ -82,6 +82,30 @@ public function testAllowEmptyFlagIsMutable() $this->assertTrue($this->input->allowEmpty()); } + public function testContinueIfEmptyFlagIsFalseByDefault() + { + $input = new Input('foo'); + $this->assertFalse($input->continueIfEmpty()); + } + + public function testContinueIfEmptyFlagIsMutable() + { + $input = new Input('foo'); + $input->setContinueIfEmpty(true); + $this->assertTrue($input->continueIfEmpty()); + } + + public function testNotEmptyValidatorNotInjectedIfContinueIfEmptyIsTrue() + { + $input = new Input('foo'); + $input->setContinueIfEmpty(true); + $input->setValue(''); + $input->isValid(); + $validators = $input->getValidatorChain() + ->getValidators(); + $this->assertTrue(0 == count($validators)); + } + public function testValueIsNullByDefault() { $this->assertNull($this->input->getValue());