Skip to content
This repository was archived by the owner on Jan 30, 2020. It is now read-only.

Commit c0708e0

Browse files
committed
Merge branch 'feature/135' into develop
Close #135
2 parents 4b43b82 + 1e24b57 commit c0708e0

6 files changed

+317
-7
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ All notable changes to this project will be documented in this file, in reverse
66

77
### Added
88

9+
- [#135](https://github.com/zendframework/zend-inputfilter/pull/135) adds
10+
`Zend\InputFilter\OptionalInputFilter`, which allows defining optional sets of
11+
data. This acts like a standard input filter, but is considered valid if no
12+
data, `null` data, or empty data sets are provided to it; if a non-empty data
13+
set is provided, it will run normal validations.
14+
915
- [#142](https://github.com/zendframework/zend-inputfilter/pull/142) adds
1016
support for PHP 7.2.
1117

docs/book/optional-input-filters.md

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Optional Input Filters
2+
3+
- Since 2.8.0
4+
5+
Normally, input filters are _required_, which means that if you compose them as
6+
a subset of another input filter (e.g., to validate a subset of a larger set of
7+
data), and no data is provided for that item, or an empty set of data is
8+
provided, then the input filter will consider the data invalid.
9+
10+
If you want to allow a set of data to be empty, you can use
11+
`Zend\InputFilter\OptionalInputFilter`.
12+
13+
To illustrate this, let's consider a form where a user provides profile
14+
information. The user can provide an optional "title" and a required "email",
15+
and _optionally_ details about a project they lead, which will include the
16+
project title and a URL, both of which are required if present.
17+
18+
First, let's create an `OptionalInputFilter` instance for the project data:
19+
20+
```php
21+
$projectFilter = new OptionalInputFilter();
22+
$projectFilter->add([
23+
'name' => 'project_name',
24+
'required' => true,
25+
]);
26+
$projectFilter->add([
27+
'name' => 'url',
28+
'required' => true,
29+
'validators' => [
30+
['type' => 'uri'],
31+
],
32+
]);
33+
```
34+
35+
Now, we'll create our primary input filter:
36+
37+
```php
38+
$profileFilter = new InputFilter();
39+
$profileFilter->add([
40+
'name' => 'title',
41+
'required' => false,
42+
]);
43+
$profileFilter->add([
44+
'name' => 'email',
45+
'required' => true,
46+
'validators' => [
47+
['type' => 'EmailAddress'],
48+
],
49+
]);
50+
51+
// And, finally, compose our project sub-filter:
52+
$profileFilter->add($projectFilter, 'project');
53+
```
54+
55+
With this defined, we can now validate the following sets of data, presented in
56+
JSON for readability:
57+
58+
- Just profile information:
59+
60+
```json
61+
{
62+
"email": "[email protected]",
63+
"title": "Software Developer"
64+
}
65+
```
66+
67+
- `null` project provided:
68+
69+
```json
70+
{
71+
"email": "[email protected]",
72+
"title": "Software Developer",
73+
"project": null
74+
}
75+
```
76+
77+
- Empty project provided:
78+
79+
```json
80+
{
81+
"email": "[email protected]",
82+
"title": "Software Developer",
83+
"project": {}
84+
}
85+
```
86+
87+
- Valid project provided:
88+
89+
```json
90+
{
91+
"email": "[email protected]",
92+
"title": "Software Developer",
93+
"project": {
94+
"project_name": "zend-inputfilter",
95+
"url": "https://github.com/zend-inputfilter",
96+
}
97+
}
98+
```

mkdocs.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ site_dir: docs/html
33
pages:
44
- index.md
55
- Intro: intro.md
6-
- Specifications: specs.md
7-
- Files: file-input.md
6+
- Reference:
7+
- Specifications: specs.md
8+
- Files: file-input.md
9+
- "Optional Input Filters": optional-input-filters.md
810
site_name: zend-inputfilter
911
site_description: zend-inputfilter
1012
repo_url: 'https://github.com/zendframework/zend-inputfilter'

src/InputFilterPluginManager.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ class InputFilterPluginManager extends AbstractPluginManager
2828
* @var string[]
2929
*/
3030
protected $aliases = [
31-
'inputfilter' => InputFilter::class,
32-
'inputFilter' => InputFilter::class,
33-
'InputFilter' => InputFilter::class,
34-
'collection' => CollectionInputFilter::class,
35-
'Collection' => CollectionInputFilter::class,
31+
'inputfilter' => InputFilter::class,
32+
'inputFilter' => InputFilter::class,
33+
'InputFilter' => InputFilter::class,
34+
'collection' => CollectionInputFilter::class,
35+
'Collection' => CollectionInputFilter::class,
36+
'optionalinputfilter' => OptionalInputFilter::class,
37+
'optionalInputFilter' => OptionalInputFilter::class,
38+
'OptionalInputFilter' => OptionalInputFilter::class,
3639
];
3740

3841
/**
@@ -43,9 +46,11 @@ class InputFilterPluginManager extends AbstractPluginManager
4346
protected $factories = [
4447
InputFilter::class => InvokableFactory::class,
4548
CollectionInputFilter::class => InvokableFactory::class,
49+
OptionalInputFilter::class => InvokableFactory::class,
4650
// v2 canonical FQCN
4751
'zendinputfilterinputfilter' => InvokableFactory::class,
4852
'zendinputfiltercollectioninputfilter' => InvokableFactory::class,
53+
'zendinputfilteroptionalinputfilter' => InvokableFactory::class,
4954
];
5055

5156
/**

src/OptionalInputFilter.php

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @link http://github.com/zendframework/zf2 for the canonical source repository
6+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license http://framework.zend.com/license/new-bsd New BSD License
8+
*/
9+
10+
namespace Zend\InputFilter;
11+
12+
/**
13+
* InputFilter which only checks the containing Inputs when non-empty data is set,
14+
* else it reports valid
15+
*
16+
* This is analog to {@see Zend\InputFilter\Input} with the option ->setRequired(false)
17+
*/
18+
class OptionalInputFilter extends InputFilter
19+
{
20+
/**
21+
* Set data to use when validating and filtering
22+
*
23+
* @param iterable|mixed $data
24+
* must be a non-empty iterable in order trigger actual validation, else it is always valid
25+
* @throws Exception\InvalidArgumentException
26+
* @return InputFilterInterface
27+
*/
28+
public function setData($data)
29+
{
30+
return parent::setData($data ?: []);
31+
}
32+
33+
/**
34+
* Run validation, or return true if the data was empty
35+
*
36+
* {@inheritDoc}
37+
*/
38+
public function isValid($context = null)
39+
{
40+
if ($this->data) {
41+
return parent::isValid($context);
42+
}
43+
44+
return true;
45+
}
46+
47+
/**
48+
* Return a list of filtered values, or null if the data was missing entirely
49+
* Null is returned instead of an empty array to prevent it being passed to a hydrator,
50+
* which would likely cause failures later on in your program
51+
* Fallbacks for the inputs are not respected by design
52+
*
53+
* @return array|null
54+
*/
55+
public function getValues()
56+
{
57+
return $this->data ? parent::getValues() : null;
58+
}
59+
}

test/OptionalInputFilterTest.php

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @link http://github.com/zendframework/zf2 for the canonical source repository
6+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license http://framework.zend.com/license/new-bsd New BSD License
8+
*/
9+
10+
namespace ZendTest\InputFilter;
11+
12+
use ArrayIterator;
13+
use Zend\InputFilter\Input;
14+
use Zend\InputFilter\InputFilter;
15+
use Zend\InputFilter\InputFilterInterface;
16+
use Zend\InputFilter\OptionalInputFilter;
17+
use PHPUnit\Framework\TestCase;
18+
19+
/**
20+
* @covers \Zend\InputFilter\OptionalInputFilter
21+
*/
22+
class OptionalInputFilterTest extends TestCase
23+
{
24+
public function testValidatesSuccessfullyWhenSetDataIsNeverCalled()
25+
{
26+
$this->assertTrue($this->getNestedCarInputFilter()->get('car')->isValid());
27+
}
28+
29+
public function testValidatesSuccessfullyWhenValidNonEmptyDataSetProvided()
30+
{
31+
$data = [
32+
'car' => [
33+
'brand' => 'Volkswagen',
34+
'model' => 'Golf',
35+
]
36+
];
37+
38+
$inputFilter = $this->getNestedCarInputFilter();
39+
$inputFilter->setData($data);
40+
41+
$this->assertTrue($inputFilter->isValid());
42+
$this->assertEquals($data, $inputFilter->getValues());
43+
}
44+
45+
public function testValidatesSuccessfullyWhenEmptyDataSetProvided()
46+
{
47+
$data = [
48+
'car' => null,
49+
];
50+
51+
$inputFilter = $this->getNestedCarInputFilter();
52+
$inputFilter->setData($data);
53+
54+
$this->assertTrue($inputFilter->isValid());
55+
$this->assertEquals($data, $inputFilter->getValues());
56+
}
57+
58+
public function testValidatesSuccessfullyWhenNoDataProvided()
59+
{
60+
$data = [];
61+
62+
$inputFilter = $this->getNestedCarInputFilter();
63+
$inputFilter->setData($data);
64+
65+
$this->assertTrue($inputFilter->isValid());
66+
$this->assertEquals(['car' => null], $inputFilter->getValues());
67+
}
68+
69+
public function testValidationFailureWhenInvalidDataSetIsProvided()
70+
{
71+
$inputFilter = $this->getNestedCarInputFilter();
72+
$inputFilter->setData([
73+
'car' => [
74+
'brand' => 'Volkswagen',
75+
]
76+
]);
77+
78+
$this->assertFalse($inputFilter->isValid());
79+
$this->assertGetValuesThrows($inputFilter);
80+
}
81+
82+
public function testStateIsClearedBetweenValidationAttempts()
83+
{
84+
$data = [
85+
'car' => null,
86+
];
87+
88+
$inputFilter = $this->getNestedCarInputFilter();
89+
$inputFilter->setData($data);
90+
91+
$this->assertTrue($inputFilter->isValid());
92+
$this->assertEquals($data, $inputFilter->getValues());
93+
}
94+
95+
/**
96+
* We are doing some boolean shenanigans in the implementation
97+
* we want to check that Iterator objects work the same as arrays
98+
*/
99+
public function testIteratorBehavesTheSameAsArray()
100+
{
101+
$optionalInputFilter = new OptionalInputFilter;
102+
$optionalInputFilter->add(new Input('brand'));
103+
104+
$optionalInputFilter->setData(['model' => 'Golf']);
105+
$this->assertFalse($optionalInputFilter->isValid());
106+
107+
$optionalInputFilter->setData(new ArrayIterator([]));
108+
$this->assertTrue($optionalInputFilter->isValid());
109+
110+
$optionalInputFilter->setData([]);
111+
$this->assertTrue($optionalInputFilter->isValid());
112+
}
113+
114+
protected function assertGetValuesThrows(InputFilterInterface $inputFilter)
115+
{
116+
try {
117+
$inputFilter->getValues();
118+
$this->assertTrue(false);
119+
// TODO: issue #143 narrow which exception should be thrown
120+
} catch (\Exception $exception) {
121+
$this->assertTrue(true);
122+
}
123+
}
124+
125+
private $nestedCarInputFilter;
126+
127+
protected function getNestedCarInputFilter()
128+
{
129+
if (! $this->nestedCarInputFilter) {
130+
$optionalInputFilter = new OptionalInputFilter;
131+
$optionalInputFilter->add(new Input('brand'));
132+
$optionalInputFilter->add(new Input('model'));
133+
134+
$this->nestedCarInputFilter = new InputFilter;
135+
$this->nestedCarInputFilter->add($optionalInputFilter, 'car');
136+
}
137+
138+
return $this->nestedCarInputFilter;
139+
}
140+
}

0 commit comments

Comments
 (0)