diff --git a/src/Features/Data/Attributes/Validation/Rule.php b/src/Features/Data/Attributes/Validation/Rule.php index 32f5b4a..6abe434 100644 --- a/src/Features/Data/Attributes/Validation/Rule.php +++ b/src/Features/Data/Attributes/Validation/Rule.php @@ -3,6 +3,7 @@ namespace Kellton\Tools\Features\Data\Attributes\Validation; use Attribute; +use Illuminate\Contracts\Validation\Rule as ValidationRule; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class Rule extends ValidationAttribute @@ -10,9 +11,10 @@ final class Rule extends ValidationAttribute /** * Rule constructor. * - * @param string $rule + * @param string|ValidationRule $rule + * @param string|null $message */ - public function __construct(public readonly string $rule) + public function __construct(public readonly string|ValidationRule $rule, public readonly ?string $message = null) { } } diff --git a/src/Features/Data/Data.php b/src/Features/Data/Data.php index d8d8fbb..4fa849f 100644 --- a/src/Features/Data/Data.php +++ b/src/Features/Data/Data.php @@ -48,6 +48,6 @@ public static function getValidationRules(): array /** @var RuleService $service */ $service = app(RuleService::class); - return $service->getByClass(static::class)->toArray(); + return $service->getByClass(static::class)->transform(fn ($value) => $value->get('rules'))->toArray(); } } diff --git a/src/Features/Data/Services/DataService.php b/src/Features/Data/Services/DataService.php index 81e345d..e083143 100644 --- a/src/Features/Data/Services/DataService.php +++ b/src/Features/Data/Services/DataService.php @@ -57,7 +57,7 @@ public function create(string $class, mixed ...$payload): Data $payload = array_merge($payload->route()->parameters, $payload->all()); } - if($payload === null) { + if ($payload === null) { $payload = []; } @@ -91,15 +91,45 @@ public function create(string $class, mixed ...$payload): Data */ private function validate(Definition $definition, Collection $payload): Collection { - $rules = $this->ruleService->get($definition); + $definitions = $this->ruleService->get($definition); - $validator = Validator::make($payload->toArray(), $rules->toArray()); + $rules = $definitions->map(fn (Collection $value) => $value->get('rules')); + $messages = $definitions->map(fn (Collection $value) => $value->get('messages')); + + $validator = Validator::make( + $payload->toArray(), + $rules->toArray(), + $this->parseValidationErrorMessages($messages) + ); $validator->validate(); return collect_all($validator->validated()); } + /** + * Parse validation error messages to array. + * + * @param Collection $validationErrorMessages + * + * @return array + */ + private function parseValidationErrorMessages(Collection $validationErrorMessages): array + { + $validationMessages = []; + + $validationErrorMessages->each(function (Collection $errorMessages, $fieldName) use (&$validationMessages) { + if ($errorMessages->isNotEmpty()) { + $errorMessages->each(function ($errorMessage, $ruleName) use ($fieldName, &$validationMessages) { + $keyName = $fieldName . '.' . explode(':', $ruleName)[0]; + $validationMessages[$keyName] = $errorMessage; + }); + } + }); + + return $validationMessages; + } + /** * Resolve map properties. * diff --git a/src/Features/Data/Services/RuleService.php b/src/Features/Data/Services/RuleService.php index eaf3866..90c7cec 100644 --- a/src/Features/Data/Services/RuleService.php +++ b/src/Features/Data/Services/RuleService.php @@ -4,6 +4,7 @@ use BackedEnum; use Carbon\Carbon; +use Kellton\Tools\Features\Data\Attributes\Validation\Rule; use Kellton\Tools\Features\Data\Attributes\Validation\ValidationAttribute; use Kellton\Tools\Features\Data\Definition; use Kellton\Tools\Features\Data\Exceptions\MissingConstructor; @@ -39,7 +40,9 @@ public function __construct(public readonly DefinitionService $definitionService */ public function get(Definition $definition): Collection { - return $definition->properties->mapWithKeys(fn (Property $property) => $this->resolve($property)->all()); + $data = $definition->properties->mapWithKeys(fn (Property $property) => $this->resolve($property)->all()); + + return $data; } /** @@ -77,7 +80,7 @@ private function resolve(Property $property): Collection return $this->getNestedRules($property, $propertyName); } - return collect([$propertyName => $this->getRulesForProperty($property)]); + return collect([$propertyName => $this->getDataForProperty($property)]); } /** @@ -87,9 +90,14 @@ private function resolve(Property $property): Collection * * @return Collection */ - protected function getRulesForProperty(Property $property): Collection + protected function getDataForProperty(Property $property): Collection { - $rules = collect(); + $data = collect([ + 'rules' => collect(), + 'messages' => collect(), + ]); + + $rules = $data->get('rules'); if ($property->isNullable) { $rules->add('nullable'); @@ -108,9 +116,9 @@ protected function getRulesForProperty(Property $property): Collection } $this->resolveTypes($property, $rules); - $this->resolveAttributeRules($property, $rules); + $this->resolveAttributeRules($property, $data); - return $rules; + return $data; } /** @@ -132,7 +140,7 @@ protected function getNestedRules(Property $property, string $propertyName): Col default => throw new TypeError() }; - $parentRules = $this->getRulesForProperty($property); + $parentRules = $this->getDataForProperty($property); $definition = $this->definitionService->get($property->dataClass); $rules = $this->get($definition); @@ -179,17 +187,20 @@ private function resolveTypes(Property $property, Collection $rules): void * Resolve rules for the attributes. * * @param Property $property - * @param Collection $rules + * @param Collection $data * * @return void */ - private function resolveAttributeRules(Property $property, Collection $rules): void + private function resolveAttributeRules(Property $property, Collection $data): void { $property ->attributes ->filter(fn (object $attribute) => is_subclass_of($attribute, ValidationAttribute::class)) - ->each(function (ValidationAttribute $rule) use ($rules) { - $rules->add($rule->rule); + ->each(function (ValidationAttribute $rule) use ($data) { + $data->get('rules')->add($rule->rule); + if ($rule instanceof Rule && $rule->message !== null) { + $data->get('messages')->put($rule->rule, $rule->message); + } }); } } diff --git a/tests/Data/TestData.php b/tests/Data/TestData.php new file mode 100644 index 0000000..54acf3d --- /dev/null +++ b/tests/Data/TestData.php @@ -0,0 +1,17 @@ +assertInstanceOf(Data::class, $data); $validationRules = $data::getValidationRules(); @@ -47,14 +49,18 @@ public function testFiltersDataShouldSucceed(): void $this->assertIsArray($validationRules); $this->assertNotEmpty($validationRules); } -} -/** - * Class TestData is used for testing readonly Data class. - */ -readonly class TestData extends Data -{ - public function __construct(public string $firstName, public string $lastName) + public function testRuleMessageShouldSucceed(): void { + try { + TestData::create([ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'email' => 'john', + ]); + } catch (ValidationException $e) { + $message = data_get($e->errors(), 'email.0'); + $this->assertSame('Wrong email address format!', $message); + } } }