Skip to content

Commit de16cb0

Browse files
authored
Merge pull request #90 from chadicus/fea/conflicts-with
Add "conflictsWith" functionality to Filterer library
2 parents a8dec17 + 5ca6340 commit de16cb0

4 files changed

Lines changed: 172 additions & 26 deletions

File tree

src/FilterOptions.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace TraderInteractive;
4+
5+
final class FilterOptions
6+
{
7+
/**
8+
* @var string
9+
*/
10+
const DEFAULT_VALUE = 'default';
11+
12+
/**
13+
* @var string
14+
*/
15+
const CUSTOM_ERROR = 'error';
16+
17+
/**
18+
* @var string
19+
*/
20+
const IS_REQUIRED = 'required';
21+
22+
/**
23+
* @var string
24+
*/
25+
const CONFLICTS_WITH = 'conflictsWith';
26+
}

src/Filterer.php

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ final class Filterer implements FiltererInterface
4646
* @var array
4747
*/
4848
const DEFAULT_OPTIONS = [
49-
'allowUnknowns' => false,
50-
'defaultRequired' => false,
51-
'responseType' => self::RESPONSE_TYPE_ARRAY,
49+
FiltererOptions::ALLOW_UNKNOWNS => false,
50+
FiltererOptions::DEFAULT_REQUIRED => false,
51+
FiltererOptions::RESPONSE_TYPE => self::RESPONSE_TYPE_ARRAY,
5252
];
5353

5454
/**
@@ -123,12 +123,15 @@ public function execute(array $input) : FilterResponse
123123
$leftOverInput = array_diff_key($input, $this->specification);
124124

125125
$errors = [];
126+
$conflicts = [];
126127
foreach ($inputToFilter as $field => $input) {
127128
$filters = $this->specification[$field];
128129
self::assertFiltersIsAnArray($filters, $field);
129130
$customError = self::validateCustomError($filters, $field);
130-
unset($filters['required']);//doesn't matter if required since we have this one
131-
unset($filters['default']);//doesn't matter if there is a default since we have a value
131+
unset($filters[FilterOptions::IS_REQUIRED]);//doesn't matter if required since we have this one
132+
unset($filters[FilterOptions::DEFAULT_VALUE]);//doesn't matter if there is a default since we have a value
133+
$conflicts = self::extractConflicts($filters, $field, $conflicts);
134+
132135
foreach ($filters as $filter) {
133136
self::assertFilterIsNotArray($filter, $field);
134137

@@ -156,15 +159,16 @@ public function execute(array $input) : FilterResponse
156159
foreach ($leftOverSpec as $field => $filters) {
157160
self::assertFiltersIsAnArray($filters, $field);
158161
$required = self::getRequired($filters, $this->defaultRequired, $field);
159-
if (array_key_exists('default', $filters)) {
160-
$inputToFilter[$field] = $filters['default'];
162+
if (array_key_exists(FilterOptions::DEFAULT_VALUE, $filters)) {
163+
$inputToFilter[$field] = $filters[FilterOptions::DEFAULT_VALUE];
161164
continue;
162165
}
163166

164167
$errors = self::handleRequiredFields($required, $field, $errors);
165168
}
166169

167170
$errors = self::handleAllowUnknowns($this->allowUnknowns, $leftOverInput, $errors);
171+
$errors = self::handleConflicts($inputToFilter, $conflicts, $errors);
168172

169173
return new FilterResponse($inputToFilter, $errors, $leftOverInput);
170174
}
@@ -179,6 +183,40 @@ public function getAliases() : array
179183
return $this->filterAliases ?? self::$registeredFilterAliases;
180184
}
181185

186+
private static function extractConflicts(array &$filters, string $field, array $conflicts) : array
187+
{
188+
$conflictsWith = $filters[FilterOptions::CONFLICTS_WITH] ?? null;
189+
unset($filters[FilterOptions::CONFLICTS_WITH]);
190+
if ($conflictsWith === null) {
191+
return $conflicts;
192+
}
193+
194+
if (!is_array($conflictsWith)) {
195+
$conflictsWith = [$conflictsWith];
196+
}
197+
198+
$conflicts[$field] = $conflictsWith;
199+
200+
return $conflicts;
201+
}
202+
203+
private static function handleConflicts(array $inputToFilter, array $conflicts, array $errors)
204+
{
205+
foreach (array_keys($inputToFilter) as $field) {
206+
if (!array_key_exists($field, $conflicts)) {
207+
continue;
208+
}
209+
210+
foreach ($conflicts[$field] as $conflictsWith) {
211+
if (array_key_exists($conflictsWith, $inputToFilter)) {
212+
$errors[] = "Field '{$field}' cannot be given if field '{$conflictsWith}' is present.";
213+
}
214+
}
215+
}
216+
217+
return $errors;
218+
}
219+
182220
/**
183221
* @return array
184222
*
@@ -219,8 +257,8 @@ public function withSpecification(array $specification) : FiltererInterface
219257
private function getOptions() : array
220258
{
221259
return [
222-
'defaultRequired' => $this->defaultRequired,
223-
'allowUnknowns' => $this->allowUnknowns,
260+
FiltererOptions::DEFAULT_REQUIRED => $this->defaultRequired,
261+
FiltererOptions::ALLOW_UNKNOWNS => $this->allowUnknowns,
224262
];
225263
}
226264

@@ -296,7 +334,7 @@ private function getOptions() : array
296334
public static function filter(array $specification, array $input, array $options = [])
297335
{
298336
$options += self::DEFAULT_OPTIONS;
299-
$responseType = $options['responseType'];
337+
$responseType = $options[FiltererOptions::RESPONSE_TYPE];
300338

301339
$filterer = new Filterer($specification, $options);
302340
$filterResponse = $filterer->execute($input);
@@ -484,9 +522,11 @@ private static function handleRequiredFields(bool $required, string $field, arra
484522

485523
private static function getRequired($filters, $defaultRequired, $field) : bool
486524
{
487-
$required = isset($filters['required']) ? $filters['required'] : $defaultRequired;
525+
$required = $filters[FilterOptions::IS_REQUIRED] ?? $defaultRequired;
488526
if ($required !== false && $required !== true) {
489-
throw new InvalidArgumentException("'required' for field '{$field}' was not a bool");
527+
throw new InvalidArgumentException(
528+
sprintf("'%s' for field '%s' was not a bool", FilterOptions::IS_REQUIRED, $field)
529+
);
490530
}
491531

492532
return $required;
@@ -508,11 +548,8 @@ private static function handleCustomError(
508548
) : array {
509549
$error = $customError;
510550
if ($error === null) {
511-
$error = sprintf(
512-
"Field '%s' with value '{value}' failed filtering, message '%s'",
513-
$field,
514-
$e->getMessage()
515-
);
551+
$errorFormat = "Field '%s' with value '{value}' failed filtering, message '%s'";
552+
$error = sprintf($errorFormat, $field, $e->getMessage());
516553
}
517554

518555
$errors[$field] = str_replace('{value}', trim(var_export($value, true), "'"), $error);
@@ -547,33 +584,37 @@ private static function assertFilterIsNotArray($filter, string $field)
547584
private static function validateCustomError(array &$filters, string $field)
548585
{
549586
$customError = null;
550-
if (array_key_exists('error', $filters)) {
551-
$customError = $filters['error'];
587+
if (array_key_exists(FilterOptions::CUSTOM_ERROR, $filters)) {
588+
$customError = $filters[FilterOptions::CUSTOM_ERROR];
552589
if (!is_string($customError) || trim($customError) === '') {
553-
throw new InvalidArgumentException("error for field '{$field}' was not a non-empty string");
590+
throw new InvalidArgumentException(
591+
sprintf("%s for field '%s' was not a non-empty string", FilterOptions::CUSTOM_ERROR, $field)
592+
);
554593
}
555594

556-
unset($filters['error']);//unset so its not used as a filter
595+
unset($filters[FilterOptions::CUSTOM_ERROR]);//unset so its not used as a filter
557596
}
558597

559598
return $customError;
560599
}
561600

562601
private static function getAllowUnknowns(array $options) : bool
563602
{
564-
$allowUnknowns = $options['allowUnknowns'];
603+
$allowUnknowns = $options[FiltererOptions::ALLOW_UNKNOWNS];
565604
if ($allowUnknowns !== false && $allowUnknowns !== true) {
566-
throw new InvalidArgumentException("'allowUnknowns' option was not a bool");
605+
throw new InvalidArgumentException(sprintf("'%s' option was not a bool", FiltererOptions::ALLOW_UNKNOWNS));
567606
}
568607

569608
return $allowUnknowns;
570609
}
571610

572611
private static function getDefaultRequired(array $options) : bool
573612
{
574-
$defaultRequired = $options['defaultRequired'];
613+
$defaultRequired = $options[FiltererOptions::DEFAULT_REQUIRED];
575614
if ($defaultRequired !== false && $defaultRequired !== true) {
576-
throw new InvalidArgumentException("'defaultRequired' option was not a bool");
615+
throw new InvalidArgumentException(
616+
sprintf("'%s' option was not a bool", FiltererOptions::DEFAULT_REQUIRED)
617+
);
577618
}
578619

579620
return $defaultRequired;
@@ -602,6 +643,6 @@ private static function generateFilterResponse(string $responseType, FilterRespo
602643
];
603644
}
604645

605-
throw new InvalidArgumentException("'responseType' was not a recognized value");
646+
throw new InvalidArgumentException(sprintf("'%s' was not a recognized value", FiltererOptions::RESPONSE_TYPE));
606647
}
607648
}

src/FiltererOptions.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace TraderInteractive;
4+
5+
final class FiltererOptions
6+
{
7+
/**
8+
* @var string
9+
*/
10+
const ALLOW_UNKNOWNS = 'allowUnknowns';
11+
12+
/**
13+
* @var string
14+
*/
15+
const DEFAULT_REQUIRED = 'defaultRequired';
16+
17+
/**
18+
* @var string
19+
*/
20+
const RESPONSE_TYPE = 'responseType';
21+
}

tests/FiltererTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,64 @@ public function provideValidFilterData() : array
238238
'options' => [],
239239
'result' => [true, ['field' => 'a string with newlines and extra spaces'], null, []],
240240
],
241+
'conflicts with single' => [
242+
'spec' => [
243+
'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']],
244+
'fieldTwo' => [['string']],
245+
'fieldThree' => [FilterOptions::CONFLICTS_WITH => 'fieldOne', ['string']],
246+
],
247+
'input' => [
248+
'fieldOne' => 'abc',
249+
'fieldTwo' => '123',
250+
'fieldThree' => 'xyz',
251+
],
252+
'options' => [],
253+
'result' => [
254+
false,
255+
null,
256+
"Field 'fieldOne' cannot be given if field 'fieldThree' is present.\n"
257+
. "Field 'fieldThree' cannot be given if field 'fieldOne' is present.",
258+
[],
259+
],
260+
],
261+
'conflicts with multiple' => [
262+
'spec' => [
263+
'fieldOne' => [FilterOptions::CONFLICTS_WITH => ['fieldTwo', 'fieldThree'], ['string']],
264+
'fieldTwo' => [['string']],
265+
'fieldThree' => [['string']],
266+
],
267+
'input' => [
268+
'fieldOne' => 'abc',
269+
'fieldTwo' => '123',
270+
],
271+
'options' => [],
272+
'result' => [
273+
false,
274+
null,
275+
"Field 'fieldOne' cannot be given if field 'fieldTwo' is present.",
276+
[],
277+
],
278+
],
279+
'conflicts with not present' => [
280+
'spec' => [
281+
'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']],
282+
'fieldTwo' => [['string']],
283+
],
284+
'input' => [
285+
'fieldOne' => 'abc',
286+
'fieldTwo' => '123',
287+
],
288+
'options' => [],
289+
'result' => [
290+
true,
291+
[
292+
'fieldOne' => 'abc',
293+
'fieldTwo' => '123',
294+
],
295+
null,
296+
[],
297+
],
298+
],
241299
];
242300
}
243301

0 commit comments

Comments
 (0)