diff --git a/src/ArrayInput.php b/src/ArrayInput.php index 3d1b0144..ef513fe0 100644 --- a/src/ArrayInput.php +++ b/src/ArrayInput.php @@ -31,6 +31,16 @@ public function setValue($value) return parent::setValue($value); } + /** + * {@inheritdoc} + */ + public function resetValue() + { + $this->value = []; + $this->hasValue = false; + return $this; + } + /** * @return array */ @@ -50,6 +60,20 @@ public function getValue() */ public function isValid($context = null) { + $hasValue = $this->hasValue(); + $required = $this->isRequired(); + $hasFallback = $this->hasFallback(); + + if (! $hasValue && $hasFallback) { + $this->setValue($this->getFallbackValue()); + return true; + } + + if (! $hasValue && $required) { + $this->setErrorMessage('Value is required'); + return false; + } + if (!$this->continueIfEmpty() && !$this->allowEmpty()) { $this->injectNotEmptyValidator(); } @@ -64,9 +88,9 @@ public function isValid($context = null) } $result = $validator->isValid($value, $context); if (!$result) { - if ($this->hasFallback()) { + if ($hasFallback) { $this->setValue($this->getFallbackValue()); - $result = true; + return true; } break; } diff --git a/src/BaseInputFilter.php b/src/BaseInputFilter.php index 028d5f40..a5cdd488 100644 --- a/src/BaseInputFilter.php +++ b/src/BaseInputFilter.php @@ -254,8 +254,6 @@ protected function validateInputs(array $inputs, $data = [], $context = null) continue; } - $hasFallback = ($input instanceof Input && $input->hasFallback()); - // If input is optional (not required), and value is not set, then ignore. if (!array_key_exists($name, $data) && !$input->isRequired() @@ -263,34 +261,6 @@ protected function validateInputs(array $inputs, $data = [], $context = null) continue; } - // If the value is required, not present in the data set, and - // has no fallback, validation fails. - if (!array_key_exists($name, $data) - && $input->isRequired() - && !$hasFallback - ) { - $input->setErrorMessage('Value is required'); - $this->invalidInputs[$name] = $input; - - if ($input->breakOnFailure()) { - return false; - } - - $valid = false; - continue; - } - - // If the value is required, not present in the data set, and - // has a fallback, validation passes, and we set the input - // value to the fallback. - if (!array_key_exists($name, $data) - && $input->isRequired() - && $hasFallback - ) { - $input->setValue($input->getFallbackValue()); - continue; - } - // Make sure we have a value (empty) for validation of context if (!array_key_exists($name, $data)) { $data[$name] = null; @@ -545,7 +515,7 @@ protected function populate() $input->clearRawValues(); } - if (!isset($this->data[$name])) { + if (!array_key_exists($name, $this->data)) { // No value; clear value in this input if ($input instanceof InputFilterInterface) { $input->setData([]); @@ -557,6 +527,11 @@ protected function populate() continue; } + if ($input instanceof Input) { + $input->resetValue(); + continue; + } + $input->setValue(null); continue; } diff --git a/src/FileInput.php b/src/FileInput.php index 80748f67..3b87a9d4 100644 --- a/src/FileInput.php +++ b/src/FileInput.php @@ -113,11 +113,17 @@ public function isEmptyFile($rawValue) public function isValid($context = null) { $rawValue = $this->getRawValue(); + $hasValue = $this->hasValue(); $empty = $this->isEmptyFile($rawValue); $required = $this->isRequired(); $allowEmpty = $this->allowEmpty(); $continueIfEmpty = $this->continueIfEmpty(); + if (! $hasValue && $required && ! $this->hasFallback()) { + $this->setErrorMessage('Value is required'); + return false; + } + if ($empty && ! $required && ! $continueIfEmpty) { return true; } diff --git a/src/Input.php b/src/Input.php index 6fa86a21..575a390b 100644 --- a/src/Input.php +++ b/src/Input.php @@ -67,6 +67,15 @@ class Input implements */ protected $value; + /** + * Flag for distinguish when $value contains a real `null` or the PHP default property value. + * + * PHP gives to all properties a default value of `null` unless other default value is set. + * + * @var bool + */ + protected $hasValue = false; + /** * @var mixed */ @@ -163,12 +172,36 @@ public function setValidatorChain(ValidatorChain $validatorChain) } /** + * Set the input value. + * + * If you want to remove/unset the current value use {@link Input::resetValue()}. + * + * @see Input::getValue() For retrieve the input value. + * @see Input::hasValue() For to know if input value was set. + * @see Input::resetValue() For reset the input value to the default state. + * * @param mixed $value * @return Input */ public function setValue($value) { $this->value = $value; + $this->hasValue = true; + return $this; + } + + /** + * Reset input value to the default state. + * + * @see Input::hasValue() For to know if input value was set. + * @see Input::setValue() For set a new value. + * + * @return Input + */ + public function resetValue() + { + $this->value = null; + $this->hasValue = false; return $this; } @@ -270,6 +303,23 @@ public function getValue() return $filter->filter($this->value); } + /** + * Flag for inform if input value was set. + * + * This flag used for distinguish when {@link Input::getValue()} will return a real `null` value or the PHP default + * value. + * + * @see Input::getValue() For retrieve the input value. + * @see Input::setValue() For set a new value. + * @see Input::resetValue() For reset the input value to the default state. + * + * @return bool + */ + public function hasValue() + { + return $this->hasValue; + } + /** * @return mixed */ @@ -321,11 +371,22 @@ public function merge(InputInterface $input) public function isValid($context = null) { $value = $this->getValue(); + $hasValue = $this->hasValue(); $empty = ($value === null || $value === '' || $value === []); $required = $this->isRequired(); $allowEmpty = $this->allowEmpty(); $continueIfEmpty = $this->continueIfEmpty(); + if (! $hasValue && $this->hasFallback()) { + $this->setValue($this->getFallbackValue()); + return true; + } + + if (! $hasValue && $required) { + $this->setErrorMessage('Value is required'); + return false; + } + if ($empty && ! $required && ! $continueIfEmpty) { return true; } diff --git a/test/ArrayInputTest.php b/test/ArrayInputTest.php index 9f049917..ed0b1a0b 100644 --- a/test/ArrayInputTest.php +++ b/test/ArrayInputTest.php @@ -187,39 +187,6 @@ public function testDoNotInjectNotEmptyValidatorIfAnywhereInChain() $this->assertEquals($notEmptyMock, $validators[1]['instance']); } - public function dataFallbackValue() - { - return [ - [ - 'fallbackValue' => [] - ], - [ - 'fallbackValue' => [''], - ], - [ - 'fallbackValue' => [null], - ], - [ - 'fallbackValue' => ['some value'], - ], - ]; - } - - /** - * @dataProvider dataFallbackValue - */ - public function testFallbackValue($fallbackValue) - { - $this->input->setFallbackValue($fallbackValue); - $validator = new Validator\Date(); - $this->input->getValidatorChain()->attach($validator); - $this->input->setValue(['123']); // not a date - - $this->assertTrue($this->input->isValid()); - $this->assertEmpty($this->input->getMessages()); - $this->assertSame($fallbackValue, $this->input->getValue()); - } - public function emptyValuesProvider() { return [ @@ -246,4 +213,16 @@ public function testNotAllowEmptyWithFilterConvertsEmptyToNonEmptyIsValid() })); $this->assertTrue($this->input->isValid()); } + + public function fallbackValueVsIsValidProvider() + { + $dataSets = parent::fallbackValueVsIsValidProvider(); + array_walk($dataSets, function (&$set) { + $set[1] = [$set[1]]; // Wrap fallback value into an array. + $set[2] = [$set[2]]; // Wrap value into an array. + $set[4] = [$set[4]]; // Wrap expected value into an array. + }); + + return $dataSets; + } } diff --git a/test/BaseInputFilterTest.php b/test/BaseInputFilterTest.php index 3fa714af..ada72dc9 100644 --- a/test/BaseInputFilterTest.php +++ b/test/BaseInputFilterTest.php @@ -1035,86 +1035,6 @@ public function testEmptyValuePassedForRequiredButAllowedEmptyInputShouldMarkInp $this->assertTrue($filter->isValid(), 'Empty value should mark input filter as valid'); } - /** - * @group 10 - */ - public function testMissingRequiredWithFallbackShouldMarkInputValid() - { - $foo = new Input('foo'); - $foo->setRequired(true); - $foo->setAllowEmpty(false); - - $bar = new Input('bar'); - $bar->setRequired(true); - $bar->setFallbackValue('baz'); - - $filter = new InputFilter(); - $filter->add($foo); - $filter->add($bar); - - $filter->setData(['foo' => 'xyz']); - $this->assertTrue($filter->isValid(), 'Missing input with fallback value should mark input filter as valid'); - $data = $filter->getValues(); - $this->assertArrayHasKey('bar', $data); - $this->assertEquals($bar->getFallbackValue(), $data['bar']); - } - - /** - * @group 10 - */ - public function testMissingRequiredThatAllowsEmptyWithFallbackShouldMarkInputValid() - { - $foo = new Input('foo'); - $foo->setRequired(true); - $foo->setAllowEmpty(false); - - $bar = new Input('bar'); - $bar->setRequired(true); - $bar->setAllowEmpty(true); - $bar->setFallbackValue('baz'); - - $filter = new InputFilter(); - $filter->add($foo); - $filter->add($bar); - - $filter->setData(['foo' => 'xyz']); - $this->assertTrue($filter->isValid(), 'Missing input with fallback value should mark input filter as valid'); - $data = $filter->getValues(); - $this->assertArrayHasKey('bar', $data); - $this->assertEquals($bar->getFallbackValue(), $data['bar']); - $this->assertArrayNotHasKey('bar', $filter->getValidInput()); - $this->assertArrayNotHasKey('bar', $filter->getInvalidInput()); - } - - /** - * @group 10 - */ - public function testEmptyRequiredValueWithFallbackShouldMarkInputValid() - { - $foo = new Input('foo'); - $foo->setRequired(true); - $foo->setAllowEmpty(false); - - $bar = new Input('bar'); - $bar->setRequired(true); - $bar->setFallbackValue('baz'); - - $filter = new InputFilter(); - $filter->add($foo); - $filter->add($bar); - - $filter->setData([ - 'foo' => 'xyz', - 'bar' => null, - ]); - $this->assertTrue($filter->isValid(), 'Empty input with fallback value should mark input filter as valid'); - $data = $filter->getValues(); - $this->assertArrayHasKey('bar', $data); - $this->assertEquals($bar->getFallbackValue(), $data['bar']); - $this->assertArrayHasKey('bar', $filter->getValidInput()); - $this->assertArrayNotHasKey('bar', $filter->getInvalidInput()); - } - /** * @group 15 */ diff --git a/test/FileInputTest.php b/test/FileInputTest.php index 51fbffa9..b22eed54 100644 --- a/test/FileInputTest.php +++ b/test/FileInputTest.php @@ -311,6 +311,27 @@ public function testRequiredNotEmptyValidatorNotAddedWhenOneExists() $this->markTestSkipped('Test is not enabled in FileInputTest'); } + public function testFallbackValueVsIsValidRules( + $required = null, + $fallbackValue = null, + $originalValue = null, + $isValid = null, + $expectedValue = null + ) { + $this->markTestSkipped('Input::setFallbackValue is not implemented on FileInput'); + } + + + public function testFallbackValueVsIsValidRulesWhenValueNotSet( + $required = null, + $fallbackValue = null, + $originalValue = null, + $isValid = null, + $expectedValue = null + ) { + $this->markTestSkipped('Input::setFallbackValue is not implemented on FileInput'); + } + public function testMerge() { $value = ['tmp_name' => 'bar']; @@ -340,11 +361,6 @@ public function testMerge() $this->assertInstanceOf('Zend\Filter\StringTrim', $filters[0]); } - public function testFallbackValue($fallbackValue = null) - { - $this->markTestSkipped('Not use fallback value'); - } - public function testIsEmptyFileNotArray() { $rawValue = 'file'; diff --git a/test/InputTest.php b/test/InputTest.php index 100d5317..dfa74763 100644 --- a/test/InputTest.php +++ b/test/InputTest.php @@ -9,11 +9,14 @@ namespace ZendTest\InputFilter; +use PHPUnit_Framework_MockObject_MockObject as MockObject; use PHPUnit_Framework_TestCase as TestCase; use RuntimeException; +use stdClass; use Zend\Filter; use Zend\InputFilter\Input; use Zend\Validator; +use Zend\Validator\ValidatorChain; class InputTest extends TestCase { @@ -95,6 +98,80 @@ public function testContinueIfEmptyFlagIsMutable() $this->assertTrue($input->continueIfEmpty()); } + /** + * @dataProvider setValueProvider + */ + public function testSetFallbackValue($fallbackValue) + { + $input = $this->input; + + $return = $input->setFallbackValue($fallbackValue); + $this->assertSame($input, $return, 'setFallbackValue() must return it self'); + + $this->assertEquals($fallbackValue, $input->getFallbackValue(), 'getFallbackValue() value not match'); + $this->assertEquals(true, $input->hasFallback(), 'hasFallback() value not match'); + } + + /** + * @dataProvider fallbackValueVsIsValidProvider + */ + public function testFallbackValueVsIsValidRules($required, $fallbackValue, $originalValue, $isValid, $expectedValue) + { + $input = $this->input; + $input->setContinueIfEmpty(true); + + $input->setRequired($required); + $input->setValidatorChain($this->createValidatorChainMock($isValid)); + $input->setFallbackValue($fallbackValue); + $input->setValue($originalValue); + + $this->assertTrue( + $input->isValid(), + 'isValid() should be return always true when fallback value is set. Detail: ' . + json_encode($input->getMessages()) + ); + $this->assertEquals([], $input->getMessages(), 'getMessages() should be empty because the input is valid'); + $this->assertSame($expectedValue, $input->getRawValue(), 'getRawValue() value not match'); + $this->assertSame($expectedValue, $input->getValue(), 'getValue() value not match'); + } + + /** + * @dataProvider fallbackValueVsIsValidProvider + */ + public function testFallbackValueVsIsValidRulesWhenValueNotSet($required, $fallbackValue, $originalValue, $isValid) + { + $expectedValue = $fallbackValue; // Should always return the fallback value + + $input = $this->input; + $input->setContinueIfEmpty(true); + + $input->setRequired($required); + $input->setValidatorChain($this->createValidatorChainMock($isValid)); + $input->setFallbackValue($fallbackValue); + + $this->assertTrue( + $input->isValid(), + 'isValid() should be return always true when fallback value is set. Detail: ' . + json_encode($input->getMessages()) + ); + $this->assertEquals([], $input->getMessages(), 'getMessages() should be empty because the input is valid'); + $this->assertSame($expectedValue, $input->getRawValue(), 'getRawValue() value not match'); + $this->assertSame($expectedValue, $input->getValue(), 'getValue() value not match'); + } + + public function testRequiredWithoutFallbackAndValueNotSetThenFail() + { + $input = $this->input; + $input->setRequired(true); + $input->setContinueIfEmpty(true); + + $this->assertFalse( + $input->isValid(), + 'isValid() should be return always false when no fallback value, is required, and not data is set.' + ); + $this->assertEquals(['Value is required'], $input->getMessages(), 'getMessages() should be empty because the input is valid'); + } + public function testNotEmptyValidatorNotInjectedIfContinueIfEmptyIsTrue() { $input = new Input('foo'); @@ -356,36 +433,6 @@ public function testDoNotInjectNotEmptyValidatorIfAnywhereInChain() $this->assertEquals($notEmptyMock, $validators[1]['instance']); } - public function dataFallbackValue() - { - return [ - [ - 'fallbackValue' => null - ], - [ - 'fallbackValue' => '' - ], - [ - 'fallbackValue' => 'some value' - ], - ]; - } - - /** - * @dataProvider dataFallbackValue - */ - public function testFallbackValue($fallbackValue) - { - $this->input->setFallbackValue($fallbackValue); - $validator = new Validator\Date(); - $this->input->getValidatorChain()->attach($validator); - $this->input->setValue('123'); // not a date - - $this->assertTrue($this->input->isValid()); - $this->assertEmpty($this->input->getMessages()); - $this->assertSame($fallbackValue, $this->input->getValue()); - } - public function testMergeRetainsContinueIfEmptyFlag() { $input = new Input('foo'); @@ -854,4 +901,116 @@ public function testWhenNotRequiredAndNotAllowEmptyAndContinueIfEmptyValidatorsA $input->setValue($value); $this->{$assertion}($input->isValid()); } + + /** + * @dataProvider emptyValuesProvider + */ + public function testSetValuePutInputInTheDesiredState($value) + { + $input = $this->input; + $this->assertFalse($input->hasValue(), 'Input should not have value by default'); + + $input->setValue($value); + $this->assertTrue($input->hasValue(), "hasValue() didn't return true when value was set"); + } + + /** + * @dataProvider emptyValuesProvider + */ + public function testResetValueReturnsInputValueToDefaultValue($value) + { + $input = $this->input; + $originalInput = clone $input; + $this->assertFalse($input->hasValue(), 'Input should not have value by default'); + + $input->setValue($value); + $this->assertTrue($input->hasValue(), "hasValue() didn't return true when value was set"); + + $return = $input->resetValue(); + $this->assertSame($input, $return, 'resetValue() must return itself'); + $this->assertEquals($originalInput, $input, 'Input was not reset to the default value state'); + } + + public function fallbackValueVsIsValidProvider() + { + $required = true; + $isValid = true; + + $originalValue = 'fooValue'; + $fallbackValue = 'fooFallbackValue'; + + // @codingStandardsIgnoreStart + return [ + // Description => [$inputIsRequired, $fallbackValue, $originalValue, $isValid, $expectedValue] + 'Required: T, Input: Invalid. getValue: fallback' => [ $required, $fallbackValue, $originalValue, !$isValid, $fallbackValue], + 'Required: T, Input: Valid. getValue: original' => [ $required, $fallbackValue, $originalValue, $isValid, $originalValue], + 'Required: F, Input: Invalid. getValue: fallback' => [!$required, $fallbackValue, $originalValue, !$isValid, $fallbackValue], + 'Required: F, Input: Valid. getValue: original' => [!$required, $fallbackValue, $originalValue, $isValid, $originalValue], + ]; + // @codingStandardsIgnoreEnd + } + + public function setValueProvider() + { + $emptyValues = $this->emptyValueProvider(); + $mixedValues = $this->mixedValueProvider(); + + $values = array_merge($emptyValues, $mixedValues); + + return $values; + } + + public function emptyValueProvider() + { + return [ + // Description => [$value] + 'null' => [null], + '""' => [''], +// '"0"' => ['0'], +// '0' => [0], +// '0.0' => [0.0], +// 'false' => [false], + '[]' => [[]], + ]; + } + + public function mixedValueProvider() + { + return [ + // Description => [$value] + '"0"' => ['0'], + '0' => [0], + '0.0' => [0.0], + 'false' => [false], + 'php' => ['php'], + 'whitespace' => [' '], + '1' => [1], + '1.0' => [1.0], + 'true' => [true], + '["php"]' => [['php']], + 'object' => [new stdClass()], + // @codingStandardsIgnoreStart + 'callable' => [function () {}], + // @codingStandardsIgnoreEnd + ]; + } + + /** + * @param null|bool $isValid If set stub isValid method for return the argument value. + * + * @return MockObject|ValidatorChain + */ + protected function createValidatorChainMock($isValid = null) + { + /** @var ValidatorChain|MockObject $validatorChain */ + $validatorChain = $this->getMock(ValidatorChain::class); + + if ($isValid !== null) { + $validatorChain->method('isValid') + ->willReturn($isValid) + ; + } + + return $validatorChain; + } }