diff --git a/lib/Core/Input/ConditionStructureBuilder.php b/lib/Core/Input/ConditionStructureBuilder.php index 5158953f..36ab3a15 100644 --- a/lib/Core/Input/ConditionStructureBuilder.php +++ b/lib/Core/Input/ConditionStructureBuilder.php @@ -81,6 +81,11 @@ public function __construct( $this->maxGroups = $config->getMaxGroups(); $this->valuesGroupLevels[0] = new ValuesGroup(); $this->path[] = $path; + + // Future compatible, this is not part of the current API. + if (method_exists($validator, 'initialize')) { + $validator->initialize($config); + } } public function getErrors(): ErrorList diff --git a/lib/Core/Input/OrderStructureBuilder.php b/lib/Core/Input/OrderStructureBuilder.php index dfede90b..e96ed14f 100644 --- a/lib/Core/Input/OrderStructureBuilder.php +++ b/lib/Core/Input/OrderStructureBuilder.php @@ -48,6 +48,11 @@ public function __construct( ) { $this->fieldSet = $config->getFieldSet(); $this->valuesGroup = new ValuesGroup(); + + // Future compatible, this is not part of the current API. + if (method_exists($validator, 'initialize')) { + $validator->initialize($config); + } } public function getErrors(): ErrorList diff --git a/lib/Core/Input/StringInput.php b/lib/Core/Input/StringInput.php index 8eb3a25d..17ec7624 100644 --- a/lib/Core/Input/StringInput.php +++ b/lib/Core/Input/StringInput.php @@ -267,8 +267,8 @@ final protected function parse(ProcessorConfig $config, string $input, FieldSet /** @var FieldConfig $field */ foreach ($fieldSet->all() as $name => $field) { if (OrderField::isOrder($name) && null !== $direction = $field->getOption('default')) { - $this->orderStructureBuilder->field($name, '[order][%s]'); - $this->orderStructureBuilder->simpleValue($direction, ''); + $this->orderStructureBuilder->field($name, ''); + $this->orderStructureBuilder->simpleValue($direction, '[order][{pos}]'); $this->orderStructureBuilder->endValues(); } } diff --git a/lib/Core/Input/Validator.php b/lib/Core/Input/Validator.php index a13e4dcb..75282db1 100644 --- a/lib/Core/Input/Validator.php +++ b/lib/Core/Input/Validator.php @@ -17,22 +17,47 @@ use Rollerworks\Component\Search\Field\FieldConfig; /** - * The Validator validates input values according to a set of - * rules (constraints). + * The Validator validates field values. + * + * The validator is first initialized with the FieldConfig using the + * `initializeConfig()` method (when present). + * + * For each field the validator is then initialized with `initializeContext()`. + * Then for each value in the current field the `validate()` method is called. * * @author Sebastiaan Stok + * + * @method void initialize(ProcessorConfig $config) */ interface Validator { /** * Initialize the validator context for the field. * - * Whenever calling validate(), this context needs to be used. + * This method is called prior to the first call to validate(). + * Errors need to be added to the ErrorList. */ public function initializeContext(FieldConfig $field, ErrorList $errorList): void; /** * Validates and returns whether the value is valid. + * + * Any error (or violation) should be added to the ErrorList with the + * corresponding path. Multiple errors can be added for the same path. + * + * Example: + * ``` + * // ErrorList was initialized in initializeContext() + * + * $this->errorList->append(new \Rollerworks\Component\Search\ConditionErrorMessage( + * $path, + * $violation->getMessage(), + * $violation->getMessageTemplate(), + * $violation->getParameters(), + * $violation->getPlural(), + * $violation, // Cause of the error (can be anything), optional used for debugging and profiling + * )); + * ``` */ public function validate($value, string $type, $originalValue, string $path): bool; } diff --git a/lib/Core/Tests/Input/JsonInputTest.php b/lib/Core/Tests/Input/JsonInputTest.php index 6d75a9f9..6a56fff4 100644 --- a/lib/Core/Tests/Input/JsonInputTest.php +++ b/lib/Core/Tests/Input/JsonInputTest.php @@ -14,9 +14,12 @@ namespace Rollerworks\Component\Search\Tests\Input; use Rollerworks\Component\Search\ConditionErrorMessage; +use Rollerworks\Component\Search\ErrorList; use Rollerworks\Component\Search\Exception\InvalidSearchConditionException; +use Rollerworks\Component\Search\Field\FieldConfig; use Rollerworks\Component\Search\Input\JsonInput; use Rollerworks\Component\Search\Input\ProcessorConfig; +use Rollerworks\Component\Search\Input\Validator; use Rollerworks\Component\Search\InputProcessor; use Rollerworks\Component\Search\Value\Compare; use Rollerworks\Component\Search\Value\PatternMatch; @@ -179,6 +182,131 @@ public static function provide_invalid_structures(): iterable ]; } + /** + * @test + */ + public function it_validates_values(): void + { + $validator = new class implements Validator { + private ?string $currentField = null; + + /** @var array */ + public array $calls = []; + + public function initialize(ProcessorConfig $config): void + { + $this->calls = []; + $this->calls['initialize'] = $config; + } + + public function initializeContext(FieldConfig $field, ErrorList $errorList): void + { + $this->currentField = $field->getName(); + $this->calls[$this->currentField] = []; + } + + public function validate($value, string $type, $originalValue, string $path): bool + { + if ($value instanceof \DateTimeImmutable) { + $value = '{DateTime}' . $value->format('Y-m-d'); + } + + $this->calls[$this->currentField][] = [$value, $type, $originalValue, $path]; + + return false; + } + }; + + $processor = new JsonInput($validator); + $config = new ProcessorConfig($this->getFieldSet(order: true)); + + $processor->process( + $config, + json_encode( + [ + 'fields' => [ + 'name' => ['simple-values' => ['value', 'value2']], + ], + 'groups' => [ + [ + 'fields' => [ + 'date' => [ + 'simple-values' => ['2014-12-16'], + 'ranges' => [ + ['lower' => '2014-12-16', 'upper' => '2015-12-16'], + ], + ], + ], + ], + ], + 'order' => [ + 'id' => 'asc', + ], + ], + \JSON_THROW_ON_ERROR, + ), + ); + + self::assertSame([ + 'initialize' => $config, + 'name' => [ + ['value', 'simple', 'value', '[fields][name][simple-values][0]'], + ['value2', 'simple', 'value2', '[fields][name][simple-values][1]'], + ], + 'date' => [ + ['{DateTime}2014-12-16', 'simple', '2014-12-16', '[groups][0][fields][date][simple-values][0]'], + ['{DateTime}2014-12-16', Range::class, '2014-12-16', '[groups][0][fields][date][ranges][0][lower]'], + ['{DateTime}2015-12-16', Range::class, '2015-12-16', '[groups][0][fields][date][ranges][0][upper]'], + ], + '@id' => [ + ['ASC', 'simple', 'asc', '[order][@id]'], + ], + ], $validator->calls); + + $processor->process( + $config, + json_encode( + [ + 'fields' => [ + 'name' => ['simple-values' => ['value3', 'value4']], + ], + 'groups' => [ + [ + 'fields' => [ + 'date' => [ + 'simple-values' => ['2014-12-14'], + 'ranges' => [ + ['lower' => '2014-12-16', 'upper' => '2015-12-16'], + ], + ], + ], + ], + ], + 'order' => [ + 'id' => 'asc', + ], + ], + \JSON_THROW_ON_ERROR, + ), + ); + + self::assertSame([ + 'initialize' => $config, + 'name' => [ + ['value3', 'simple', 'value3', '[fields][name][simple-values][0]'], + ['value4', 'simple', 'value4', '[fields][name][simple-values][1]'], + ], + 'date' => [ + ['{DateTime}2014-12-14', 'simple', '2014-12-14', '[groups][0][fields][date][simple-values][0]'], + ['{DateTime}2014-12-16', Range::class, '2014-12-16', '[groups][0][fields][date][ranges][0][lower]'], + ['{DateTime}2015-12-16', Range::class, '2015-12-16', '[groups][0][fields][date][ranges][0][upper]'], + ], + '@id' => [ + ['ASC', 'simple', 'asc', '[order][@id]'], + ], + ], $validator->calls); + } + public static function provideEmptyInputTests(): iterable { return [ diff --git a/lib/Core/Tests/Input/StringQueryInputTest.php b/lib/Core/Tests/Input/StringQueryInputTest.php index 7c5f9f3f..acab67cd 100644 --- a/lib/Core/Tests/Input/StringQueryInputTest.php +++ b/lib/Core/Tests/Input/StringQueryInputTest.php @@ -14,6 +14,7 @@ namespace Rollerworks\Component\Search\Tests\Input; use Rollerworks\Component\Search\ConditionErrorMessage; +use Rollerworks\Component\Search\ErrorList; use Rollerworks\Component\Search\Exception\InputProcessorException; use Rollerworks\Component\Search\Exception\OrderStructureException; use Rollerworks\Component\Search\Exception\StringLexerException; @@ -23,6 +24,7 @@ use Rollerworks\Component\Search\Input\ProcessorConfig; use Rollerworks\Component\Search\Input\StringLexer; use Rollerworks\Component\Search\Input\StringQueryInput; +use Rollerworks\Component\Search\Input\Validator; use Rollerworks\Component\Search\InputProcessor; use Rollerworks\Component\Search\SearchCondition; use Rollerworks\Component\Search\SearchConditionBuilder; @@ -85,6 +87,79 @@ public function isEqual($value, $nextValue, array $options): bool return $build ? $fieldSet->getFieldSet() : $fieldSet; } + /** + * @test + */ + public function it_validates_values(): void + { + $validator = new class implements Validator { + private ?string $currentField = null; + + /** @var array */ + public array $calls = []; + + public function initialize(ProcessorConfig $config): void + { + $this->calls = []; + $this->calls['initialize'] = $config; + } + + public function initializeContext(FieldConfig $field, ErrorList $errorList): void + { + $this->currentField = $field->getName(); + $this->calls[$this->currentField] = []; + } + + public function validate($value, string $type, $originalValue, string $path): bool + { + if ($value instanceof \DateTimeImmutable) { + $value = '{DateTime}' . $value->format('Y-m-d'); + } + + $this->calls[$this->currentField][] = [$value, $type, $originalValue, $path]; + + return false; + } + }; + + $processor = new StringQueryInput($validator, null); + $config = new ProcessorConfig($this->getFieldSet(order: true)); + + $processor->process($config, 'name: value, value2; (date: "12-16-2014", "12-16-2014" ~ "12-16-2015"); @id: asc;'); + self::assertSame([ + 'initialize' => $config, + 'name' => [ + ['value', 'simple', 'value', '[name][0]'], + ['value2', 'simple', 'value2', '[name][1]'], + ], + 'date' => [ + ['{DateTime}2014-12-16', 'simple', '12-16-2014', '[0][date][0]'], + ['{DateTime}2014-12-16', Range::class, '12-16-2014', '[0][date][1][lower]'], + ['{DateTime}2015-12-16', Range::class, '12-16-2015', '[0][date][1][upper]'], + ], + '@id' => [ + ['ASC', 'simple', 'asc', '[@id]'], + ], + ], $validator->calls); + + $processor->process($config, 'name: value4, value3; (date: "12-16-2014", "12-16-2014" ~ "12-16-2015"); @id: asc;'); + self::assertSame([ + 'initialize' => $config, + 'name' => [ + ['value4', 'simple', 'value4', '[name][0]'], + ['value3', 'simple', 'value3', '[name][1]'], + ], + 'date' => [ + ['{DateTime}2014-12-16', 'simple', '12-16-2014', '[0][date][0]'], + ['{DateTime}2014-12-16', Range::class, '12-16-2014', '[0][date][1][lower]'], + ['{DateTime}2015-12-16', Range::class, '12-16-2015', '[0][date][1][upper]'], + ], + '@id' => [ + ['ASC', 'simple', 'asc', '[@id]'], + ], + ], $validator->calls); + } + /** * @test * diff --git a/phpstan.neon b/phpstan.neon index 394bd9a2..7f1aeedf 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -34,5 +34,9 @@ parameters: identifier: argument.type path: lib/Core/Tests/SearchConditionSerializerTest.php + - + # Forward compatible + message: '#^Call to function method_exists\(\) with Rollerworks\\Component\\Search\\Input\\Validator and ''initialize'' will always evaluate to true\.$#' + path: lib/Core/Input/*.php - '#Call to an undefined static method Carbon\\Translator\:\:get\(\)#'