From 7ceac2ca631fbeab42927ec6e4aeea771c8c8d40 Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 14:56:50 -0400 Subject: [PATCH 1/7] Add PHP 8.4 and 8.5 to build matrix --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 54655f5..481d73a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout uses: actions/checkout@v4 From 5bea0dcc325d3bf0956c3409a50c7b47431f57e0 Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 15:00:43 -0400 Subject: [PATCH 2/7] Remove dev requirement for symfony/yaml --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d029fb9..049a928 100644 --- a/composer.json +++ b/composer.json @@ -40,8 +40,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.0", - "squizlabs/php_codesniffer": "^3.2", - "symfony/yaml": "^3.4" + "squizlabs/php_codesniffer": "^3.2" }, "autoload": { "psr-4": { "TraderInteractive\\": "src/" } From ca456240c4d211937ea38185329b40a8a0541c4f Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 15:01:21 -0400 Subject: [PATCH 3/7] Remove scrutinizer integration --- .scrutinizer.yml | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .scrutinizer.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 3d2d9e8..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,31 +0,0 @@ -filter: - excluded_paths: - - 'vendor/*' -before_commands: - - 'composer install' -build: - nodes: - analysis: - tests: - override: - - php-scrutinizer-run - tests: - override: - - phpcs-run --standard=PSR2 -tools: - php_analyzer: true - php_mess_detector: true - php_code_sniffer: - config: - standard: PSR2 - sensiolabs_security_checker: true - php_loc: - excluded_dirs: - - vendor - php_pdepend: true - php_sim: true -build_failure_conditions: - - 'elements.rating(< C).new.exists' - - 'issues.label("coding-style").new.exists' - - 'issues.severity(>= MAJOR).new.exists' - - 'project.metric("scrutinizer.quality", < 6.8)' From bed9e8c2315c701774966bb8f793fe2d87607046 Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 15:01:31 -0400 Subject: [PATCH 4/7] Remove coveralls integration --- .coveralls.yml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 4eecff5..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1,3 +0,0 @@ -service_name: travis-ci -coverage_clover: clover.xml -json_path: coveralls-upload.json From c34c8f7d9e7574314b655c8b180b9c11aed1afb2 Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 15:06:51 -0400 Subject: [PATCH 5/7] Add FilterOptions::REQUIRES --- src/FilterOptions.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/FilterOptions.php b/src/FilterOptions.php index 17ad992..b6161ad 100644 --- a/src/FilterOptions.php +++ b/src/FilterOptions.php @@ -24,6 +24,11 @@ final class FilterOptions */ const IS_REQUIRED = 'required'; + /** + * @var string + */ + const REQUIRES = 'requires'; + /** * @var string */ From 9262fea6c6c5e504aeb908ffe9bc8a49c0b63ac6 Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 15:09:10 -0400 Subject: [PATCH 6/7] integrate requires option into filterer --- src/Filterer.php | 38 +++++++++++++++++++++++++ tests/FiltererTest.php | 64 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/Filterer.php b/src/Filterer.php index e9ded47..229f857 100644 --- a/src/Filterer.php +++ b/src/Filterer.php @@ -161,6 +161,7 @@ public function execute(array $input) : FilterResponse $filteredInput = []; $errors = []; $conflicts = []; + $requires = []; foreach ($inputToFilter as $field => $input) { $filters = $this->specification[$field]; self::assertFiltersIsAnArray($filters, $field); @@ -170,6 +171,7 @@ public function execute(array $input) : FilterResponse unset($filters[FilterOptions::IS_REQUIRED]);//doesn't matter if required since we have this one unset($filters[FilterOptions::DEFAULT_VALUE]);//doesn't matter if there is a default since we have a value $conflicts = self::extractConflicts($filters, $field, $conflicts); + $requires = self::extractRequires($filters, $field, $requires); foreach ($filters as $filter) { self::assertFilterIsArray($filter, $field); @@ -218,6 +220,7 @@ public function execute(array $input) : FilterResponse $errors = self::handleAllowUnknowns($this->allowUnknowns, $leftOverInput, $errors); $errors = self::handleConflicts($filteredInput, $conflicts, $errors); + $errors = self::handleRequires($filteredInput, $requires, $errors); return new FilterResponse($filteredInput, $errors, $leftOverInput); } @@ -249,6 +252,41 @@ private static function extractConflicts(array &$filters, string $field, array $ return $conflicts; } + private static function extractRequires(array &$filters, string $field, array $requires): array + { + $required = $filters[FilterOptions::REQUIRES] ?? null; + unset($filters[FilterOptions::REQUIRES]); + if ($required === null) { + return $requires; + } + + if (!is_array($required)) { + $required = [$required]; + } + + $requires[$field] = $required; + + return $requires; + } + + private static function handleRequires(array $inputToFilter, array $requires, array $errors) + { + foreach (array_keys($inputToFilter) as $field) { + if (!array_key_exists($field, $requires)) { + continue; + } + + + foreach ($requires[$field] as $requiredField) { + if (!array_key_exists($requiredField, $inputToFilter)) { + $errors[] = "Field '{$field}' requires field '{$requiredField}' but it is not present."; + } + } + } + + return $errors; + } + private static function handleConflicts(array $inputToFilter, array $conflicts, array $errors) { foreach (array_keys($inputToFilter) as $field) { diff --git a/tests/FiltererTest.php b/tests/FiltererTest.php index 07280a2..fa2c43a 100644 --- a/tests/FiltererTest.php +++ b/tests/FiltererTest.php @@ -265,6 +265,70 @@ public function provideValidFilterData() : array 'options' => [], 'result' => [true, ['field' => 'a string with newlines and extra spaces'], null, []], ], + 'requires single' => [ + 'spec' => [ + 'fieldOne' => [FilterOptions::REQUIRES => 'fieldThree', ['string']], + 'fieldTwo' => [['string']], + 'fieldThree' => [['string']], + ], + 'input' => [ + 'fieldOne' => 'abc', + 'fieldTwo' => '123', + ], + 'options' => [], + 'result' => [ + false, + null, + "Field 'fieldOne' requires field 'fieldThree' but it is not present.", + [], + ], + ], + 'requires multiple' => [ + 'spec' => [ + 'fieldOne' => [FilterOptions::REQUIRES => ['fieldThree', 'fieldFour'], ['string']], + 'fieldTwo' => [['string']], + 'fieldThree' => [['string']], + 'fieldFour' => [['uint']], + ], + 'input' => [ + 'fieldOne' => 'abc', + 'fieldTwo' => '123', + ], + 'options' => [], + 'result' => [ + false, + null, + "Field 'fieldOne' requires field 'fieldThree' but it is not present.\n" + . "Field 'fieldOne' requires field 'fieldFour' but it is not present.", + [], + ], + ], + 'requires multiple all present' => [ + 'spec' => [ + 'fieldOne' => [FilterOptions::REQUIRES => ['fieldThree', 'fieldFour'], ['string']], + 'fieldTwo' => [['string']], + 'fieldThree' => [['string']], + 'fieldFour' => [['uint']], + ], + 'input' => [ + 'fieldOne' => 'abc', + 'fieldTwo' => '123', + 'fieldThree' => 'foo', + 'fieldFour' => '789', + ], + 'options' => [], + 'result' => [ + true, + [ + 'fieldOne' => 'abc', + 'fieldTwo' => '123', + 'fieldThree' => 'foo', + 'fieldFour' => 789, + ], + null, + [], + ], + ], 'conflicts with single' => [ 'spec' => [ 'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']], From b8e78437742ac709107bc6212fbfd2344b407449 Mon Sep 17 00:00:00 2001 From: chadicus Date: Fri, 2 Oct 2026 15:24:24 -0400 Subject: [PATCH 7/7] Update docs for FilterOptions::REQUIRES --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 47ad102..359fb27 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,52 @@ $specification = [ ]; ``` +## requires + +#### Summary + +Defines any input field(s) which are required by a separate given field. Used when one field requires one or more additional fields to be operate properly. + +#### Types + +* string||string[] + +#### Default + +There is no default value for this option. + +#### Constant + +```php +TraderInteractive\FilterOptions::REQUIRES +``` + +#### Examples + +```php +$specification = [ + 'duration' => [['uint']], + 'startDate' => [ + TraderInteractive\FilterOptions::REQUIRES => 'duration', + ['date'], + ], +]; +``` + +```php +$specification = [ + 'id' => [['string']], + 'typeId' => [ + TraderInteractive\FilterOptions::REQUIRES => ['typeCode'], + ['uint'], + ], + 'typeCode' => [ + TraderInteractive\FilterOptions::REQUIRES => ['typeId'], + ['string'], + ], +]; +``` + ## uses #### Summary