diff --git a/src/Attributes/ChoiceControlItems.php b/src/Attributes/ChoiceControlItems.php new file mode 100644 index 0000000..472220f --- /dev/null +++ b/src/Attributes/ChoiceControlItems.php @@ -0,0 +1,25 @@ +items = $items; + $this->useKeys = $useKeys; + } + +} diff --git a/src/Attributes/ContainerType.php b/src/Attributes/ContainerType.php new file mode 100644 index 0000000..a02d7a6 --- /dev/null +++ b/src/Attributes/ContainerType.php @@ -0,0 +1,22 @@ +value = $value; + } + +} diff --git a/src/Attributes/Control.php b/src/Attributes/Control.php new file mode 100644 index 0000000..64ac1a7 --- /dev/null +++ b/src/Attributes/Control.php @@ -0,0 +1,34 @@ +value = $value; + } + +} diff --git a/src/Attributes/RequiredRule.php b/src/Attributes/RequiredRule.php new file mode 100644 index 0000000..8dcb91a --- /dev/null +++ b/src/Attributes/RequiredRule.php @@ -0,0 +1,18 @@ +required = $required; + } + +} diff --git a/src/Attributes/ValidationRule.php b/src/Attributes/ValidationRule.php new file mode 100644 index 0000000..af628ae --- /dev/null +++ b/src/Attributes/ValidationRule.php @@ -0,0 +1,25 @@ +validator = $validator; + $this->errorMessage = $errorMessage; + $this->args = $args; + } + +} diff --git a/src/Helpers/FormBuilder.php b/src/Helpers/FormBuilder.php index 09e8ce1..bd81e2d 100644 --- a/src/Helpers/FormBuilder.php +++ b/src/Helpers/FormBuilder.php @@ -6,61 +6,81 @@ use Nette\ArgumentOutOfRangeException; use Nette\Forms\Container; use Nette\Forms\Controls\BaseControl; +use Nette\Forms\Controls\ChoiceControl; use Nette\NotSupportedException; +use ReflectionProperty; +use Throwable; +use Wedo\Api\Attributes\ChoiceControlItems; +use Wedo\Api\Attributes\ContainerType; +use Wedo\Api\Attributes\Control; use Wedo\Api\Requests\BaseRequest; -use Wedo\Utilities\ClassNameHelper; class FormBuilder { /** - * @param array $properties + * @param ReflectionProperty[] $properties * @param mixed[] $data */ public function createForm(array $properties, BaseRequest $request, Container $form, array $data): void { - /** - * @var string $property - * @var string[][] $annotations - */ - foreach ($properties as $property => $annotations) { - $controlType = $this->getControlType($annotations); + foreach ($properties as $property) { + $controlType = $this->getControlType($property); $controlAddCallback = [$form, 'add' . $controlType]; - if ((!method_exists($form, $controlAddCallback[1]) && $controlType !== 'Date') || !is_callable($controlAddCallback)) { - throw new NotSupportedException('Control of type ' . $controlType . ' does not exist!'); + try { + $control = $controlAddCallback($property->getName()); //@phpstan-ignore-line + } catch (Throwable $ex) { + throw new NotSupportedException('Cannot add control of type ' . $controlType, 0, $ex); } - $control = call_user_func($controlAddCallback, $property); - - if ($controlType === 'Container') { + if ($controlType === Control::CONTAINER) { /** @phpstan-ignore-next-line */ $request->$property = []; /** @phpstan-ignore-next-line */ - if (empty($data[$property])) { + if (empty($data[$property->getName()])) { continue; } - $values = $data[$property]; + $values = $data[$property->getName()]; + + $requestTypeAttributes = $property->getAttributes(ContainerType::class); + + if (count($requestTypeAttributes) === 0) { + throw new NotSupportedException('ContainerType attribute not found for ' . + $property->getDeclaringClass()->getName() . '::' . $property->getName()); + } + + /** @var ContainerType $requestTypeAttribute */ + $requestTypeAttribute = $requestTypeAttributes[0]->newInstance(); - $reqType = rtrim($annotations['var'][0], ']['); - $reqType = ClassNameHelper::extractFqnFromObjectUseStatements($request, $reqType); + $requestType = $requestTypeAttribute->value; foreach ($values as $key => $value) { /** @var Container $container */ $container = $control->addContainer($key); - $item = new $reqType(); + $item = new $requestType(); $item->buildForm($value, $container, $item); /** @phpstan-ignore-next-line */ - $request->$property[] = $item; + $request->{$property->getName()}[] = $item; } } - unset($annotations['var'], $annotations['description'], $annotations['control']); - if ($control instanceof BaseControl) { - $request->setValidationRules($annotations, $control); + $request->setValidationRules($property, $control); + } + + if ($control instanceof ChoiceControl) { + $itemsAttributes = $property->getAttributes(ChoiceControlItems::class); + + if (count($itemsAttributes) === 0) { + throw new NotSupportedException('Choice control must have ChoiceControlItems attribute set!'); + } + + /** @var ChoiceControlItems $itemAttribute */ + $itemAttribute = $itemsAttributes[0]->newInstance(); + $control->setItems($itemAttribute->items, $itemAttribute->useKeys); } } } @@ -71,17 +91,20 @@ public function createEmptyForm(): Form } /** - * @param string[][] $annotations * @throws ArgumentOutOfRangeException - * @throws NotSupportedException */ - protected function getControlType(array $annotations): string + protected function getControlType(ReflectionProperty $property): string { - if (!isset($annotations['control'])) { - throw new ArgumentOutOfRangeException('@control annotation not set on request!'); + $controlAttributes = $property->getAttributes(Control::class); + if (count($controlAttributes) === 0) { + throw new ArgumentOutOfRangeException('#[Control] Attribute not set on ' . + $property->getDeclaringClass()->getName() . '::' . $property->getName()); } - return $annotations['control'][0]; + /** @var Control $controlAttribute */ + $controlAttribute = $controlAttributes[0]->newInstance(); + + return $controlAttribute->value; } } diff --git a/src/Requests/BaseRequest.php b/src/Requests/BaseRequest.php index e7d4a4b..218a455 100644 --- a/src/Requests/BaseRequest.php +++ b/src/Requests/BaseRequest.php @@ -5,15 +5,14 @@ use Nette\ArgumentOutOfRangeException; use Nette\Forms\Container; use Nette\Forms\Controls\BaseControl; -use Nette\Forms\Controls\ChoiceControl; use Nette\Forms\Form; use Nette\NotSupportedException; -use Nette\Reflection\AnnotationsParser; use Nette\Utils\ArrayHash; -use Nette\Utils\Strings; use ReflectionNamedType; use ReflectionObject; use ReflectionProperty; +use Wedo\Api\Attributes\RequiredRule; +use Wedo\Api\Attributes\ValidationRule; use Wedo\Api\Entities\ValidationError; use Wedo\Api\Exceptions\ValidationException; use Wedo\Api\Helpers\FormBuilder; @@ -53,76 +52,31 @@ public function buildForm(array $data = [], ?Container $form = null, ?BaseReques $request ??= $this; $form = $request->setForm($form); - $properties = $request->getAnnotations() ?? []; + $properties = $request->getReflectionProperties(); $this->formBuilder->createForm($properties, $request, $form, $data); $request->setValues($data); } /** - * Read annotations from public properties in current class - * - * @return array|null * @internal */ - public function getAnnotations(): ?array + public function setValidationRules(ReflectionProperty $property, BaseControl $control): void { - $reflectionProperties = $this->getReflectionProperties(); - $annotations = []; + $validationRulesAttributes = $property->getAttributes(ValidationRule::class); - foreach ($reflectionProperties as $rp) { - $annotations[$rp->name] = AnnotationsParser::getAll($rp); + foreach ($validationRulesAttributes as $validationRuleAttribute) { + /** @var ValidationRule $validationRule */ + $validationRule = $validationRuleAttribute->newInstance(); + $control->addRule($validationRule->validator, $validationRule->errorMessage, $validationRule->args); } - return $annotations ?? null; - } - - /** - * @internal - * @param string[][] $annotations - */ - public function setValidationRules(array $annotations, BaseControl $control): void - { - foreach ($annotations as $annotation => $args) { - $this->setValidationRule($annotation, $control, $args); - } - } - - /** - * @param string[] $args - */ - public function setValidationRule(string $annotation, BaseControl $control, array $args): void - { - switch ($annotation) { - case 'addCustomRule': - $this->addCustomRule($control, $args); - - return; - case 'addRule': - $this->addRule($control, $args); - - return; - case 'setRequired': - $this->addRequiredRule($control, $args); - - return; - case 'items': - if (!$control instanceof ChoiceControl) { - throw new NotSupportedException('Items annotation cannot be set on on control type "' - . $control::class . '" in ' . static::class); - } - - $this->setItems($control, $args); - - return; - default: - $callable = [$control->getRules(), $annotation]; + $requiredRuleAttributes = $property->getAttributes(RequiredRule::class); - if (!method_exists($control->getRules(), $annotation) || !is_callable($callable)) { - throw new NotSupportedException('Cannot apply rule "' . $annotation . '"!'); - } - - call_user_func_array($callable, $args); + if (count($requiredRuleAttributes) > 0) { + /** @var RequiredRule $requiredRule */ + $requiredRule = $requiredRuleAttributes[0]->newInstance(); + $control->setRequired($requiredRule->required); } } @@ -310,54 +264,4 @@ protected function setForm(?Container $form = null): Container return $form; } - /** - * @param mixed[] $args - */ - private function addRule(BaseControl $control, array $args): void - { - foreach ($args as $arg) { - if ($arg instanceof ArrayHash) { - $addRuleArgs = (array) $arg; - array_unshift($addRuleArgs, constant(Form::class . '::' . strtoupper(array_shift($addRuleArgs)))); - } else { - $addRuleArgs = [constant(Form::class . '::' . strtoupper($arg))]; - } - - call_user_func_array([$control, 'addRule'], $addRuleArgs); - } - } - - /** - * @param mixed[] $args - */ - private function addCustomRule(BaseControl $control, array $args): void - { - foreach ($args as $arg) { - $addRuleArgs = $arg instanceof ArrayHash ? (array) $arg : $arg; - call_user_func_array([$control, 'addRule'], $addRuleArgs); - } - } - - /** - * @param mixed[] $args - */ - private function addRequiredRule(BaseControl $control, array $args): void - { - call_user_func_array([$control->getRules(), 'setRequired'], $args); - } - - /** - * @param mixed[] $args - * @throws NotSupportedException - */ - private function setItems(ChoiceControl $control, array $args): void - { - if (Strings::startsWith($args[0], '[') || Strings::startsWith($args[0], '{')) { - $items = json_decode($args[0], true); - $control->setItems($items); - } else { - throw new NotSupportedException('Only Json is allowed for setting items on select in request!'); - } - } - } diff --git a/tests/Requests/RequestTest.php b/tests/Requests/RequestTest.php index c8a2f4b..26e1499 100644 --- a/tests/Requests/RequestTest.php +++ b/tests/Requests/RequestTest.php @@ -62,21 +62,14 @@ public function testSetRequiredFalse(): void public function testNonExistingRule(): void { - $this->expectExceptionMessage('Cannot apply rule "addBlaSomething"!'); + $this->expectExceptionMessage('Unknown validator \'BlaSomething\' for control \'name\'.'); $request = new TestNonExistingRuleRequest(); $request->buildForm(); } - public function testItemsOnTextControlType_ShouldThrowException(): void - { - $this->expectExceptionMessage('Items annotation cannot be set on on control type'); - $request = new TestWithItemsOnTextRequest(); - $request->buildForm(); - } - public function testUnsupportedControlType(): void { - $this->expectExceptionMessage('Control of type nonExistingType123 does not exist!'); + $this->expectExceptionMessage('#[Control] Attribute not set on Wedo\Api\Tests\Requests\TestUnsupportedControlTypeRequest::name'); $request = new TestUnsupportedControlTypeRequest(); $request->buildForm(); } @@ -180,13 +173,6 @@ public function testValidate_WithValidData_ShouldReturnTrue(): void $this->assertEquals($request->name, 'Dalibor Korpar'); } - public function testItemsNotJsonRequest(): void - { - $this->expectExceptionMessage('Only Json is allowed for setting items on select in request!'); - $req = new TestItemsNotJsonRequest(); - $req->buildForm(); - } - public function testArrayRequest(): void { $json = '{"items":[{"name":"first", "name": null, "name": "bla"}]}'; diff --git a/tests/Requests/RequiredFalseRequest.php b/tests/Requests/RequiredFalseRequest.php index 2b2ccd1..2f0863a 100644 --- a/tests/Requests/RequiredFalseRequest.php +++ b/tests/Requests/RequiredFalseRequest.php @@ -2,15 +2,15 @@ namespace Wedo\Api\Tests\Requests; +use Wedo\Api\Attributes\Control; +use Wedo\Api\Attributes\RequiredRule; use Wedo\Api\Requests\BaseRequest; class RequiredFalseRequest extends BaseRequest { - /** - * @setRequired(FALSE) - * @control Text - */ + #[Control(Control::TEXT)] + #[RequiredRule(false)] public string $name; } diff --git a/tests/Requests/SimpleRequest.php b/tests/Requests/SimpleRequest.php index 801553a..1382ba9 100644 --- a/tests/Requests/SimpleRequest.php +++ b/tests/Requests/SimpleRequest.php @@ -2,15 +2,15 @@ namespace Wedo\Api\Tests\Requests; +use Wedo\Api\Attributes\Control; +use Wedo\Api\Attributes\RequiredRule; use Wedo\Api\Requests\BaseRequest; class SimpleRequest extends BaseRequest { - /** - * @setRequired - * @control Text - */ + #[Control(Control::TEXT)] + #[RequiredRule()] public string $name; } diff --git a/tests/Requests/SimpleRequest2.php b/tests/Requests/SimpleRequest2.php index 3521bd3..f23f27d 100644 --- a/tests/Requests/SimpleRequest2.php +++ b/tests/Requests/SimpleRequest2.php @@ -2,12 +2,13 @@ namespace Wedo\Api\Tests\Requests; +use Wedo\Api\Attributes\Control; use Wedo\Api\Requests\BaseRequest; class SimpleRequest2 extends BaseRequest { - /** @control Text */ + #[Control(Control::TEXT)] public string $name; } diff --git a/tests/Requests/TestArrayRequest.php b/tests/Requests/TestArrayRequest.php index d059c9a..3def550 100644 --- a/tests/Requests/TestArrayRequest.php +++ b/tests/Requests/TestArrayRequest.php @@ -2,22 +2,24 @@ namespace Wedo\Api\Tests\Requests; +use Nette\Forms\Form; +use Wedo\Api\Attributes\ContainerType; +use Wedo\Api\Attributes\Control; +use Wedo\Api\Attributes\ValidationRule; use Wedo\Api\Requests\BaseRequest; class TestArrayRequest extends BaseRequest { - /** - * @var SimpleRequest[] - * @control Container - * @setRequired() - */ + /** @var SimpleRequest[] $items */ + #[Control(Control::CONTAINER)] + #[ValidationRule(Form::REQUIRED)] + #[ContainerType(SimpleRequest::class)] public array $items; - /** - * @var SimpleRequest2[] - * @control Container - */ + /** @var SimpleRequest2[] $item2 */ + #[Control(Control::CONTAINER)] + #[ContainerType(SimpleRequest2::class)] public array $item2; } diff --git a/tests/Requests/TestItemsNotJsonRequest.php b/tests/Requests/TestItemsNotJsonRequest.php deleted file mode 100644 index 077aae8..0000000 --- a/tests/Requests/TestItemsNotJsonRequest.php +++ /dev/null @@ -1,16 +0,0 @@ - 1, 2, 3, 4, 5])] + #[RequiredRule()] public int $rating; } diff --git a/tests/Requests/TestNonExistingRuleRequest.php b/tests/Requests/TestNonExistingRuleRequest.php index c18eedf..91b0bfa 100644 --- a/tests/Requests/TestNonExistingRuleRequest.php +++ b/tests/Requests/TestNonExistingRuleRequest.php @@ -2,15 +2,15 @@ namespace Wedo\Api\Tests\Requests; +use Wedo\Api\Attributes\Control; +use Wedo\Api\Attributes\ValidationRule; use Wedo\Api\Requests\BaseRequest; class TestNonExistingRuleRequest extends BaseRequest { - /** - * @control Text - * @addBlaSomething() - */ + #[Control(Control::TEXT)] + #[ValidationRule('BlaSomething')] public string $name; } diff --git a/tests/Requests/TestNotBuiltInTypeRequest.php b/tests/Requests/TestNotBuiltInTypeRequest.php index b580ae9..bf615d2 100644 --- a/tests/Requests/TestNotBuiltInTypeRequest.php +++ b/tests/Requests/TestNotBuiltInTypeRequest.php @@ -2,6 +2,7 @@ namespace Wedo\Api\Tests\Requests; +use Wedo\Api\Attributes\Control; use Wedo\Api\Requests\BaseRequest; //phpcs:disable @@ -10,8 +11,8 @@ class TestNotBuiltInTypeRequest extends BaseRequest /** * typehint not set here and this should throw an exception - * @control Text */ + #[Control(Control::TEXT)] public \stdClass $name; } diff --git a/tests/Requests/TestRequest.php b/tests/Requests/TestRequest.php index f85333e..b42a93c 100644 --- a/tests/Requests/TestRequest.php +++ b/tests/Requests/TestRequest.php @@ -2,6 +2,9 @@ namespace Wedo\Api\Tests\Requests; +use Nette\Forms\Form; +use Wedo\Api\Attributes\Control; +use Wedo\Api\Attributes\ValidationRule; use Wedo\Api\Requests\BaseRequest; class TestRequest extends BaseRequest @@ -9,50 +12,43 @@ class TestRequest extends BaseRequest /** * Customer name - * - * @control Text - * @addRule(max_length, NULL, 20) - * @setRequired */ + #[Control(Control::TEXT)] + #[ValidationRule(Form::MAX_LENGTH, null, 20)] + #[ValidationRule(Form::REQUIRED)] public string $name; /** * Customers E-mail - * - * @control Email - * @addRule(min_length, NULL, 5) */ + #[Control(Control::EMAIL)] + #[ValidationRule(Form::MIN_LENGTH, null, 5)] public string $email; /** * Customers phone - * - * @addCustomRule('Wedo\Api\Tests\Requests\CustomValidator::validate', 'phone not valid') - * @control Text */ + #[Control(Control::TEXT)] + #[ValidationRule([CustomValidator::class, 'validate'])] public string $phone; /** * Customers birthYear - * - * @control Text - * @addRule(numeric) */ + #[Control(Control::TEXT)] + #[ValidationRule(Form::NUMERIC)] public ?int $birth_year; /** - * Customers birthYear - * - * @control Text - * @isRequired() + * Amount */ + #[Control(Control::TEXT)] public float $amount; /** * Accept tos - * - * @control CheckBox */ + #[Control(Control::CHECKBOX)] public bool $accept_tos; } diff --git a/tests/Requests/TestWithItemsOnTextRequest.php b/tests/Requests/TestWithItemsOnTextRequest.php deleted file mode 100644 index 5737d03..0000000 --- a/tests/Requests/TestWithItemsOnTextRequest.php +++ /dev/null @@ -1,16 +0,0 @@ -