Skip to content

Commit 09303eb

Browse files
authored
Merge pull request #84 from jncarver/filter-response
Add Filterer responseType option
2 parents cc04632 + c7c175a commit 09303eb

5 files changed

Lines changed: 303 additions & 28 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"license": "MIT",
3131
"require": {
3232
"php": "^7.0",
33+
"traderinteractive/exceptions": "^1.2",
3334
"traderinteractive/filter-arrays": "^3.1",
3435
"traderinteractive/filter-bools": "^3.0",
3536
"traderinteractive/filter-dates": "^3.0",

src/FilterResponse.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace TraderInteractive;
4+
5+
use InvalidArgumentException;
6+
use TraderInteractive\Exceptions\ReadOnlyViolationException;
7+
8+
/**
9+
* This object contains the various data returned by a filter action.
10+
*
11+
* @property bool $success TRUE if the filter was successful or FALSE if errors were encountered.
12+
* @property mixed $filteredValue The input values after being filtered.
13+
* @property array $errors Any errors encountered during the filter process.
14+
* @property string|null $errorMessage An error message generated from the errors. NULL if no errors.
15+
* @property mixed $unknowns The values that were unknown during filtering.
16+
*/
17+
final class FilterResponse
18+
{
19+
/**
20+
* @var array
21+
*/
22+
private $response;
23+
24+
/**
25+
* @param array $filteredValue The input values after being filtered.
26+
* @param array $errors Any errors encountered during the filter process.
27+
* @param array $unknowns The values that were unknown during filtering.
28+
*/
29+
public function __construct(
30+
array $filteredValue,
31+
array $errors = [],
32+
array $unknowns = []
33+
) {
34+
$success = count($errors) === 0;
35+
$this->response = [
36+
'success' => $success,
37+
'filteredValue' => $filteredValue,
38+
'errors' => $errors,
39+
'errorMessage' => $success ? null : implode("\n", $errors),
40+
'unknowns' => $unknowns,
41+
];
42+
}
43+
44+
public function __get($name)
45+
{
46+
if (array_key_exists($name, $this->response)) {
47+
return $this->response[$name];
48+
}
49+
50+
throw new InvalidArgumentException("Property '{$name}' does not exist");
51+
}
52+
53+
public function __set($name, $value)
54+
{
55+
if (array_key_exists($name, $this->response)) {
56+
throw new ReadOnlyViolationException("Property '{$name}' is read-only");
57+
}
58+
59+
throw new InvalidArgumentException("Property '{$name}' does not exist");
60+
}
61+
}

src/Filterer.php

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace TraderInteractive;
44

55
use Exception;
6+
use InvalidArgumentException;
67
use Throwable;
78
use TraderInteractive\Exceptions\FilterException;
89

@@ -40,6 +41,16 @@ final class Filterer
4041
'url' => '\\TraderInteractive\\Filter\\Url::filter',
4142
];
4243

44+
/**
45+
* @var string
46+
*/
47+
const RESPONSE_TYPE_ARRAY = 'array';
48+
49+
/**
50+
* @var string
51+
*/
52+
const RESPONSE_TYPE_FILTER = FilterResponse::class;
53+
4354
/**
4455
* @var array
4556
*/
@@ -105,23 +116,29 @@ final class Filterer
105116
* @param array $options 'allowUnknowns' (default false) true to allow unknowns or false to treat as error,
106117
* 'defaultRequired' (default false) true to make fields required by default and treat as
107118
* error on absence and false to allow their absence by default
119+
* 'responseType' (default RESPONSE_TYPE_ARRAY) Determines the return type, as described
120+
* in the return section.
108121
*
109-
* @return array on success [true, $input filtered, null, array of unknown fields]
110-
* on error [false, null, 'error message', array of unknown fields]
122+
* @return array|FilterResponse If 'responseType' option is RESPONSE_TYPE_ARRAY:
123+
* on success [true, $input filtered, null, array of unknown fields]
124+
* on error [false, null, 'error message', array of unknown fields]
125+
* If 'responseType' option is RESPONSE_TYPE_FILTER: a FilterResponse instance.
111126
*
112127
* @throws Exception
113-
* @throws \InvalidArgumentException if 'allowUnknowns' option was not a bool
114-
* @throws \InvalidArgumentException if 'defaultRequired' option was not a bool
115-
* @throws \InvalidArgumentException if filters for a field was not a array
116-
* @throws \InvalidArgumentException if a filter for a field was not a array
117-
* @throws \InvalidArgumentException if 'required' for a field was not a bool
128+
* @throws InvalidArgumentException if 'allowUnknowns' option was not a bool
129+
* @throws InvalidArgumentException if 'defaultRequired' option was not a bool
130+
* @throws InvalidArgumentException if 'responseType' option was not a recognized type
131+
* @throws InvalidArgumentException if filters for a field was not an array
132+
* @throws InvalidArgumentException if a filter for a field was not an array
133+
* @throws InvalidArgumentException if 'required' for a field was not a bool
118134
*/
119-
public static function filter(array $spec, array $input, array $options = []) : array
135+
public static function filter(array $spec, array $input, array $options = [])
120136
{
121-
$options += ['allowUnknowns' => false, 'defaultRequired' => false];
137+
$options += ['allowUnknowns' => false, 'defaultRequired' => false, 'responseType' => self::RESPONSE_TYPE_ARRAY];
122138

123139
$allowUnknowns = self::getAllowUnknowns($options);
124140
$defaultRequired = self::getDefaultRequired($options);
141+
$responseType = $options['responseType'];
125142

126143
$inputToFilter = array_intersect_key($input, $spec);
127144
$leftOverSpec = array_diff_key($spec, $input);
@@ -171,11 +188,7 @@ public static function filter(array $spec, array $input, array $options = []) :
171188

172189
$errors = self::handleAllowUnknowns($allowUnknowns, $leftOverInput, $errors);
173190

174-
if (empty($errors)) {
175-
return [true, $inputToFilter, null, $leftOverInput];
176-
}
177-
178-
return [false, null, implode("\n", $errors), $leftOverInput];
191+
return self::generateFilterResponse($responseType, $inputToFilter, $errors, $leftOverInput);
179192
}
180193

181194
/**
@@ -319,7 +332,7 @@ public static function ofArray(array $value, array $spec) : array
319332
private static function assertIfStringOrInt($alias)
320333
{
321334
if (!is_string($alias) && !is_int($alias)) {
322-
throw new \InvalidArgumentException('$alias was not a string or int');
335+
throw new InvalidArgumentException('$alias was not a string or int');
323336
}
324337
}
325338

@@ -333,7 +346,7 @@ private static function assertIfAliasExists($alias, bool $overwrite)
333346
private static function checkForUnknowns(array $leftOverInput, array $errors) : array
334347
{
335348
foreach ($leftOverInput as $field => $value) {
336-
$errors[] = "Field '{$field}' with value '" . trim(var_export($value, true), "'") . "' is unknown";
349+
$errors[$field] = "Field '{$field}' with value '" . trim(var_export($value, true), "'") . "' is unknown";
337350
}
338351

339352
return $errors;
@@ -351,7 +364,7 @@ private static function handleAllowUnknowns(bool $allowUnknowns, array $leftOver
351364
private static function handleRequiredFields(bool $required, string $field, array $errors) : array
352365
{
353366
if ($required) {
354-
$errors[] = "Field '{$field}' was required and not present";
367+
$errors[$field] = "Field '{$field}' was required and not present";
355368
}
356369
return $errors;
357370
}
@@ -360,7 +373,7 @@ private static function getRequired($filters, $defaultRequired, $field) : bool
360373
{
361374
$required = isset($filters['required']) ? $filters['required'] : $defaultRequired;
362375
if ($required !== false && $required !== true) {
363-
throw new \InvalidArgumentException("'required' for field '{$field}' was not a bool");
376+
throw new InvalidArgumentException("'required' for field '{$field}' was not a bool");
364377
}
365378

366379
return $required;
@@ -369,7 +382,7 @@ private static function getRequired($filters, $defaultRequired, $field) : bool
369382
private static function assertFiltersIsAnArray($filters, string $field)
370383
{
371384
if (!is_array($filters)) {
372-
throw new \InvalidArgumentException("filters for field '{$field}' was not a array");
385+
throw new InvalidArgumentException("filters for field '{$field}' was not a array");
373386
}
374387
}
375388

@@ -389,7 +402,7 @@ private static function handleCustomError(
389402
);
390403
}
391404

392-
$errors[] = str_replace('{value}', trim(var_export($value, true), "'"), $error);
405+
$errors[$field] = str_replace('{value}', trim(var_export($value, true), "'"), $error);
393406
return $errors;
394407
}
395408

@@ -414,7 +427,7 @@ private static function handleFilterAliases($function)
414427
private static function assertFilterIsNotArray($filter, string $field)
415428
{
416429
if (!is_array($filter)) {
417-
throw new \InvalidArgumentException("filter for field '{$field}' was not a array");
430+
throw new InvalidArgumentException("filter for field '{$field}' was not a array");
418431
}
419432
}
420433

@@ -424,7 +437,7 @@ private static function validateCustomError(array &$filters, string $field)
424437
if (array_key_exists('error', $filters)) {
425438
$customError = $filters['error'];
426439
if (!is_string($customError) || trim($customError) === '') {
427-
throw new \InvalidArgumentException("error for field '{$field}' was not a non-empty string");
440+
throw new InvalidArgumentException("error for field '{$field}' was not a non-empty string");
428441
}
429442

430443
unset($filters['error']);//unset so its not used as a filter
@@ -437,7 +450,7 @@ private static function getAllowUnknowns(array $options) : bool
437450
{
438451
$allowUnknowns = $options['allowUnknowns'];
439452
if ($allowUnknowns !== false && $allowUnknowns !== true) {
440-
throw new \InvalidArgumentException("'allowUnknowns' option was not a bool");
453+
throw new InvalidArgumentException("'allowUnknowns' option was not a bool");
441454
}
442455

443456
return $allowUnknowns;
@@ -447,9 +460,43 @@ private static function getDefaultRequired(array $options) : bool
447460
{
448461
$defaultRequired = $options['defaultRequired'];
449462
if ($defaultRequired !== false && $defaultRequired !== true) {
450-
throw new \InvalidArgumentException("'defaultRequired' option was not a bool");
463+
throw new InvalidArgumentException("'defaultRequired' option was not a bool");
451464
}
452465

453466
return $defaultRequired;
454467
}
468+
469+
/**
470+
* @param string $responseType The type of object that should be returned.
471+
* @param array $filteredValue The filtered input to return.
472+
* @param array $errors The errors to return.
473+
* @param array $unknowns The unknowns to return.
474+
*
475+
* @return array|FilterResponse
476+
*
477+
* @see filter For more information on how responseType is handled and returns are structured.
478+
*/
479+
private static function generateFilterResponse(
480+
string $responseType,
481+
array $filteredValue,
482+
array $errors,
483+
array $unknowns
484+
) {
485+
$filterResponse = new FilterResponse($filteredValue, $errors, $unknowns);
486+
487+
if ($responseType === self::RESPONSE_TYPE_FILTER) {
488+
return $filterResponse;
489+
}
490+
491+
if ($responseType === self::RESPONSE_TYPE_ARRAY) {
492+
return [
493+
$filterResponse->success,
494+
$filterResponse->success ? $filterResponse->filteredValue : null,
495+
$filterResponse->errorMessage,
496+
$filterResponse->unknowns
497+
];
498+
}
499+
500+
throw new InvalidArgumentException("'responseType' was not a recognized value");
501+
}
455502
}

tests/FilterResponseTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace TraderInteractiveTest;
4+
5+
use InvalidArgumentException;
6+
use PHPUnit\Framework\TestCase;
7+
use TraderInteractive\Exceptions\ReadOnlyViolationException;
8+
use TraderInteractive\FilterResponse;
9+
10+
/**
11+
* @coversDefaultClass \TraderInteractive\FilterResponse
12+
*/
13+
class FilterResponseTest extends TestCase
14+
{
15+
/**
16+
* @test
17+
* @covers ::__construct
18+
*/
19+
public function construct()
20+
{
21+
$value = ['foo' => 'bar'];
22+
$errors = [];
23+
$unknowns = ['other' => 'unknown'];
24+
25+
$response = new FilterResponse($value, $errors, $unknowns);
26+
27+
$this->assertSame(true, $response->success);
28+
$this->assertSame($value, $response->filteredValue);
29+
$this->assertSame($errors, $response->errors);
30+
$this->assertSame(null, $response->errorMessage);
31+
$this->assertSame($unknowns, $response->unknowns);
32+
}
33+
34+
/**
35+
* @test
36+
* @covers ::__construct
37+
*/
38+
public function constructWithErrors()
39+
{
40+
$value = ['foo' => 'bar'];
41+
$errors = ['something bad happened', 'and something else too'];
42+
$unknowns = ['other' => 'unknown'];
43+
44+
$response = new FilterResponse($value, $errors, $unknowns);
45+
46+
$this->assertSame(false, $response->success);
47+
$this->assertSame($value, $response->filteredValue);
48+
$this->assertSame($errors, $response->errors);
49+
$this->assertSame("something bad happened\nand something else too", $response->errorMessage);
50+
$this->assertSame($unknowns, $response->unknowns);
51+
}
52+
53+
/**
54+
* @test
55+
* @covers ::__construct
56+
*/
57+
public function constructDefault()
58+
{
59+
$input = ['filtered' => 'input'];
60+
61+
$response = new FilterResponse($input);
62+
63+
$this->assertSame(true, $response->success);
64+
$this->assertSame($input, $response->filteredValue);
65+
$this->assertSame([], $response->errors);
66+
$this->assertSame(null, $response->errorMessage);
67+
$this->assertSame([], $response->unknowns);
68+
}
69+
70+
/**
71+
* @test
72+
* @covers ::__construct
73+
*/
74+
public function gettingInvalidPropertyThrowsException()
75+
{
76+
$this->expectException(InvalidArgumentException::class);
77+
$this->expectExceptionMessage("Property 'foo' does not exist");
78+
79+
$response = new FilterResponse([]);
80+
$response->foo;
81+
}
82+
83+
/**
84+
* @test
85+
* @covers ::__construct
86+
*/
87+
public function settingValidPropertyThrowsAnException()
88+
{
89+
$this->expectException(ReadOnlyViolationException::class);
90+
$this->expectExceptionMessage("Property 'success' is read-only");
91+
92+
$response = new FilterResponse([]);
93+
$response->success = false;
94+
}
95+
96+
/**
97+
* @test
98+
* @covers ::__construct
99+
*/
100+
public function settingInvalidPropertyThrowsAnException()
101+
{
102+
$this->expectException(InvalidArgumentException::class);
103+
$this->expectExceptionMessage("Property 'foo' does not exist");
104+
105+
$response = new FilterResponse([]);
106+
$response->foo = false;
107+
}
108+
}

0 commit comments

Comments
 (0)