diff --git a/README.md b/README.md index 332f7ef..f54f2a2 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,36 @@ public function getData(int $id, ?string $name): array { } ``` +### Reusable Parameter Sets + +Implement the `ParameterSet` interface to group related parameters: + +```php +class PaginationParams implements ParameterSet { + public function getParameters(): array { + return [ + 'page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 1], + 'per_page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 20], + ]; + } +} +``` + +Use with attributes: + +```php +#[GetMapping] +#[ResponseBody] +#[UseParameterSet(PaginationParams::class)] +public function listItems(int $page = 1, int $perPage = 20): array { ... } +``` + +Or traditionally: + +```php +$this->addParameterSet(new PaginationParams()); +``` + ## Dynamic Status Codes with ResponseEntity The `ResponseEntity` class allows `#[ResponseBody]` methods to return different HTTP status codes based on runtime logic: diff --git a/WebFiori/Http/Annotations/UseParameterSet.php b/WebFiori/Http/Annotations/UseParameterSet.php new file mode 100644 index 0000000..0049fec --- /dev/null +++ b/WebFiori/Http/Annotations/UseParameterSet.php @@ -0,0 +1,21 @@ +deprecated; } - /** - * Returns the description. - * - * @return string|null Returns the value, or null if not set. - */ - public function getDescription(): ?string { - return $this->description; - } - /** * Returns the example. * @@ -190,19 +172,6 @@ public function setDeprecated(bool $deprecated): HeaderObj { return $this; } - /** - * Sets the description of the header. - * - * @param string $description A brief description of the header. - * - * @return HeaderObj Returns self for method chaining. - */ - public function setDescription(string $description): HeaderObj { - $this->description = $description; - - return $this; - } - /** * Sets an example of the header's potential value. * @@ -289,37 +258,14 @@ public function setStyle(string $style): HeaderObj { public function toJSON(): Json { $json = new Json(); - if ($this->getDescription() !== null) { - $json->add('description', $this->getDescription()); - } - - if ($this->getRequired()) { - $json->add('required', $this->getRequired()); - } - - if ($this->getDeprecated()) { - $json->add('deprecated', $this->getDeprecated()); - } - - if ($this->getStyle() !== null) { - $json->add('style', $this->getStyle()); - } - - if ($this->getExplode() !== null) { - $json->add('explode', $this->getExplode()); - } - - if ($this->getSchema() !== null) { - $json->add('schema', $this->getSchema()); - } - - if ($this->getExample() !== null) { - $json->add('example', $this->getExample()); - } - - if ($this->getExamples() !== null) { - $json->add('examples', $this->getExamples()); - } + $this->addIfNotNull($json, 'description', $this->getDescription()); + $this->addIfTruthy($json, 'required', $this->getRequired()); + $this->addIfTruthy($json, 'deprecated', $this->getDeprecated()); + $this->addIfNotNull($json, 'style', $this->getStyle()); + $this->addIfNotNull($json, 'explode', $this->getExplode()); + $this->addIfNotNull($json, 'schema', $this->getSchema()); + $this->addIfNotNull($json, 'example', $this->getExample()); + $this->addIfNotNull($json, 'examples', $this->getExamples()); return $json; } diff --git a/WebFiori/Http/OpenAPI/OpenAPIObject.php b/WebFiori/Http/OpenAPI/OpenAPIObject.php new file mode 100644 index 0000000..de4f3bc --- /dev/null +++ b/WebFiori/Http/OpenAPI/OpenAPIObject.php @@ -0,0 +1,58 @@ +description; + } + + public function setDescription(string $description) : static { + $this->description = $description; + return $this; + } + /** + * Adds a value to a Json object only if it is not null. + * + * @param Json $json The target Json object. + * @param string $key The JSON key. + * @param mixed $value The value to add. + */ + protected function addIfNotNull(Json $json, string $key, mixed $value) : void { + if ($value !== null) { + $json->add($key, $value); + } + } + /** + * Adds a value to a Json object only if it is truthy (non-null, non-false, non-empty). + * + * @param Json $json The target Json object. + * @param string $key The JSON key. + * @param mixed $value The value to add. + */ + protected function addIfTruthy(Json $json, string $key, mixed $value) : void { + if (!empty($value)) { + $json->add($key, $value); + } + } +} diff --git a/WebFiori/Http/OpenAPI/ParameterObj.php b/WebFiori/Http/OpenAPI/ParameterObj.php index 45d6b87..4297d60 100644 --- a/WebFiori/Http/OpenAPI/ParameterObj.php +++ b/WebFiori/Http/OpenAPI/ParameterObj.php @@ -27,7 +27,7 @@ * * @see https://spec.openapis.org/oas/v3.1.0#parameter-object */ -class ParameterObj implements JsonI { +class ParameterObj extends OpenAPIObject implements JsonI { /** * If true, clients MAY pass a zero-length string value in place of parameters * that would otherwise be omitted entirely. @@ -58,16 +58,6 @@ class ParameterObj implements JsonI { */ private bool $deprecated = false; - /** - * A brief description of the parameter. - * - * This could contain examples of use. - * CommonMark syntax MAY be used for rich text representation. - * - * @var string|null - */ - private ?string $description = null; - /** * Example of the parameter's potential value. * @@ -176,15 +166,6 @@ public function getDeprecated(): bool { return $this->deprecated; } - /** - * Returns the description. - * - * @return string|null Returns the value, or null if not set. - */ - public function getDescription(): ?string { - return $this->description; - } - /** * Returns the example. * @@ -325,19 +306,6 @@ public function setDeprecated(bool $deprecated): ParameterObj { return $this; } - /** - * Sets the description of the parameter. - * - * @param string $description A brief description of the parameter. - * - * @return ParameterObj Returns self for method chaining. - */ - public function setDescription(string $description): ParameterObj { - $this->description = $description; - - return $this; - } - /** * Sets an example of the parameter's potential value. * @@ -457,45 +425,16 @@ public function toJSON(): Json { 'in' => $this->getIn() ]); - if ($this->getDescription() !== null) { - $json->add('description', $this->getDescription()); - } - - if ($this->getRequired()) { - $json->add('required', $this->getRequired()); - } - - if ($this->getDeprecated()) { - $json->add('deprecated', $this->getDeprecated()); - } - - if ($this->getAllowEmptyValue()) { - $json->add('allowEmptyValue', $this->getAllowEmptyValue()); - } - - if ($this->getStyle() !== null) { - $json->add('style', $this->getStyle()); - } - - if ($this->getExplode() !== null) { - $json->add('explode', $this->getExplode()); - } - - if ($this->getAllowReserved() !== null) { - $json->add('allowReserved', $this->getAllowReserved()); - } - - if ($this->getSchema() !== null) { - $json->add('schema', $this->getSchema()); - } - - if ($this->getExample() !== null) { - $json->add('example', $this->getExample()); - } - - if ($this->getExamples() !== null) { - $json->add('examples', $this->getExamples()); - } + $this->addIfNotNull($json, 'description', $this->getDescription()); + $this->addIfTruthy($json, 'required', $this->getRequired()); + $this->addIfTruthy($json, 'deprecated', $this->getDeprecated()); + $this->addIfTruthy($json, 'allowEmptyValue', $this->getAllowEmptyValue()); + $this->addIfNotNull($json, 'style', $this->getStyle()); + $this->addIfNotNull($json, 'explode', $this->getExplode()); + $this->addIfNotNull($json, 'allowReserved', $this->getAllowReserved()); + $this->addIfNotNull($json, 'schema', $this->getSchema()); + $this->addIfNotNull($json, 'example', $this->getExample()); + $this->addIfNotNull($json, 'examples', $this->getExamples()); return $json; } diff --git a/WebFiori/Http/ParameterSet.php b/WebFiori/Http/ParameterSet.php new file mode 100644 index 0000000..9930fe9 --- /dev/null +++ b/WebFiori/Http/ParameterSet.php @@ -0,0 +1,31 @@ + Parameter definitions keyed by name. + */ + public function getParameters(): array; +} diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 0d08afb..5e41d3c 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -235,6 +235,14 @@ public function addParameters(array $params) { } } } + /** + * Adds all parameters from a ParameterSet to this service. + * + * @param ParameterSet $set The parameter set to add. + */ + public function addParameterSet(ParameterSet $set) : void { + $this->addParameters($set->getParameters()); + } /** * Adds new request method. * @@ -1304,6 +1312,23 @@ private function configureParametersForHttpMethod(string $httpMethod): void { * Configure parameters from method RequestParam annotations. */ private function configureParametersFromMethod(\ReflectionMethod $method): void { + // Process #[UseParameterSet] attributes first + $setAttributes = $method->getAttributes(Annotations\UseParameterSet::class); + + foreach ($setAttributes as $setAttr) { + $setAnnotation = $setAttr->newInstance(); + $className = $setAnnotation->class; + + if (class_exists($className)) { + $setInstance = new $className(); + + if ($setInstance instanceof ParameterSet) { + $this->addParameterSet($setInstance); + } + } + } + + // Then process #[RequestParam] attributes $paramAttributes = $method->getAttributes(Annotations\RequestParam::class); foreach ($paramAttributes as $attribute) { @@ -1363,15 +1388,37 @@ private function getMethodParameters(string $methodName): array { $mappedObject = $this->getObject($mapEntity->entityClass, $mapEntity->setters); $params[] = $mappedObject; } else { - // Use #[RequestParam] attributes for positional matching + // Build combined parameter name list: UseParameterSet params first, then RequestParam + $setAttrs = $reflection->getAttributes(Annotations\UseParameterSet::class); + $paramNames = []; + + foreach ($setAttrs as $setAttr) { + $setAnnotation = $setAttr->newInstance(); + $className = $setAnnotation->class; + + if (class_exists($className)) { + $setInstance = new $className(); + + if ($setInstance instanceof ParameterSet) { + foreach (array_keys($setInstance->getParameters()) as $name) { + $paramNames[] = $name; + } + } + } + } + $requestParamAttrs = $reflection->getAttributes(Annotations\RequestParam::class); + + foreach ($requestParamAttrs as $rpAttr) { + $rp = $rpAttr->newInstance(); + $paramNames[] = $rp->name; + } + $methodParams = $reflection->getParameters(); foreach ($methodParams as $index => $param) { - // If a RequestParam attribute exists at this position, use its name - if (isset($requestParamAttrs[$index])) { - $annotation = $requestParamAttrs[$index]->newInstance(); - $value = $this->getParamVal($annotation->name); + if (isset($paramNames[$index])) { + $value = $this->getParamVal($paramNames[$index]); } else { $value = $this->getParamVal($param->getName()); } diff --git a/examples/01-core/07-reusable-parameter-sets/README.md b/examples/01-core/07-reusable-parameter-sets/README.md new file mode 100644 index 0000000..e1ed701 --- /dev/null +++ b/examples/01-core/07-reusable-parameter-sets/README.md @@ -0,0 +1,82 @@ +# Reusable Parameter Sets + +Demonstrates grouping related parameters into reusable sets that can be shared across services. + +## What This Example Demonstrates + +- Implementing the `ParameterSet` interface +- Using `#[UseParameterSet]` attribute on methods +- Combining parameter sets with explicit `#[RequestParam]` attributes +- Validation (pattern, allowed-values) works through parameter sets + +## Files + +- [`index.php`](index.php) - Defines parameter sets and uses them in a service + +## How to Run + +```bash +php -S localhost:8080 +``` + +## Testing + +```bash +# List orders with pagination (uses PaginationParams set) +curl "http://localhost:8080?page=2&per_page=50" + +# List orders with defaults +curl "http://localhost:8080" + +# Create order (uses AddressParams set + explicit total param) +curl -X POST http://localhost:8080 \ + -d "street=123+Main+St&city=Springfield&zip=12345&country=US&total=99.99" + +# Invalid zip (pattern validation from set) +curl -X POST http://localhost:8080 \ + -d "street=123+Main+St&city=Springfield&zip=ABCDE&country=US&total=99.99" + +# Invalid country (allowed-values validation from set) +curl -X POST http://localhost:8080 \ + -d "street=123+Main+St&city=Springfield&zip=12345&country=JP&total=99.99" +``` + +## Code Explanation + +### Define a Parameter Set + +```php +class PaginationParams implements ParameterSet { + public function getParameters(): array { + return [ + 'page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 1], + 'per_page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 20], + ]; + } +} +``` + +### Use with Attributes + +```php +#[GetMapping] +#[ResponseBody] +#[UseParameterSet(PaginationParams::class)] +public function listItems(int $page = 1, int $perPage = 20): array { ... } +``` + +### Use Traditionally + +```php +$this->addParameterSet(new PaginationParams()); +``` + +### Combine Sets with Explicit Params + +```php +#[UseParameterSet(AddressParams::class)] +#[RequestParam('total', ParamType::DOUBLE)] +public function createOrder(string $street, string $city, string $zip, string $country, float $total): array { ... } +``` + +Method parameters are matched positionally: set params first, then `#[RequestParam]` params. diff --git a/examples/01-core/07-reusable-parameter-sets/index.php b/examples/01-core/07-reusable-parameter-sets/index.php new file mode 100644 index 0000000..709f34a --- /dev/null +++ b/examples/01-core/07-reusable-parameter-sets/index.php @@ -0,0 +1,78 @@ + [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 1, ParamOption::MIN => 1], + 'per_page' => [ParamOption::TYPE => ParamType::INT, ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 20, ParamOption::MIN => 1, ParamOption::MAX => 100], + ]; + } +} + +class AddressParams implements ParameterSet { + public function getParameters(): array { + return [ + 'street' => [ParamOption::TYPE => ParamType::STRING], + 'city' => [ParamOption::TYPE => ParamType::STRING], + 'zip' => [ParamOption::TYPE => ParamType::STRING, ParamOption::PATTERN => '/^[0-9]{5}$/'], + 'country' => [ParamOption::TYPE => ParamType::STRING, ParamOption::ALLOWED_VALUES => ['US', 'UK', 'DE']], + ]; + } +} + +// Use parameter sets in services via attributes + +#[RestController('orders')] +class OrderService extends WebService { + + #[GetMapping] + #[ResponseBody] + #[AllowAnonymous] + #[UseParameterSet(PaginationParams::class)] + public function listOrders(int $page = 1, int $perPage = 20): array { + return [ + 'page' => $page, + 'per_page' => $perPage, + 'orders' => [ + ['id' => 1, 'total' => 29.99], + ['id' => 2, 'total' => 59.99], + ] + ]; + } + + #[PostMapping] + #[ResponseBody] + #[AllowAnonymous] + #[UseParameterSet(AddressParams::class)] + #[RequestParam('total', ParamType::DOUBLE)] + public function createOrder(string $street, string $city, string $zip, string $country, float $total): array { + return [ + 'message' => 'Order created', + 'address' => compact('street', 'city', 'zip', 'country'), + 'total' => $total, + ]; + } + + public function isAuthorized(): bool { return true; } + public function processRequest() {} +} + +$processor = new RequestProcessor(); +$processor->process(new OrderService()); diff --git a/sonar-project.properties b/sonar-project.properties index 2670196..c1f3e65 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,6 +6,7 @@ sonar.projectName=http sonar.projectVersion=1.0 sonar.exclusions=examples/**, tests/** +sonar.cpd.exclusions=tests/**, examples/** sonar.php.coverage.reportPaths=clover.xml # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 \ No newline at end of file diff --git a/tests/WebFiori/Tests/Http/ParameterSetTest.php b/tests/WebFiori/Tests/Http/ParameterSetTest.php new file mode 100644 index 0000000..dbb805c --- /dev/null +++ b/tests/WebFiori/Tests/Http/ParameterSetTest.php @@ -0,0 +1,237 @@ +assertInstanceOf(ParameterSet::class, $set); + + $params = $set->getParameters(); + $this->assertArrayHasKey('page', $params); + $this->assertArrayHasKey('per_page', $params); + } + + public function testAddParameterSetTraditional() { + $service = new class extends WebService { + public function __construct() { + parent::__construct('test-set'); + $this->addRequestMethod('GET'); + $this->addParameterSet(new PaginationParams()); + } + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $this->assertTrue($service->hasParameter('page')); + $this->assertTrue($service->hasParameter('per_page')); + } + + public function testAddMultipleSets() { + $service = new class extends WebService { + public function __construct() { + parent::__construct('multi-set'); + $this->addRequestMethod('POST'); + $this->addParameterSet(new PaginationParams()); + $this->addParameterSet(new AddressParams()); + } + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + // Pagination params + $this->assertTrue($service->hasParameter('page')); + $this->assertTrue($service->hasParameter('per_page')); + // Address params + $this->assertTrue($service->hasParameter('street')); + $this->assertTrue($service->hasParameter('city')); + $this->assertTrue($service->hasParameter('zip')); + $this->assertTrue($service->hasParameter('country')); + } + + public function testParameterSetPreservesOptions() { + $service = new class extends WebService { + public function __construct() { + parent::__construct('options-test'); + $this->addRequestMethod('GET'); + $this->addParameterSet(new PaginationParams()); + } + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $pageParam = $service->getParameterByName('page'); + $this->assertNotNull($pageParam); + $this->assertTrue($pageParam->isOptional()); + $this->assertEquals(1, $pageParam->getDefault()); + $this->assertEquals(1, $pageParam->getMinValue()); + } + + public function testParameterSetWithPatternAndAllowedValues() { + $service = new class extends WebService { + public function __construct() { + parent::__construct('validation-test'); + $this->addRequestMethod('GET'); + $this->addParameterSet(new AddressParams()); + } + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $zipParam = $service->getParameterByName('zip'); + $this->assertNotNull($zipParam); + $this->assertEquals('/^[0-9]{5}$/', $zipParam->getPattern()); + + $countryParam = $service->getParameterByName('country'); + $this->assertNotNull($countryParam); + $this->assertEquals(['US', 'UK', 'DE'], $countryParam->getAllowedValues()); + } + + // ========================================================================= + // #[UseParameterSet] attribute tests + // ========================================================================= + + public function testUseParameterSetAttribute() { + $manager = new WebServicesManager(); + $manager->addService(new ParameterSetService()); + + $output = $this->getRequest($manager, 'parameter-set-service', [ + 'page' => '3', + 'per_page' => '50', + ]); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals(3, $response['page']); + $this->assertEquals(50, $response['per_page']); + } + + public function testUseParameterSetWithDefaults() { + $manager = new WebServicesManager(); + $manager->addService(new ParameterSetService()); + + // No params — should use defaults (page=1, per_page=20) + $output = $this->getRequest($manager, 'parameter-set-service'); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals(1, $response['page']); + $this->assertEquals(20, $response['per_page']); + } + + public function testUseParameterSetWithRequestParam() { + $manager = new WebServicesManager(); + $manager->addService(new ParameterSetService()); + + $output = $this->postRequest($manager, 'parameter-set-service', [ + 'street' => '123 Main St', + 'city' => 'Springfield', + 'zip' => '12345', + 'country' => 'US', + 'note' => 'Leave at door', + ]); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('123 Main St', $response['street']); + $this->assertEquals('Springfield', $response['city']); + $this->assertEquals('12345', $response['zip']); + $this->assertEquals('US', $response['country']); + $this->assertEquals('Leave at door', $response['note']); + } + + public function testUseParameterSetValidationApplied() { + $manager = new WebServicesManager(); + $manager->addService(new ParameterSetService()); + + // Invalid zip (not 5 digits) and invalid country (not in allowed values) + $output = $this->postRequest($manager, 'parameter-set-service', [ + 'street' => '123 Main St', + 'city' => 'Springfield', + 'zip' => 'ABCDE', + 'country' => 'JP', + ]); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('error', $response['type']); + } + + public function testUseParameterSetMissingRequired() { + $manager = new WebServicesManager(); + $manager->addService(new ParameterSetService()); + + // Missing required address fields + $output = $this->postRequest($manager, 'parameter-set-service', [ + 'street' => '123 Main St', + ]); + + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('error', $response['type']); + } + + public function testInvalidSetClassIgnored() { + // A UseParameterSet pointing to a non-ParameterSet class should be silently ignored + $service = new class extends WebService { + public function __construct() { + parent::__construct('invalid-set'); + $this->addRequestMethod('GET'); + } + public function isAuthorized(): bool { return true; } + public function processRequest() { + $this->sendResponse('ok', 200, 'success'); + } + }; + + // Manually test that configuring with a non-ParameterSet class doesn't crash + $manager = new WebServicesManager(); + $manager->addService($service); + + $output = $this->getRequest($manager, 'invalid-set'); + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('success', $response['type']); + } + + public function testNonExistentClassIgnored() { + // Verify that a non-existent class in UseParameterSet doesn't crash + $service = new class extends WebService { + public function __construct() { + parent::__construct('nonexistent-set'); + $this->addRequestMethod('GET'); + } + public function isAuthorized(): bool { return true; } + public function processRequest() { + $this->sendResponse('ok', 200, 'success'); + } + }; + + $manager = new WebServicesManager(); + $manager->addService($service); + + $output = $this->getRequest($manager, 'nonexistent-set'); + $response = json_decode($output, true); + $this->assertIsArray($response); + $this->assertEquals('success', $response['type']); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/AddressParams.php b/tests/WebFiori/Tests/Http/TestServices/AddressParams.php new file mode 100644 index 0000000..10932b4 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/AddressParams.php @@ -0,0 +1,27 @@ + [ + ParamOption::TYPE => ParamType::STRING, + ], + 'city' => [ + ParamOption::TYPE => ParamType::STRING, + ], + 'zip' => [ + ParamOption::TYPE => ParamType::STRING, + ParamOption::PATTERN => '/^[0-9]{5}$/', + ], + 'country' => [ + ParamOption::TYPE => ParamType::STRING, + ParamOption::ALLOWED_VALUES => ['US', 'UK', 'DE'], + ], + ]; + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/PaginationParams.php b/tests/WebFiori/Tests/Http/TestServices/PaginationParams.php new file mode 100644 index 0000000..68defa6 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/PaginationParams.php @@ -0,0 +1,26 @@ + [ + ParamOption::TYPE => ParamType::INT, + ParamOption::OPTIONAL => true, + ParamOption::DEFAULT => 1, + ParamOption::MIN => 1 + ], + 'per_page' => [ + ParamOption::TYPE => ParamType::INT, + ParamOption::OPTIONAL => true, + ParamOption::DEFAULT => 20, + ParamOption::MIN => 1, + ParamOption::MAX => 100 + ], + ]; + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/ParameterSetService.php b/tests/WebFiori/Tests/Http/TestServices/ParameterSetService.php new file mode 100644 index 0000000..1c14848 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/ParameterSetService.php @@ -0,0 +1,50 @@ +add('page', $page); + $json->add('per_page', $perPage); + return $json; + } + + #[PostMapping] + #[ResponseBody] + #[AllowAnonymous] + #[UseParameterSet(AddressParams::class)] + #[RequestParam('note', ParamType::STRING, true)] + public function createWithAddress(string $street, string $city, string $zip, string $country, ?string $note): Json { + $json = new Json(); + $json->add('street', $street); + $json->add('city', $city); + $json->add('zip', $zip); + $json->add('country', $country); + $json->add('note', $note); + return $json; + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + } +}