From 8e0d27da097f582cd868be5f2a363b6592b02da3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 30 Mar 2012 09:05:31 -0500 Subject: [PATCH 01/23] [zen-46] Created Input interface and implementation - Wrote tests describing behavior - Wrote interface for Inputs - Wrote implementation for Inputs - Wrote initial tests for InputFilter --- src/Input.php | 139 ++++++++++++++++++++++++++++ src/InputInterface.php | 51 +++++++++++ test/InputFilterTest.php | 191 +++++++++++++++++++++++++++++++++++++++ test/InputTest.php | 165 +++++++++++++++++++++++++++++++++ 4 files changed, 546 insertions(+) create mode 100644 src/Input.php create mode 100644 src/InputInterface.php create mode 100644 test/InputFilterTest.php create mode 100644 test/InputTest.php diff --git a/src/Input.php b/src/Input.php new file mode 100644 index 00000000..833a6862 --- /dev/null +++ b/src/Input.php @@ -0,0 +1,139 @@ +name = $name; + } + + public function setAllowEmpty($allowEmpty) + { + $this->allowEmpty = (bool) $allowEmpty; + return $this; + } + + public function setFilterChain(FilterChain $filterChain) + { + $this->filterChain = $filterChain; + return $this; + } + + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + public function setRequired($required) + { + $this->required = (bool) $required; + return $this; + } + + public function setValidatorChain(ValidatorChain $validatorChain) + { + $this->validatorChain = $validatorChain; + return $this; + } + + public function setValue($value) + { + $this->value = $value; + return $this; + } + + + public function allowEmpty() + { + return $this->allowEmpty; + } + + public function getFilterChain() + { + if (!$this->filterChain) { + $this->setFilterChain(new FilterChain()); + } + return $this->filterChain; + } + + public function getName() + { + return $this->name; + } + + public function getRawValue() + { + return $this->value; + } + + public function isRequired() + { + return $this->required; + } + + public function getValidatorChain() + { + if (!$this->validatorChain) { + $this->setValidatorChain(new ValidatorChain()); + } + return $this->validatorChain; + } + + public function getValue() + { + $filter = $this->getFilterChain(); + return $filter->filter($this->value); + } + + + public function isValid() + { + $validator = $this->getValidatorChain(); + $value = $this->getValue(); + return $validator->isValid($value); + } + + public function getMessages() + { + $validator = $this->getValidatorChain(); + return $validator->getMessages(); + } +} + diff --git a/src/InputInterface.php b/src/InputInterface.php new file mode 100644 index 00000000..2f91b829 --- /dev/null +++ b/src/InputInterface.php @@ -0,0 +1,51 @@ +assertEquals(0, count($filter)); + } + + public function testAddingInputsIncreasesCountOfFilter() + { + $filter = new InputFilter(); + $filter->add('foo'); + $this->assertEquals(1, count($filter)); + $filter->add('bar'); + $this->assertEquals(2, count($filter)); + } + + public function testAddingAnInputCreatesAnEmptyValidatorChain() + { + $filter = new InputFilter(); + $filter->add('foo'); + $input = $filter->get('foo'); + $validators = $input->getValidatorChain(); + $this->assertInstanceOf('Zend\Validator\ValidatorChain', $validators); + $this->assertEquals(0, count($validators)); + } + + public function testAddingAnInputCreatesAnEmptyFilterChain() + { + $filter = new InputFilter(); + $filter->add('foo'); + $input = $filter->get('foo'); + $filters = $input->getFilterChain(); + $this->assertInstanceOf('Zend\Filter\FilterChain', $filters); + $this->assertEquals(0, count($filters)); + } + + public function testCanAddInputWithValidatorChain() + { + $filter = new InputFilter(); + $chain = new Validator\ValidatorChain(); + $filter->add('foo', $chain); + $input = $filter->get('foo'); + $test = $input->getValidatorChain(); + $this->assertSame($chain, $test); + } + + public function testCanAddInputWithFilterChain() + { + $filter = new InputFilter(); + $chain = new Filter\FilterChain(); + $filter->add('foo', $chain); + $input = $filter->get('foo'); + $test = $input->getFilterChain(); + $this->assertSame($chain, $test); + } + + public function testCanAddInputWithBothFilterAndValidatorChains() + { + $filter = new InputFilter(); + $validators = new Validator\ValidatorChain(); + $filters = new Filter\FilterChain(); + $filter->add('foo', $filters, $validators); + $input = $filter->get('foo'); + $test = $input->getFilterChain(); + $this->assertSame($filters, $test); + $test = $input->getValidatorChain(); + $this->assertSame($validators, $test); + } + + public function testCanAddInputWithBothFilterAndValidatorChainsAndOrderOfArgumentsDoesNotMatter() + { + $filter = new InputFilter(); + $validators = new Validator\ValidatorChain(); + $filters = new Filter\FilterChain(); + $filter->add('foo', $validators, $filters); + $input = $filter->get('foo'); + $test = $input->getFilterChain(); + $this->assertSame($filters, $test); + $test = $input->getValidatorChain(); + $this->assertSame($validators, $test); + } + + public function testCanAddConcreteInputToInputFilter() + { + $this->markTestIncomplete(); + } + + public function testUsesFrameworkFilterBrokerByDefault() + { + $this->markTestIncomplete(); + } + + public function testCanInjectAFilterBroker() + { + $this->markTestIncomplete(); + } + + public function testInjectedFilterBrokerIsInjectedIntoAllInputsCreatedByInputFilter() + { + $this->markTestIncomplete(); + } + + public function testInjectedFilterBrokerIsNotInjectedIntoConcreteInputsAddedToInputFilter() + { + $this->markTestIncomplete(); + } + + public function testUsesFrameworkValidatorBrokerByDefault() + { + $this->markTestIncomplete(); + } + + public function testCanInjectAValidatorBroker() + { + $this->markTestIncomplete(); + } + + public function testInjectedValidatorBrokerIsInjectedIntoAllInputsCreatedByInputFilter() + { + $this->markTestIncomplete(); + } + + public function testInjectedValidatorBrokerIsNotInjectedIntoConcreteInputsAddedToInputFilter() + { + $this->markTestIncomplete(); + } + + public function testCanAddInputFilterAsInput() + { + $this->markTestIncomplete(); + } + + public function testCanValidateEntireDataset() + { + $this->markTestIncomplete(); + } + + public function testCanValidatePartialDataset() + { + $this->markTestIncomplete(); + } + + public function testCanRetrieveInvalidInputsOnFailedValidation() + { + $this->markTestIncomplete(); + } + + public function testCanRetrieveValidInputsOnFailedValidation() + { + $this->markTestIncomplete(); + } + + public function testValuesRetrievedAreFiltered() + { + $this->markTestIncomplete(); + } + + public function testCanGetRawInputValues() + { + $this->markTestIncomplete(); + } +} diff --git a/test/InputTest.php b/test/InputTest.php new file mode 100644 index 00000000..f0c16d6c --- /dev/null +++ b/test/InputTest.php @@ -0,0 +1,165 @@ +assertEquals('foo', $input->getName()); + } + + public function testInputHasEmptyFilterChainByDefault() + { + $input = new Input('foo'); + $filters = $input->getFilterChain(); + $this->assertInstanceOf('Zend\Filter\FilterChain', $filters); + $this->assertEquals(0, count($filters)); + } + + public function testInputHasEmptyValidatorChainByDefault() + { + $input = new Input('foo'); + $validators = $input->getValidatorChain(); + $this->assertInstanceOf('Zend\Validator\ValidatorChain', $validators); + $this->assertEquals(0, count($validators)); + } + + public function testCanInjectFilterChain() + { + $input = new Input('foo'); + $chain = new Filter\FilterChain(); + $input->setFilterChain($chain); + $this->assertSame($chain, $input->getFilterChain()); + } + + public function testCanInjectValidatorChain() + { + $input = new Input('foo'); + $chain = new Validator\ValidatorChain(); + $input->setValidatorChain($chain); + $this->assertSame($chain, $input->getValidatorChain()); + } + + public function testInputIsMarkedAsRequiredByDefault() + { + $input = new Input('foo'); + $this->assertTrue($input->isRequired()); + } + + public function testRequiredFlagIsMutable() + { + $input = new Input('foo'); + $input->setRequired(false); + $this->assertFalse($input->isRequired()); + } + + public function testInputDoesNotAllowEmptyValuesByDefault() + { + $input = new Input('foo'); + $this->assertFalse($input->allowEmpty()); + } + + public function testAllowEmptyFlagIsMutable() + { + $input = new Input('foo'); + $input->setAllowEmpty(true); + $this->assertTrue($input->allowEmpty()); + } + + public function testValueIsNullByDefault() + { + $input = new Input('foo'); + $this->assertNull($input->getValue()); + } + + public function testValueMayBeInjected() + { + $input = new Input('foo'); + $input->setValue('bar'); + $this->assertEquals('bar', $input->getValue()); + } + + public function testRetrievingValueFiltersTheValue() + { + $input = new Input('foo'); + $input->setValue('bar'); + $filter = new Filter\StringToUpper(); + $input->getFilterChain()->attach($filter); + $this->assertEquals('BAR', $input->getValue()); + } + + public function testCanRetrieveRawValue() + { + $input = new Input('foo'); + $input->setValue('bar'); + $filter = new Filter\StringToUpper(); + $input->getFilterChain()->attach($filter); + $this->assertEquals('bar', $input->getRawValue()); + } + + public function testIsValidReturnsFalseIfValidationChainFails() + { + $input = new Input('foo'); + $input->setValue('bar'); + $validator = new Validator\Digits(); + $input->getValidatorChain()->addValidator($validator); + $this->assertFalse($input->isValid()); + } + + public function testIsValidReturnsTrueIfValidationChainSucceeds() + { + $input = new Input('foo'); + $input->setValue('bar'); + $validator = new Validator\Alpha(); + $input->getValidatorChain()->addValidator($validator); + $this->assertTrue($input->isValid()); + } + + public function testValidationOperatesOnFilteredValue() + { + $input = new Input('foo'); + $input->setValue(' bar '); + $filter = new Filter\StringTrim(); + $input->getFilterChain()->attach($filter); + $validator = new Validator\Alpha(); + $input->getValidatorChain()->addValidator($validator); + $this->assertTrue($input->isValid()); + } + + public function testGetMessagesReturnsValidationMessages() + { + $input = new Input('foo'); + $input->setValue('bar'); + $validator = new Validator\Digits(); + $input->getValidatorChain()->addValidator($validator); + $this->assertFalse($input->isValid()); + $messages = $input->getMessages(); + $this->assertArrayHasKey(Validator\Digits::NOT_DIGITS, $messages); + } +} From 1ca0828c60c09f451ac0bfb4a8c60c00dae3f696 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 30 Mar 2012 12:12:48 -0500 Subject: [PATCH 02/23] [zen-46] Identified all tests for InputFilter, Factory - Identified unit tests for InputFilter - Identified unit tests for InputFilter Factory --- test/FactoryTest.php | 102 ++++++++++++++++++++++++++++++++ test/InputFilterTest.php | 123 ++++----------------------------------- 2 files changed, 112 insertions(+), 113 deletions(-) create mode 100644 test/FactoryTest.php diff --git a/test/FactoryTest.php b/test/FactoryTest.php new file mode 100644 index 00000000..8a852e01 --- /dev/null +++ b/test/FactoryTest.php @@ -0,0 +1,102 @@ +markTestIncomplete(); + } + + public function testFactoryComposesFrameworkValidatorChainByDefault() + { + $this->markTestIncomplete(); + } + + public function testFactoryAllowsInjectingFilterChain() + { + $this->markTestIncomplete(); + } + + public function testFactoryAllowsInjectingValidatorChain() + { + $this->markTestIncomplete(); + } + + public function testFactoryUsesComposedFilterChainWhenCreatingNewInputObjects() + { + $this->markTestIncomplete(); + } + + public function testFactoryUsesComposedValidatorChainWhenCreatingNewInputObjects() + { + $this->markTestIncomplete(); + } + + public function testFactoryInjectsComposedFilterChainWhenCreatingNewInputFilterObjects() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateInputWithSuggestedFilters() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateInputWithSuggestedValidators() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateInputWithSuggestedRequiredFlag() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateInputWithSuggestedAllowEmptyFlag() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateInputWithSuggestedName() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateInputFilterAndAllInputObjectsFromGivenConfiguration() + { + $this->markTestIncomplete(); + } + + public function testFactoryWillCreateNestedInputFiltersFromGivenConfiguration() + { + $this->markTestIncomplete(); + } +} diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php index 3c4ef892..8e13e370 100644 --- a/test/InputFilterTest.php +++ b/test/InputFilterTest.php @@ -22,6 +22,7 @@ namespace ZendTest\InputFilter; use PHPUnit_Framework_TestCase as TestCase; +use Zend\InputFilter\Input; use Zend\InputFilter\InputFilter; use Zend\Filter; use Zend\Validator; @@ -37,123 +38,14 @@ public function testInputFilterIsEmptyByDefault() public function testAddingInputsIncreasesCountOfFilter() { $filter = new InputFilter(); - $filter->add('foo'); + $foo = new Input('foo'); + $filter->add($foo); $this->assertEquals(1, count($filter)); - $filter->add('bar'); + $bar = new Input('bar'); + $filter->add($bar); $this->assertEquals(2, count($filter)); } - public function testAddingAnInputCreatesAnEmptyValidatorChain() - { - $filter = new InputFilter(); - $filter->add('foo'); - $input = $filter->get('foo'); - $validators = $input->getValidatorChain(); - $this->assertInstanceOf('Zend\Validator\ValidatorChain', $validators); - $this->assertEquals(0, count($validators)); - } - - public function testAddingAnInputCreatesAnEmptyFilterChain() - { - $filter = new InputFilter(); - $filter->add('foo'); - $input = $filter->get('foo'); - $filters = $input->getFilterChain(); - $this->assertInstanceOf('Zend\Filter\FilterChain', $filters); - $this->assertEquals(0, count($filters)); - } - - public function testCanAddInputWithValidatorChain() - { - $filter = new InputFilter(); - $chain = new Validator\ValidatorChain(); - $filter->add('foo', $chain); - $input = $filter->get('foo'); - $test = $input->getValidatorChain(); - $this->assertSame($chain, $test); - } - - public function testCanAddInputWithFilterChain() - { - $filter = new InputFilter(); - $chain = new Filter\FilterChain(); - $filter->add('foo', $chain); - $input = $filter->get('foo'); - $test = $input->getFilterChain(); - $this->assertSame($chain, $test); - } - - public function testCanAddInputWithBothFilterAndValidatorChains() - { - $filter = new InputFilter(); - $validators = new Validator\ValidatorChain(); - $filters = new Filter\FilterChain(); - $filter->add('foo', $filters, $validators); - $input = $filter->get('foo'); - $test = $input->getFilterChain(); - $this->assertSame($filters, $test); - $test = $input->getValidatorChain(); - $this->assertSame($validators, $test); - } - - public function testCanAddInputWithBothFilterAndValidatorChainsAndOrderOfArgumentsDoesNotMatter() - { - $filter = new InputFilter(); - $validators = new Validator\ValidatorChain(); - $filters = new Filter\FilterChain(); - $filter->add('foo', $validators, $filters); - $input = $filter->get('foo'); - $test = $input->getFilterChain(); - $this->assertSame($filters, $test); - $test = $input->getValidatorChain(); - $this->assertSame($validators, $test); - } - - public function testCanAddConcreteInputToInputFilter() - { - $this->markTestIncomplete(); - } - - public function testUsesFrameworkFilterBrokerByDefault() - { - $this->markTestIncomplete(); - } - - public function testCanInjectAFilterBroker() - { - $this->markTestIncomplete(); - } - - public function testInjectedFilterBrokerIsInjectedIntoAllInputsCreatedByInputFilter() - { - $this->markTestIncomplete(); - } - - public function testInjectedFilterBrokerIsNotInjectedIntoConcreteInputsAddedToInputFilter() - { - $this->markTestIncomplete(); - } - - public function testUsesFrameworkValidatorBrokerByDefault() - { - $this->markTestIncomplete(); - } - - public function testCanInjectAValidatorBroker() - { - $this->markTestIncomplete(); - } - - public function testInjectedValidatorBrokerIsInjectedIntoAllInputsCreatedByInputFilter() - { - $this->markTestIncomplete(); - } - - public function testInjectedValidatorBrokerIsNotInjectedIntoConcreteInputsAddedToInputFilter() - { - $this->markTestIncomplete(); - } - public function testCanAddInputFilterAsInput() { $this->markTestIncomplete(); @@ -188,4 +80,9 @@ public function testCanGetRawInputValues() { $this->markTestIncomplete(); } + + public function testCanGetValidationMessages() + { + $this->markTestIncomplete(); + } } From a9fdb9b480d2c0c081e9bc18adb54bcdc90fe785 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 30 Mar 2012 16:34:18 -0500 Subject: [PATCH 03/23] [zen-46] InputFilter tests - InputFilter tests are mostly complete. Incomplete tests include: - conditional validation (i.e., only validate if another input passed/failed/is present, etc) - ability to pass context to validators - Skip validation of fields marked required when no data is present - skip validation of required fields when allow empty is true and no value is passed - invalid data on to required fields when allow empty is false and no value is passed --- src/Input.php | 2 +- test/InputFilterTest.php | 248 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 242 insertions(+), 8 deletions(-) diff --git a/src/Input.php b/src/Input.php index 833a6862..ff34cd9b 100644 --- a/src/Input.php +++ b/src/Input.php @@ -38,7 +38,7 @@ class Input implements InputInterface protected $validatorChain; protected $value; - public function __construct($name) + public function __construct($name = null) { $this->name = $name; } diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php index 8e13e370..45ebdee9 100644 --- a/test/InputFilterTest.php +++ b/test/InputFilterTest.php @@ -46,42 +46,276 @@ public function testAddingInputsIncreasesCountOfFilter() $this->assertEquals(2, count($filter)); } + public function testAddingInputWithNameInjectsNameInInput() + { + $filter = new InputFilter(); + $foo = new Input('foo'); + $filter->add($foo, 'bar'); + $test = $filter->get('bar'); + $this->assertSame($foo, $test); + $this->assertEquals('bar', $foo->getName()); + } + public function testCanAddInputFilterAsInput() { - $this->markTestIncomplete(); + $parent = new InputFilter(); + $child = new InputFilter(); + $parent->add($child, 'child'); + $this->assertEquals(1, count($parent)); + $this->assertSame($child, $parent->get('child')); + } + + public function getInputFilter() + { + $filter = new InputFilter(); + + $foo = new Input(); + $foo->getFilterChain()->attachByName('stringtrim') + ->attachByName('alpha'); + $foo->getValidatorChain()->addValidator(new Validator\StringLength(3, 6)); + + $bar = new Input(); + $bar->getFilterChain()->attachByName('stringtrim'); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + + $filter->add($foo, 'foo') + ->add($bar, 'bar') + ->add($this->getChildInputFilter(), 'nest'); + + return $filter; + } + + public function getChildInputFilter() + { + $filter = new InputFilter(); + + $foo = new Input(); + $foo->getFilterChain()->attachByName('stringtrim') + ->attachByName('alpha'); + $foo->getValidatorChain()->addValidator(new Validator\StringLength(3, 6)); + + $bar = new Input(); + $bar->getFilterChain()->attachByName('stringtrim'); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + + $filter->add($foo, 'foo') + ->add($bar, 'bar'); + return $filter; } public function testCanValidateEntireDataset() { - $this->markTestIncomplete(); + $filter = $this->getInputFilter(); + $validData = array( + 'foo' => ' bazbat ', + 'bar' => '12345', + 'nest' => array( + 'foo' => ' bazbat ', + 'bar' => '12345', + ), + ); + $filter->setData($validData); + $this->assertTrue($filter->isValid()); + + $invalidData = array( + 'foo' => ' baz bat ', + 'bar' => 'abc45', + 'nest' => array( + 'foo' => ' baz bat ', + 'bar' => '123ab', + ), + ); + $filter->setData($invalidData); + $this->assertFalse($filter->isValid()); } public function testCanValidatePartialDataset() { - $this->markTestIncomplete(); + $filter = $this->getInputFilter(); + $validData = array( + 'foo' => ' bazbat ', + 'bar' => '12345', + ); + $filter->setValidationGroup('foo', 'bar'); + $filter->setData($validData); + $this->assertTrue($filter->isValid()); + + $invalidData = array( + 'bar' => 'abc45', + 'nest' => array( + 'foo' => ' 123bat ', + 'bar' => '123ab', + ), + ); + $filter->setValidationGroup('bar', 'nest'); + $filter->setData($invalidData); + $this->assertFalse($filter->isValid()); } public function testCanRetrieveInvalidInputsOnFailedValidation() { - $this->markTestIncomplete(); + $filter = $this->getInputFilter(); + $invalidData = array( + 'foo' => ' bazbat ', + 'bar' => 'abc45', + 'nest' => array( + 'foo' => ' baz bat ', + 'bar' => '12345', + ), + ); + $filter->setData($invalidData); + $this->assertFalse($filter->isValid()); + $invalidInputs = $filter->getInvalidInput(); + $this->assertArrayNotHasKey('foo', $invalidInputs); + $this->assertArrayHasKey('bar', $invalidInputs); + $this->assertInstanceOf('Zend\InputFilter\Input', $invalidInputs['bar']); + $this->assertArrayHasKey('nest', $invalidInputs); + $this->assertInternalType('array', $invalidInputs['nest']); + $nestInvalids = $invalidInputs['nest']; + $this->assertArrayHasKey('foo', $nestInvalids); + $this->assertInstanceOf('Zend\InputFilter\Input', $nestInvalids['foo']); + $this->assertArrayNotHasKey('bar', $invalidInputs); } public function testCanRetrieveValidInputsOnFailedValidation() { - $this->markTestIncomplete(); + $filter = $this->getInputFilter(); + $invalidData = array( + 'foo' => ' bazbat ', + 'bar' => 'abc45', + 'nest' => array( + 'foo' => ' baz bat ', + 'bar' => '12345', + ), + ); + $filter->setData($invalidData); + $this->assertFalse($filter->isValid()); + $validInputs = $filter->getValidInput(); + $this->assertArrayHasKey('foo', $invalidInputs); + $this->assertInstanceOf('Zend\InputFilter\Input', $invalidInputs['foo']); + $this->assertArrayNotHasKey('bar', $invalidInputs); + $this->assertArrayHasKey('nest', $invalidInputs); + $this->assertInternalType('array', $invalidInputs['nest']); + $nestValids = $validInputs['nest']; + $this->assertArrayNotHasKey('foo', $nestInvalids); + $this->assertArrayHasKey('bar', $invalidInputs); + $this->assertInstanceOf('Zend\InputFilter\Input', $nestInvalids['bar']); } public function testValuesRetrievedAreFiltered() { - $this->markTestIncomplete(); + $filter = $this->getInputFilter(); + $validData = array( + 'foo' => ' bazbat ', + 'bar' => '12345', + 'nest' => array( + 'foo' => ' bazbat ', + 'bar' => '12345', + ), + ); + $filter->setData($validData); + $this->assertTrue($filter->isValid()); + $expected = array( + 'foo' => 'bazbat', + 'bar' => '12345', + 'nest' => array( + 'foo' => 'bazbat', + 'bar' => '12345', + ), + ); + $this->assertEquals($expected, $filter->getValues()); } public function testCanGetRawInputValues() { - $this->markTestIncomplete(); + $filter = $this->getInputFilter(); + $validData = array( + 'foo' => ' bazbat ', + 'bar' => '12345', + 'nest' => array( + 'foo' => ' bazbat ', + 'bar' => '12345', + ), + ); + $filter->setData($validData); + $this->assertTrue($filter->isValid()); + $this->assertEquals($validData, $filter->getRawValues()); } public function testCanGetValidationMessages() + { + $filter = $this->getInputFilter(); + $invalidData = array( + 'foo' => ' bazbat ', + 'bar' => 'abc45', + 'nest' => array( + 'foo' => ' baz bat ', + 'bar' => '12345', + ), + ); + $filter->setData($invalidData); + $this->assertFalse($filter->isValid()); + $messages = $filter->getMessages(); + foreach ($invalidData as $key => $value) { + $this->assertArrayHasKey($key, $messages); + $currentMessages = $messages[$key]; + switch ($key) { + case 'foo': + $this->assertArrayHasKey(Validator\StringLength::TOO_LONG, $currentMessages); + break; + case 'bar': + $this->assertArrayHasKey(Validator\Digits::NOT_DIGITS, $currentMessages); + break; + case 'nest': + foreach ($value as $k => $v) { + $this->assertArrayHasKey($k, $messages[$key]); + $currentMessages = $messages[$key][$k]; + switch ($k) { + case 'foo': + $this->assertArrayHasKey(Validator\StringLength::TOO_LONG, $currentMessages); + break; + case 'bar': + $this->assertArrayHasKey(Validator\Digits::NOT_DIGITS, $currentMessages); + break; + default: + $this->fail(sprintf('Invalid key "%s" encountered in messages array', $k)); + } + } + break; + default: + $this->fail(sprintf('Invalid key "%s" encountered in messages array', $k)); + } + } + } + + /** + * Idea for this one is that one input may only need to be validated if another input is present. + */ + public function testCanConditionallyInvokeValidators() + { + $this->markTestIncomplete(); + } + + /** + * Idea for this one is that validation may need to rely on context -- e.g., a "password confirmation" + * field may need to know what the original password entered was in order to compare. + */ + public function testValidationCanUseContext() + { + $this->markTestIncomplete(); + } + + public function testValidationSkipsFieldsMarkedNotRequiredWhenNoDataPresent() + { + $this->markTestIncomplete(); + } + + public function testValidationAllowsEmptyValuesToRequiredInputWhenAllowEmptyFlagIsTrue() + { + $this->markTestIncomplete(); + } + + public function testValidationMarksInputInvalidWhenRequiredAndAllowEmptyFlagIsFalse() { $this->markTestIncomplete(); } From 30b0df9dbfa8b94fb4c1203464de3af45a0161fb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 3 Apr 2012 15:43:57 -0500 Subject: [PATCH 04/23] Added break-on-failure, error-message fields to Input - Added breakOnFailure flag - Added errorMessage string - Added more tests to InputFilter suite --- src/Input.php | 24 ++++++++++++++++++++++++ src/InputInterface.php | 3 +++ test/InputTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/Input.php b/src/Input.php index ff34cd9b..6fadf0c3 100644 --- a/src/Input.php +++ b/src/Input.php @@ -32,6 +32,8 @@ class Input implements InputInterface { protected $allowEmpty = false; + protected $breakOnFailure = false; + protected $errorMessage; protected $filterChain; protected $name; protected $required = true; @@ -49,6 +51,19 @@ public function setAllowEmpty($allowEmpty) return $this; } + public function setBreakOnFailure($breakOnFailure) + { + $this->breakOnFailure = (bool) $breakOnFailure; + return $this; + } + + public function setErrorMessage($errorMessage) + { + $errorMessage = (null === $errorMessage) ? null : (string) $errorMessage; + $this->errorMessage = $errorMessage; + return $this; + } + public function setFilterChain(FilterChain $filterChain) { $this->filterChain = $filterChain; @@ -85,6 +100,11 @@ public function allowEmpty() return $this->allowEmpty; } + public function breakOnFailure() + { + return $this->breakOnFailure; + } + public function getFilterChain() { if (!$this->filterChain) { @@ -132,6 +152,10 @@ public function isValid() public function getMessages() { + if (null !== $this->errorMessage) { + return (array) $this->errorMessage; + } + $validator = $this->getValidatorChain(); return $validator->getMessages(); } diff --git a/src/InputInterface.php b/src/InputInterface.php index 2f91b829..253ee190 100644 --- a/src/InputInterface.php +++ b/src/InputInterface.php @@ -32,6 +32,8 @@ interface InputInterface { public function setAllowEmpty($allowEmpty); + public function setBreakOnFailure($breakOnFailure); + public function setErrorMessage($errorMessage); public function setFilterChain(FilterChain $filterChain); public function setName($name); public function setRequired($required); @@ -39,6 +41,7 @@ public function setValidatorChain(ValidatorChain $validatorChain); public function setValue($value); public function allowEmpty(); + public function breakOnFailure(); public function getFilterChain(); public function getName(); public function getRawValue(); diff --git a/test/InputTest.php b/test/InputTest.php index f0c16d6c..a70bb010 100644 --- a/test/InputTest.php +++ b/test/InputTest.php @@ -162,4 +162,30 @@ public function testGetMessagesReturnsValidationMessages() $messages = $input->getMessages(); $this->assertArrayHasKey(Validator\Digits::NOT_DIGITS, $messages); } + + public function testSpecifyingMessagesToInputReturnsThoseOnFailedValidation() + { + $input = new Input('foo'); + $input->setValue('bar'); + $validator = new Validator\Digits(); + $input->getValidatorChain()->addValidator($validator); + $input->setErrorMessage('Please enter only digits'); + $this->assertFalse($input->isValid()); + $messages = $input->getMessages(); + $this->assertArrayNotHasKey(Validator\Digits::NOT_DIGITS, $messages); + $this->assertContains('Please enter only digits', $messages); + } + + public function testBreakOnFailureFlagIsOffByDefault() + { + $input = new Input('foo'); + $this->assertFalse($input->breakOnFailure()); + } + + public function testBreakOnFailureFlagIsMutable() + { + $input = new Input('foo'); + $input->setBreakOnFailure(true); + $this->assertTrue($input->breakOnFailure()); + } } From b2416fe21ecb94bb0c745d5c33170157f76f1b56 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 3 Apr 2012 16:11:09 -0500 Subject: [PATCH 05/23] Fleshed out remaining use case tests for InputFilter --- test/InputFilterTest.php | 116 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php index 45ebdee9..11c7eb65 100644 --- a/test/InputFilterTest.php +++ b/test/InputFilterTest.php @@ -22,6 +22,7 @@ namespace ZendTest\InputFilter; use PHPUnit_Framework_TestCase as TestCase; +use stdClass; use Zend\InputFilter\Input; use Zend\InputFilter\InputFilter; use Zend\Filter; @@ -290,11 +291,13 @@ public function testCanGetValidationMessages() /** * Idea for this one is that one input may only need to be validated if another input is present. - */ + * + * Commenting out for now, as validation context may make this irrelevant, and unsure what API to expose. public function testCanConditionallyInvokeValidators() { $this->markTestIncomplete(); } + */ /** * Idea for this one is that validation may need to rely on context -- e.g., a "password confirmation" @@ -302,21 +305,124 @@ public function testCanConditionallyInvokeValidators() */ public function testValidationCanUseContext() { - $this->markTestIncomplete(); + $filter = new InputFilter(); + + $store = new stdClass; + $foo = new Input(); + $foo->getValidatorChain()->addValidator(new Validator\Callback(function($value, $context) use ($store) { + $store->value = $value; + $store->context = $context; + return true; + })); + + $bar = new Input(); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + + $filter->add($foo, 'foo') + ->add($bar, 'bar'); + + $data = array('foo' => 'foo', 'bar' => 123); + $filter->setData($data); + + $this->assertTrue($filter->isValid()); + $this->assertEquals('foo', $store->value); + $this->assertEquals($data, $store->context); + } + + /** + * Idea here is that you can indicate that if validation for a particular input fails, we should not + * attempt any further validation of any other inputs. + */ + public function testInputBreakOnFailureFlagIsHonoredWhenValidating() + { + $filter = new InputFilter(); + + $store = new stdClass; + $foo = new Input(); + $foo->getValidatorChain()->addValidator(new Validator\Callback(function($value, $context) use ($store) { + $store->value = $value; + $store->context = $context; + return true; + })); + + $bar = new Input(); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + $bar->setBreakOnFailure(true); + + $filter->add($bar, 'bar') // adding bar first, as we want it to validate first and break the chain + ->add($foo, 'foo'); + + $data = array('bar' => 'bar', 'foo' => 'foo'); + $filter->setData($data); + + $this->assertFalse($filter->isValid()); + $this->assertObjectNotHasAttribute('value', $store); + $this->assertObjectNotHasAttribute('context', $store); } public function testValidationSkipsFieldsMarkedNotRequiredWhenNoDataPresent() { - $this->markTestIncomplete(); + $filter = new InputFilter(); + + $foo = new Input(); + $foo->getValidatorChain()->addValidator(new Validator\StringLength(3, 5)); + $foo->setRequired(false); + + $bar = new Input(); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + $bar->setRequired(true); + + $filter->add($foo, 'foo') + ->add($bar, 'bar'); + + $data = array('bar' => 124); + $filter->setData($data); + + $this->assertTrue($filter->isValid()); } public function testValidationAllowsEmptyValuesToRequiredInputWhenAllowEmptyFlagIsTrue() { - $this->markTestIncomplete(); + $filter = new InputFilter(); + + $foo = new Input(); + $foo->getValidatorChain()->addValidator(new Validator\StringLength(3, 5)); + $foo->setRequired(true); + $foo->setAllowEmpty(true); + + $bar = new Input(); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + $bar->setRequired(true); + + $filter->add($foo, '') + ->add($bar, 'bar'); + + $data = array('bar' => 124); + $filter->setData($data); + + $this->assertTrue($filter->isValid()); + $this->assertEquals('', $filter->getValue('foo')); } public function testValidationMarksInputInvalidWhenRequiredAndAllowEmptyFlagIsFalse() { - $this->markTestIncomplete(); + $filter = new InputFilter(); + + $foo = new Input(); + $foo->getValidatorChain()->addValidator(new Validator\StringLength(3, 5)); + $foo->setRequired(true); + $foo->setAllowEmpty(false); + + $bar = new Input(); + $bar->getValidatorChain()->addValidator(new Validator\Digits()); + $bar->setRequired(true); + + $filter->add($foo, '') + ->add($bar, 'bar'); + + $data = array('bar' => 124); + $filter->setData($data); + + $this->assertFalse($filter->isValid()); } } From cde1289954184932fe91734de825b422006949a3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 3 Apr 2012 16:28:24 -0500 Subject: [PATCH 06/23] Created InputFilterInterface - Created InputFilterInterface based on test cases - Marked one test with a todo -- questioning whether we should change the name on an Input, or simply store a hashtable internally --- src/InputFilterInterface.php | 127 +++++++++++++++++++++++++++++++++++ test/InputFilterTest.php | 3 + 2 files changed, 130 insertions(+) create mode 100644 src/InputFilterInterface.php diff --git a/src/InputFilterInterface.php b/src/InputFilterInterface.php new file mode 100644 index 00000000..b0b9e1f0 --- /dev/null +++ b/src/InputFilterInterface.php @@ -0,0 +1,127 @@ +assertEquals(2, count($filter)); } + /** + * @todo Should we do this? or simply alias the input internally? + */ public function testAddingInputWithNameInjectsNameInInput() { $filter = new InputFilter(); From 290de16f676039a7cda60610b1482cfd209b6231 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 3 Apr 2012 17:04:54 -0500 Subject: [PATCH 07/23] [zen-46] initial implementation of InputFilter - adding/retrieving input objects - setting data to validate - partial isValid() implementation - fails tests - raises questions about nesting input filters, how to mark empty, required elements as invalid so as to receive a message --- src/Exception.php | 33 +++ src/Exception/InvalidArgumentException.php | 35 +++ src/Exception/RuntimeException.php | 35 +++ src/Input.php | 5 +- src/InputFilter.php | 263 +++++++++++++++++++++ src/InputFilterInterface.php | 7 +- 6 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 src/Exception.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/InputFilter.php diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 00000000..a433edc2 --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,33 @@ +filter($this->value); } - - public function isValid() + public function isValid($context = null) { $validator = $this->getValidatorChain(); $value = $this->getValue(); - return $validator->isValid($value); + return $validator->isValid($value, $context); } public function getMessages() diff --git a/src/InputFilter.php b/src/InputFilter.php new file mode 100644 index 00000000..66e35691 --- /dev/null +++ b/src/InputFilter.php @@ -0,0 +1,263 @@ +inputs); + } + + /** + * Add an input to the input filter + * + * @param InputInterface|InputFilterInterface $input + * @param null|string $name Name used to retrieve this input + * @return InputFilterInterface + */ + public function add($input, $name = null) + { + if (!$input instanceof InputInterface && !$input instanceof InputFilterInterface) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an instance of %s or %s as its first argument; received "%s"', + __METHOD__, + 'Zend\InputFilter\InputInterface', + 'Zend\InputFilter\InputFilterInterface', + (is_object($input) ? get_class($input) : gettype($input)) + )); + } + + if ($name === null) { + $name = $input->getName(); + } + $this->inputs[$name] = $input; + return $this; + } + + /** + * Retrieve a named input + * + * @param string $name + * @return InputInterface|InputFilterInterface + */ + public function get($name) + { + if (!array_key_exists($name, $this->inputs)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: no input found matching "%s"', + __METHOD__, + $name + )); + } + return $this->inputs[$name]; + } + + /** + * Set data to use when validating and filtering + * + * @param array|Traversable $data + * @return InputFilterInterface + */ + public function setData($data) + { + if (!is_array($data) && !$data instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or Traversable argument; received %s', + __METHOD__, + (is_object($data) ? get_class($data) : gettype($data)) + )); + } + if (is_object($data) && !$data instanceof ArrayAccess) { + $data = ArrayUtils::iteratorToArray($data); + } + $this->data = $data; + return $this; + } + + /** + * Is the data set valid? + * + * @return bool + */ + public function isValid() + { + if (null === $this->data) { + throw new Exception\RuntimeException(sprintf( + '%s: no data present to validate!', + __METHOD__ + )); + } + + $valid = true; + foreach ($this->inputs as $name => $input) { + if (!isset($this->data[$name])) { + // Not sure how to handle input filters in this case + if ($input instanceof InputFilterInterface) { + $input->setData(array()); + if (!$input->isValid()) { + $valid = false; + } + continue; + } + + // no matching value in data + // - test if input is required + // - test if input allows empty + if (!$input->isRequired()) { + continue; + } + + if ($input->allowEmpty()) { + continue; + } + + // How do we mark the input as invalid in this case? + + // Mark validation as having failed + $valid = false; + if ($input->breakOnFailure()) { + // We failed validation, and this input is marked to + // break on failure + return false; + } + continue; + } + + $value = $this->data[$name]; + if ($input instanceof InputFilterInterface) { + $input->setData($value); + if (!$input->isValid()) { + $valid = false; + } + } + if ($input instanceof InputInterface) { + $input->setValue($value); + if (!$input->isValid($this->data)) { + // Validation failure + $valid = false; + + if ($input->breakOnFailure()) { + return false; + } + } + } + } + } + + /** + * Provide a list of one or more elements indicating the complete set to validate + * + * When provided, calls to {@link isValid()} will only validate the provided set. + * + * If the initial value is {@link VALIDATE_ALL}, the current validation group, if + * any, should be cleared. + * + * Implementations should allow passing a single array value, or multiple arguments, + * each specifying a single input. + * + * @param mixed $name + * @return InputFilterInterface + */ + public function setValidationGroup($name) + { + } + + /** + * Return a list of inputs that were invalid. + * + * Implementations should return an associative array of name/input pairs + * that failed validation. + * + * @return InputInterface[] + */ + public function getInvalidInput() + { + } + + /** + * Return a list of inputs that were valid. + * + * Implementations should return an associative array of name/input pairs + * that passed validation. + * + * @return InputInterface[] + */ + public function getValidInput() + { + } + + /** + * Return a list of filtered values + * + * List should be an associative array, with the values filtered. If + * validation failed, this should raise an exception. + * + * @return array + */ + public function getValues() + { + } + + /** + * Return a list of unfiltered values + * + * List should be an associative array of named input/value pairs, + * with the values unfiltered. + * + * @return array + */ + public function getRawValues() + { + } + + /** + * Return a list of validation failure messages + * + * Should return an associative array of named input/message list pairs. + * Pairs should only be returned for inputs that failed validation. + * + * @return array + */ + public function getMessages() + { + } +} diff --git a/src/InputFilterInterface.php b/src/InputFilterInterface.php index b0b9e1f0..7c3209a5 100644 --- a/src/InputFilterInterface.php +++ b/src/InputFilterInterface.php @@ -14,7 +14,6 @@ * * @category Zend * @package Zend_InputFilter - * @subpackage UnitTest * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ @@ -23,6 +22,12 @@ use Countable; +/** + * @category Zend + * @package Zend_InputFilter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ interface InputFilterInterface extends Countable { const VALIDATE_ALL = 'INPUT_FILTER_ALL'; From a5e2139974c9e63dba65a0381fa6a8af9fd3fcfc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 4 Apr 2012 10:26:55 -0500 Subject: [PATCH 08/23] [zen-46] completed input retrieval - Completed get(Raw)?Values?() implementations - All tests now run; failures are for unimplemented functionality around partial validation, contextual validation, and retrieval of valid/invalid values and error messages --- src/InputFilter.php | 63 +++++++++++++++++++++++++++++++++++- src/InputFilterInterface.php | 16 +++++++++ test/InputFilterTest.php | 21 ++++++------ 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/InputFilter.php b/src/InputFilter.php index 66e35691..6a2742c3 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -66,7 +66,7 @@ public function add($input, $name = null) )); } - if ($name === null) { + if (empty($name)) { $name = $input->getName(); } $this->inputs[$name] = $input; @@ -181,6 +181,8 @@ public function isValid() } } } + + return $valid; } /** @@ -211,6 +213,7 @@ public function setValidationGroup($name) */ public function getInvalidInput() { + return array(); } /** @@ -223,6 +226,26 @@ public function getInvalidInput() */ public function getValidInput() { + return array(); + } + + /** + * Retrieve a value from a named input + * + * @param string $name + * @return mixed + */ + public function getValue($name) + { + if (!array_key_exists($name, $this->inputs)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a valid input name; "%s" was not found in the filter', + __METHOD__, + $name + )); + } + $input = $this->inputs[$name]; + return $input->getValue(); } /** @@ -235,6 +258,34 @@ public function getValidInput() */ public function getValues() { + $values = array(); + foreach ($this->inputs as $name => $input) { + if ($input instanceof InputFilterInterface) { + $values[$name] = $input->getValues(); + continue; + } + $values[$name] = $input->getValue(); + } + return $values; + } + + /** + * Retrieve a raw (unfiltered) value from a named input + * + * @param string $name + * @return mixed + */ + public function getRawValue($name) + { + if (!array_key_exists($name, $this->inputs)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a valid input name; "%s" was not found in the filter', + __METHOD__, + $name + )); + } + $input = $this->inputs[$name]; + return $input->getRawValue(); } /** @@ -247,6 +298,15 @@ public function getValues() */ public function getRawValues() { + $values = array(); + foreach ($this->inputs as $name => $input) { + if ($input instanceof InputFilterInterface) { + $values[$name] = $input->getRawValues(); + continue; + } + $values[$name] = $input->getRawValue(); + } + return $values; } /** @@ -259,5 +319,6 @@ public function getRawValues() */ public function getMessages() { + return array(); } } diff --git a/src/InputFilterInterface.php b/src/InputFilterInterface.php index 7c3209a5..0be270a7 100644 --- a/src/InputFilterInterface.php +++ b/src/InputFilterInterface.php @@ -100,6 +100,14 @@ public function getInvalidInput(); */ public function getValidInput(); + /** + * Retrieve a value from a named input + * + * @param string $name + * @return mixed + */ + public function getValue($name); + /** * Return a list of filtered values * @@ -110,6 +118,14 @@ public function getValidInput(); */ public function getValues(); + /** + * Retrieve a raw (unfiltered) value from a named input + * + * @param string $name + * @return mixed + */ + public function getRawValue($name); + /** * Return a list of unfiltered values * diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php index 84eba94a..ca4a3d4d 100644 --- a/test/InputFilterTest.php +++ b/test/InputFilterTest.php @@ -47,17 +47,14 @@ public function testAddingInputsIncreasesCountOfFilter() $this->assertEquals(2, count($filter)); } - /** - * @todo Should we do this? or simply alias the input internally? - */ - public function testAddingInputWithNameInjectsNameInInput() + public function testAddingInputWithNameDoesNotInjectNameInInput() { $filter = new InputFilter(); $foo = new Input('foo'); $filter->add($foo, 'bar'); $test = $filter->get('bar'); $this->assertSame($foo, $test); - $this->assertEquals('bar', $foo->getName()); + $this->assertEquals('foo', $foo->getName()); } public function testCanAddInputFilterAsInput() @@ -195,14 +192,14 @@ public function testCanRetrieveValidInputsOnFailedValidation() $filter->setData($invalidData); $this->assertFalse($filter->isValid()); $validInputs = $filter->getValidInput(); - $this->assertArrayHasKey('foo', $invalidInputs); - $this->assertInstanceOf('Zend\InputFilter\Input', $invalidInputs['foo']); - $this->assertArrayNotHasKey('bar', $invalidInputs); - $this->assertArrayHasKey('nest', $invalidInputs); - $this->assertInternalType('array', $invalidInputs['nest']); + $this->assertArrayHasKey('foo', $validInputs); + $this->assertInstanceOf('Zend\InputFilter\Input', $validInputs['foo']); + $this->assertArrayNotHasKey('bar', $validInputs); + $this->assertArrayHasKey('nest', $validInputs); + $this->assertInternalType('array', $validInputs['nest']); $nestValids = $validInputs['nest']; $this->assertArrayNotHasKey('foo', $nestInvalids); - $this->assertArrayHasKey('bar', $invalidInputs); + $this->assertArrayHasKey('bar', $validInputs); $this->assertInstanceOf('Zend\InputFilter\Input', $nestInvalids['bar']); } @@ -388,7 +385,7 @@ public function testValidationAllowsEmptyValuesToRequiredInputWhenAllowEmptyFlag { $filter = new InputFilter(); - $foo = new Input(); + $foo = new Input('foo'); $foo->getValidatorChain()->addValidator(new Validator\StringLength(3, 5)); $foo->setRequired(true); $foo->setAllowEmpty(true); From d193d3e16cf74e2a0c865096c1bcd75dbc368632 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 4 Apr 2012 10:58:06 -0500 Subject: [PATCH 09/23] [zen-46] implemented capturing of valid/invalid inputs --- src/InputFilter.php | 25 ++++++++++++++++++++++--- test/InputFilterTest.php | 21 +++++++++++---------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/InputFilter.php b/src/InputFilter.php index 6a2742c3..9ba55f26 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -34,6 +34,8 @@ class InputFilter implements InputFilterInterface { protected $data; protected $inputs = array(); + protected $invalidInputs; + protected $validInputs; /** * Countable: number of inputs in this input filter @@ -127,15 +129,20 @@ public function isValid() )); } - $valid = true; + $this->validInputs = array(); + $this->invalidInputs = array(); + $valid = true; foreach ($this->inputs as $name => $input) { if (!isset($this->data[$name])) { // Not sure how to handle input filters in this case if ($input instanceof InputFilterInterface) { $input->setData(array()); if (!$input->isValid()) { + $this->invalidInputs[$name] = $input; $valid = false; + continue; } + $this->validInputs[$name] = $input; continue; } @@ -143,16 +150,20 @@ public function isValid() // - test if input is required // - test if input allows empty if (!$input->isRequired()) { + $this->validInputs[$name] = $input; continue; } if ($input->allowEmpty()) { + $this->validInputs[$name] = $input; continue; } // How do we mark the input as invalid in this case? + // (for purposes of a validation error message) // Mark validation as having failed + $this->invalidInputs[$name] = $input; $valid = false; if ($input->breakOnFailure()) { // We failed validation, and this input is marked to @@ -166,19 +177,27 @@ public function isValid() if ($input instanceof InputFilterInterface) { $input->setData($value); if (!$input->isValid()) { + $this->invalidInputs[$name] = $input; $valid = false; + continue; } + $this->validInputs[$name] = $input; + continue; } if ($input instanceof InputInterface) { $input->setValue($value); if (!$input->isValid($this->data)) { // Validation failure + $this->invalidInputs[$name] = $input; $valid = false; if ($input->breakOnFailure()) { return false; } + continue; } + $this->validInputs[$name] = $input; + continue; } } @@ -213,7 +232,7 @@ public function setValidationGroup($name) */ public function getInvalidInput() { - return array(); + return (is_array($this->invalidInputs) ? $this->invalidInputs : array()); } /** @@ -226,7 +245,7 @@ public function getInvalidInput() */ public function getValidInput() { - return array(); + return (is_array($this->validInputs) ? $this->validInputs : array()); } /** diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php index ca4a3d4d..aa378aae 100644 --- a/test/InputFilterTest.php +++ b/test/InputFilterTest.php @@ -160,7 +160,7 @@ public function testCanRetrieveInvalidInputsOnFailedValidation() 'foo' => ' bazbat ', 'bar' => 'abc45', 'nest' => array( - 'foo' => ' baz bat ', + 'foo' => ' baz bat boo ', 'bar' => '12345', ), ); @@ -170,12 +170,12 @@ public function testCanRetrieveInvalidInputsOnFailedValidation() $this->assertArrayNotHasKey('foo', $invalidInputs); $this->assertArrayHasKey('bar', $invalidInputs); $this->assertInstanceOf('Zend\InputFilter\Input', $invalidInputs['bar']); - $this->assertArrayHasKey('nest', $invalidInputs); - $this->assertInternalType('array', $invalidInputs['nest']); - $nestInvalids = $invalidInputs['nest']; + $this->assertArrayHasKey('nest', $invalidInputs, var_export($invalidInputs, 1)); + $this->assertInstanceOf('Zend\InputFilter\InputFilterInterface', $invalidInputs['nest']); + $nestInvalids = $invalidInputs['nest']->getInvalidInput(); $this->assertArrayHasKey('foo', $nestInvalids); $this->assertInstanceOf('Zend\InputFilter\Input', $nestInvalids['foo']); - $this->assertArrayNotHasKey('bar', $invalidInputs); + $this->assertArrayNotHasKey('bar', $nestInvalids); } public function testCanRetrieveValidInputsOnFailedValidation() @@ -196,11 +196,12 @@ public function testCanRetrieveValidInputsOnFailedValidation() $this->assertInstanceOf('Zend\InputFilter\Input', $validInputs['foo']); $this->assertArrayNotHasKey('bar', $validInputs); $this->assertArrayHasKey('nest', $validInputs); - $this->assertInternalType('array', $validInputs['nest']); - $nestValids = $validInputs['nest']; - $this->assertArrayNotHasKey('foo', $nestInvalids); - $this->assertArrayHasKey('bar', $validInputs); - $this->assertInstanceOf('Zend\InputFilter\Input', $nestInvalids['bar']); + $this->assertInstanceOf('Zend\InputFilter\InputFilterInterface', $validInputs['nest']); + $nestValids = $validInputs['nest']->getValidInput(); + $this->assertArrayHasKey('foo', $nestValids); + $this->assertInstanceOf('Zend\InputFilter\Input', $nestValids['foo']); + $this->assertArrayHasKey('bar', $nestValids); + $this->assertInstanceOf('Zend\InputFilter\Input', $nestValids['bar']); } public function testValuesRetrievedAreFiltered() From 27b1bfd1f10d2a5105e2ab118b8c9c672cb20335 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 4 Apr 2012 11:02:25 -0500 Subject: [PATCH 10/23] [zen-46] Implemented getMessages() --- src/InputFilter.php | 6 +++++- test/InputFilterTest.php | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/InputFilter.php b/src/InputFilter.php index 9ba55f26..7b560345 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -338,6 +338,10 @@ public function getRawValues() */ public function getMessages() { - return array(); + $messages = array(); + foreach ($this->getInvalidInput() as $name => $input) { + $messages[$name] = $input->getMessages(); + } + return $messages; } } diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php index aa378aae..68044eea 100644 --- a/test/InputFilterTest.php +++ b/test/InputFilterTest.php @@ -248,11 +248,11 @@ public function testCanGetValidationMessages() { $filter = $this->getInputFilter(); $invalidData = array( - 'foo' => ' bazbat ', + 'foo' => ' bazbat boo ', 'bar' => 'abc45', 'nest' => array( - 'foo' => ' baz bat ', - 'bar' => '12345', + 'foo' => ' baz bat boo ', + 'bar' => '123yz', ), ); $filter->setData($invalidData); From b97305efa8a6cc526f0d2c52568132b459f034dc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 4 Apr 2012 11:09:11 -0500 Subject: [PATCH 11/23] [zen-46] Implemented partial validation --- src/InputFilter.php | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/InputFilter.php b/src/InputFilter.php index 7b560345..9e9c0417 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -35,6 +35,7 @@ class InputFilter implements InputFilterInterface protected $data; protected $inputs = array(); protected $invalidInputs; + protected $validationGroup; protected $validInputs; /** @@ -132,7 +133,11 @@ public function isValid() $this->validInputs = array(); $this->invalidInputs = array(); $valid = true; - foreach ($this->inputs as $name => $input) { + + $inputs = $this->validationGroup ?: array_keys($this->inputs); + foreach ($inputs as $name) { + $input = $this->inputs[$name]; + if (!isset($this->data[$name])) { // Not sure how to handle input filters in this case if ($input instanceof InputFilterInterface) { @@ -220,6 +225,21 @@ public function isValid() */ public function setValidationGroup($name) { + if ($name === self::VALIDATE_ALL) { + $this->validationGroup = null; + return $this; + } + + if (is_array($name)) { + $this->validateValidationGroup($name); + $this->validationGroup = $name; + return $this; + } + + $inputs = func_get_args(); + $this->validateValidationGroup($inputs); + $this->validationGroup = $inputs; + return $this; } /** @@ -344,4 +364,23 @@ public function getMessages() } return $messages; } + + /** + * Ensure all names of a validation group exist as input in the filter + * + * @param array $inputs + * @return void + * @throws Exception\InvalidArgumentException + */ + protected function validateValidationGroup(array $inputs) + { + foreach ($inputs as $name) { + if (!array_key_exists($name, $this->inputs)) { + throw new Exception\InvalidArgumentException(sprintf( + 'setValidationGroup() expects a list of valid input names; "%s" was not found', + $name + )); + } + } + } } From 3318af3cd540dbde00224bc2fbab00b228714da9 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 4 Apr 2012 11:29:57 -0500 Subject: [PATCH 12/23] [zen-46] Added todo item - How should we deal with required input when data is missing? should a message be returned? if so, what message? Does any message need to be returned, as getInvalidInput() will return this input anyways? --- src/InputFilter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/InputFilter.php b/src/InputFilter.php index 9e9c0417..a9ea1bc2 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -25,6 +25,8 @@ use Zend\Stdlib\ArrayUtils; /** + * @todo How should we deal with required input when data is missing? + * should a message be returned? if so, what message? * @category Zend * @package Zend_InputFilter * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) From 6babd3abcca3f60b70c83c81ca686c42420120c2 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 4 Apr 2012 15:45:42 -0500 Subject: [PATCH 13/23] [zen-46] Completed factory implementation - Allows specifying a default filter chain and/or validator chain for elements it creates - Allows creating input objects from specifications - Allows creating filters from specifications - Allows creating validators from specifications - Allows creating input filter objects from specifications - Allows creating nested input filter objects from specifications - Updated FilterChain to be Pluggable - Updated ValidatorChain to be Pluggable --- src/Factory.php | 303 +++++++++++++++++++++++++++++++++ test/FactoryTest.php | 284 +++++++++++++++++++++++++++--- test/TestAsset/CustomInput.php | 35 ++++ 3 files changed, 602 insertions(+), 20 deletions(-) create mode 100644 src/Factory.php create mode 100644 test/TestAsset/CustomInput.php diff --git a/src/Factory.php b/src/Factory.php new file mode 100644 index 00000000..81e67b9a --- /dev/null +++ b/src/Factory.php @@ -0,0 +1,303 @@ +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 + * @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); + break; + case 'allow_empty': + $input->setAllowEmpty($value); + break; + case 'filters': + 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; received "%s"', + __METHOD__, + (is_object($value) ? get_class($value) : gettype($value)) + )); + } + $this->populateFilters($input->getFilterChain(), $value); + break; + case 'validators': + 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; 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 + * @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'])) { + $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 + )); + } + + foreach ($inputFilterSpecification as $key => $value) { + $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 Validator) { + $chain->addValidator($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->addByName($name, $options, $breakChainOnFailure); + continue; + } + + throw new Exception\RuntimeException( + 'Invalid validator specification provided; was neither a validator instance nor an array specification' + ); + } + } +} diff --git a/test/FactoryTest.php b/test/FactoryTest.php index 8a852e01..b07d4761 100644 --- a/test/FactoryTest.php +++ b/test/FactoryTest.php @@ -26,77 +26,321 @@ use Zend\InputFilter\Factory; use Zend\InputFilter\Input; use Zend\InputFilter\InputFilter; +use Zend\Loader\PluginBroker; use Zend\Validator; class FactoryTest extends TestCase { - public function testFactoryComposesFrameworkFilterChainByDefault() + public function testFactoryDoesNotComposeFilterChainByDefault() { - $this->markTestIncomplete(); + $factory = new Factory(); + $this->assertNull($factory->getDefaultFilterChain()); } - public function testFactoryComposesFrameworkValidatorChainByDefault() + public function testFactoryDoesNotComposeValidatorChainByDefault() { - $this->markTestIncomplete(); + $factory = new Factory(); + $this->assertNull($factory->getDefaultValidatorChain()); } public function testFactoryAllowsInjectingFilterChain() { - $this->markTestIncomplete(); + $factory = new Factory(); + $filterChain = new Filter\FilterChain(); + $factory->setDefaultFilterChain($filterChain); + $this->assertSame($filterChain, $factory->getDefaultFilterChain()); } public function testFactoryAllowsInjectingValidatorChain() { - $this->markTestIncomplete(); + $factory = new Factory(); + $validatorChain = new Validator\ValidatorChain(); + $factory->setDefaultValidatorChain($validatorChain); + $this->assertSame($validatorChain, $factory->getDefaultValidatorChain()); } public function testFactoryUsesComposedFilterChainWhenCreatingNewInputObjects() { - $this->markTestIncomplete(); + $factory = new Factory(); + $filterChain = new Filter\FilterChain(); + $broker = new PluginBroker; + $filterChain->setBroker($broker); + $factory->setDefaultFilterChain($filterChain); + $input = $factory->createInput(array( + 'name' => 'foo', + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $inputFilterChain = $input->getFilterChain(); + $this->assertNotSame($filterChain, $inputFilterChain); + $this->assertSame($broker, $inputFilterChain->getBroker()); } public function testFactoryUsesComposedValidatorChainWhenCreatingNewInputObjects() { - $this->markTestIncomplete(); + $factory = new Factory(); + $validatorChain = new Validator\ValidatorChain(); + $broker = new PluginBroker; + $validatorChain->setBroker($broker); + $factory->setDefaultValidatorChain($validatorChain); + $input = $factory->createInput(array( + 'name' => 'foo', + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $inputValidatorChain = $input->getValidatorChain(); + $this->assertNotSame($validatorChain, $inputValidatorChain); + $this->assertSame($broker, $inputValidatorChain->getBroker()); } - public function testFactoryInjectsComposedFilterChainWhenCreatingNewInputFilterObjects() + public function testFactoryInjectsComposedFilterAndValidatorChainsIntoInputObjectsWhenCreatingNewInputFilterObjects() { - $this->markTestIncomplete(); + $factory = new Factory(); + $broker = new PluginBroker; + $filterChain = new Filter\FilterChain(); + $validatorChain = new Validator\ValidatorChain(); + $filterChain->setBroker($broker); + $validatorChain->setBroker($broker); + $factory->setDefaultFilterChain($filterChain); + $factory->setDefaultValidatorChain($validatorChain); + + $inputFilter = $factory->createInputFilter(array( + 'foo' => array( + 'name' => 'foo', + ), + )); + $this->assertInstanceOf('Zend\InputFilter\InputFilterInterface', $inputFilter); + $this->assertEquals(1, count($inputFilter)); + $input = $inputFilter->get('foo'); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $inputFilterChain = $input->getFilterChain(); + $inputValidatorChain = $input->getValidatorChain(); + $this->assertSame($broker, $inputFilterChain->getBroker()); + $this->assertSame($broker, $inputValidatorChain->getBroker()); } public function testFactoryWillCreateInputWithSuggestedFilters() { - $this->markTestIncomplete(); + $factory = new Factory(); + $htmlEntities = new Filter\HtmlEntities(); + $input = $factory->createInput(array( + 'name' => 'foo', + 'filters' => array( + array( + 'name' => 'string_trim', + ), + $htmlEntities, + array( + 'name' => 'string_to_lower', + 'options' => array( + 'encoding' => 'ISO-8859-1', + ), + ), + ), + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $this->assertEquals('foo', $input->getName()); + $chain = $input->getFilterChain(); + $index = 0; + foreach ($chain as $filter) { + switch ($index) { + case 0: + $this->assertInstanceOf('Zend\Filter\StringTrim', $filter); + break; + case 1: + $this->assertSame($htmlEntities, $filter); + break; + case 2: + $this->assertInstanceOf('Zend\Filter\StringToLower', $filter); + $this->assertEquals('ISO-8859-1', $filter->getEncoding()); + break; + default: + $this->fail('Found more filters than expected'); + } + $index++; + } } public function testFactoryWillCreateInputWithSuggestedValidators() { - $this->markTestIncomplete(); + $factory = new Factory(); + $digits = new Validator\Digits(); + $input = $factory->createInput(array( + 'name' => 'foo', + 'validators' => array( + array( + 'name' => 'not_empty', + ), + $digits, + array( + 'name' => 'string_length', + 'options' => array( + 'min' => 3, + 'max' => 5, + ), + ), + ), + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $this->assertEquals('foo', $input->getName()); + $chain = $input->getValidatorChain(); + $index = 0; + foreach ($chain as $validator) { + switch ($index) { + case 0: + $this->assertInstanceOf('Zend\Validator\NotEmpty', $validator); + break; + case 1: + $this->assertSame($digits, $validator); + break; + case 2: + $this->assertInstanceOf('Zend\Validator\StringLength', $validator); + $this->assertEquals(3, $validator->getMin()); + $this->assertEquals(5, $validator->getMax()); + break; + default: + $this->fail('Found more validators than expected'); + } + $index++; + } } public function testFactoryWillCreateInputWithSuggestedRequiredFlag() { - $this->markTestIncomplete(); + $factory = new Factory(); + $input = $factory->createInput(array( + 'name' => 'foo', + 'required' => false, + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $this->assertFalse($input->isRequired()); } public function testFactoryWillCreateInputWithSuggestedAllowEmptyFlag() { - $this->markTestIncomplete(); + $factory = new Factory(); + $input = $factory->createInput(array( + 'name' => 'foo', + 'allow_empty' => true, + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $this->assertTrue($input->allowEmpty()); } public function testFactoryWillCreateInputWithSuggestedName() { - $this->markTestIncomplete(); + $factory = new Factory(); + $input = $factory->createInput(array( + 'name' => 'foo', + )); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $input); + $this->assertEquals('foo', $input->getName()); } public function testFactoryWillCreateInputFilterAndAllInputObjectsFromGivenConfiguration() { - $this->markTestIncomplete(); - } + $factory = new Factory(); + $inputFilter = $factory->createInputFilter(array( + 'foo' => array( + 'name' => 'foo', + 'required' => false, + 'validators' => array( + array( + 'name' => 'not_empty', + ), + array( + 'name' => 'string_length', + 'options' => array( + 'min' => 3, + 'max' => 5, + ), + ), + ), + ), + 'bar' => array( + 'allow_empty' => true, + 'filters' => array( + array( + 'name' => 'string_trim', + ), + array( + 'name' => 'string_to_lower', + 'options' => array( + 'encoding' => 'ISO-8859-1', + ), + ), + ), + ), + 'baz' => array( + 'type' => 'Zend\InputFilter\InputFilter', + 'foo' => array( + 'name' => 'foo', + 'required' => false, + 'validators' => array( + array( + 'name' => 'not_empty', + ), + array( + 'name' => 'string_length', + 'options' => array( + 'min' => 3, + 'max' => 5, + ), + ), + ), + ), + 'bar' => array( + 'allow_empty' => true, + 'filters' => array( + array( + 'name' => 'string_trim', + ), + array( + 'name' => 'string_to_lower', + 'options' => array( + 'encoding' => 'ISO-8859-1', + ), + ), + ), + ), + ), + 'bat' => array( + 'type' => 'ZendTest\InputFilter\TestAsset\CustomInput', + 'name' => 'bat', + ), + )); + $this->assertInstanceOf('Zend\InputFilter\InputFilter', $inputFilter); + $this->assertEquals(4, count($inputFilter)); + + foreach (array('foo', 'bar', 'baz', 'bat') as $name) { + $input = $inputFilter->get($name); - public function testFactoryWillCreateNestedInputFiltersFromGivenConfiguration() - { - $this->markTestIncomplete(); + switch ($name) { + case 'foo': + $this->assertInstanceOf('Zend\InputFilter\Input', $input); + $this->assertFalse($input->isRequired()); + $this->assertEquals(2, count($input->getValidatorChain())); + break; + case 'bar': + $this->assertInstanceOf('Zend\InputFilter\Input', $input); + $this->assertTrue($input->allowEmpty()); + $this->assertEquals(2, count($input->getFilterChain())); + break; + case 'baz': + $this->assertInstanceOf('Zend\InputFilter\InputFilter', $input); + $this->assertEquals(2, count($input)); + $foo = $input->get('foo'); + $this->assertInstanceOf('Zend\InputFilter\Input', $foo); + $this->assertFalse($foo->isRequired()); + $this->assertEquals(2, count($foo->getValidatorChain())); + $bar = $input->get('bar'); + $this->assertInstanceOf('Zend\InputFilter\Input', $bar); + $this->assertTrue($bar->allowEmpty()); + $this->assertEquals(2, count($bar->getFilterChain())); + break; + case 'bat': + $this->assertInstanceOf('ZendTest\InputFilter\TestAsset\CustomInput', $input); + $this->assertEquals('bat', $input->getName()); + break; + } + } } } diff --git a/test/TestAsset/CustomInput.php b/test/TestAsset/CustomInput.php new file mode 100644 index 00000000..4d3523cb --- /dev/null +++ b/test/TestAsset/CustomInput.php @@ -0,0 +1,35 @@ + Date: Tue, 17 Apr 2012 07:08:06 -0500 Subject: [PATCH 14/23] [zen-47] Completed Form tests - Added InputFilterAwareInterface; FormInterface now extends it - Added TestAsset\ValidatingModel --- src/InputFilterAwareInterface.php | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/InputFilterAwareInterface.php diff --git a/src/InputFilterAwareInterface.php b/src/InputFilterAwareInterface.php new file mode 100644 index 00000000..ef2a5290 --- /dev/null +++ b/src/InputFilterAwareInterface.php @@ -0,0 +1,45 @@ + Date: Tue, 17 Apr 2012 15:09:42 -0500 Subject: [PATCH 15/23] [zen-47] Form implementation - must setData() before calling isValid() - must isValid() before calling getData() - hydration only occurs if data is valid - populate value attributes of all elements/fieldsets when data is set - allow binding raw values to bound model - allow retrieving normalized or raw values with no bound model - allow forcing retrieval of data as array with bound model - ensure input filter only returns partial set on partial validation --- src/InputFilter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/InputFilter.php b/src/InputFilter.php index a9ea1bc2..f96d0e01 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -299,8 +299,11 @@ public function getValue($name) */ public function getValues() { + $inputs = $this->validationGroup ?: array_keys($this->inputs); $values = array(); - foreach ($this->inputs as $name => $input) { + foreach ($inputs as $name) { + $input = $this->inputs[$name]; + if ($input instanceof InputFilterInterface) { $values[$name] = $input->getValues(); continue; From b3d80af5640870be8e142d4e7da8db7ad5647d7a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 9 May 2012 14:19:59 -0500 Subject: [PATCH 16/23] [zen-12] Created input/filter provider interfaces - Zend\InputFilter\InputProviderInterface -- to return a specification that can be used to create an input for the given object - Zend\InputFilter\InputFilterProviderInterface -- to return a specification that can be used to create an input filter for the given object --- src/InputFilterProviderInterface.php | 38 ++++++++++++++++++++++++++++ src/InputProviderInterface.php | 38 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/InputFilterProviderInterface.php create mode 100644 src/InputProviderInterface.php diff --git a/src/InputFilterProviderInterface.php b/src/InputFilterProviderInterface.php new file mode 100644 index 00000000..c8d179c0 --- /dev/null +++ b/src/InputFilterProviderInterface.php @@ -0,0 +1,38 @@ + Date: Wed, 9 May 2012 16:53:10 -0500 Subject: [PATCH 17/23] [zen-12] Initial input marshalling - Updated InputFilterInterface to define has() - Form now defines attachInputFilterDefaults(), called from getInputFilter() - iterates through elements - if element implements InputProviderInterface, and input filter does not have an input by that name, generates an input from the returned specification and attaches to the input filter - TODO: similar logic for fieldsets (InputFilerProviderInterface) --- src/InputFilter.php | 11 +++++++++++ src/InputFilterInterface.php | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/InputFilter.php b/src/InputFilter.php index f96d0e01..3d51b530 100644 --- a/src/InputFilter.php +++ b/src/InputFilter.php @@ -96,6 +96,17 @@ public function get($name) return $this->inputs[$name]; } + /** + * Test if an input or input filter by the given name is attached + * + * @param string $name + * @return bool + */ + public function has($name) + { + return (array_key_exists($name, $this->inputs)); + } + /** * Set data to use when validating and filtering * diff --git a/src/InputFilterInterface.php b/src/InputFilterInterface.php index 0be270a7..65e0f774 100644 --- a/src/InputFilterInterface.php +++ b/src/InputFilterInterface.php @@ -49,6 +49,14 @@ public function add($input, $name = null); */ public function get($name); + /** + * Test if an input or input filter by the given name is attached + * + * @param string $name + * @return bool + */ + public function has($name); + /** * Set data to use when validating and filtering * From 1b3ec3a73b8aa8717afdcfbe110a501a4461e4e3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 10 May 2012 16:32:13 -0500 Subject: [PATCH 18/23] [zen-12] Fixes based on functional testing - BaseForm should set $filter to the object's filter, instead of just returning it; ensures Form is using the same instance - Typehint on ValidatorInterface in InputFilter factory, not Validator --- src/Factory.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index 81e67b9a..e7641120 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -23,7 +23,7 @@ use Traversable; use Zend\Filter\FilterChain; use Zend\Stdlib\ArrayUtils; -use Zend\Validator\Validator; +use Zend\Validator\ValidatorInterface; use Zend\Validator\ValidatorChain; /** @@ -271,7 +271,8 @@ protected function populateFilters(FilterChain $chain, $filters) protected function populateValidators(ValidatorChain $chain, $validators) { foreach ($validators as $validator) { - if ($validator instanceof Validator) { +echo "Attempting to add validator: " . (is_object($validator) ? get_class($validator) : var_export($validator, 1)) . "
\n"; + if ($validator instanceof ValidatorInterface) { $chain->addValidator($validator); continue; } From 6c218812c7cd3f60d3c1b60685c6a79bd2694269 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 10 May 2012 16:37:49 -0500 Subject: [PATCH 19/23] Remove debug code... --- src/Factory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Factory.php b/src/Factory.php index e7641120..6061f66a 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -271,7 +271,6 @@ protected function populateFilters(FilterChain $chain, $filters) protected function populateValidators(ValidatorChain $chain, $validators) { foreach ($validators as $validator) { -echo "Attempting to add validator: " . (is_object($validator) ? get_class($validator) : var_export($validator, 1)) . "
\n"; if ($validator instanceof ValidatorInterface) { $chain->addValidator($validator); continue; From 77ca91528037636c425641c179734b3e00594d15 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 10 May 2012 21:59:47 -0500 Subject: [PATCH 20/23] [zen-12] Rename InputFilter to BaseInputFilter - in preparation for a factory-based input filter implementation --- src/{InputFilter.php => BaseInputFilter.php} | 2 +- test/{InputFilterTest.php => BaseInputFilterTest.php} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{InputFilter.php => BaseInputFilter.php} (99%) rename test/{InputFilterTest.php => BaseInputFilterTest.php} (99%) diff --git a/src/InputFilter.php b/src/BaseInputFilter.php similarity index 99% rename from src/InputFilter.php rename to src/BaseInputFilter.php index 3d51b530..0517884a 100644 --- a/src/InputFilter.php +++ b/src/BaseInputFilter.php @@ -32,7 +32,7 @@ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -class InputFilter implements InputFilterInterface +class BaseInputFilter implements InputFilterInterface { protected $data; protected $inputs = array(); diff --git a/test/InputFilterTest.php b/test/BaseInputFilterTest.php similarity index 99% rename from test/InputFilterTest.php rename to test/BaseInputFilterTest.php index 68044eea..e4c70405 100644 --- a/test/InputFilterTest.php +++ b/test/BaseInputFilterTest.php @@ -24,11 +24,11 @@ use PHPUnit_Framework_TestCase as TestCase; use stdClass; use Zend\InputFilter\Input; -use Zend\InputFilter\InputFilter; +use Zend\InputFilter\BaseInputFilter as InputFilter; use Zend\Filter; use Zend\Validator; -class InputFilterTest extends TestCase +class BaseInputFilterTest extends TestCase { public function testInputFilterIsEmptyByDefault() { From 892fbdc7955e8912e9dd6fddb83232629985aa1c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 10 May 2012 22:11:55 -0500 Subject: [PATCH 21/23] [zen-12] Created factory-backed InputFilter implementation - Composes Factory (lazily, or explicitly) - add() will create an input using the spec given before passing on to parent implementation --- src/InputFilter.php | 82 ++++++++++++++++++++++++++++++++++++++++ test/InputFilterTest.php | 61 ++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/InputFilter.php create mode 100644 test/InputFilterTest.php diff --git a/src/InputFilter.php b/src/InputFilter.php new file mode 100644 index 00000000..ece34036 --- /dev/null +++ b/src/InputFilter.php @@ -0,0 +1,82 @@ +factory = $factory; + return $this; + } + + /** + * Get factory to use when adding inputs and filters by spec + * + * Lazy-loads a Factory instance if none attached. + * + * @return Factory + */ + public function getFactory() + { + if (null === $this->factory) { + $this->setFactory(new Factory()); + } + return $this->factory; + } + + /** + * Add an input to the input filter + * + * @param array|Traversable|InputInterface|InputFilterInterface $input + * @param null|string $name + * @return InputFilter + */ + public function add($input, $name = null) + { + if (is_array($input) + || ($input instanceof Traversable && !$input instanceof InputFilterInterface) + ) { + $factory = $this->getFactory(); + $input = $factory->createInput($input); + } + return parent::add($input, $name); + } +} diff --git a/test/InputFilterTest.php b/test/InputFilterTest.php new file mode 100644 index 00000000..2916fb10 --- /dev/null +++ b/test/InputFilterTest.php @@ -0,0 +1,61 @@ +filter = new InputFilter(); + } + + public function testLazilyComposesAFactoryByDefault() + { + $factory = $this->filter->getFactory(); + $this->assertInstanceOf('Zend\InputFilter\Factory', $factory); + } + + public function testCanComposeAFactory() + { + $factory = new Factory(); + $this->filter->setFactory($factory); + $this->assertSame($factory, $this->filter->getFactory()); + } + + public function testCanAddUsingSpecification() + { + $this->filter->add(array( + 'name' => 'foo', + )); + $this->assertTrue($this->filter->has('foo')); + $foo = $this->filter->get('foo'); + $this->assertInstanceOf('Zend\InputFilter\InputInterface', $foo); + } +} From e82354d6db50c0145ef318474b2a0df9c4e028dc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 11 May 2012 08:17:31 -0500 Subject: [PATCH 22/23] [zen-12] Removed Dojo from test suite - Dojo integration largely relied on integration with Zend\Form; removing from testing for now, as it has not been refactored to work with the new Zend\Form code. --- .travis/skipped-components | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis/skipped-components b/.travis/skipped-components index 31bcaa87..171dfe9d 100644 --- a/.travis/skipped-components +++ b/.travis/skipped-components @@ -1,5 +1,6 @@ Zend/Amf Zend/Date +Zend/Dojo Zend/Queue Zend/Service Zend/Test From 4c3a25ed936c699098ac5b27dc25581fa3cbdcf9 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 11 May 2012 10:57:49 -0500 Subject: [PATCH 23/23] [zen-12] Inject not-empty validator in input - Injected if required or allow_empty is false; prepended to validator chain --- src/Input.php | 13 ++++++++++++- test/InputTest.php | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Input.php b/src/Input.php index 908b913d..b889d991 100644 --- a/src/Input.php +++ b/src/Input.php @@ -36,6 +36,7 @@ class Input implements InputInterface protected $errorMessage; protected $filterChain; protected $name; + protected $notEmptyValidator = false; protected $required = true; protected $validatorChain; protected $value; @@ -144,6 +145,7 @@ public function getValue() public function isValid($context = null) { + $this->injectNotEmptyValidator(); $validator = $this->getValidatorChain(); $value = $this->getValue(); return $validator->isValid($value, $context); @@ -158,5 +160,14 @@ public function getMessages() $validator = $this->getValidatorChain(); return $validator->getMessages(); } -} + protected function injectNotEmptyValidator() + { + if (!$this->isRequired() && $this->allowEmpty() && !$this->notEmptyValidator) { + return; + } + $chain = $this->getValidatorChain(); + $chain->prependByName('NotEmpty', array(), true); + $this->notEmptyValidator = true; + } +} diff --git a/test/InputTest.php b/test/InputTest.php index a70bb010..13d79807 100644 --- a/test/InputTest.php +++ b/test/InputTest.php @@ -188,4 +188,14 @@ public function testBreakOnFailureFlagIsMutable() $input->setBreakOnFailure(true); $this->assertTrue($input->breakOnFailure()); } + + public function testNotEmptyValidatorAddedWhenIsValidIsCalled() + { + $input = new Input('foo'); + $this->assertTrue($input->isRequired()); + $input->setValue(''); + $this->assertFalse($input->isValid()); + $messages = $input->getMessages(); + $this->assertArrayHasKey('isEmpty', $messages); + } }