diff --git a/src/Exception/ValidateRuntimeException.php b/src/Exception/ValidateRuntimeException.php new file mode 100644 index 0000000000000000000000000000000000000000..ee09e6c1ff9dff9ce02fa47be1efd7e03324e16e --- /dev/null +++ b/src/Exception/ValidateRuntimeException.php @@ -0,0 +1,19 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Exception; + +use RuntimeException; + +class ValidateRuntimeException extends RuntimeException +{ +} diff --git a/src/Support/Concerns/FilterInterface.php b/src/Support/Concerns/FilterInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..fb23c880caba49a428812675f56c36178fbe0856 --- /dev/null +++ b/src/Support/Concerns/FilterInterface.php @@ -0,0 +1,18 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support\Concerns; + +interface FilterInterface +{ + public function handle($value); +} diff --git a/src/Support/ValidateScene.php b/src/Support/ValidateScene.php index 6fd0c545b1d7d78f81db07f64777e1a7f983985e..d02dc173716068d4854f29bd3cc376256a1bca72 100644 --- a/src/Support/ValidateScene.php +++ b/src/Support/ValidateScene.php @@ -15,6 +15,7 @@ namespace W7\Validate\Support; use Closure; use Illuminate\Support\Arr; use RuntimeException; +use W7\Validate\Support\Concerns\FilterInterface; use W7\Validate\Support\Rule\BaseRule; use W7\Validate\Support\Storage\ValidateCollection; @@ -26,6 +27,7 @@ use W7\Validate\Support\Storage\ValidateCollection; * @property-read array $befores Methods to be executed before this validate * @property-read array $afters Methods to be executed after this validate * @property-read array $defaults This validation requires a default value for the value + * @property-read array $filters The filter. This can be a global function name, anonymous function, etc. * @property-read bool $eventPriority Event Priority */ class ValidateScene extends RuleManagerScene @@ -59,6 +61,12 @@ class ValidateScene extends RuleManagerScene * @var array */ private $defaults = []; + + /** + * The filter. This can be a global function name, anonymous function, etc. + * @var array + */ + private $filters = []; /** * Event Priority @@ -182,6 +190,30 @@ class ValidateScene extends RuleManagerScene return $this; } + /** + * Set a filter for the specified field + * + * Filter is a data processor. + * It invokes the specified filter callback to process the attribute value + * and save the processed value back to the attribute. + * @param string $field Name of the data field to be processed + * @param string|callable|Closure|FilterInterface $callback The filter. This can be a global function name, anonymous function, etc. + * The filter must be a valid PHP callback with the following signature: + * + * function foo($value) { + * // compute $newValue here + * return $newValue; + * } + * + * Many PHP functions qualify this signature (e.g. `trim()`). + * @return $this + */ + public function filter(string $field, $callback): ValidateScene + { + $this->filters[$field] = $callback; + return $this; + } + /** * Set event priority * diff --git a/src/Validate.php b/src/Validate.php index 15ebd0d9d9692a7cebe95256f6e78824ba3c9dec..7e51ae032d2e9314accd579d1bd3a140be76d541 100644 --- a/src/Validate.php +++ b/src/Validate.php @@ -13,10 +13,14 @@ namespace W7\Validate; use Closure; +use Illuminate\Support\Str; use Illuminate\Validation\Factory; +use Illuminate\Validation\ValidationData; use Illuminate\Validation\ValidationException; use LogicException; use W7\Validate\Exception\ValidateException; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\Concerns\FilterInterface; use W7\Validate\Support\Concerns\MessageProviderInterface; use W7\Validate\Support\Event\ValidateEventAbstract; use W7\Validate\Support\MessageProvider; @@ -86,6 +90,12 @@ class Validate extends RuleManager */ private $defaults = []; + /** + * Filters to be passed for this validation + * @var array + */ + private $filters = []; + /** * Error Message Provider * @var MessageProviderInterface @@ -137,9 +147,12 @@ class Validate extends RuleManager $this->init(); $this->checkData = $data; $this->addEvent($this->event); - $this->defaults = array_merge($this->default, $this->defaults); $rule = $this->getCheckRules($this->getInitialRules()); - $data = $this->handleDefault($data, $rule); + $fields = array_keys($rule); + $this->defaults = array_merge($this->default, $this->defaults); + $this->filters = array_merge($this->filter, $this->filters); + $data = $this->handleDefault($data, $fields); + if ($this->filled) { $rule = $this->addFilledRule($rule); } @@ -157,6 +170,7 @@ class Validate extends RuleManager } $data = $this->getValidationFactory()->make($data, $rule, $this->message, $this->customAttributes)->validate(); + $data = $this->handlerFilter($data, $fields); if ($this->eventPriority) { $this->handleCallback($data, 2); @@ -204,6 +218,7 @@ class Validate extends RuleManager $this->afters = array_merge($this->afters, $scene->afters); $this->befores = array_merge($this->befores, $scene->befores); $this->defaults = array_merge($this->defaults, $scene->defaults); + $this->filters = array_merge($this->filters, $scene->filters); $this->eventPriority = $scene->eventPriority; return $scene->getRules(); } @@ -333,26 +348,83 @@ class Validate extends RuleManager throw new ValidateException($message, 403); } } else { - throw new ValidateException('Event error or nonexistence'); + throw new ValidateRuntimeException('Event error or nonexistence'); } } } /** - * Processing the set defaults + * Filters for processing settings * * @param array $data - * @param array $rule + * @param array $fields * @return array */ - private function handleDefault(array $data, array $rule): array + private function handlerFilter(array $data, array $fields): array + { + if (empty($this->filters)) { + return $data; + } + + $newData = validate_collect($data); + $filters = array_intersect_key($this->filters, array_flip($fields)); + foreach ($filters as $field => $callback) { + if (false !== strpos($field, '*')) { + $flatData = ValidationData::initializeAndGatherData($field, $data); + $pattern = str_replace('\*', '[^\.]*', preg_quote($field)); + foreach ($flatData as $key => $value) { + if (Str::startsWith($key, $field) || preg_match('/^' . $pattern . '\z/', $key)) { + $this->filterValue($key, $callback, $newData); + } + } + } else { + $this->filterValue($field, $callback, $newData); + } + } + + return $newData->toArray(); + } + + /** + * Filter the given value + * + * @param string $field Name of the data field to be processed + * @param callable|Closure|FilterInterface $callback The filter. This can be a global function name, anonymous function, etc. + * @param ValidateCollection $data + */ + private function filterValue(string $field, $callback, ValidateCollection $data) + { + $value = $data->get($field); + + if (is_callable($callback)) { + $value = call_user_func($callback, $value); + } elseif (class_exists($callback) && is_subclass_of($callback, FilterInterface::class)) { + /** @var FilterInterface $filter */ + $filter = new $callback; + $value = $filter->handle($value); + } elseif (is_string($callback) && method_exists($this, 'filter' . ucfirst($callback))) { + $value = call_user_func([$this, 'filter' . ucfirst($callback)], $value); + } else { + throw new ValidateRuntimeException('The provided filter is wrong'); + } + + $data->set($field, $value); + } + + /** + * Defaults for processing settings + * + * @param array $data + * @param array $fields + * @return array + */ + private function handleDefault(array $data, array $fields): array { if (empty($this->defaults)) { return $data; } $newData = validate_collect($data); - $fields = array_keys($rule); $defaults = array_intersect_key($this->defaults, array_flip($fields)); foreach ($defaults as $field => $value) { // Skip array members @@ -374,22 +446,9 @@ class Validate extends RuleManager * Applying default settings to data * * @param string $field Name of the data field to be processed - * @param callable|Closure|mixed $callback the default value or an anonymous function that returns the default value which will - * be assigned to the attributes being validated if they are empty. The signature of the anonymous function - * should be as follows,The anonymous function has two parameters: - * - * - * e.g: - * - * function($value,$originalData){ - * return $value; - * } - * - * @param ValidateCollection $data Data to be processed - * @param bool $any Whether to handle arbitrary values, default only handle values that are not null + * @param callable|Closure|mixed $callback The default value or an anonymous function that returns the default value which will + * @param ValidateCollection $data Data to be processed + * @param bool $any Whether to handle arbitrary values, default only handle values that are not null */ private function setDefaultData(string $field, $callback, ValidateCollection $data, bool $any = false) { @@ -418,6 +477,7 @@ class Validate extends RuleManager $this->afters = []; $this->befores = []; $this->defaults = []; + $this->filters = []; $this->eventPriority = true; } @@ -439,7 +499,7 @@ class Validate extends RuleManager $this->setMessageProvider($messageProvider); return $this; } else { - throw new ValidateException('The provided message processor needs to implement the MessageProviderInterface interface'); + throw new ValidateRuntimeException('The provided message processor needs to implement the MessageProviderInterface interface'); } return $this; diff --git a/tests/Test/TestDataFilter.php b/tests/Test/TestDataFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..6766882f9ff89d06ad6377c115d5a1e9e62d1a5d --- /dev/null +++ b/tests/Test/TestDataFilter.php @@ -0,0 +1,104 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Support\Concerns\FilterInterface; +use W7\Validate\Validate; + +class UniqueFilter implements FilterInterface +{ + public function handle($value) + { + return array_unique($value); + } +} +class TestDataFilter extends BaseTestValidate +{ + public function testSetFilterIsSystemMethod() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|numeric' + ]; + + protected $filter = [ + 'id' => 'intval' + ]; + }; + + $data = $v->check(['id' => '1']); + + $this->assertTrue(1 === $data['id']); + } + + public function testSetFilterIsClassMethod() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required' + ]; + + protected $filter = [ + 'id' => 'toArray' + ]; + + public function filterToArray($value) + { + return explode(',', $value); + } + }; + + $data = $v->check(['id' => '1,2,3,4,5']); + + $this->assertEquals([1, 2, 3, 4, 5], $data['id']); + } + + public function testSetFilterIsFilterClass() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|array', + 'id.*' => 'numeric' + ]; + + protected $filter = [ + 'id' => UniqueFilter::class + ]; + }; + + $data = $v->check(['id' => [1, 1, 2, 3, 4, 4, 5, 6, 7]]); + + $this->assertEquals([1, 2, 3, 4, 5, 6, 7], array_values($data['id'])); + } + + public function testSetFilterForArrayField() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|array', + 'id.*' => 'numeric' + ]; + + protected $filter = [ + 'id.*' => 'intval' + ]; + }; + + $data = $v->check(['id' => ['1', '2', 3, '4']]); + + foreach ($data['id'] as $id) { + $this->assertEquals('integer', gettype($id)); + } + } +}