From 25b03bbdccbaa2815b66082e8861e25b70726088 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 4 Mar 2026 15:36:43 +0300 Subject: [PATCH 1/4] refactor: Changed How Response is Handled --- WebFiori/Http/WebService.php | 34 +++++++++++++++---- .../Tests/Http/RestControllerTest.php | 30 +++------------- .../Http/TestServices/AnnotatedService.php | 9 +++-- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index c4597c7..604dd7c 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -16,6 +16,7 @@ use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Exceptions\HttpException; use WebFiori\Json\Json; +use WebFiori\Json\JsonConverter; use WebFiori\Json\JsonI; /** * A class that represents one web service. @@ -511,12 +512,23 @@ protected function handleMethodResponse(mixed $result, string $methodName): void } $responseBody = $responseBodyAttrs[0]->newInstance(); - + $contentType = $responseBody->contentType; // Handle custom content types - if ($responseBody->contentType !== 'application/json') { + if ($contentType !== 'application/json') { // For non-JSON content types, send raw result - $content = is_string($result) ? $result : (is_array($result) || is_object($result) ? json_encode($result) : (string)$result); - $this->send($responseBody->contentType, $content, $responseBody->status); + if (is_array($result)) { + $content = new Json(); + $content->addArray('data', $result); + $contentType = 'application/json'; + } else if (is_object($result)) { + $content = new Json(); + $content->addObject('data', $result); + $contentType = 'application/json'; + } else { + $content = (string)$result; + } + + $this->send($contentType, $content, $responseBody->status); return; } @@ -524,12 +536,20 @@ protected function handleMethodResponse(mixed $result, string $methodName): void if ($result === null) { // Null return = empty response with configured status $this->sendResponse('', $responseBody->status, $responseBody->type); - } elseif (is_array($result) || is_object($result)) { + } else if (is_array($result) || is_object($result)) { // Array/object = JSON response - $this->sendResponse('Success', $responseBody->status, $responseBody->type, $result); + if ($result instanceof Json) { + $json = $result; + } else if ($result instanceof JsonI) { + $json = $result->toJSON(); + } else { + $json = new Json(); + $json->add('data', $result); + } + $this->send($responseBody->contentType, $json, $responseBody->status); } else { // String/scalar = plain response - $this->sendResponse($result, $responseBody->status, $responseBody->type); + $this->send($responseBody->contentType, $result, $responseBody->status); } } diff --git a/tests/WebFiori/Tests/Http/RestControllerTest.php b/tests/WebFiori/Tests/Http/RestControllerTest.php index 9ffcc7a..2399975 100644 --- a/tests/WebFiori/Tests/Http/RestControllerTest.php +++ b/tests/WebFiori/Tests/Http/RestControllerTest.php @@ -55,17 +55,9 @@ public function testAnnotatedServiceWithManager() { . ' "http-code":405'.self::NL . '}', $this->postRequest($manager, 'annotated-service')); - $this->assertEquals('{'.self::NL - . ' "message":"Hi user!",'.self::NL - . ' "type":"success",'.self::NL - . ' "http-code":200'.self::NL - . '}', $this->getRequest($manager, 'annotated-service')); + $this->assertEquals('Hi user!', $this->getRequest($manager, 'annotated-service')); - $this->assertEquals('{'.self::NL - . ' "message":"Hi Ibrahim!",'.self::NL - . ' "type":"success",'.self::NL - . ' "http-code":200'.self::NL - . '}', $this->getRequest($manager, 'annotated-service', [ + $this->assertEquals('Hi Ibrahim!', $this->getRequest($manager, 'annotated-service', [ 'name' => 'Ibrahim' ])); } @@ -90,17 +82,9 @@ public function testAnnotatedGet() { $this->assertNotNull($retrievedService); $this->assertEquals('A service configured via annotations', $retrievedService->getDescription()); - $this->assertEquals('{'.self::NL - . ' "message":"Hi user!",'.self::NL - . ' "type":"success",'.self::NL - . ' "http-code":200'.self::NL - . '}', $this->getRequest($manager, 'annotated-service')); + $this->assertEquals('Hi user!', $this->getRequest($manager, 'annotated-service')); - $this->assertEquals('{'.self::NL - . ' "message":"Hi Ibrahim!",'.self::NL - . ' "type":"success",'.self::NL - . ' "http-code":200'.self::NL - . '}', $this->getRequest($manager, 'annotated-service', [ + $this->assertEquals('Hi Ibrahim!', $this->getRequest($manager, 'annotated-service', [ 'name' => 'Ibrahim' ])); } @@ -152,11 +136,7 @@ public function testAnnotatedDelete() { 'id' => 1 ], [], new TestUser(1, ['ADMIN'], ['USER_DELETE'], false))); //valid user - $this->assertEquals('{'.self::NL - . ' "message":"Delete user with ID: 1",'.self::NL - . ' "type":"success",'.self::NL - . ' "http-code":200'.self::NL - . '}', $this->deleteRequest($manager, 'annotated-service', [ + $this->assertEquals('Delete user with ID: 1', $this->deleteRequest($manager, 'annotated-service', [ 'id' => 1 ], [], new TestUser(1, ['ADMIN'], ['USER_DELETE'], true))); } diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php index 098cec6..e2d68ce 100644 --- a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php +++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php @@ -4,6 +4,7 @@ use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\DeleteMapping; use WebFiori\Http\Annotations\GetMapping; +use WebFiori\Http\Annotations\MapEntity; use WebFiori\Http\Annotations\PreAuthorize; use WebFiori\Http\Annotations\RequestParam; use WebFiori\Http\Annotations\ResponseBody; @@ -17,9 +18,8 @@ class AnnotatedService extends WebService { #[ResponseBody] #[AllowAnonymous] #[RequestParam('name', ParamType::STRING, true)] - public function sayHi() { - $name = $this->getParamVal('name'); - + public function sayHi(?string $name) { + if ($name !== null) { return "Hi ".$name.'!'; } @@ -29,8 +29,7 @@ public function sayHi() { #[ResponseBody] #[RequestParam('id', ParamType::INT)] #[PreAuthorize("isAuthenticated() && hasRole('ADMIN') && hasAuthority('USER_DELETE')")] - public function delete() { - $id = $this->getParamVal('id'); + public function delete(int $id) { return "Delete user with ID: ".$id; } } From a37745f62e1bec3109542224832b8ea19db5dbc8 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 4 Mar 2026 15:37:06 +0300 Subject: [PATCH 2/4] fix: Deprecation Warning --- WebFiori/Http/WebServicesManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index c090446..56acb85 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -167,7 +167,7 @@ public function addService(WebService $service) : WebServicesManager { * * @return WebServicesManager Returns the same instance for method chaining. */ - public function autoDiscoverServices(string $path = null) : WebServicesManager { + public function autoDiscoverServices(?string $path = null) : WebServicesManager { if ($path === null) { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); $path = dirname($trace[0]['file']); From 6de946bff25ef2b518769f7f61a6f39ba9ce4dd0 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 4 Mar 2026 15:37:28 +0300 Subject: [PATCH 3/4] chore: Added PHP CS Fixer --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d124467..ff23ea1 100644 --- a/composer.json +++ b/composer.json @@ -32,9 +32,11 @@ }, "scripts": { "test": "phpunit --configuration tests/phpunit.xml", - "test10": "phpunit --configuration tests/phpunit10.xml" + "test10": "phpunit --configuration tests/phpunit10.xml", + "fix-cs": "php-cs-fixer fix --config=php_cs.php.dist" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.0", + "friendsofphp/php-cs-fixer": "^3.92" } } From d2d015b4a89191245d1f63f38c55d23429d53a65 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 4 Mar 2026 15:46:05 +0300 Subject: [PATCH 4/4] chore: Run CS Fixer --- WebFiori/Http/APIFilter.php | 33 +- WebFiori/Http/APITestCase.php | 297 +-- WebFiori/Http/AbstractWebService.php | 1 + WebFiori/Http/Annotations/AllowAnonymous.php | 1 + WebFiori/Http/Annotations/DeleteMapping.php | 1 + WebFiori/Http/Annotations/GetMapping.php | 1 + WebFiori/Http/Annotations/MapEntity.php | 6 +- WebFiori/Http/Annotations/PostMapping.php | 1 + WebFiori/Http/Annotations/PreAuthorize.php | 4 +- WebFiori/Http/Annotations/PutMapping.php | 1 + WebFiori/Http/Annotations/RequestParam.php | 4 +- WebFiori/Http/Annotations/RequiresAuth.php | 1 + WebFiori/Http/Annotations/ResponseBody.php | 4 +- WebFiori/Http/Annotations/RestController.php | 4 +- WebFiori/Http/AuthHeader.php | 21 +- .../Http/Exceptions/BadRequestException.php | 2 +- .../Exceptions/DuplicateMappingException.php | 2 +- .../Http/Exceptions/ForbiddenException.php | 2 +- WebFiori/Http/Exceptions/HttpException.php | 17 +- .../Http/Exceptions/NotFoundException.php | 2 +- .../Http/Exceptions/UnauthorizedException.php | 2 +- WebFiori/Http/HeadersPool.php | 4 +- WebFiori/Http/HttpCookie.php | 1 + WebFiori/Http/HttpHeader.php | 1 + WebFiori/Http/HttpMessage.php | 120 +- WebFiori/Http/ManagerInfoService.php | 1 + WebFiori/Http/ObjectMapper.php | 2 +- .../Http/OpenAPI/APIResponseDefinition.php | 35 +- WebFiori/Http/OpenAPI/ComponentsObj.php | 21 +- WebFiori/Http/OpenAPI/ContactObj.php | 89 +- WebFiori/Http/OpenAPI/ContentType.php | 9 +- WebFiori/Http/OpenAPI/ExternalDocObj.php | 55 +- WebFiori/Http/OpenAPI/HeaderObj.php | 278 +-- WebFiori/Http/OpenAPI/InfoObj.php | 243 +-- WebFiori/Http/OpenAPI/LicenseObj.php | 83 +- WebFiori/Http/OpenAPI/MediaTypeObj.php | 18 +- WebFiori/Http/OpenAPI/OAuthFlowObj.php | 132 +- WebFiori/Http/OpenAPI/OAuthFlowsObj.php | 124 +- WebFiori/Http/OpenAPI/OpenAPIObj.php | 89 +- WebFiori/Http/OpenAPI/OperationObj.php | 18 +- WebFiori/Http/OpenAPI/ParameterObj.php | 463 ++--- WebFiori/Http/OpenAPI/PathItemObj.php | 95 +- WebFiori/Http/OpenAPI/PathsObj.php | 11 +- WebFiori/Http/OpenAPI/ReferenceObj.php | 91 +- WebFiori/Http/OpenAPI/ResponseObj.php | 18 +- WebFiori/Http/OpenAPI/ResponsesObj.php | 12 +- WebFiori/Http/OpenAPI/Schema.php | 182 +- .../Http/OpenAPI/SecurityRequirementObj.php | 12 +- WebFiori/Http/OpenAPI/SecuritySchemeObj.php | 268 +-- WebFiori/Http/OpenAPI/ServerObj.php | 60 +- WebFiori/Http/OpenAPI/TagObj.php | 87 +- WebFiori/Http/ParamOption.php | 9 +- WebFiori/Http/ParamType.php | 1 + WebFiori/Http/Request.php | 176 +- WebFiori/Http/RequestMethod.php | 1 + WebFiori/Http/RequestParameter.php | 115 +- WebFiori/Http/RequestUri.php | 327 +-- WebFiori/Http/Response.php | 1 + WebFiori/Http/ResponseMessage.php | 1 + WebFiori/Http/SecurityContext.php | 239 +-- WebFiori/Http/SecurityPrincipal.php | 22 +- WebFiori/Http/Uri.php | 189 +- WebFiori/Http/UriParameter.php | 22 +- WebFiori/Http/WebService.php | 1772 +++++++++-------- WebFiori/Http/WebServicesManager.php | 274 +-- .../00-basic/01-hello-world/HelloService.php | 7 +- .../02-with-parameters/GreetingService.php | 9 +- .../03-multiple-methods/TaskService.php | 67 +- .../04-simple-manager/InfoService.php | 7 +- .../04-simple-manager/ProductService.php | 7 +- .../04-simple-manager/UserService.php | 7 +- .../ValidationService.php | 15 +- .../02-error-handling/ErrorService.php | 59 +- .../01-core/03-json-requests/JsonService.php | 87 +- .../01-core/04-file-uploads/UploadService.php | 106 +- .../JsonResponseService.php | 7 +- .../05-response-formats/ResponseService.php | 257 +-- .../TextResponseService.php | 11 +- .../XmlResponseService.php | 18 +- .../01-basic-auth/BasicAuthService.php | 57 +- .../02-bearer-tokens/LoginService.php | 21 +- .../02-bearer-tokens/ProfileService.php | 19 +- .../02-bearer-tokens/TokenAuthService.php | 149 +- .../02-bearer-tokens/TokenHelper.php | 49 +- .../04-role-based-access/AdminService.php | 12 +- .../04-role-based-access/DemoUser.php | 20 +- .../04-role-based-access/PublicService.php | 9 +- .../UserManagerService.php | 32 +- .../04-role-based-access/UserService.php | 12 +- .../GetObjectMappingService.php | 9 +- .../ManualMappingService.php | 27 +- .../MapEntityCustomMappingService.php | 9 +- .../MapEntityMappingService.php | 9 +- .../TraditionalMappingService.php | 11 +- .../04-advanced/01-object-mapping/User.php | 118 +- .../03-manual-openapi/OpenAPIService.php | 7 +- .../03-manual-openapi/ProductService.php | 47 +- .../03-manual-openapi/UserService.php | 33 +- php_cs.php.dist | 2 +- 99 files changed, 3844 insertions(+), 3650 deletions(-) diff --git a/WebFiori/Http/APIFilter.php b/WebFiori/Http/APIFilter.php index 0cfcfce..d099c14 100644 --- a/WebFiori/Http/APIFilter.php +++ b/WebFiori/Http/APIFilter.php @@ -1,4 +1,5 @@ paramDefs[] = $attribute; $this->requestParameters[] = $reqParam; } - public function getParameters() : array { - return $this->requestParameters; - } /** * Clears the arrays that are used to store filtered and not-filtered variables. * @@ -200,17 +198,6 @@ public static function filter(APIFilter $apiFilter, array $arr): array { return $retVal; } - private static function decodeArray(array $array) { - $retVal = []; - foreach ($array as $arrEl) { - if (gettype($arrEl) == 'array') { - $retVal[] = self::decodeArray($arrEl); - } else { - $retVal[] = urldecode($arrEl.''); - } - } - return $retVal; - } /** * Validate and sanitize GET parameters. * @@ -321,6 +308,9 @@ public function getInputStreamPath() { public final function getNonFiltered() { return $this->nonFilteredInputs; } + public function getParameters() : array { + return $this->requestParameters; + } /** * Sets the stream at which the filter will use to read the inputs. * @@ -557,6 +547,19 @@ private function cleanJsonStr($extraClean, $def, $toBeFiltered) { $extraClean->add($name, null); } } + private static function decodeArray(array $array) { + $retVal = []; + + foreach ($array as $arrEl) { + if (gettype($arrEl) == 'array') { + $retVal[] = self::decodeArray($arrEl); + } else { + $retVal[] = urldecode($arrEl.''); + } + } + + return $retVal; + } /** * Converts a string to an array. * diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php index 05c565d..12df52b 100644 --- a/WebFiori/Http/APITestCase.php +++ b/WebFiori/Http/APITestCase.php @@ -1,4 +1,5 @@ globalsBackup = [ - 'GET' => $_GET, - 'POST' => $_POST, - 'FILES' => $_FILES, - 'SERVER' => $_SERVER - ]; - } - - protected function tearDown(): void { - $_GET = $this->globalsBackup['GET']; - $_POST = $this->globalsBackup['POST']; - $_FILES = $this->globalsBackup['FILES']; - $_SERVER = $this->globalsBackup['SERVER']; - SecurityContext::clear(); - parent::tearDown(); - } /** - * Sets the path to the file which is used to store API output temporarily. - * - * @param string $path The absolute path to the file. - */ - public function setOutputFile(string $path) { - $this->outputStreamPath = $path; - } - /** - * Returns the path to the file which is used to store API output temporarily. + * The path to the output stream file. * - * @return string + * @var string */ - public function getOutputFile() : string { - if ($this->outputStreamPath === null) { - $this->outputStreamPath = self::DEFAULT_OUTPUT_STREAM; - } - return $this->outputStreamPath; - } + private $outputStreamPath; /** * Adds a file to the array $_FILES for testing API with upload. * @@ -137,99 +100,45 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false) public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { $method = strtoupper($requestMethod); $serviceName = $this->resolveServiceName($apiEndpointName); - + $this->setupRequest($method, $serviceName, $parameters, $httpHeaders); - + $manager->setOutputStream(fopen($this->getOutputFile(), 'w')); $manager->setRequest(Request::createFromGlobals()); SecurityContext::setCurrentUser($user); $manager->process(); - + $result = $manager->readOutputStream(); - + if (file_exists($this->getOutputFile())) { unlink($this->getOutputFile()); } - + return $this->formatOutput($result); } - /** - * Resolves service name from class name or returns the name as-is. + * Sends a DELETE request to specific endpoint. * - * @param string $nameOrClass Service name or class name + * @param WebServicesManager $manager The manager which is used to manage the endpoint. * - * @return string The resolved service name - */ - private function resolveServiceName(string $nameOrClass): string { - if (class_exists($nameOrClass)) { - $reflection = new \ReflectionClass($nameOrClass); - - if ($reflection->isSubclassOf(WebService::class)) { - $constructor = $reflection->getConstructor(); - - if ($constructor && $constructor->getNumberOfRequiredParameters() === 0) { - $service = $reflection->newInstance(); - return $service->getName(); - } - } - } - - return $nameOrClass; - } - - /** - * Sets up the request environment. + * @param string $endpoint The name of the endpoint. + * + * @param array $parameters An optional array of request parameters that can be + * passed to the endpoint. * - * @param string $method HTTP method - * @param string $serviceName Service name - * @param array $parameters Request parameters * @param array $httpHeaders An optional associative array that can be used * to mimic HTTP request headers. The keys of the array are names of headers * and the value of each key represents the value of the header. * * @param SecurityPrincipal|null $user Optional user to authenticate the request with. - */ - private function setupRequest(string $method, string $serviceName, array $parameters, array $httpHeaders) { - putenv('REQUEST_METHOD=' . $method); - - // Normalize header names to lowercase for case-insensitive comparison - $normalizedHeaders = []; - foreach ($httpHeaders as $name => $value) { - $normalizedHeaders[strtolower($name)] = $value; - } - - if (in_array($method, [RequestMethod::POST, RequestMethod::PUT, RequestMethod::PATCH])) { - $_POST = $parameters; - $_POST['service'] = $serviceName; - $_SERVER['CONTENT_TYPE'] = $normalizedHeaders['content-type'] ?? 'application/x-www-form-urlencoded'; - } else { - $_GET = $parameters; - $_GET['service'] = $serviceName; - } - - foreach ($normalizedHeaders as $name => $value) { - if ($name !== 'content-type') { - $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $name))] = $value; - } - } - } - - /** - * Formats the output, attempting to pretty-print JSON if possible. - * - * @param string $output Raw output + * to mimic HTTP request headers. The keys of the array are names of headers + * and the value of each key represents the value of the header. * - * @return string Formatted output + * @return string The method will return the output that was produced by + * the endpoint as string. */ - private function formatOutput(string $output): string { - try { - $json = Json::decode($output); - $json->setIsFormatted(true); - return $json . ''; - } catch (JsonException $ex) { - return $output; - } + public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders, $user); } /** * Creates a formatted string from calling an API. @@ -245,7 +154,7 @@ public function format(string $output) { $expl = explode(self::NL, $output); $nl = '.self::NL\n'; $count = count($expl); - + for ($x = 0 ; $x < count($expl) ; $x++) { if ($x + 1 == $count) { $nl = ''; @@ -254,28 +163,16 @@ public function format(string $output) { } } /** - * Sends a DELETE request to specific endpoint. - * - * @param WebServicesManager $manager The manager which is used to manage the endpoint. - * - * @param string $endpoint The name of the endpoint. - * - * @param array $parameters An optional array of request parameters that can be - * passed to the endpoint. - * - * @param array $httpHeaders An optional associative array that can be used - * to mimic HTTP request headers. The keys of the array are names of headers - * and the value of each key represents the value of the header. - * - * @param SecurityPrincipal|null $user Optional user to authenticate the request with. - * to mimic HTTP request headers. The keys of the array are names of headers - * and the value of each key represents the value of the header. + * Returns the path to the file which is used to store API output temporarily. * - * @return string The method will return the output that was produced by - * the endpoint as string. + * @return string */ - public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { - return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders, $user); + public function getOutputFile() : string { + if ($this->outputStreamPath === null) { + $this->outputStreamPath = self::DEFAULT_OUTPUT_STREAM; + } + + return $this->outputStreamPath; } /** * Sends a GET request to specific endpoint. @@ -294,7 +191,7 @@ public function getRequest(WebServicesManager $manager, string $endpoint, array return $this->callEndpoint($manager, RequestMethod::GET, $endpoint, $parameters, $httpHeaders, $user); } /** - * Sends a POST request to specific endpoint. + * Sends a HEAD request to specific endpoint. * * @param WebServicesManager $manager The manager which is used to manage the endpoint. * @@ -314,11 +211,11 @@ public function getRequest(WebServicesManager $manager, string $endpoint, array * @return string The method will return the output that was produced by * the endpoint as string. */ - public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { - return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders, $user); + public function headRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders, $user); } /** - * Sends a PUT request to specific endpoint. + * Sends an OPTIONS request to specific endpoint. * * @param WebServicesManager $manager The manager which is used to manage the endpoint. * @@ -338,8 +235,8 @@ public function postRequest(WebServicesManager $manager, string $endpoint, array * @return string The method will return the output that was produced by * the endpoint as string. */ - public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { - return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders, $user); + public function optionsRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends a PATCH request to specific endpoint. @@ -366,7 +263,7 @@ public function patchRequest(WebServicesManager $manager, string $endpoint, arra return $this->callEndpoint($manager, RequestMethod::PATCH, $endpoint, $parameters, $httpHeaders, $user); } /** - * Sends an OPTIONS request to specific endpoint. + * Sends a POST request to specific endpoint. * * @param WebServicesManager $manager The manager which is used to manage the endpoint. * @@ -386,11 +283,11 @@ public function patchRequest(WebServicesManager $manager, string $endpoint, arra * @return string The method will return the output that was produced by * the endpoint as string. */ - public function optionsRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { - return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders, $user); + public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders, $user); } /** - * Sends a HEAD request to specific endpoint. + * Sends a PUT request to specific endpoint. * * @param WebServicesManager $manager The manager which is used to manage the endpoint. * @@ -410,8 +307,16 @@ public function optionsRequest(WebServicesManager $manager, string $endpoint, ar * @return string The method will return the output that was produced by * the endpoint as string. */ - public function headRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { - return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders, $user); + public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?SecurityPrincipal $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders, $user); + } + /** + * Sets the path to the file which is used to store API output temporarily. + * + * @param string $path The absolute path to the file. + */ + public function setOutputFile(string $path) { + $this->outputStreamPath = $path; } private function extractPathAndName($absPath): array { $DS = DIRECTORY_SEPARATOR; @@ -437,4 +342,104 @@ private function extractPathAndName($absPath): array { 'path' => '' ]; } + + /** + * Formats the output, attempting to pretty-print JSON if possible. + * + * @param string $output Raw output + * + * @return string Formatted output + */ + private function formatOutput(string $output): string { + try { + $json = Json::decode($output); + $json->setIsFormatted(true); + + return $json.''; + } catch (JsonException $ex) { + return $output; + } + } + + /** + * Resolves service name from class name or returns the name as-is. + * + * @param string $nameOrClass Service name or class name + * + * @return string The resolved service name + */ + private function resolveServiceName(string $nameOrClass): string { + if (class_exists($nameOrClass)) { + $reflection = new \ReflectionClass($nameOrClass); + + if ($reflection->isSubclassOf(WebService::class)) { + $constructor = $reflection->getConstructor(); + + if ($constructor && $constructor->getNumberOfRequiredParameters() === 0) { + $service = $reflection->newInstance(); + + return $service->getName(); + } + } + } + + return $nameOrClass; + } + + /** + * Sets up the request environment. + * + * @param string $method HTTP method + * @param string $serviceName Service name + * @param array $parameters Request parameters + * @param array $httpHeaders An optional associative array that can be used + * to mimic HTTP request headers. The keys of the array are names of headers + * and the value of each key represents the value of the header. + * + * @param SecurityPrincipal|null $user Optional user to authenticate the request with. + */ + private function setupRequest(string $method, string $serviceName, array $parameters, array $httpHeaders) { + putenv('REQUEST_METHOD='.$method); + + // Normalize header names to lowercase for case-insensitive comparison + $normalizedHeaders = []; + + foreach ($httpHeaders as $name => $value) { + $normalizedHeaders[strtolower($name)] = $value; + } + + if (in_array($method, [RequestMethod::POST, RequestMethod::PUT, RequestMethod::PATCH])) { + $_POST = $parameters; + $_POST['service'] = $serviceName; + $_SERVER['CONTENT_TYPE'] = $normalizedHeaders['content-type'] ?? 'application/x-www-form-urlencoded'; + } else { + $_GET = $parameters; + $_GET['service'] = $serviceName; + } + + foreach ($normalizedHeaders as $name => $value) { + if ($name !== 'content-type') { + $_SERVER['HTTP_'.strtoupper(str_replace('-', '_', $name))] = $value; + } + } + } + + protected function setUp(): void { + parent::setUp(); + $this->globalsBackup = [ + 'GET' => $_GET, + 'POST' => $_POST, + 'FILES' => $_FILES, + 'SERVER' => $_SERVER + ]; + } + + protected function tearDown(): void { + $_GET = $this->globalsBackup['GET']; + $_POST = $this->globalsBackup['POST']; + $_FILES = $this->globalsBackup['FILES']; + $_SERVER = $this->globalsBackup['SERVER']; + SecurityContext::clear(); + parent::tearDown(); + } } diff --git a/WebFiori/Http/AbstractWebService.php b/WebFiori/Http/AbstractWebService.php index 997fd2f..7d7d2df 100644 --- a/WebFiori/Http/AbstractWebService.php +++ b/WebFiori/Http/AbstractWebService.php @@ -1,4 +1,5 @@ credentials; + } /** * Returns the scheme type which is used in authorization. * @@ -60,12 +67,4 @@ public function __construct(string $value = '') { public function getScheme() : string { return $this->scheme; } - /** - * Returns credentials part of the authorization header. - * - * @return string The returned string structure will depend on scheme type. - */ - public function getCredentials() : string { - return $this->credentials; - } } diff --git a/WebFiori/Http/Exceptions/BadRequestException.php b/WebFiori/Http/Exceptions/BadRequestException.php index 89ded39..5b6dff2 100644 --- a/WebFiori/Http/Exceptions/BadRequestException.php +++ b/WebFiori/Http/Exceptions/BadRequestException.php @@ -1,4 +1,5 @@ statusCode = $statusCode; $this->responseType = $responseType; } - - public function getStatusCode(): int { - return $this->statusCode; - } - + public function getResponseType(): string { return $this->responseType; } + + public function getStatusCode(): int { + return $this->statusCode; + } } diff --git a/WebFiori/Http/Exceptions/NotFoundException.php b/WebFiori/Http/Exceptions/NotFoundException.php index d554afc..97a1ef0 100644 --- a/WebFiori/Http/Exceptions/NotFoundException.php +++ b/WebFiori/Http/Exceptions/NotFoundException.php @@ -1,4 +1,5 @@ getHeaders() as $headerObj) { if ($headerObj->getName() == $trimmed) { $retVal[] = $headerObj; @@ -139,6 +140,7 @@ public function getHeaders() : array { public function hasHeader(string $name, ?string $val) : bool { $headers = $this->getHeaderAsObj($name); $trimmedVal = trim($val.''); + if ($val === null || strlen($trimmedVal) == 0) { return count($headers) !== 0; } diff --git a/WebFiori/Http/HttpCookie.php b/WebFiori/Http/HttpCookie.php index 77580fd..b90def3 100644 --- a/WebFiori/Http/HttpCookie.php +++ b/WebFiori/Http/HttpCookie.php @@ -1,4 +1,5 @@ protocolVersion = '1.1'; $this->requestMethod = 'GET'; } - + /** - * Returns the headers pool. + * Adds a header to the message. * - * @return HeadersPool + * @param string $name The name of the header. + * @param string $value The value of the header. + * @param string|null $replaceValue Optional value to replace. + * + * @return bool */ - public function getHeadersPool() : HeadersPool { - return $this->headersPool; + public function addHeader(string $name, string $value, ?string $replaceValue = '') : bool { + return $this->headersPool->addHeader($name, $value, $replaceValue); + } + + /** + * Gets the body of the message. + * + * @return string + */ + public function getBody() : string { + return $this->body; } - + /** * Returns the value(s) of specific HTTP header. * @@ -64,7 +77,7 @@ public function getHeadersPool() : HeadersPool { public function getHeader(string $name) : array { return $this->headersPool->getHeader($name); } - + /** * Returns an array that contains all headers. * @@ -73,7 +86,34 @@ public function getHeader(string $name) : array { public function getHeaders() : array { return $this->headersPool->getHeaders(); } - + + /** + * Returns the headers pool. + * + * @return HeadersPool + */ + public function getHeadersPool() : HeadersPool { + return $this->headersPool; + } + + /** + * Gets the protocol version. + * + * @return string + */ + public function getProtocolVersion() : string { + return $this->protocolVersion; + } + + /** + * Gets the request method. + * + * @return string + */ + public function getRequestMethod() : string { + return $this->requestMethod; + } + /** * Checks if specific header exists. * @@ -85,7 +125,7 @@ public function getHeaders() : array { public function hasHeader(string $name, ?string $val = '') : bool { return $this->headersPool->hasHeader($name, $val); } - + /** * Removes specific header. * @@ -97,29 +137,7 @@ public function hasHeader(string $name, ?string $val = '') : bool { public function removeHeader(string $name, ?string $val = '') : bool { return $this->headersPool->removeHeader($name, $val); } - - /** - * Adds a header to the message. - * - * @param string $name The name of the header. - * @param string $value The value of the header. - * @param string|null $replaceValue Optional value to replace. - * - * @return bool - */ - public function addHeader(string $name, string $value, ?string $replaceValue = '') : bool { - return $this->headersPool->addHeader($name, $value, $replaceValue); - } - - /** - * Gets the body of the message. - * - * @return string - */ - public function getBody() : string { - return $this->body; - } - + /** * Sets the body of the message. * @@ -128,16 +146,7 @@ public function getBody() : string { public function setBody(string $body) { $this->body = $body; } - - /** - * Gets the protocol version. - * - * @return string - */ - public function getProtocolVersion() : string { - return $this->protocolVersion; - } - + /** * Sets the protocol version. * @@ -146,16 +155,7 @@ public function getProtocolVersion() : string { public function setProtocolVersion(string $version) { $this->protocolVersion = $version; } - - /** - * Gets the request method. - * - * @return string - */ - public function getRequestMethod() : string { - return $this->requestMethod; - } - + /** * Sets the request method. * diff --git a/WebFiori/Http/ManagerInfoService.php b/WebFiori/Http/ManagerInfoService.php index b34a7d9..e45fc90 100644 --- a/WebFiori/Http/ManagerInfoService.php +++ b/WebFiori/Http/ManagerInfoService.php @@ -1,4 +1,5 @@ statusCode = $statusCode; $this->description = $description; } - - /** - * Gets the status code. - * - * @return string - */ - public function getStatusCode(): string { - return $this->statusCode; - } - + /** * Adds content for a specific media type. * @@ -53,9 +45,19 @@ public function getStatusCode(): string { public function addContent(string $mediaType, Schema $schema): ContentType { $content = new ContentType($mediaType, $schema); $this->content[$mediaType] = $content; + return $content; } - + + /** + * Gets the status code. + * + * @return string + */ + public function getStatusCode(): string { + return $this->statusCode; + } + /** * Converts the response to JSON representation. * @@ -63,15 +65,16 @@ public function addContent(string $mediaType, Schema $schema): ContentType { */ public function toJson(): Json { $json = new Json(['description' => $this->description]); - + if (!empty($this->content)) { $contentJson = new Json(); + foreach ($this->content as $mediaType => $contentType) { $contentJson->add($mediaType, $contentType->toJson()); } $json->add('content', $contentJson); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ComponentsObj.php b/WebFiori/Http/OpenAPI/ComponentsObj.php index 710bde0..0afd27a 100644 --- a/WebFiori/Http/OpenAPI/ComponentsObj.php +++ b/WebFiori/Http/OpenAPI/ComponentsObj.php @@ -1,4 +1,5 @@ schemas[$name] = $schema; + return $this; } - + /** * Adds a reusable Security Scheme Object to the components. * @@ -66,9 +68,10 @@ public function addSchema(string $name, $schema): ComponentsObj { */ public function addSecurityScheme(string $name, SecuritySchemeObj $scheme): ComponentsObj { $this->securitySchemes[$name] = $scheme; + return $this; } - + /** * Returns all schemas. * @@ -77,7 +80,7 @@ public function addSecurityScheme(string $name, SecuritySchemeObj $scheme): Comp public function getSchemas(): array { return $this->schemas; } - + /** * Returns all security schemes. * @@ -86,7 +89,7 @@ public function getSchemas(): array { public function getSecuritySchemes(): array { return $this->securitySchemes; } - + /** * Returns a Json object that represents the Components Object. * @@ -96,15 +99,15 @@ public function getSecuritySchemes(): array { */ public function toJSON(): Json { $json = new Json(); - + if (!empty($this->schemas)) { $json->add('schemas', $this->schemas); } - + if (!empty($this->securitySchemes)) { $json->add('securitySchemes', $this->securitySchemes); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ContactObj.php b/WebFiori/Http/OpenAPI/ContactObj.php index 3bbec66..2b856e9 100644 --- a/WebFiori/Http/OpenAPI/ContactObj.php +++ b/WebFiori/Http/OpenAPI/ContactObj.php @@ -1,4 +1,5 @@ name = $name; - return $this; + public function getEmail(): ?string { + return $this->email; } - + /** * Returns the contact name. * @@ -68,19 +65,7 @@ public function setName(string $name): ContactObj { public function getName(): ?string { return $this->name; } - - /** - * Sets the URI for the contact information. - * - * @param string $url The URI for the contact information. This MUST be in the form of a URI. - * - * @return ContactObj Returns self for method chaining. - */ - public function setUrl(string $url): ContactObj { - $this->url = $url; - return $this; - } - + /** * Returns the contact URL. * @@ -89,7 +74,7 @@ public function setUrl(string $url): ContactObj { public function getUrl(): ?string { return $this->url; } - + /** * Sets the email address of the contact person/organization. * @@ -99,18 +84,36 @@ public function getUrl(): ?string { */ public function setEmail(string $email): ContactObj { $this->email = $email; + return $this; } - + /** - * Returns the contact email. + * Sets the identifying name of the contact person/organization. * - * @return string|null Returns the value, or null if not set. + * @param string $name The identifying name. + * + * @return ContactObj Returns self for method chaining. */ - public function getEmail(): ?string { - return $this->email; + public function setName(string $name): ContactObj { + $this->name = $name; + + return $this; } - + + /** + * Sets the URI for the contact information. + * + * @param string $url The URI for the contact information. This MUST be in the form of a URI. + * + * @return ContactObj Returns self for method chaining. + */ + public function setUrl(string $url): ContactObj { + $this->url = $url; + + return $this; + } + /** * Returns a Json object that represents the Contact Object. * @@ -118,19 +121,19 @@ public function getEmail(): ?string { */ public function toJSON(): Json { $json = new Json(); - + if ($this->getName() !== null) { $json->add('name', $this->getName()); } - + if ($this->getUrl() !== null) { $json->add('url', $this->getUrl()); } - + if ($this->getEmail() !== null) { $json->add('email', $this->getEmail()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ContentType.php b/WebFiori/Http/OpenAPI/ContentType.php index f495fd0..1704ba6 100644 --- a/WebFiori/Http/OpenAPI/ContentType.php +++ b/WebFiori/Http/OpenAPI/ContentType.php @@ -1,4 +1,5 @@ mediaType = $mediaType; $this->schema = $schema; } - + /** * Gets the media type. * @@ -40,7 +41,7 @@ public function __construct(string $mediaType, Schema $schema) { public function getMediaType(): string { return $this->mediaType; } - + /** * Gets the schema. * @@ -49,7 +50,7 @@ public function getMediaType(): string { public function getSchema(): Schema { return $this->schema; } - + /** * Converts the content type to JSON representation. * diff --git a/WebFiori/Http/OpenAPI/ExternalDocObj.php b/WebFiori/Http/OpenAPI/ExternalDocObj.php index 8a9321e..6e36e38 100644 --- a/WebFiori/Http/OpenAPI/ExternalDocObj.php +++ b/WebFiori/Http/OpenAPI/ExternalDocObj.php @@ -1,4 +1,5 @@ setUrl($url); - + if ($description !== null) { $this->setDescription($description); } } - + + /** + * Returns the description of the target documentation. + * + * @return string|null The description, or null if not set. + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Returns the URI for the target documentation. + * + * @return string The URI for the target documentation. + */ + public function getUrl(): string { + return $this->url; + } + /** * Sets the description of the target documentation. * @@ -66,18 +85,10 @@ public function __construct(string $url, ?string $description = null) { */ public function setDescription(string $description): ExternalDocObj { $this->description = $description; + return $this; } - - /** - * Returns the description of the target documentation. - * - * @return string|null The description, or null if not set. - */ - public function getDescription(): ?string { - return $this->description; - } - + /** * Sets the URI for the target documentation. * @@ -89,18 +100,10 @@ public function getDescription(): ?string { */ public function setUrl(string $url): ExternalDocObj { $this->url = $url; + return $this; } - - /** - * Returns the URI for the target documentation. - * - * @return string The URI for the target documentation. - */ - public function getUrl(): string { - return $this->url; - } - + /** * Returns a Json object that represents the External Documentation Object. * @@ -112,11 +115,11 @@ public function toJSON(): Json { $json = new Json([ 'url' => $this->getUrl() ]); - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/HeaderObj.php b/WebFiori/Http/OpenAPI/HeaderObj.php index b89ebbd..e8a3d88 100644 --- a/WebFiori/Http/OpenAPI/HeaderObj.php +++ b/WebFiori/Http/OpenAPI/HeaderObj.php @@ -1,4 +1,5 @@ deprecated; + } + /** - * When this is true, header values of type array or object generate a single header - * whose value is a comma-separated list of the array items or key-value pairs of the map. - * - * For other data types this field has no effect. The default value is false. + * Returns the description. * - * @var bool|null + * @return string|null Returns the value, or null if not set. */ - private ?bool $explode = null; - + public function getDescription(): ?string { + return $this->description; + } + /** - * The schema defining the type used for the header. + * Returns the example. * - * @var mixed + * @return mixed */ - private $schema = null; - + public function getExample() { + return $this->example; + } + /** - * Example of the header's potential value. + * Returns the examples. * - * @var mixed + * @return array|null Returns the value, or null if not set. */ - private $example = null; - + public function getExamples(): ?array { + return $this->examples; + } + /** - * Examples of the header's potential value. - * - * Map of string to Example Object or Reference Object. + * Returns the explode value. * - * @var array|null + * @return bool|null Returns the value, or null if not set. */ - private ?array $examples = null; - + public function getExplode(): ?bool { + return $this->explode; + } + + public function getRequired(): bool { + return $this->required; + } + /** - * Sets the description of the header. - * - * @param string $description A brief description of the header. + * Returns the schema. * - * @return HeaderObj Returns self for method chaining. + * @return mixed */ - public function setDescription(string $description): HeaderObj { - $this->description = $description; - return $this; + public function getSchema() { + return $this->schema; } - + /** - * Returns the description. + * Returns the style. * * @return string|null Returns the value, or null if not set. */ - public function getDescription(): ?string { - return $this->description; + public function getStyle(): ?string { + return $this->style; } - + /** - * Sets whether this header is mandatory. - * - * @param bool $required True if required. + * Returns whether this header is deprecated. * - * @return HeaderObj Returns self for method chaining. + * @return bool */ - public function setRequired(bool $required): HeaderObj { - $this->required = $required; - return $this; + public function isDeprecated(): bool { + return $this->deprecated; } - + /** * Returns whether this header is required. * @@ -138,11 +176,7 @@ public function setRequired(bool $required): HeaderObj { public function isRequired(): bool { return $this->required; } - - public function getRequired(): bool { - return $this->required; - } - + /** * Sets whether this header is deprecated. * @@ -152,43 +186,49 @@ public function getRequired(): bool { */ public function setDeprecated(bool $deprecated): HeaderObj { $this->deprecated = $deprecated; + return $this; } - + /** - * Returns whether this header is deprecated. + * Sets the description of the header. * - * @return bool + * @param string $description A brief description of the header. + * + * @return HeaderObj Returns self for method chaining. */ - public function isDeprecated(): bool { - return $this->deprecated; - } - - public function getDeprecated(): bool { - return $this->deprecated; + public function setDescription(string $description): HeaderObj { + $this->description = $description; + + return $this; } - + /** - * Sets the serialization style. + * Sets an example of the header's potential value. * - * @param string $style The style value. Default is "simple". + * @param mixed $example Example value. * * @return HeaderObj Returns self for method chaining. */ - public function setStyle(string $style): HeaderObj { - $this->style = $style; + public function setExample($example): HeaderObj { + $this->example = $example; + return $this; } - + /** - * Returns the style. + * Sets examples of the header's potential value. * - * @return string|null Returns the value, or null if not set. + * @param array $examples Map of example names to Example Objects or Reference Objects. + * + * @return HeaderObj Returns self for method chaining. */ - public function getStyle(): ?string { - return $this->style; + public function setExamples(array $examples): HeaderObj { + $this->examples = $examples; + + return $this; } - + /** * Sets the explode value. * @@ -198,18 +238,23 @@ public function getStyle(): ?string { */ public function setExplode(bool $explode): HeaderObj { $this->explode = $explode; + return $this; } - + /** - * Returns the explode value. + * Sets whether this header is mandatory. * - * @return bool|null Returns the value, or null if not set. + * @param bool $required True if required. + * + * @return HeaderObj Returns self for method chaining. */ - public function getExplode(): ?bool { - return $this->explode; + public function setRequired(bool $required): HeaderObj { + $this->required = $required; + + return $this; } - + /** * Sets the schema defining the type used for the header. * @@ -219,60 +264,23 @@ public function getExplode(): ?bool { */ public function setSchema($schema): HeaderObj { $this->schema = $schema; + return $this; } - - /** - * Returns the schema. - * - * @return mixed - */ - public function getSchema() { - return $this->schema; - } - - /** - * Sets an example of the header's potential value. - * - * @param mixed $example Example value. - * - * @return HeaderObj Returns self for method chaining. - */ - public function setExample($example): HeaderObj { - $this->example = $example; - return $this; - } - - /** - * Returns the example. - * - * @return mixed - */ - public function getExample() { - return $this->example; - } - + /** - * Sets examples of the header's potential value. + * Sets the serialization style. * - * @param array $examples Map of example names to Example Objects or Reference Objects. + * @param string $style The style value. Default is "simple". * * @return HeaderObj Returns self for method chaining. */ - public function setExamples(array $examples): HeaderObj { - $this->examples = $examples; + public function setStyle(string $style): HeaderObj { + $this->style = $style; + return $this; } - - /** - * Returns the examples. - * - * @return array|null Returns the value, or null if not set. - */ - public function getExamples(): ?array { - return $this->examples; - } - + /** * Returns a Json object that represents the Header Object. * @@ -280,39 +288,39 @@ public function getExamples(): ?array { */ 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()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/InfoObj.php b/WebFiori/Http/OpenAPI/InfoObj.php index dcfb86b..8659253 100644 --- a/WebFiori/Http/OpenAPI/InfoObj.php +++ b/WebFiori/Http/OpenAPI/InfoObj.php @@ -1,4 +1,5 @@ setTitle($title); $this->setVersion($version); } - + /** - * Sets the title of the API. + * Returns the contact information. * - * @param string $title The title of the API. + * @return ContactObj|null Returns the value, or null if not set. + */ + public function getContact(): ?ContactObj { + return $this->contact; + } + + /** + * Returns the description of the API. * - * @return InfoObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setTitle(string $title): InfoObj { - $this->title = $title; - return $this; + public function getDescription(): ?string { + return $this->description; } - + /** - * Returns the title of the API. + * Returns the license information. * - * @return string + * @return LicenseObj|null Returns the value, or null if not set. */ - public function getTitle(): string { - return $this->title; + public function getLicense(): ?LicenseObj { + return $this->license; } - + /** - * Sets the version of the OpenAPI Document. + * Returns the summary of the API. * - * @param string $version The version of the OpenAPI Document. + * @return string|null Returns the value, or null if not set. + */ + public function getSummary(): ?string { + return $this->summary; + } + + /** + * Returns the Terms of Service URI. * - * @return InfoObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setVersion(string $version): InfoObj { - $this->version = $version; - return $this; + public function getTermsOfService(): ?string { + return $this->termsOfService; + } + + /** + * Returns the title of the API. + * + * @return string + */ + public function getTitle(): string { + return $this->title; } - + /** * Returns the version of the OpenAPI Document. * @@ -137,28 +158,20 @@ public function setVersion(string $version): InfoObj { public function getVersion(): string { return $this->version; } - + /** - * Sets a short summary of the API. + * Sets the contact information for the exposed API. * - * @param string $summary A short summary of the API. + * @param ContactObj $contact Contact Object. * * @return InfoObj Returns self for method chaining. */ - public function setSummary(string $summary): InfoObj { - $this->summary = $summary; + public function setContact(ContactObj $contact): InfoObj { + $this->contact = $contact; + return $this; } - - /** - * Returns the summary of the API. - * - * @return string|null Returns the value, or null if not set. - */ - public function getSummary(): ?string { - return $this->summary; - } - + /** * Sets a description of the API. * @@ -169,81 +182,75 @@ public function getSummary(): ?string { */ public function setDescription(string $description): InfoObj { $this->description = $description; + return $this; } - - /** - * Returns the description of the API. - * - * @return string|null Returns the value, or null if not set. - */ - public function getDescription(): ?string { - return $this->description; - } - + /** - * Sets a URI for the Terms of Service for the API. + * Sets the license information for the exposed API. * - * @param string $termsOfService A URI for the Terms of Service. This MUST be in the form of a URI. + * @param LicenseObj $license License Object. * * @return InfoObj Returns self for method chaining. */ - public function setTermsOfService(string $termsOfService): InfoObj { - $this->termsOfService = $termsOfService; + public function setLicense(LicenseObj $license): InfoObj { + $this->license = $license; + return $this; } - - /** - * Returns the Terms of Service URI. - * - * @return string|null Returns the value, or null if not set. - */ - public function getTermsOfService(): ?string { - return $this->termsOfService; - } - + /** - * Sets the contact information for the exposed API. + * Sets a short summary of the API. * - * @param ContactObj $contact Contact Object. + * @param string $summary A short summary of the API. * * @return InfoObj Returns self for method chaining. */ - public function setContact(ContactObj $contact): InfoObj { - $this->contact = $contact; + public function setSummary(string $summary): InfoObj { + $this->summary = $summary; + return $this; } - + /** - * Returns the contact information. + * Sets a URI for the Terms of Service for the API. * - * @return ContactObj|null Returns the value, or null if not set. + * @param string $termsOfService A URI for the Terms of Service. This MUST be in the form of a URI. + * + * @return InfoObj Returns self for method chaining. */ - public function getContact(): ?ContactObj { - return $this->contact; + public function setTermsOfService(string $termsOfService): InfoObj { + $this->termsOfService = $termsOfService; + + return $this; } - + /** - * Sets the license information for the exposed API. + * Sets the title of the API. * - * @param LicenseObj $license License Object. + * @param string $title The title of the API. * * @return InfoObj Returns self for method chaining. */ - public function setLicense(LicenseObj $license): InfoObj { - $this->license = $license; + public function setTitle(string $title): InfoObj { + $this->title = $title; + return $this; } - + /** - * Returns the license information. + * Sets the version of the OpenAPI Document. * - * @return LicenseObj|null Returns the value, or null if not set. + * @param string $version The version of the OpenAPI Document. + * + * @return InfoObj Returns self for method chaining. */ - public function getLicense(): ?LicenseObj { - return $this->license; + public function setVersion(string $version): InfoObj { + $this->version = $version; + + return $this; } - + /** * Returns a Json object that represents the Info Object. * @@ -254,28 +261,28 @@ public function toJSON(): Json { 'title' => $this->getTitle(), 'version' => $this->getVersion() ]); - - + + if ($this->getSummary() !== null) { $json->add('summary', $this->getSummary()); } - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + if ($this->getTermsOfService() !== null) { $json->add('termsOfService', $this->getTermsOfService()); } - + if ($this->getContact() !== null) { $json->add('contact', $this->getContact()); } - + if ($this->getLicense() !== null) { $json->add('license', $this->getLicense()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/LicenseObj.php b/WebFiori/Http/OpenAPI/LicenseObj.php index 20756d8..3df1166 100644 --- a/WebFiori/Http/OpenAPI/LicenseObj.php +++ b/WebFiori/Http/OpenAPI/LicenseObj.php @@ -1,4 +1,5 @@ setName($name); } - + /** - * Sets the license name used for the API. - * - * @param string $name The license name. + * Returns the SPDX license identifier. * - * @return LicenseObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setName(string $name): LicenseObj { - $this->name = $name; - return $this; + public function getIdentifier(): ?string { + return $this->identifier; } - + /** * Returns the license name. * @@ -81,7 +78,16 @@ public function setName(string $name): LicenseObj { public function getName(): string { return $this->name; } - + + /** + * Returns the license URL. + * + * @return string|null Returns the value, or null if not set. + */ + public function getUrl(): ?string { + return $this->url; + } + /** * Sets an SPDX license expression for the API. * @@ -93,18 +99,23 @@ public function getName(): string { public function setIdentifier(string $identifier): LicenseObj { $this->identifier = $identifier; $this->url = null; + return $this; } - + /** - * Returns the SPDX license identifier. + * Sets the license name used for the API. * - * @return string|null Returns the value, or null if not set. + * @param string $name The license name. + * + * @return LicenseObj Returns self for method chaining. */ - public function getIdentifier(): ?string { - return $this->identifier; + public function setName(string $name): LicenseObj { + $this->name = $name; + + return $this; } - + /** * Sets a URI for the license used for the API. * @@ -116,18 +127,10 @@ public function getIdentifier(): ?string { public function setUrl(string $url): LicenseObj { $this->url = $url; $this->identifier = null; + return $this; } - - /** - * Returns the license URL. - * - * @return string|null Returns the value, or null if not set. - */ - public function getUrl(): ?string { - return $this->url; - } - + /** * Returns a Json object that represents the License Object. * @@ -137,15 +140,15 @@ public function toJSON(): Json { $json = new Json([ 'name' => $this->getName() ]); - + if ($this->getIdentifier() !== null) { $json->add('identifier', $this->getIdentifier()); } - + if ($this->getUrl() !== null) { $json->add('url', $this->getUrl()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/MediaTypeObj.php b/WebFiori/Http/OpenAPI/MediaTypeObj.php index aca91b8..c898e44 100644 --- a/WebFiori/Http/OpenAPI/MediaTypeObj.php +++ b/WebFiori/Http/OpenAPI/MediaTypeObj.php @@ -1,4 +1,5 @@ schema; + } + public function setSchema($schema): MediaTypeObj { $this->schema = $schema; + return $this; } - - public function getSchema() { - return $this->schema; - } - + public function toJSON(): Json { $json = new Json(); - + if ($this->getSchema() !== null) { $json->add('schema', $this->getSchema()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/OAuthFlowObj.php b/WebFiori/Http/OpenAPI/OAuthFlowObj.php index 5f760fb..1d5c64d 100644 --- a/WebFiori/Http/OpenAPI/OAuthFlowObj.php +++ b/WebFiori/Http/OpenAPI/OAuthFlowObj.php @@ -1,4 +1,5 @@ scopes = $scopes; } - + /** - * Sets the authorization URL to be used for this flow. + * Adds a scope to the OAuth2 security scheme. * - * @param string $authorizationUrl The authorization URL. This MUST be in the form of a URL. - * REQUIRED for implicit and authorizationCode flows. + * @param string $name The scope name. + * @param string $description A short description for the scope. * - * @return OAuthFlowObj Returns self for method chaining. + * @return OAuthFlowObj */ - public function setAuthorizationUrl(string $authorizationUrl): OAuthFlowObj { - $this->authorizationUrl = $authorizationUrl; + public function addScope(string $name, string $description): OAuthFlowObj { + $this->scopes[$name] = $description; + return $this; } - + /** * Returns the authorization URL. * @@ -96,20 +98,25 @@ public function setAuthorizationUrl(string $authorizationUrl): OAuthFlowObj { public function getAuthorizationUrl(): ?string { return $this->authorizationUrl; } - + /** - * Sets the token URL to be used for this flow. + * Returns the refresh URL. * - * @param string $tokenUrl The token URL. This MUST be in the form of a URL. - * REQUIRED for password, clientCredentials, and authorizationCode flows. + * @return string|null Returns the value, or null if not set. + */ + public function getRefreshUrl(): ?string { + return $this->refreshUrl; + } + + /** + * Returns the available scopes. * - * @return OAuthFlowObj Returns self for method chaining. + * @return array */ - public function setTokenUrl(string $tokenUrl): OAuthFlowObj { - $this->tokenUrl = $tokenUrl; - return $this; + public function getScopes(): array { + return $this->scopes; } - + /** * Returns the token URL. * @@ -118,28 +125,34 @@ public function setTokenUrl(string $tokenUrl): OAuthFlowObj { public function getTokenUrl(): ?string { return $this->tokenUrl; } - + /** - * Sets the URL to be used for obtaining refresh tokens. + * Sets the authorization URL to be used for this flow. * - * @param string $refreshUrl The refresh URL. This MUST be in the form of a URL. + * @param string $authorizationUrl The authorization URL. This MUST be in the form of a URL. + * REQUIRED for implicit and authorizationCode flows. * * @return OAuthFlowObj Returns self for method chaining. */ - public function setRefreshUrl(string $refreshUrl): OAuthFlowObj { - $this->refreshUrl = $refreshUrl; + public function setAuthorizationUrl(string $authorizationUrl): OAuthFlowObj { + $this->authorizationUrl = $authorizationUrl; + return $this; } - + /** - * Returns the refresh URL. + * Sets the URL to be used for obtaining refresh tokens. * - * @return string|null Returns the value, or null if not set. + * @param string $refreshUrl The refresh URL. This MUST be in the form of a URL. + * + * @return OAuthFlowObj Returns self for method chaining. */ - public function getRefreshUrl(): ?string { - return $this->refreshUrl; + public function setRefreshUrl(string $refreshUrl): OAuthFlowObj { + $this->refreshUrl = $refreshUrl; + + return $this; } - + /** * Sets the available scopes for the OAuth2 security scheme. * @@ -149,31 +162,24 @@ public function getRefreshUrl(): ?string { */ public function setScopes(array $scopes): OAuthFlowObj { $this->scopes = $scopes; + return $this; } - + /** - * Adds a scope to the OAuth2 security scheme. + * Sets the token URL to be used for this flow. * - * @param string $name The scope name. - * @param string $description A short description for the scope. + * @param string $tokenUrl The token URL. This MUST be in the form of a URL. + * REQUIRED for password, clientCredentials, and authorizationCode flows. * - * @return OAuthFlowObj + * @return OAuthFlowObj Returns self for method chaining. */ - public function addScope(string $name, string $description): OAuthFlowObj { - $this->scopes[$name] = $description; + public function setTokenUrl(string $tokenUrl): OAuthFlowObj { + $this->tokenUrl = $tokenUrl; + return $this; } - - /** - * Returns the available scopes. - * - * @return array - */ - public function getScopes(): array { - return $this->scopes; - } - + /** * Returns a Json object that represents the OAuth Flow Object. * @@ -183,19 +189,19 @@ public function toJSON(): Json { $json = new Json([ 'scopes' => $this->getScopes() ]); - + if ($this->getAuthorizationUrl() !== null) { $json->add('authorizationUrl', $this->getAuthorizationUrl()); } - + if ($this->getTokenUrl() !== null) { $json->add('tokenUrl', $this->getTokenUrl()); } - + if ($this->getRefreshUrl() !== null) { $json->add('refreshUrl', $this->getRefreshUrl()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php index ef6aa15..a4fd477 100644 --- a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php +++ b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php @@ -1,4 +1,5 @@ implicit = $implicit; - return $this; + public function getAuthorizationCode(): ?OAuthFlowObj { + return $this->authorizationCode; } - + /** - * Returns the implicit flow configuration. + * Returns the client credentials flow configuration. * * @return OAuthFlowObj|null Returns the value, or null if not set. */ - public function getImplicit(): ?OAuthFlowObj { - return $this->implicit; + public function getClientCredentials(): ?OAuthFlowObj { + return $this->clientCredentials; } - + /** - * Sets configuration for the OAuth Resource Owner Password flow. - * - * @param OAuthFlowObj $password OAuth Flow Object for password flow. + * Returns the implicit flow configuration. * - * @return OAuthFlowsObj Returns self for method chaining. + * @return OAuthFlowObj|null Returns the value, or null if not set. */ - public function setPassword(OAuthFlowObj $password): OAuthFlowsObj { - $this->password = $password; - return $this; + public function getImplicit(): ?OAuthFlowObj { + return $this->implicit; } - + /** * Returns the password flow configuration. * @@ -96,49 +90,59 @@ public function setPassword(OAuthFlowObj $password): OAuthFlowsObj { public function getPassword(): ?OAuthFlowObj { return $this->password; } - + /** - * Sets configuration for the OAuth Client Credentials flow. + * Sets configuration for the OAuth Authorization Code flow. * - * @param OAuthFlowObj $clientCredentials OAuth Flow Object for client credentials flow. + * @param OAuthFlowObj $authorizationCode OAuth Flow Object for authorization code flow. * * @return OAuthFlowsObj Returns self for method chaining. */ - public function setClientCredentials(OAuthFlowObj $clientCredentials): OAuthFlowsObj { - $this->clientCredentials = $clientCredentials; + public function setAuthorizationCode(OAuthFlowObj $authorizationCode): OAuthFlowsObj { + $this->authorizationCode = $authorizationCode; + return $this; } - + /** - * Returns the client credentials flow configuration. + * Sets configuration for the OAuth Client Credentials flow. * - * @return OAuthFlowObj|null Returns the value, or null if not set. + * @param OAuthFlowObj $clientCredentials OAuth Flow Object for client credentials flow. + * + * @return OAuthFlowsObj Returns self for method chaining. */ - public function getClientCredentials(): ?OAuthFlowObj { - return $this->clientCredentials; + public function setClientCredentials(OAuthFlowObj $clientCredentials): OAuthFlowsObj { + $this->clientCredentials = $clientCredentials; + + return $this; } - + /** - * Sets configuration for the OAuth Authorization Code flow. + * Sets configuration for the OAuth Implicit flow. * - * @param OAuthFlowObj $authorizationCode OAuth Flow Object for authorization code flow. + * @param OAuthFlowObj $implicit OAuth Flow Object for implicit flow. * * @return OAuthFlowsObj Returns self for method chaining. */ - public function setAuthorizationCode(OAuthFlowObj $authorizationCode): OAuthFlowsObj { - $this->authorizationCode = $authorizationCode; + public function setImplicit(OAuthFlowObj $implicit): OAuthFlowsObj { + $this->implicit = $implicit; + return $this; } - + /** - * Returns the authorization code flow configuration. + * Sets configuration for the OAuth Resource Owner Password flow. * - * @return OAuthFlowObj|null Returns the value, or null if not set. + * @param OAuthFlowObj $password OAuth Flow Object for password flow. + * + * @return OAuthFlowsObj Returns self for method chaining. */ - public function getAuthorizationCode(): ?OAuthFlowObj { - return $this->authorizationCode; + public function setPassword(OAuthFlowObj $password): OAuthFlowsObj { + $this->password = $password; + + return $this; } - + /** * Returns a Json object that represents the OAuth Flows Object. * @@ -146,23 +150,23 @@ public function getAuthorizationCode(): ?OAuthFlowObj { */ public function toJSON(): Json { $json = new Json(); - + if ($this->getImplicit() !== null) { $json->add('implicit', $this->getImplicit()); } - + if ($this->getPassword() !== null) { $json->add('password', $this->getPassword()); } - + if ($this->getClientCredentials() !== null) { $json->add('clientCredentials', $this->getClientCredentials()); } - + if ($this->getAuthorizationCode() !== null) { $json->add('authorizationCode', $this->getAuthorizationCode()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/OpenAPIObj.php b/WebFiori/Http/OpenAPI/OpenAPIObj.php index 6df46ef..039b397 100644 --- a/WebFiori/Http/OpenAPI/OpenAPIObj.php +++ b/WebFiori/Http/OpenAPI/OpenAPIObj.php @@ -1,4 +1,5 @@ info = $info; $this->openapi = $openapi; } - + /** - * Sets the OpenAPI Specification version number. - * - * This string MUST be the version number of the OpenAPI Specification - * that the OpenAPI Document uses (e.g., "3.1.0"). - * - * @param string $openapi The OpenAPI Specification version. + * Returns the Info Object containing API metadata. * - * @return OpenAPIObj Returns self for method chaining. + * @return InfoObj The Info Object. */ - public function setOpenapi(string $openapi): OpenAPIObj { - $this->openapi = $openapi; - return $this; + public function getInfo(): InfoObj { + return $this->info; } - + /** * Returns the OpenAPI Specification version number. * @@ -89,7 +83,16 @@ public function setOpenapi(string $openapi): OpenAPIObj { public function getOpenapi(): string { return $this->openapi; } - + + /** + * Returns the Paths Object containing API paths and operations. + * + * @return PathsObj|null The Paths Object or null if not set. + */ + public function getPaths(): ?PathsObj { + return $this->paths; + } + /** * Sets the Info Object containing API metadata. * @@ -99,18 +102,26 @@ public function getOpenapi(): string { */ public function setInfo(InfoObj $info): OpenAPIObj { $this->info = $info; + return $this; } - + /** - * Returns the Info Object containing API metadata. + * Sets the OpenAPI Specification version number. * - * @return InfoObj The Info Object. + * This string MUST be the version number of the OpenAPI Specification + * that the OpenAPI Document uses (e.g., "3.1.0"). + * + * @param string $openapi The OpenAPI Specification version. + * + * @return OpenAPIObj Returns self for method chaining. */ - public function getInfo(): InfoObj { - return $this->info; + public function setOpenapi(string $openapi): OpenAPIObj { + $this->openapi = $openapi; + + return $this; } - + /** * Sets the Paths Object containing API paths and operations. * @@ -120,18 +131,10 @@ public function getInfo(): InfoObj { */ public function setPaths(PathsObj $paths): OpenAPIObj { $this->paths = $paths; + return $this; } - - /** - * Returns the Paths Object containing API paths and operations. - * - * @return PathsObj|null The Paths Object or null if not set. - */ - public function getPaths(): ?PathsObj { - return $this->paths; - } - + /** * Returns a Json object that represents the OpenAPI Object. * @@ -145,11 +148,11 @@ public function toJSON(): Json { 'openapi' => $this->getOpenapi(), 'info' => $this->getInfo() ]); - + if ($this->getPaths() !== null) { $json->add('paths', $this->getPaths()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/OperationObj.php b/WebFiori/Http/OpenAPI/OperationObj.php index 0c2092f..e55e20b 100644 --- a/WebFiori/Http/OpenAPI/OperationObj.php +++ b/WebFiori/Http/OpenAPI/OperationObj.php @@ -1,4 +1,5 @@ responses = $responses; } - + + public function getResponses(): ResponsesObj { + return $this->responses; + } + public function setResponses(ResponsesObj $responses): OperationObj { $this->responses = $responses; + return $this; } - - public function getResponses(): ResponsesObj { - return $this->responses; - } - + public function toJSON(): Json { $json = new Json([ 'responses' => $this->getResponses() ]); - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ParameterObj.php b/WebFiori/Http/OpenAPI/ParameterObj.php index 56f0ab2..45d6b87 100644 --- a/WebFiori/Http/OpenAPI/ParameterObj.php +++ b/WebFiori/Http/OpenAPI/ParameterObj.php @@ -1,4 +1,5 @@ setName($name); $this->setIn($in); } - + + public function getAllowEmptyValue(): bool { + return $this->allowEmptyValue; + } + /** - * Sets the name of the parameter. - * - * @param string $name The name of the parameter. + * Returns whether reserved characters are allowed. * - * @return ParameterObj Returns self for method chaining. + * @return bool|null Returns the value, or null if not set. */ - public function setName(string $name): ParameterObj { - $this->name = $name; - return $this; + public function getAllowReserved(): ?bool { + return $this->allowReserved; } - + /** - * Returns the name of the parameter. + * Returns whether this parameter is deprecated. * - * @return string + * Alias for isDeprecated() for consistency with toJSON(). + * + * @return bool */ - public function getName(): string { - return $this->name; + public function getDeprecated(): bool { + return $this->deprecated; } - + /** - * Sets the location of the parameter. - * - * @param string $in The location. Possible values: "query", "header", "path", "cookie". + * Returns the description. * - * @return ParameterObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setIn(string $in): ParameterObj { - $this->in = $in; - if ($in === 'path') { - $this->required = true; - } - return $this; + public function getDescription(): ?string { + return $this->description; } - + /** - * Returns the location of the parameter. + * Returns the example. * - * @return string + * @return mixed */ - public function getIn(): string { - return $this->in; + public function getExample() { + return $this->example; } - + /** - * Sets the description of the parameter. - * - * @param string $description A brief description of the parameter. + * Returns the examples. * - * @return ParameterObj Returns self for method chaining. + * @return array|null Returns the value, or null if not set. */ - public function setDescription(string $description): ParameterObj { - $this->description = $description; - return $this; + public function getExamples(): ?array { + return $this->examples; } - + /** - * Returns the description. + * Returns the explode value. * - * @return string|null Returns the value, or null if not set. + * @return bool|null Returns the value, or null if not set. */ - public function getDescription(): ?string { - return $this->description; + public function getExplode(): ?bool { + return $this->explode; } - + /** - * Sets whether this parameter is mandatory. - * - * @param bool $required True if required. + * Returns the location of the parameter. * - * @return ParameterObj Returns self for method chaining. + * @return string */ - public function setRequired(bool $required): ParameterObj { - $this->required = $required; - return $this; + public function getIn(): string { + return $this->in; } - + /** - * Returns whether this parameter is required. + * Returns the name of the parameter. * - * @return bool + * @return string */ - public function isRequired(): bool { - return $this->required; + public function getName(): string { + return $this->name; } - + /** * Returns whether this parameter is required. * @@ -249,19 +240,34 @@ public function isRequired(): bool { public function getRequired(): bool { return $this->required; } - + /** - * Sets whether this parameter is deprecated. + * Returns the schema. * - * @param bool $deprecated True if deprecated. + * @return mixed + */ + public function getSchema() { + return $this->schema; + } + + /** + * Returns the style. * - * @return ParameterObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setDeprecated(bool $deprecated): ParameterObj { - $this->deprecated = $deprecated; - return $this; + public function getStyle(): ?string { + return $this->style; + } + + /** + * Returns whether empty value is allowed. + * + * @return bool + */ + public function isAllowEmptyValue(): bool { + return $this->allowEmptyValue; } - + /** * Returns whether this parameter is deprecated. * @@ -270,18 +276,16 @@ public function setDeprecated(bool $deprecated): ParameterObj { public function isDeprecated(): bool { return $this->deprecated; } - + /** - * Returns whether this parameter is deprecated. - * - * Alias for isDeprecated() for consistency with toJSON(). + * Returns whether this parameter is required. * * @return bool */ - public function getDeprecated(): bool { - return $this->deprecated; + public function isRequired(): bool { + return $this->required; } - + /** * Sets whether to allow empty value. * @@ -291,148 +295,157 @@ public function getDeprecated(): bool { */ public function setAllowEmptyValue(bool $allowEmptyValue): ParameterObj { $this->allowEmptyValue = $allowEmptyValue; + return $this; } - - /** - * Returns whether empty value is allowed. - * - * @return bool - */ - public function isAllowEmptyValue(): bool { - return $this->allowEmptyValue; - } - - public function getAllowEmptyValue(): bool { - return $this->allowEmptyValue; - } - + /** - * Sets the serialization style. + * Sets whether to allow reserved characters. * - * @param string $style The style value. + * @param bool $allowReserved True to allow reserved characters. * * @return ParameterObj Returns self for method chaining. */ - public function setStyle(string $style): ParameterObj { - $this->style = $style; + public function setAllowReserved(bool $allowReserved): ParameterObj { + $this->allowReserved = $allowReserved; + return $this; } - - /** - * Returns the style. - * - * @return string|null Returns the value, or null if not set. - */ - public function getStyle(): ?string { - return $this->style; - } - + /** - * Sets the explode value. + * Sets whether this parameter is deprecated. * - * @param bool $explode The explode value. + * @param bool $deprecated True if deprecated. * * @return ParameterObj Returns self for method chaining. */ - public function setExplode(bool $explode): ParameterObj { - $this->explode = $explode; + public function setDeprecated(bool $deprecated): ParameterObj { + $this->deprecated = $deprecated; + return $this; } - + /** - * Returns the explode value. + * Sets the description of the parameter. * - * @return bool|null Returns the value, or null if not set. + * @param string $description A brief description of the parameter. + * + * @return ParameterObj Returns self for method chaining. */ - public function getExplode(): ?bool { - return $this->explode; + public function setDescription(string $description): ParameterObj { + $this->description = $description; + + return $this; } - + /** - * Sets whether to allow reserved characters. + * Sets an example of the parameter's potential value. * - * @param bool $allowReserved True to allow reserved characters. + * @param mixed $example Example value. * * @return ParameterObj Returns self for method chaining. */ - public function setAllowReserved(bool $allowReserved): ParameterObj { - $this->allowReserved = $allowReserved; + public function setExample($example): ParameterObj { + $this->example = $example; + return $this; } - + /** - * Returns whether reserved characters are allowed. + * Sets examples of the parameter's potential value. * - * @return bool|null Returns the value, or null if not set. + * @param array $examples Map of example names to Example Objects or Reference Objects. + * + * @return ParameterObj Returns self for method chaining. */ - public function getAllowReserved(): ?bool { - return $this->allowReserved; + public function setExamples(array $examples): ParameterObj { + $this->examples = $examples; + + return $this; } - + /** - * Sets the schema defining the type used for the parameter. + * Sets the explode value. * - * @param mixed $schema Schema Object or any schema definition. + * @param bool $explode The explode value. * * @return ParameterObj Returns self for method chaining. */ - public function setSchema($schema): ParameterObj { - $this->schema = $schema; + public function setExplode(bool $explode): ParameterObj { + $this->explode = $explode; + return $this; } - + /** - * Returns the schema. + * Sets the location of the parameter. * - * @return mixed + * @param string $in The location. Possible values: "query", "header", "path", "cookie". + * + * @return ParameterObj Returns self for method chaining. */ - public function getSchema() { - return $this->schema; + public function setIn(string $in): ParameterObj { + $this->in = $in; + + if ($in === 'path') { + $this->required = true; + } + + return $this; } - + /** - * Sets an example of the parameter's potential value. + * Sets the name of the parameter. * - * @param mixed $example Example value. + * @param string $name The name of the parameter. * * @return ParameterObj Returns self for method chaining. */ - public function setExample($example): ParameterObj { - $this->example = $example; + public function setName(string $name): ParameterObj { + $this->name = $name; + return $this; } - + /** - * Returns the example. + * Sets whether this parameter is mandatory. * - * @return mixed + * @param bool $required True if required. + * + * @return ParameterObj Returns self for method chaining. */ - public function getExample() { - return $this->example; + public function setRequired(bool $required): ParameterObj { + $this->required = $required; + + return $this; } - + /** - * Sets examples of the parameter's potential value. + * Sets the schema defining the type used for the parameter. * - * @param array $examples Map of example names to Example Objects or Reference Objects. + * @param mixed $schema Schema Object or any schema definition. * * @return ParameterObj Returns self for method chaining. */ - public function setExamples(array $examples): ParameterObj { - $this->examples = $examples; + public function setSchema($schema): ParameterObj { + $this->schema = $schema; + return $this; } - + /** - * Returns the examples. + * Sets the serialization style. * - * @return array|null Returns the value, or null if not set. + * @param string $style The style value. + * + * @return ParameterObj Returns self for method chaining. */ - public function getExamples(): ?array { - return $this->examples; + public function setStyle(string $style): ParameterObj { + $this->style = $style; + + return $this; } - + /** * Returns a Json object that represents the Parameter Object. * @@ -443,47 +456,47 @@ public function toJSON(): Json { 'name' => $this->getName(), '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()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/PathItemObj.php b/WebFiori/Http/OpenAPI/PathItemObj.php index 3528cd7..47e7c7e 100644 --- a/WebFiori/Http/OpenAPI/PathItemObj.php +++ b/WebFiori/Http/OpenAPI/PathItemObj.php @@ -1,4 +1,5 @@ get = $operation; - return $this; + private ?OperationObj $put = null; + + public function getDelete(): ?OperationObj { + return $this->delete; } - + public function getGet(): ?OperationObj { return $this->get; } - - public function setPost(OperationObj $operation): PathItemObj { - $this->post = $operation; - return $this; + + public function getPatch(): ?OperationObj { + return $this->patch; } - + public function getPost(): ?OperationObj { return $this->post; } - - public function setPut(OperationObj $operation): PathItemObj { - $this->put = $operation; - return $this; - } - + public function getPut(): ?OperationObj { return $this->put; } - + public function setDelete(OperationObj $operation): PathItemObj { $this->delete = $operation; + return $this; } - - public function getDelete(): ?OperationObj { - return $this->delete; + + public function setGet(OperationObj $operation): PathItemObj { + $this->get = $operation; + + return $this; } - + public function setPatch(OperationObj $operation): PathItemObj { $this->patch = $operation; + return $this; } - - public function getPatch(): ?OperationObj { - return $this->patch; + + public function setPost(OperationObj $operation): PathItemObj { + $this->post = $operation; + + return $this; + } + + public function setPut(OperationObj $operation): PathItemObj { + $this->put = $operation; + + return $this; } - + public function toJSON(): Json { $json = new Json(); - + if ($this->getGet() !== null) { $json->add('get', $this->getGet()); } - + if ($this->getPost() !== null) { $json->add('post', $this->getPost()); } - + if ($this->getPut() !== null) { $json->add('put', $this->getPut()); } - + if ($this->getDelete() !== null) { $json->add('delete', $this->getDelete()); } - + if ($this->getPatch() !== null) { $json->add('patch', $this->getPatch()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/PathsObj.php b/WebFiori/Http/OpenAPI/PathsObj.php index 6ac15dd..605077c 100644 --- a/WebFiori/Http/OpenAPI/PathsObj.php +++ b/WebFiori/Http/OpenAPI/PathsObj.php @@ -1,4 +1,5 @@ paths[$path] = $pathItem; + return $this; } - + /** * Returns all paths and their operations. * @@ -56,7 +58,7 @@ public function addPath(string $path, PathItemObj $pathItem): PathsObj { public function getPaths(): array { return $this->paths; } - + /** * Returns a Json object that represents the Paths Object. * @@ -66,10 +68,11 @@ public function getPaths(): array { */ public function toJSON(): Json { $json = new Json(); - + foreach ($this->paths as $path => $pathItem) { $json->add($path, $pathItem); } + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ReferenceObj.php b/WebFiori/Http/OpenAPI/ReferenceObj.php index d13e2e7..b3019a0 100644 --- a/WebFiori/Http/OpenAPI/ReferenceObj.php +++ b/WebFiori/Http/OpenAPI/ReferenceObj.php @@ -1,4 +1,5 @@ setRef($ref); } - + /** - * Sets the reference identifier. - * - * @param string $ref The reference identifier. This MUST be in the form of a URI. + * Returns the description. * - * @return ReferenceObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setRef(string $ref): ReferenceObj { - $this->ref = $ref; - return $this; + public function getDescription(): ?string { + return $this->description; } - + /** * Returns the reference identifier. * @@ -86,19 +83,7 @@ public function setRef(string $ref): ReferenceObj { public function getRef(): string { return $this->ref; } - - /** - * Sets a short summary which by default SHOULD override that of the referenced component. - * - * @param string $summary A short summary. - * - * @return ReferenceObj Returns self for method chaining. - */ - public function setSummary(string $summary): ReferenceObj { - $this->summary = $summary; - return $this; - } - + /** * Returns the summary. * @@ -107,7 +92,7 @@ public function setSummary(string $summary): ReferenceObj { public function getSummary(): ?string { return $this->summary; } - + /** * Sets a description which by default SHOULD override that of the referenced component. * @@ -118,18 +103,36 @@ public function getSummary(): ?string { */ public function setDescription(string $description): ReferenceObj { $this->description = $description; + return $this; } - + /** - * Returns the description. + * Sets the reference identifier. * - * @return string|null Returns the value, or null if not set. + * @param string $ref The reference identifier. This MUST be in the form of a URI. + * + * @return ReferenceObj Returns self for method chaining. */ - public function getDescription(): ?string { - return $this->description; + public function setRef(string $ref): ReferenceObj { + $this->ref = $ref; + + return $this; } - + + /** + * Sets a short summary which by default SHOULD override that of the referenced component. + * + * @param string $summary A short summary. + * + * @return ReferenceObj Returns self for method chaining. + */ + public function setSummary(string $summary): ReferenceObj { + $this->summary = $summary; + + return $this; + } + /** * Returns a Json object that represents the Reference Object. * @@ -139,15 +142,15 @@ public function toJSON(): Json { $json = new Json([ '$ref' => $this->getRef() ]); - + if ($this->getSummary() !== null) { $json->add('summary', $this->getSummary()); } - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ResponseObj.php b/WebFiori/Http/OpenAPI/ResponseObj.php index 855667b..76cfab1 100644 --- a/WebFiori/Http/OpenAPI/ResponseObj.php +++ b/WebFiori/Http/OpenAPI/ResponseObj.php @@ -1,4 +1,5 @@ description = $description; } - + + public function getDescription(): string { + return $this->description; + } + public function setDescription(string $description): ResponseObj { $this->description = $description; + return $this; } - - public function getDescription(): string { - return $this->description; - } - + public function toJSON(): Json { $json = new Json([ 'description' => $this->getDescription() ]); - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ResponsesObj.php b/WebFiori/Http/OpenAPI/ResponsesObj.php index c69bfa9..a4293a4 100644 --- a/WebFiori/Http/OpenAPI/ResponsesObj.php +++ b/WebFiori/Http/OpenAPI/ResponsesObj.php @@ -1,4 +1,5 @@ responses[$statusCode] = $response; + return $this; } - + /** * Returns all responses mapped by status code. * @@ -61,7 +63,7 @@ public function addResponse(string $statusCode, ResponseObj|string $response): R public function getResponses(): array { return $this->responses; } - + /** * Returns a Json object that represents the Responses Object. * @@ -71,11 +73,11 @@ public function getResponses(): array { */ public function toJSON(): Json { $json = new Json(); - + foreach ($this->responses as $code => $response) { $json->add($code, $response); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/Schema.php b/WebFiori/Http/OpenAPI/Schema.php index c0ac3f5..d4c0661 100644 --- a/WebFiori/Http/OpenAPI/Schema.php +++ b/WebFiori/Http/OpenAPI/Schema.php @@ -1,4 +1,5 @@ type = $type; } - + + /** + * Adds an example value. + * + * @param mixed $example Example value + * + * @return Schema + */ + public function addExample(mixed $example): self { + $this->examples[] = $example; + + return $this; + } + /** * Creates a Schema from a RequestParameter. * @@ -51,14 +65,14 @@ public function __construct(?string $type = null) { public static function fromRequestParameter(RequestParameter $param): self { $schema = new self(); $schema->type = self::mapType($param->getType()); - + // Set format for special types if ($param->getType() === ParamType::EMAIL) { $schema->format = 'email'; } else if ($param->getType() === ParamType::URL) { $schema->format = 'uri'; } - + // Constraints $schema->minimum = $param->getMinValue(); $schema->maximum = $param->getMaxValue(); @@ -66,10 +80,71 @@ public static function fromRequestParameter(RequestParameter $param): self { $schema->maxLength = $param->getMaxLength(); $schema->default = $param->getDefault(); $schema->description = $param->getDescription(); - + return $schema; } - + + /** + * Maps internal parameter types to OpenAPI types. + * + * @param string $type Internal type + * + * @return string OpenAPI type + */ + public static function mapType(string $type): string { + $typeMap = [ + ParamType::INT => 'integer', + ParamType::DOUBLE => 'number', + ParamType::STRING => 'string', + ParamType::BOOL => 'boolean', + ParamType::ARR => 'array', + ParamType::EMAIL => 'string', + ParamType::URL => 'string', + ParamType::JSON_OBJ => 'object' + ]; + + return $typeMap[strtolower($type)] ?? 'string'; + } + + /** + * Sets allowed enum values. + * + * @param array $values Array of allowed values + * + * @return Schema + */ + public function setEnum(array $values): self { + $this->enum = $values; + + return $this; + } + + /** + * Sets the format. + * + * @param string $format Format (e.g., 'email', 'uri', 'date-time') + * + * @return Schema + */ + public function setFormat(string $format): self { + $this->format = $format; + + return $this; + } + + /** + * Sets the pattern (regex). + * + * @param string $pattern Regular expression pattern + * + * @return Schema + */ + public function setPattern(string $pattern): self { + $this->pattern = $pattern; + + return $this; + } + /** * Converts the schema to JSON representation. * @@ -77,108 +152,47 @@ public static function fromRequestParameter(RequestParameter $param): self { */ public function toJson(): Json { $json = new Json(); - + if ($this->type !== null) { $json->add('type', $this->type); } + if ($this->format !== null) { $json->add('format', $this->format); } + if ($this->default !== null) { $json->add('default', $this->default); } + if ($this->minimum !== null) { $json->add('minimum', $this->minimum); } + if ($this->maximum !== null) { $json->add('maximum', $this->maximum); } + if ($this->minLength !== null) { $json->add('minLength', $this->minLength); } + if ($this->maxLength !== null) { $json->add('maxLength', $this->maxLength); } + if ($this->pattern !== null) { $json->add('pattern', $this->pattern); } + if ($this->enum !== null) { $json->add('enum', $this->enum); } + if (!empty($this->examples)) { $json->add('examples', $this->examples); } - + return $json; } - - /** - * Sets the format. - * - * @param string $format Format (e.g., 'email', 'uri', 'date-time') - * - * @return Schema - */ - public function setFormat(string $format): self { - $this->format = $format; - return $this; - } - - /** - * Sets the pattern (regex). - * - * @param string $pattern Regular expression pattern - * - * @return Schema - */ - public function setPattern(string $pattern): self { - $this->pattern = $pattern; - return $this; - } - - /** - * Sets allowed enum values. - * - * @param array $values Array of allowed values - * - * @return Schema - */ - public function setEnum(array $values): self { - $this->enum = $values; - return $this; - } - - /** - * Adds an example value. - * - * @param mixed $example Example value - * - * @return Schema - */ - public function addExample(mixed $example): self { - $this->examples[] = $example; - return $this; - } - - /** - * Maps internal parameter types to OpenAPI types. - * - * @param string $type Internal type - * - * @return string OpenAPI type - */ - public static function mapType(string $type): string { - $typeMap = [ - ParamType::INT => 'integer', - ParamType::DOUBLE => 'number', - ParamType::STRING => 'string', - ParamType::BOOL => 'boolean', - ParamType::ARR => 'array', - ParamType::EMAIL => 'string', - ParamType::URL => 'string', - ParamType::JSON_OBJ => 'object' - ]; - - return $typeMap[strtolower($type)] ?? 'string'; - } } diff --git a/WebFiori/Http/OpenAPI/SecurityRequirementObj.php b/WebFiori/Http/OpenAPI/SecurityRequirementObj.php index 3e6fa05..3478246 100644 --- a/WebFiori/Http/OpenAPI/SecurityRequirementObj.php +++ b/WebFiori/Http/OpenAPI/SecurityRequirementObj.php @@ -1,4 +1,5 @@ requirements[$name] = $scopes; + return $this; } - + /** * Returns all security requirements. * @@ -62,7 +64,7 @@ public function addRequirement(string $name, array $scopes = []): SecurityRequir public function getRequirements(): array { return $this->requirements; } - + /** * Returns a Json object that represents the Security Requirement Object. * @@ -72,11 +74,11 @@ public function getRequirements(): array { */ public function toJSON(): Json { $json = new Json(); - + foreach ($this->requirements as $name => $scopes) { $json->add($name, $scopes); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/SecuritySchemeObj.php b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php index 2cbe785..f793df6 100644 --- a/WebFiori/Http/OpenAPI/SecuritySchemeObj.php +++ b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php @@ -1,4 +1,5 @@ setType($type); } - - /** - * Sets the type of the security scheme. - * - * @param string $type Valid values are "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect". - * - * @return SecuritySchemeObj Returns self for method chaining. - */ - public function setType(string $type): SecuritySchemeObj { - $this->type = $type; - return $this; - } - - /** - * Returns the type of the security scheme. - * - * @return string - */ - public function getType(): string { - return $this->type; - } - + /** - * Sets the description for security scheme. - * - * @param string $description A description for security scheme. + * Returns the bearer token format. * - * @return SecuritySchemeObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setDescription(string $description): SecuritySchemeObj { - $this->description = $description; - return $this; + public function getBearerFormat(): ?string { + return $this->bearerFormat; } - + /** * Returns the description. * @@ -160,61 +136,43 @@ public function setDescription(string $description): SecuritySchemeObj { public function getDescription(): ?string { return $this->description; } - + /** - * Sets the name of the header, query or cookie parameter to be used. - * - * @param string $name The parameter name. REQUIRED for apiKey type. + * Returns the OAuth flows configuration. * - * @return SecuritySchemeObj Returns self for method chaining. + * @return OAuthFlowsObj|null Returns the value, or null if not set. */ - public function setName(string $name): SecuritySchemeObj { - $this->name = $name; - return $this; + public function getFlows(): ?OAuthFlowsObj { + return $this->flows; } - + /** - * Returns the parameter name. + * Returns the location of the API key. * * @return string|null Returns the value, or null if not set. */ - public function getName(): ?string { - return $this->name; - } - - /** - * Sets the location of the API key. - * - * @param string $in Valid values are "query", "header", or "cookie". REQUIRED for apiKey type. - * - * @return SecuritySchemeObj Returns self for method chaining. - */ - public function setIn(string $in): SecuritySchemeObj { - $this->in = $in; - return $this; + public function getIn(): ?string { + return $this->in; } - + /** - * Returns the location of the API key. + * Returns the parameter name. * * @return string|null Returns the value, or null if not set. */ - public function getIn(): ?string { - return $this->in; + public function getName(): ?string { + return $this->name; } - + /** - * Sets the name of the HTTP Authentication scheme. - * - * @param string $scheme The HTTP Authentication scheme. REQUIRED for http type. + * Returns the OpenID Connect discovery URL. * - * @return SecuritySchemeObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setScheme(string $scheme): SecuritySchemeObj { - $this->scheme = $scheme; - return $this; + public function getOpenIdConnectUrl(): ?string { + return $this->openIdConnectUrl; } - + /** * Returns the HTTP Authentication scheme. * @@ -223,7 +181,16 @@ public function setScheme(string $scheme): SecuritySchemeObj { public function getScheme(): ?string { return $this->scheme; } - + + /** + * Returns the type of the security scheme. + * + * @return string + */ + public function getType(): string { + return $this->type; + } + /** * Sets a hint to identify how the bearer token is formatted. * @@ -233,18 +200,23 @@ public function getScheme(): ?string { */ public function setBearerFormat(string $bearerFormat): SecuritySchemeObj { $this->bearerFormat = $bearerFormat; + return $this; } - + /** - * Returns the bearer token format. + * Sets the description for security scheme. * - * @return string|null Returns the value, or null if not set. + * @param string $description A description for security scheme. + * + * @return SecuritySchemeObj Returns self for method chaining. */ - public function getBearerFormat(): ?string { - return $this->bearerFormat; + public function setDescription(string $description): SecuritySchemeObj { + $this->description = $description; + + return $this; } - + /** * Sets configuration information for the OAuth2 flow types supported. * @@ -254,18 +226,36 @@ public function getBearerFormat(): ?string { */ public function setFlows(OAuthFlowsObj $flows): SecuritySchemeObj { $this->flows = $flows; + return $this; } - + /** - * Returns the OAuth flows configuration. + * Sets the location of the API key. * - * @return OAuthFlowsObj|null Returns the value, or null if not set. + * @param string $in Valid values are "query", "header", or "cookie". REQUIRED for apiKey type. + * + * @return SecuritySchemeObj Returns self for method chaining. */ - public function getFlows(): ?OAuthFlowsObj { - return $this->flows; + public function setIn(string $in): SecuritySchemeObj { + $this->in = $in; + + return $this; } - + + /** + * Sets the name of the header, query or cookie parameter to be used. + * + * @param string $name The parameter name. REQUIRED for apiKey type. + * + * @return SecuritySchemeObj Returns self for method chaining. + */ + public function setName(string $name): SecuritySchemeObj { + $this->name = $name; + + return $this; + } + /** * Sets the OpenID Connect discovery URL. * @@ -276,18 +266,36 @@ public function getFlows(): ?OAuthFlowsObj { */ public function setOpenIdConnectUrl(string $openIdConnectUrl): SecuritySchemeObj { $this->openIdConnectUrl = $openIdConnectUrl; + return $this; } - + /** - * Returns the OpenID Connect discovery URL. + * Sets the name of the HTTP Authentication scheme. * - * @return string|null Returns the value, or null if not set. + * @param string $scheme The HTTP Authentication scheme. REQUIRED for http type. + * + * @return SecuritySchemeObj Returns self for method chaining. */ - public function getOpenIdConnectUrl(): ?string { - return $this->openIdConnectUrl; + public function setScheme(string $scheme): SecuritySchemeObj { + $this->scheme = $scheme; + + return $this; } - + + /** + * Sets the type of the security scheme. + * + * @param string $type Valid values are "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect". + * + * @return SecuritySchemeObj Returns self for method chaining. + */ + public function setType(string $type): SecuritySchemeObj { + $this->type = $type; + + return $this; + } + /** * Returns a Json object that represents the Security Scheme Object. * @@ -297,35 +305,35 @@ public function toJSON(): Json { $json = new Json([ 'type' => $this->getType() ]); - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + if ($this->getName() !== null) { $json->add('name', $this->getName()); } - + if ($this->getIn() !== null) { $json->add('in', $this->getIn()); } - + if ($this->getScheme() !== null) { $json->add('scheme', $this->getScheme()); } - + if ($this->getBearerFormat() !== null) { $json->add('bearerFormat', $this->getBearerFormat()); } - + if ($this->getFlows() !== null) { $json->add('flows', $this->getFlows()); } - + if ($this->getOpenIdConnectUrl() !== null) { $json->add('openIdConnectUrl', $this->getOpenIdConnectUrl()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ServerObj.php b/WebFiori/Http/OpenAPI/ServerObj.php index 9b61658..0db2c76 100644 --- a/WebFiori/Http/OpenAPI/ServerObj.php +++ b/WebFiori/Http/OpenAPI/ServerObj.php @@ -1,4 +1,5 @@ setUrl($url); - + if ($description !== null) { $this->setDescription($description); } } - + /** - * Sets the URL to the target host. - * - * @param string $url A URL to the target host. This URL supports Server Variables and MAY be relative. + * Returns the description of the host. * - * @return ServerObj Returns self for method chaining. + * @return string|null Returns the value, or null if not set. */ - public function setUrl(string $url): ServerObj { - $this->url = $url; - return $this; + public function getDescription(): ?string { + return $this->description; } - + /** * Returns the URL to the target host. * @@ -80,7 +77,7 @@ public function setUrl(string $url): ServerObj { public function getUrl(): string { return $this->url; } - + /** * Sets the description of the host designated by the URL. * @@ -91,18 +88,23 @@ public function getUrl(): string { */ public function setDescription(string $description): ServerObj { $this->description = $description; + return $this; } - + /** - * Returns the description of the host. + * Sets the URL to the target host. * - * @return string|null Returns the value, or null if not set. + * @param string $url A URL to the target host. This URL supports Server Variables and MAY be relative. + * + * @return ServerObj Returns self for method chaining. */ - public function getDescription(): ?string { - return $this->description; + public function setUrl(string $url): ServerObj { + $this->url = $url; + + return $this; } - + /** * Returns a Json object that represents the Server Object. * @@ -112,11 +114,11 @@ public function toJSON(): Json { $json = new Json([ 'url' => $this->getUrl() ]); - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + return $json; } } diff --git a/WebFiori/Http/OpenAPI/TagObj.php b/WebFiori/Http/OpenAPI/TagObj.php index a26e64f..3447cb3 100644 --- a/WebFiori/Http/OpenAPI/TagObj.php +++ b/WebFiori/Http/OpenAPI/TagObj.php @@ -1,4 +1,5 @@ setName($name); - + if ($description !== null) { $this->setDescription($description); } } - + /** - * Sets the name of the tag. - * - * The tag name is used to group operations in the OpenAPI Description. + * Returns the description for the tag. * - * @param string $name The name of the tag. + * @return string|null The description, or null if not set. + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Returns the external documentation for this tag. * - * @return TagObj Returns self for method chaining. + * @return ExternalDocObj|null The external documentation object, or null if not set. */ - public function setName(string $name): TagObj { - $this->name = $name; - return $this; + public function getExternalDocs(): ?ExternalDocObj { + return $this->externalDocs; } - + /** * Returns the name of the tag. * @@ -85,7 +89,7 @@ public function setName(string $name): TagObj { public function getName(): string { return $this->name; } - + /** * Sets the description for the tag. * @@ -97,18 +101,10 @@ public function getName(): string { */ public function setDescription(string $description): TagObj { $this->description = $description; + return $this; } - - /** - * Returns the description for the tag. - * - * @return string|null The description, or null if not set. - */ - public function getDescription(): ?string { - return $this->description; - } - + /** * Sets additional external documentation for this tag. * @@ -118,18 +114,25 @@ public function getDescription(): ?string { */ public function setExternalDocs(ExternalDocObj $externalDocs): TagObj { $this->externalDocs = $externalDocs; + return $this; } - + /** - * Returns the external documentation for this tag. + * Sets the name of the tag. * - * @return ExternalDocObj|null The external documentation object, or null if not set. + * The tag name is used to group operations in the OpenAPI Description. + * + * @param string $name The name of the tag. + * + * @return TagObj Returns self for method chaining. */ - public function getExternalDocs(): ?ExternalDocObj { - return $this->externalDocs; + public function setName(string $name): TagObj { + $this->name = $name; + + return $this; } - + /** * Returns a Json object that represents the Tag Object. * @@ -141,15 +144,15 @@ public function toJSON(): Json { $json = new Json([ 'name' => $this->getName() ]); - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + if ($this->getExternalDocs() !== null) { $json->add('externalDocs', $this->getExternalDocs()); } - + return $json; } } diff --git a/WebFiori/Http/ParamOption.php b/WebFiori/Http/ParamOption.php index 8de958b..33b33ca 100644 --- a/WebFiori/Http/ParamOption.php +++ b/WebFiori/Http/ParamOption.php @@ -1,4 +1,5 @@ extractHeaders(); $request->setRequestMethod($request->getMethodFromGlobals()); $request->setBody(file_get_contents('php://input')); - - return $request; - } - /** - * Returns an associative array of request headers. - * - * @return array The indices of the array will be headers names and the - * values are sub-arrays. Each array contains the values of the header. - */ - public function getHeadersAssoc() : array { - $retVal = []; - $headers = $this->getHeaders(); - - foreach ($headers as $headerObj) { - if (!isset($retVal[$headerObj->getName()])) { - $retVal[$headerObj->getName()] = []; - } - $retVal[$headerObj->getName()][] = $headerObj->getValue(); - } - return $retVal; + return $request; } /** * Returns authorization header. @@ -61,11 +40,11 @@ public function getHeadersAssoc() : array { */ public function getAuthHeader() { $header = $this->getHeader('authorization'); - + if (count($header) == 1) { return new AuthHeader($header[0]); } - + return null; } @@ -78,15 +57,15 @@ public function getClientIP() : string { if (!isset($_SERVER['REMOTE_ADDR'])) { return '127.0.0.1'; } - + $ip = $_SERVER['REMOTE_ADDR']; - + if ($ip === '::1') { return '127.0.0.1'; } - + $validated = filter_var($ip, FILTER_VALIDATE_IP); - + return $validated !== false ? $validated : ''; } @@ -99,6 +78,42 @@ public function getContentType() { return isset($_SERVER['CONTENT_TYPE']) ? filter_var($_SERVER['CONTENT_TYPE']) : null; } + /** + * Returns the value of a cookie. + * + * @param string $cookieName + * + * @return string|null + */ + public function getCookieValue(string $cookieName) { + $trimmedName = trim($cookieName); + + if (isset($_COOKIE[$trimmedName])) { + return filter_var($_COOKIE[$trimmedName]); + } + + return null; + } + /** + * Returns an associative array of request headers. + * + * @return array The indices of the array will be headers names and the + * values are sub-arrays. Each array contains the values of the header. + */ + public function getHeadersAssoc() : array { + $retVal = []; + $headers = $this->getHeaders(); + + foreach ($headers as $headerObj) { + if (!isset($retVal[$headerObj->getName()])) { + $retVal[$headerObj->getName()] = []; + } + $retVal[$headerObj->getName()][] = $headerObj->getValue(); + } + + return $retVal; + } + /** * Returns HTTP request method. * @@ -117,24 +132,8 @@ public function getMethod() : string { */ public function getParam(string $paramName) { $params = $this->getParams(); - return $params[trim($paramName)] ?? null; - } - /** - * Returns the value of a cookie. - * - * @param string $cookieName - * - * @return string|null - */ - public function getCookieValue(string $cookieName) { - $trimmedName = trim($cookieName); - - if (isset($_COOKIE[$trimmedName])) { - return filter_var($_COOKIE[$trimmedName]); - } - - return null; + return $params[trim($paramName)] ?? null; } /** @@ -145,7 +144,7 @@ public function getCookieValue(string $cookieName) { public function getParams() : array { $method = $this->getMethod(); $retVal = []; - + if ($method == RequestMethod::GET) { foreach ($_GET as $param => $val) { $retVal[$param] = $this->filter(INPUT_GET, $param); @@ -155,7 +154,7 @@ public function getParams() : array { $retVal[$param] = $this->filter(INPUT_POST, $param); } } - + return $retVal; } @@ -166,27 +165,27 @@ public function getParams() : array { */ public function getPath() : string { $path = $this->getPathHelper('REQUEST_URI'); - + if ($path === null) { $path = $this->getPathHelper('PATH_INFO'); } - + if ($path === null) { $path = $this->getPathHelper('HTTP_REQUEST_URI'); } - + if ($path === null) { $path = $this->getPathHelper('HTTP_X_ORIGINAL_URL'); } - + if ($path === null) { $path = $this->getPathHelper('SCRIPT_NAME'); } - + if ($path === null) { return '/'; } - + return parse_url($path, PHP_URL_PATH); } @@ -199,11 +198,11 @@ public function getPath() : string { */ public function getRequestedURI(string $pathToAppend = '') : string { $base = RequestUri::getBaseURL(); - + if (strlen($pathToAppend) != 0) { $path = $this->getPath(); $cleanPath = trim($pathToAppend, '/'); - + if (strlen($cleanPath) != 0) { if ($path[strlen($path) - 1] == '/') { return $base.$path.$cleanPath; @@ -212,13 +211,13 @@ public function getRequestedURI(string $pathToAppend = '') : string { } } } - + $uri = $base.$this->getPath(); - + if (!empty($_GET)) { $uri .= '?'.http_build_query($_GET); } - + return $uri; } @@ -231,34 +230,18 @@ public function getUri() : RequestUri { return new RequestUri($this->getRequestedURI()); } - private function getMethodFromGlobals() : string { - $meth = getenv('REQUEST_METHOD'); - - if ($meth === false) { - $meth = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : RequestMethod::GET; - } - - $method = filter_var($meth); - - if ($method !== false && in_array($method, RequestMethod::getAll())) { - return $method; - } - - return RequestMethod::GET; - } - private function extractHeaders() { $this->getHeadersPool()->clear(); - + if (function_exists('apache_request_headers')) { $headers = apache_request_headers(); - + foreach ($headers as $k => $v) { $this->addHeader($k, filter_var($v, FILTER_SANITIZE_FULL_SPECIAL_CHARS)); } } else { $headersArr = $this->getRequestHeadersFromServer(); - + foreach ($headersArr as $header) { $this->addHeader($header->getName(), $header->getValue()); } @@ -267,49 +250,66 @@ private function extractHeaders() { private function filter($inputSource, $varName) { $val = filter_input($inputSource, $varName); - + if ($val === null) { if ($inputSource == INPUT_POST && isset($_POST[$varName])) { $val = filter_var(urldecode($_POST[$varName])); } else if ($inputSource == INPUT_GET && isset($_GET[$varName])) { $val = filter_var(urldecode($_GET[$varName])); - } else if ($inputSource == INPUT_COOKIE && isset ($_COOKIE[$varName])) { + } else if ($inputSource == INPUT_COOKIE && isset($_COOKIE[$varName])) { $val = filter_var(urldecode($_COOKIE[$varName])); } } - + return $val; } + private function getMethodFromGlobals() : string { + $meth = getenv('REQUEST_METHOD'); + + if ($meth === false) { + $meth = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : RequestMethod::GET; + } + + $method = filter_var($meth); + + if ($method !== false && in_array($method, RequestMethod::getAll())) { + return $method; + } + + return RequestMethod::GET; + } + private function getPathHelper(string $header) { $envVal = getenv($header); + if ($envVal !== false) { return $envVal; } - + if (isset($_SERVER[$header])) { return $_SERVER[$header]; } - + $headerVals = $this->getHeader($header); - + if (count($headerVals) == 1) { return $headerVals[0]; } - + return null; } private function getRequestHeadersFromServer() : array { $retVal = []; - + foreach ($_SERVER as $name => $value) { if (substr($name, 0, 5) == 'HTTP_') { $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); $retVal[] = new HttpHeader($name, $value); } } - + return $retVal; } } diff --git a/WebFiori/Http/RequestMethod.php b/WebFiori/Http/RequestMethod.php index b317e5f..1f73c9a 100644 --- a/WebFiori/Http/RequestMethod.php +++ b/WebFiori/Http/RequestMethod.php @@ -1,4 +1,5 @@ '$maxLength'\n]\n"; } + + /** + * Adds a request method to the parameter. + * + * @param string $requestMethod The request method name (e.g., 'GET', 'POST'). + * + * @return RequestParameter Returns self for method chaining. + */ + public function addMethod(string $requestMethod): RequestParameter { + $method = strtoupper(trim($requestMethod)); + + if (!in_array($method, $this->methods) && in_array($method, RequestMethod::getAll())) { + $this->methods[] = $method; + } + + return $this; + } + + /** + * Adds multiple request methods to the parameter. + * + * @param array $arr An array of request method names. + * + * @return RequestParameter Returns self for method chaining. + */ + public function addMethods(array $arr): RequestParameter { + foreach ($arr as $method) { + $this->addMethod($method); + } + + return $this; + } /** * Creates an object of the class given an associative array of options. * @@ -312,6 +345,14 @@ public function getMaxLength() { public function getMaxValue() { return $this->maxVal; } + /** + * Returns an array of request methods at which the parameter must exist. + * + * @return array An array of request method names (e.g., ['GET', 'POST']). + */ + public function getMethods(): array { + return $this->methods; + } /** * Returns the minimum length the parameter can accept. * @@ -646,9 +687,10 @@ public function setName(string $name) : bool { if (WebService::isValidName($nameTrimmed)) { $this->name = $nameTrimmed; + // Check for reserved parameter names if (in_array(strtolower($nameTrimmed), self::RESERVED_NAMES)) { - throw new \InvalidArgumentException("Parameter name '$nameTrimmed' is reserved and cannot be used. Reserved names: " . implode(', ', self::RESERVED_NAMES)); + throw new \InvalidArgumentException("Parameter name '$nameTrimmed' is reserved and cannot be used. Reserved names: ".implode(', ', self::RESERVED_NAMES)); } @@ -705,29 +747,27 @@ public function setType(string $type) : bool { public function toJSON() : Json { $json = new Json(); $json->add('name', $this->getName()); - + $methods = $this->getMethods(); + // Default to 'query' for GET/DELETE, 'body' for others if (count($methods) === 0 || in_array(RequestMethod::GET, $methods) || in_array(RequestMethod::DELETE, $methods)) { $json->add('in', 'query'); } else { $json->add('in', 'body'); } - + $json->add('required', !$this->isOptional()); - + if ($this->getDescription() !== null) { $json->add('description', $this->getDescription()); } - + $json->add('schema', $this->getSchema()); - + return $json; } - private function getSchema() : Json { - return Schema::fromRequestParameter($this)->toJson(); - } - + /** * * @param RequestParameter $param @@ -763,6 +803,7 @@ private static function checkParamAttrs(RequestParameter $param, array $options) if (isset($options[ParamOption::METHODS])) { $type = gettype($options[ParamOption::METHODS]); + if ($type == 'string') { $param->addMethod($options[ParamOption::METHODS]); } else if ($type == 'array') { @@ -778,41 +819,7 @@ private static function checkParamAttrs(RequestParameter $param, array $options) $param->setDescription($options[ParamOption::DESCRIPTION]); } } - /** - * Returns an array of request methods at which the parameter must exist. - * - * @return array An array of request method names (e.g., ['GET', 'POST']). - */ - public function getMethods(): array { - return $this->methods; - } - - /** - * Adds a request method to the parameter. - * - * @param string $requestMethod The request method name (e.g., 'GET', 'POST'). - * - * @return RequestParameter Returns self for method chaining. - */ - public function addMethod(string $requestMethod): RequestParameter { - $method = strtoupper(trim($requestMethod)); - if (!in_array($method, $this->methods) && in_array($method, RequestMethod::getAll())) { - $this->methods[] = $method; - } - return $this; - } - - /** - * Adds multiple request methods to the parameter. - * - * @param array $arr An array of request method names. - * - * @return RequestParameter Returns self for method chaining. - */ - public function addMethods(array $arr): RequestParameter { - foreach ($arr as $method) { - $this->addMethod($method); - } - return $this; + private function getSchema() : Json { + return Schema::fromRequestParameter($this)->toJson(); } } diff --git a/WebFiori/Http/RequestUri.php b/WebFiori/Http/RequestUri.php index cd18310..5da631c 100644 --- a/WebFiori/Http/RequestUri.php +++ b/WebFiori/Http/RequestUri.php @@ -1,4 +1,5 @@ getName(); - }, $this->getParameters()); - } - /** - * Returns the base URL of the framework. - * - * The returned value will depend on the folder where the library files - * are located. For example, if your domain is 'example.com' and the library - * is placed at the root and the requested resource is 'http://example.com/x/y/z', - * then the base URL will be 'http://example.com/'. If the library is - * placed inside a folder in the server which has the name 'system', and - * the same resource is requested, then the base URL will be - * 'http://example.com/system'. - * - * @return string The base URL (such as 'http//www.example.com/') - * - */ - public static function getBaseURL(?array $serverInputs = null) : string { - $server = $serverInputs ?? $_SERVER; - $tempHost = $server['HTTP_HOST'] ?? '127.0.0.1'; - $host = trim(filter_var($tempHost),'/'); - - if (isset($server['HTTPS'])) { - $secureHost = filter_var($server['HTTPS']); - } else { - $secureHost = ''; - } - $protocol = 'http://'; - $useHttp = defined('USE_HTTP') && USE_HTTP === true; - - if (strlen($secureHost) != 0 && !$useHttp) { - $protocol = "https://"; - } - - if (isset($server['DOCUMENT_ROOT'])) { - $docRoot = filter_var($server['DOCUMENT_ROOT']); - } else { - //Fix for IIS since the $_SERVER['DOCUMENT_ROOT'] is not set - //in some cases - $docRoot = getcwd(); - } - - $docRootLen = strlen($docRoot); - - if ($docRootLen == 0) { - $docRoot = __DIR__; - $docRootLen = strlen($docRoot); - } - - if (!defined('ROOT_PATH')) { - define('ROOT_PATH', __DIR__); - } - $toAppend = str_replace('\\', '/', substr(ROOT_PATH, $docRootLen, strlen(ROOT_PATH) - $docRootLen)); - - if (defined('WF_PATH_TO_REMOVE')) { - $toAppend = str_replace(str_replace('\\', '/', WF_PATH_TO_REMOVE),'' ,$toAppend); - } - $xToAppend = str_replace('\\', '/', $toAppend); - - if (defined('WF_PATH_TO_APPEND')) { - $xToAppend = $xToAppend.'/'.trim(str_replace('\\', '/', WF_PATH_TO_APPEND), '/'); - } - - if (strlen($xToAppend) == 0) { - return $protocol.$host; - } else { - return $protocol.$host.'/'.trim($xToAppend, '/'); - } - } - /** - * Returns the value of URI parameter given its name. - * - * A URI parameter is a string which is defined while creating the route. - * it is name is included between '{}'. - * - * @param string $varName The name of the parameter. Note that this value - * must not include braces. - * - * @return string|null The method will return the value of the - * parameter if found. If the parameter is not set or the parameter - * does not exist, the method will return null. - * - */ - public function getParameterValue(string $varName) : ?string { - $param = $this->getParameter($varName); - - if ($param !== null) { - return $param->getValue(); - } - - return null; - } /** * Creates new instance of the class. * @@ -132,8 +39,8 @@ public function __construct(string $requestedUri = '') { $addedParams = []; $pathArr = $this->getPathArray(); - - foreach($pathArr as $part) { + + foreach ($pathArr as $part) { $conv = mb_convert_encoding(urldecode($part), 'UTF-8', 'ISO-8859-1'); if ($conv[0] == '{' && $conv[strlen($conv) - 1] == '}') { @@ -144,35 +51,10 @@ public function __construct(string $requestedUri = '') { $this->vars[] = new UriParameter($name); } } - } $this->verifyOrderOfParams(); } - private function verifyOrderOfParams() { - $currentOptional = false; - foreach($this->getParameters() as $param) { - if ($currentOptional == true && !$param->isOptional()) { - throw new \Exception('Requred paramater cannot appear after optional'); - } - $currentOptional = $param->isOptional() || $currentOptional; - } - } - - /** - * Adds new request method to the allowed methods. - * - * @param string $method The request method (e.g. 'GET', 'POST', 'PUT', etc...). - */ - public function addRequestMethod(string $method) : RequestUri { - $normalizedMethod = strtoupper(trim($method)); - - if (!in_array($normalizedMethod, $this->allowedMethods)) { - $this->allowedMethods[] = $normalizedMethod; - } - return $this; - } - /** * Adds a value to allowed URI parameter values. * @@ -180,17 +62,18 @@ public function addRequestMethod(string $method) : RequestUri { * @param string $value The value to add. */ public function addAllowedParameterValue(string $paramName, string $value) : RequestUri { - $normalized = trim($paramName); + foreach ($this->getParameters() as $param) { if ($param->getName() == $normalized) { $param->addAllowedValue($value); break; } } + return $this; } - public function addAllowedParameterValues(string $name, array $vals) : RequestUri { + public function addAllowedParameterValues(string $name, array $vals) : RequestUri { $normalizedName = trim($name); foreach ($this->getParameters() as $param) { @@ -199,9 +82,25 @@ public function addAllowedParameterValues(string $name, array $vals) : RequestUr break; } } + + return $this; + } + + /** + * Adds new request method to the allowed methods. + * + * @param string $method The request method (e.g. 'GET', 'POST', 'PUT', etc...). + */ + public function addRequestMethod(string $method) : RequestUri { + $normalizedMethod = strtoupper(trim($method)); + + if (!in_array($normalizedMethod, $this->allowedMethods)) { + $this->allowedMethods[] = $normalizedMethod; + } + return $this; } - + /** * Checks if two URIs are equal or not. * @@ -216,23 +115,104 @@ public function equals(Uri $otherUri) : bool { } $thisPath = $this->getPath(); $otherPath = $otherUri->getPath(); - + $thisMethods = $this->getRequestMethods(); $otherMethods = $otherUri->getRequestMethods(); - + if (count($thisMethods) != count($otherMethods)) { return false; } - + foreach ($thisMethods as $method) { if (!in_array($method, $otherMethods)) { return false; } } - + return true; } - + + /** + * Returns an array that contains allowed URI parameters values. + * + * @return array + */ + public function getAllowedParameterValues(string $varName) : array { + $param = $this->getParameter($varName); + + if ($param !== null) { + return $param->getAllowedValues(); + } + + return []; + } + /** + * Returns the base URL of the framework. + * + * The returned value will depend on the folder where the library files + * are located. For example, if your domain is 'example.com' and the library + * is placed at the root and the requested resource is 'http://example.com/x/y/z', + * then the base URL will be 'http://example.com/'. If the library is + * placed inside a folder in the server which has the name 'system', and + * the same resource is requested, then the base URL will be + * 'http://example.com/system'. + * + * @return string The base URL (such as 'http//www.example.com/') + * + */ + public static function getBaseURL(?array $serverInputs = null) : string { + $server = $serverInputs ?? $_SERVER; + $tempHost = $server['HTTP_HOST'] ?? '127.0.0.1'; + $host = trim(filter_var($tempHost),'/'); + + if (isset($server['HTTPS'])) { + $secureHost = filter_var($server['HTTPS']); + } else { + $secureHost = ''; + } + $protocol = 'http://'; + $useHttp = defined('USE_HTTP') && USE_HTTP === true; + + if (strlen($secureHost) != 0 && !$useHttp) { + $protocol = "https://"; + } + + if (isset($server['DOCUMENT_ROOT'])) { + $docRoot = filter_var($server['DOCUMENT_ROOT']); + } else { + //Fix for IIS since the $_SERVER['DOCUMENT_ROOT'] is not set + //in some cases + $docRoot = getcwd(); + } + + $docRootLen = strlen($docRoot); + + if ($docRootLen == 0) { + $docRoot = __DIR__; + $docRootLen = strlen($docRoot); + } + + if (!defined('ROOT_PATH')) { + define('ROOT_PATH', __DIR__); + } + $toAppend = str_replace('\\', '/', substr(ROOT_PATH, $docRootLen, strlen(ROOT_PATH) - $docRootLen)); + + if (defined('WF_PATH_TO_REMOVE')) { + $toAppend = str_replace(str_replace('\\', '/', WF_PATH_TO_REMOVE),'' ,$toAppend); + } + $xToAppend = str_replace('\\', '/', $toAppend); + + if (defined('WF_PATH_TO_APPEND')) { + $xToAppend = $xToAppend.'/'.trim(str_replace('\\', '/', WF_PATH_TO_APPEND), '/'); + } + + if (strlen($xToAppend) == 0) { + return $protocol.$host; + } else { + return $protocol.$host.'/'.trim($xToAppend, '/'); + } + } + /** * Returns the value of URI parameter given its name. * @@ -243,16 +223,15 @@ public function equals(Uri $otherUri) : bool { * return null. */ public function getParameter(string $paramName) : ?UriParameter { - foreach ($this->getParameters() as $param) { if ($param->getName() == $paramName) { return $param; } } - + return null; } - + /** * Returns an array that contains all URI parameters. * @@ -261,23 +240,36 @@ public function getParameter(string $paramName) : ?UriParameter { public function getParameters() : array { return $this->vars; } - + public function getParametersNames() : array { + return array_map(function ($paramObj) + { + return $paramObj->getName(); + }, $this->getParameters()); + } /** - * Returns an array that contains allowed URI parameters values. + * Returns the value of URI parameter given its name. + * + * A URI parameter is a string which is defined while creating the route. + * it is name is included between '{}'. + * + * @param string $varName The name of the parameter. Note that this value + * must not include braces. + * + * @return string|null The method will return the value of the + * parameter if found. If the parameter is not set or the parameter + * does not exist, the method will return null. * - * @return array */ - public function getAllowedParameterValues(string $varName) : array { - + public function getParameterValue(string $varName) : ?string { $param = $this->getParameter($varName); + if ($param !== null) { - - return $param->getAllowedValues(); + return $param->getValue(); } - return []; + return null; } - + /** * Returns an array that contains all allowed request methods. * @@ -286,17 +278,7 @@ public function getAllowedParameterValues(string $varName) : array { public function getRequestMethods() : array { return $this->allowedMethods; } - - /** - * Checks if the URI has any parameters or not. - * - * @return bool The method will return true if the URI has any parameters. - * false if not. - */ - public function hasParameters() : bool { - return count($this->getParameters()) > 0; - } - + /** * Checks if the URI has a specific parameter or not. * @@ -308,7 +290,17 @@ public function hasParameters() : bool { public function hasParameter(string $paramName) : bool { return $this->getParameter($paramName) !== null; } - + + /** + * Checks if the URI has any parameters or not. + * + * @return bool The method will return true if the URI has any parameters. + * false if not. + */ + public function hasParameters() : bool { + return count($this->getParameters()) > 0; + } + /** * Checks if all URI parameters have values or not. * @@ -317,16 +309,16 @@ public function hasParameter(string $paramName) : bool { */ public function isAllParametersSet() : bool { $uriVars = $this->getParameters(); - + foreach ($uriVars as $param) { if ($param->getValue() === null && !$param->isOptional()) { return false; } } - + return true; } - + /** * Checks if a request method is allowed or not. * @@ -341,16 +333,17 @@ public function isAllParametersSet() : bool { public function isRequestMethodAllowed(?string $method = null) : bool { if ($method === null) { $method = getenv('REQUEST_METHOD'); + if (!in_array($method, RequestMethod::getAll())) { return false; } } $normalizedMethod = strtoupper(trim($method)); $methods = $this->getRequestMethods(); - + return count($methods) == 0 || in_array($normalizedMethod, $methods); } - + /** * Sets the value of a URI parameter. * @@ -359,13 +352,14 @@ public function isRequestMethodAllowed(?string $method = null) : bool { */ public function setParameterValue(string $paramName, string $value) : bool { $param = $this->getParameter($paramName); - + if ($param !== null) { return $param->setValue($value); } + return false; } - + /** * Sets the array of allowed request methods. * @@ -373,10 +367,21 @@ public function setParameterValue(string $paramName, string $value) : bool { */ public function setRequestMethods(array $methods) : RequestUri { $this->allowedMethods = []; - + foreach ($methods as $method) { $this->addRequestMethod($method); } + return $this; } + private function verifyOrderOfParams() { + $currentOptional = false; + + foreach ($this->getParameters() as $param) { + if ($currentOptional == true && !$param->isOptional()) { + throw new \Exception('Requred paramater cannot appear after optional'); + } + $currentOptional = $param->isOptional() || $currentOptional; + } + } } diff --git a/WebFiori/Http/Response.php b/WebFiori/Http/Response.php index b2701a4..2271506 100644 --- a/WebFiori/Http/Response.php +++ b/WebFiori/Http/Response.php @@ -1,4 +1,5 @@ getRoles(); - self::$authorities = $user->getAuthorities(); - } else { - self::$roles = []; - self::$authorities = []; + public static function evaluateExpression(string $expression): bool { + $expression = trim($expression); + + if (empty($expression)) { + throw new \InvalidArgumentException('Security expression cannot be empty'); } + + // Handle complex boolean expressions with && and || + if (strpos($expression, '&&') !== false) { + return self::evaluateAndExpression($expression); + } + + if (strpos($expression, '||') !== false) { + return self::evaluateOrExpression($expression); + } + + // Handle single expressions + return self::evaluateSingleExpression($expression); } - + /** - * Get the current authenticated user. + * Get user authorities/permissions. * - * @return SecurityPrincipal|null User object or null if not authenticated + * @return array Array of authority names */ - public static function getCurrentUser(): ?SecurityPrincipal { - return self::$currentUser; + public static function getAuthorities(): array { + return self::$authorities; } - + /** - * Set user roles. + * Get the current authenticated user. * - * @param array $roles Array of role names - * Example: ['USER', 'ADMIN', 'MODERATOR'] + * @return SecurityPrincipal|null User object or null if not authenticated */ - public static function setRoles(array $roles): void { - self::$roles = $roles; + public static function getCurrentUser(): ?SecurityPrincipal { + return self::$currentUser; } - + /** * Get user roles. * @@ -73,26 +106,18 @@ public static function setRoles(array $roles): void { public static function getRoles(): array { return self::$roles; } - - /** - * Set user authorities/permissions. - * - * @param array $authorities Array of authority names - * Example: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'REPORT_VIEW'] - */ - public static function setAuthorities(array $authorities): void { - self::$authorities = $authorities; - } - + /** - * Get user authorities/permissions. + * Check if user has a specific authority/permission. * - * @return array Array of authority names + * @param string $authority Authority name to check + * Example: 'USER_CREATE', 'USER_DELETE', 'REPORT_VIEW' + * @return bool True if user has the authority */ - public static function getAuthorities(): array { - return self::$authorities; + public static function hasAuthority(string $authority): bool { + return in_array($authority, self::$authorities); } - + /** * Check if user has a specific role. * @@ -103,18 +128,7 @@ public static function getAuthorities(): array { public static function hasRole(string $role): bool { return in_array($role, self::$roles); } - - /** - * Check if user has a specific authority/permission. - * - * @param string $authority Authority name to check - * Example: 'USER_CREATE', 'USER_DELETE', 'REPORT_VIEW' - * @return bool True if user has the authority - */ - public static function hasAuthority(string $authority): bool { - return in_array($authority, self::$authorities); - } - + /** * Check if a user is currently authenticated. * @@ -123,89 +137,75 @@ public static function hasAuthority(string $authority): bool { public static function isAuthenticated(): bool { return self::$currentUser !== null && self::$currentUser->isActive(); } - + /** - * Clear all security context data. + * Set user authorities/permissions. + * + * @param array $authorities Array of authority names + * Example: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'REPORT_VIEW'] */ - public static function clear(): void { - self::$currentUser = null; - self::$roles = []; - self::$authorities = []; + public static function setAuthorities(array $authorities): void { + self::$authorities = $authorities; } - + /** - * Evaluate security expression. - * - * @param string $expression Security expression to evaluate - * - * Simple expressions: - * - "hasRole('ADMIN')" - Check single role - * - "hasAuthority('USER_CREATE')" - Check single authority - * - "isAuthenticated()" - Check if user is logged in - * - "permitAll()" - Always allow access - * - * Multiple values: - * - "hasAnyRole('ADMIN', 'MODERATOR')" - Check any of multiple roles - * - "hasAnyAuthority('USER_CREATE', 'USER_UPDATE')" - Check any of multiple authorities - * - * Complex boolean expressions: - * - "hasRole('ADMIN') && hasAuthority('USER_CREATE')" - Both conditions must be true - * - "hasRole('ADMIN') || hasRole('MODERATOR')" - Either condition can be true - * - "isAuthenticated() && hasAnyRole('USER', 'ADMIN')" - Authenticated with any role + * Set the current authenticated user. * - * @return bool True if expression evaluates to true - * @throws \InvalidArgumentException If expression is invalid + * @param SecurityPrincipal|null $user User object or null for unauthenticated */ - public static function evaluateExpression(string $expression): bool { - $expression = trim($expression); - - if (empty($expression)) { - throw new \InvalidArgumentException('Security expression cannot be empty'); - } - - // Handle complex boolean expressions with && and || - if (strpos($expression, '&&') !== false) { - return self::evaluateAndExpression($expression); - } - - if (strpos($expression, '||') !== false) { - return self::evaluateOrExpression($expression); + public static function setCurrentUser(?SecurityPrincipal $user): void { + self::$currentUser = $user; + + // Update legacy arrays for backward compatibility + if ($user) { + self::$roles = $user->getRoles(); + self::$authorities = $user->getAuthorities(); + } else { + self::$roles = []; + self::$authorities = []; } - - // Handle single expressions - return self::evaluateSingleExpression($expression); } - + + /** + * Set user roles. + * + * @param array $roles Array of role names + * Example: ['USER', 'ADMIN', 'MODERATOR'] + */ + public static function setRoles(array $roles): void { + self::$roles = $roles; + } + /** * Evaluate AND expression (all conditions must be true). */ private static function evaluateAndExpression(string $expression): bool { $parts = array_map('trim', explode('&&', $expression)); - + foreach ($parts as $part) { if (!self::evaluateSingleExpression($part)) { return false; } } - + return true; } - + /** * Evaluate OR expression (at least one condition must be true). */ private static function evaluateOrExpression(string $expression): bool { $parts = array_map('trim', explode('||', $expression)); - + foreach ($parts as $part) { if (self::evaluateSingleExpression($part)) { return true; } } - + return false; } - + /** * Evaluate single security expression. */ @@ -214,63 +214,68 @@ private static function evaluateSingleExpression(string $expression): bool { if (preg_match("/hasRole\('([^']+)'\)/", $expression, $matches)) { return self::hasRole($matches[1]); } - + // Handle hasAnyRole('ROLE1', 'ROLE2', ...) if (preg_match("/hasAnyRole\(([^)]+)\)/", $expression, $matches)) { $roles = self::parseArgumentList($matches[1]); + foreach ($roles as $role) { if (self::hasRole($role)) { return true; } } + return false; } - + // Handle hasAuthority('AUTHORITY_NAME') if (preg_match("/hasAuthority\('([^']+)'\)/", $expression, $matches)) { return self::hasAuthority($matches[1]); } - + // Handle hasAnyAuthority('AUTH1', 'AUTH2', ...) if (preg_match("/hasAnyAuthority\(([^)]+)\)/", $expression, $matches)) { $authorities = self::parseArgumentList($matches[1]); + foreach ($authorities as $authority) { if (self::hasAuthority($authority)) { return true; } } + return false; } - + // Handle isAuthenticated() if ($expression === 'isAuthenticated()') { return self::isAuthenticated(); } - + // Handle permitAll() if ($expression === 'permitAll()') { return true; } - + throw new \InvalidArgumentException("Invalid security expression: '$expression'"); } - + /** * Parse comma-separated argument list from function call. */ private static function parseArgumentList(string $args): array { $result = []; $parts = explode(',', $args); - + foreach ($parts as $part) { $part = trim($part); + if (preg_match("/^'([^']+)'$/", $part, $matches)) { $result[] = $matches[1]; } else { throw new \InvalidArgumentException("Invalid argument format: '$part'"); } } - + return $result; } } diff --git a/WebFiori/Http/SecurityPrincipal.php b/WebFiori/Http/SecurityPrincipal.php index b28b203..36b3731 100644 --- a/WebFiori/Http/SecurityPrincipal.php +++ b/WebFiori/Http/SecurityPrincipal.php @@ -1,4 +1,5 @@ uriBroken = self::splitURI($requestedUri); - + if ($this->uriBroken === false) { throw new InvalidArgumentException('Invalid URI: \''.$requestedUri.'\''); } } } - /** - * Returns the original requested URI. - * - * @param boolean $incQueryStr If set to true, the query string part - * will be included in the URL. Default is false. - * - * @param boolean $incFragment If set to true, the fragment part - * will be included in the URL. Default is false. - * - * @return string The original requested URI. - * - */ - public function getUri(bool $incQueryStr = false, bool $incFragment = false) : string { - $retVal = $this->getScheme().':'.$this->getAuthority().$this->getPath(); - - if ($incQueryStr === true && $incFragment === true) { - $queryStr = $this->getQueryString(); - - if (strlen($queryStr) != 0) { - $retVal .= '?'.$queryStr; - } - $fragment = $this->getFragment(); - - if (strlen($fragment) != 0) { - $retVal .= '#'.$fragment; - } - } else { - if ($incQueryStr === true && $incFragment === false) { - $queryStr = $this->getQueryString(); - - if (strlen($queryStr) != 0) { - $retVal .= '?'.$queryStr; - } - } else { - if ($incQueryStr === false && $incFragment === true) { - $fragment = $this->getFragment(); - - if (strlen($fragment) != 0) { - $retVal .= '#'.$fragment; - } - } - } - } - - return $retVal; + public function equals(Uri $uri) : bool { + return $this->getUri(true, true) == $uri->getUri(true, true); } /** * Returns the authority part of the URI. @@ -97,7 +55,7 @@ public function getUri(bool $incQueryStr = false, bool $incFragment = false) : s public function getAuthority() : string { return $this->uriBroken['authority']; } - + /** * Returns the base URL of the framework. * @@ -154,7 +112,7 @@ public static function getBaseURL() : string { return $protocol.$host.'/'.trim($xToAppend, '/'); } } - + /** * Returns an array that contains all URI parts. * @@ -177,7 +135,7 @@ public static function getBaseURL() : string { public function getComponents() : array { return $this->uriBroken; } - + /** * Returns the fragment part of the URI. * @@ -187,7 +145,7 @@ public function getComponents() : array { public function getFragment() : string { return $this->uriBroken['fragment']; } - + /** * Returns the host name from the authority part of the URI. * @@ -196,17 +154,6 @@ public function getFragment() : string { public function getHost() : string { return $this->uriBroken['host']; } - /** - * Returns an array which contains the names of URI directories. - * - * @return array An array which contains the names of URI directories. - * For example, if the path part of the URI is '/path1/path2', the - * array will contain the value 'path1' at index 0 and 'path2' at index 1. - * - */ - public function getPathArray() : array { - return $this->uriBroken['path']; - } /** * Returns the path part of the URI. * @@ -214,14 +161,25 @@ public function getPathArray() : array { */ public function getPath() : string { $path = $this->uriBroken['path']; - + if (count($path) == 0) { return '/'; } - + return '/'.implode('/', $path); } - + /** + * Returns an array which contains the names of URI directories. + * + * @return array An array which contains the names of URI directories. + * For example, if the path part of the URI is '/path1/path2', the + * array will contain the value 'path1' at index 0 and 'path2' at index 1. + * + */ + public function getPathArray() : array { + return $this->uriBroken['path']; + } + /** * Returns the port number of the authority part of the URI. * @@ -231,7 +189,7 @@ public function getPath() : string { public function getPort() : string { return $this->uriBroken['port']; } - + /** * Returns the query string that was appended to the URI. * @@ -241,7 +199,7 @@ public function getPort() : string { public function getQueryString() : string { return $this->uriBroken['query-string']; } - + /** * Returns an associative array which contains query string parameters. * @@ -252,7 +210,7 @@ public function getQueryString() : string { public function getQueryStringVars() : array { return $this->uriBroken['query-string-vars']; } - + /** * Returns the scheme part of the URI. * @@ -262,46 +220,51 @@ public function getQueryStringVars() : array { public function getScheme() : string { return $this->uriBroken['scheme']; } - /** - * Splits a string based on character mask. + * Returns the original requested URI. * - * @param string $split The string to split. + * @param boolean $incQueryStr If set to true, the query string part + * will be included in the URL. Default is false. * - * @param string $char The character that the split is based on. + * @param boolean $incFragment If set to true, the fragment part + * will be included in the URL. Default is false. * - * @param string $encoded The character when encoded in URI. + * @return string The original requested URI. * - * @return array */ - private static function _queryOrFragment(string $split, string $char, string $encoded) : array { - $split2 = explode($char, $split); - $spCount = count($split2); + public function getUri(bool $incQueryStr = false, bool $incFragment = false) : string { + $retVal = $this->getScheme().':'.$this->getAuthority().$this->getPath(); - if ($spCount > 2) { - $temp = []; + if ($incQueryStr === true && $incFragment === true) { + $queryStr = $this->getQueryString(); - for ($x = 0 ; $x < $spCount - 1 ; $x++) { - $temp[] = $split2[$x]; + if (strlen($queryStr) != 0) { + $retVal .= '?'.$queryStr; } - $lastStr = $split2[$spCount - 1]; + $fragment = $this->getFragment(); - if (strlen($lastStr) == 0) { - $split2 = [ - implode($encoded, $temp).$encoded - ]; + if (strlen($fragment) != 0) { + $retVal .= '#'.$fragment; + } + } else { + if ($incQueryStr === true && $incFragment === false) { + $queryStr = $this->getQueryString(); + + if (strlen($queryStr) != 0) { + $retVal .= '?'.$queryStr; + } } else { - $split2 = [ - implode($encoded, $temp), - $split2[$spCount - 1] - ]; + if ($incQueryStr === false && $incFragment === true) { + $fragment = $this->getFragment(); + + if (strlen($fragment) != 0) { + $retVal .= '#'.$fragment; + } + } } } - return $split2; - } - public function equals(Uri $uri) : bool { - return $this->getUri(true, true) == $uri->getUri(true, true); + return $retVal; } /** * Splits a URI into its basic components. @@ -387,4 +350,42 @@ public static function splitURI(string $uri) { return $retVal; } + + /** + * Splits a string based on character mask. + * + * @param string $split The string to split. + * + * @param string $char The character that the split is based on. + * + * @param string $encoded The character when encoded in URI. + * + * @return array + */ + private static function _queryOrFragment(string $split, string $char, string $encoded) : array { + $split2 = explode($char, $split); + $spCount = count($split2); + + if ($spCount > 2) { + $temp = []; + + for ($x = 0 ; $x < $spCount - 1 ; $x++) { + $temp[] = $split2[$x]; + } + $lastStr = $split2[$spCount - 1]; + + if (strlen($lastStr) == 0) { + $split2 = [ + implode($encoded, $temp).$encoded + ]; + } else { + $split2 = [ + implode($encoded, $temp), + $split2[$spCount - 1] + ]; + } + } + + return $split2; + } } diff --git a/WebFiori/Http/UriParameter.php b/WebFiori/Http/UriParameter.php index ca9f7c5..e688669 100644 --- a/WebFiori/Http/UriParameter.php +++ b/WebFiori/Http/UriParameter.php @@ -1,4 +1,5 @@ name = trim($trimmed, '?'); $this->allowedValues = []; } - public function addAllowedValues(array $vals) : UriParameter { - foreach ($vals as $val) { - $this->addAllowedValue($val); - } - return $this; - } public function addAllowedValue(string $val) : UriParameter { $this->allowedValues[] = trim($val); $currentVal = $this->getValue(); + if ($currentVal !== null && !in_array($currentVal, $this->allowedValues)) { $this->value = null; } + + return $this; + } + public function addAllowedValues(array $vals) : UriParameter { + foreach ($vals as $val) { + $this->addAllowedValue($val); + } + return $this; } public function getAllowedValues() : array { @@ -112,13 +116,17 @@ public function isOptional() : bool { public function setValue(string $val) : bool { $allowed = $this->getAllowedValues(); $trimmed = trim($val); + if (count($allowed) > 0 && !in_array($trimmed, $allowed)) { return false; } + if ($trimmed != '') { $this->value = $trimmed; + return true; } + return false; } } diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 604dd7c..1090897 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -1,4 +1,5 @@ sinceVersion = '1.0.0'; $this->serviceDesc = ''; $this->request = Request::createFromGlobals(); - + $this->configureFromAnnotations($name); } - - /** - * Configure service from annotations if present. - */ - private function configureFromAnnotations(string $fallbackName): void { - $reflection = new \ReflectionClass($this); - $attributes = $reflection->getAttributes(\WebFiori\Http\Annotations\RestController::class); - - if (!empty($attributes)) { - $restController = $attributes[0]->newInstance(); - $serviceName = $restController->name ?: $fallbackName; - $description = $restController->description; - } else { - $serviceName = $fallbackName; - $description = ''; - } - - if (!$this->setName($serviceName)) { - $this->setName('new-service'); - } - - if ($description) { - $this->setDescription($description); - } - - $this->configureMethodMappings(); - $this->configureAuthentication(); - } - - /** - * Process the web service request with auto-processing support. - * This method should be called instead of processRequest() for auto-processing. - */ - public function processWithAutoHandling(): void { - $targetMethod = $this->getTargetMethod(); - - if ($targetMethod && $this->hasResponseBodyAnnotation($targetMethod)) { - // Check method-level authorization first - if (!$this->checkMethodAuthorization()) { - $this->sendResponse('Access denied', 403, 'error'); - return; - } - - try { - // Inject parameters into method call - $params = $this->getMethodParameters($targetMethod); - $result = $this->$targetMethod(...$params); - $this->handleMethodResponse($result, $targetMethod); - } catch (HttpException $e) { - // Handle HTTP exceptions automatically - $this->handleException($e); - } catch (\Exception $e) { - // Handle other exceptions as 500 Internal Server Error - $this->sendResponse($e->getMessage(), 500, 'error'); - } - } else { - // Fall back to traditional processRequest() approach - $this->processRequest(); - } + public function &getRequestMethods() : array { + return $this->reqMethods; } - /** - * Check if a method has the ResponseBody annotation. + * Returns an array that contains an objects of type RequestParameter. + * + * @return array an array that contains an objects of type RequestParameter. * - * @param string $methodName The method name to check - * @return bool True if the method has ResponseBody annotation */ - public function hasResponseBodyAnnotation(string $methodName): bool { - try { - $reflection = new \ReflectionMethod($this, $methodName); - return !empty($reflection->getAttributes(ResponseBody::class)); - } catch (\ReflectionException $e) { - return false; - } + public final function &getParameters() : array { + return $this->parameters; } - /** - * Handle HTTP exceptions by converting them to appropriate responses. * - * @param HttpException $exception The HTTP exception to handle + * @return string + * */ - protected function handleException(HttpException $exception): void { - $this->sendResponse( - $exception->getMessage(), - $exception->getStatusCode(), - $exception->getResponseType() - ); + public function __toString() { + return $this->toJSON().''; } - /** - * Configure parameters dynamically for a specific method. + * Adds new request parameter to the service. + * + * The parameter will only be added if no parameter which has the same + * name as the given one is added before. + * + * @param RequestParameter|array $param The parameter that will be added. It + * can be an object of type 'RequestParameter' or an associative array of + * options. The array can have the following indices: + *
    + *
  • name: The name of the parameter. It must be provided.
  • + *
  • type: The datatype of the parameter. If not provided, 'string' is used.
  • + *
  • optional: A boolean. If set to true, it means the parameter is + * optional. If not provided, 'false' is used.
  • + *
  • min: Minimum value of the parameter. Applicable only for + * numeric types.
  • + *
  • max: Maximum value of the parameter. Applicable only for + * numeric types.
  • + *
  • allow-empty: A boolean. If the type of the parameter is string or string-like + * type and this is set to true, then empty strings will be allowed. If + * not provided, 'false' is used.
  • + *
  • custom-filter: A PHP function that can be used to filter the + * parameter even further
  • + *
  • default: An optional default value to use if the parameter is + * not provided and is optional.
  • + *
  • description: The description of the attribute.
  • + *
+ * + * @return bool If the given request parameter is added, the method will + * return true. If it was not added for any reason, the method will return + * false. * - * @param string $methodName The method name to configure parameters for */ - public function configureParametersForMethod(string $methodName): void { - try { - $reflection = new \ReflectionMethod($this, $methodName); - $this->configureParametersFromMethod($reflection); - } catch (\ReflectionException $e) { - // Method doesn't exist, ignore + public function addParameter($param) : bool { + if (gettype($param) == 'array') { + $param = RequestParameter::create($param); } - } - /** - * Configure parameters for all methods with RequestParam annotations. - */ - private function configureAllAnnotatedParameters(): void { - $reflection = new \ReflectionClass($this); - foreach ($reflection->getMethods() as $method) { - $paramAttributes = $method->getAttributes(\WebFiori\Http\Annotations\RequestParam::class); - if (!empty($paramAttributes)) { - $this->configureParametersFromMethod($method); + if ($param instanceof RequestParameter && !$this->hasParameter($param->getName())) { + // Additional validation for reserved parameter names + if (in_array(strtolower($param->getName()), RequestParameter::RESERVED_NAMES)) { + throw new \InvalidArgumentException("Cannot add parameter '".$param->getName()."' to service '".$this->getName()."': parameter name is reserved. Reserved names: ".implode(', ', RequestParameter::RESERVED_NAMES)); } + + $this->parameters[] = $param; + + return true; } + + return false; } - /** - * Configure parameters for methods with specific HTTP method mapping. + * Adds multiple parameters to the web service in one batch. + * + * @param array $params An associative or indexed array. If the array is indexed, + * each index should hold an object of type 'RequestParameter'. If it is associative, + * then the key will represent the name of the web service and the value of the + * key should be a sub-associative array that holds parameter options. * - * @param string $httpMethod HTTP method (GET, POST, PUT, DELETE, etc.) */ - private function configureParametersForHttpMethod(string $httpMethod): void { - $reflection = new \ReflectionClass($this); - $httpMethod = strtoupper($httpMethod); - - foreach ($reflection->getMethods() as $method) { - // Check if method has HTTP method mapping annotation - $mappingFound = false; - - // Check for specific HTTP method annotations - $annotations = [ - 'GET' => \WebFiori\Http\Annotations\GetMapping::class, - 'POST' => \WebFiori\Http\Annotations\PostMapping::class, - 'PUT' => \WebFiori\Http\Annotations\PutMapping::class, - 'DELETE' => \WebFiori\Http\Annotations\DeleteMapping::class, - 'PATCH' => \WebFiori\Http\Annotations\PatchMapping::class, - ]; - - if (isset($annotations[$httpMethod])) { - $mappingFound = !empty($method->getAttributes($annotations[$httpMethod])); - } - - if ($mappingFound) { - $this->configureParametersFromMethod($method); + public function addParameters(array $params) { + foreach ($params as $paramIndex => $param) { + if ($param instanceof RequestParameter) { + $this->addParameter($param); + } else if (gettype($param) == 'array') { + $param['name'] = $paramIndex; + $this->addParameter(RequestParameter::create($param)); } } } - /** - * Configure authentication from annotations. + * Adds new request method. + * + * The value that will be passed to this method can be any string + * that represents HTTP request method (e.g. 'get', 'post', 'options' ...). It + * can be in upper case or lower case. + * + * @param string $method The request method. + * + * @return bool true in case the request method is added. If the given + * request method is already added or the method is unknown, the method + * will return false. + * */ - private function configureAuthentication(): void { - $reflection = new \ReflectionClass($this); - - // Check class-level authentication - $classAuth = $this->getAuthenticationFromClass($reflection); - - // If class has AllowAnonymous, disable auth requirement - if ($classAuth['allowAnonymous']) { - $this->setIsAuthRequired(false); - } else if ($classAuth['requiresAuth'] || $classAuth['preAuthorize']) { - $this->setIsAuthRequired(true); + public final function addRequestMethod(string $method) : bool { + $uMethod = strtoupper(trim($method)); + + if (in_array($uMethod, RequestMethod::getAll()) && !in_array($uMethod, $this->reqMethods)) { + $this->reqMethods[] = $uMethod; + + return true; } + + return false; } - /** - * Get authentication configuration from class annotations. + * Adds response description. + * + * It is used to describe the API for front-end developers and help them + * identify possible responses if they call the API using the specified service. + * + * @param string $description A paragraph that describes one of + * the possible responses due to calling the service. */ - private function getAuthenticationFromClass(\ReflectionClass $reflection): array { - return [ - 'allowAnonymous' => !empty($reflection->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class)), - 'requiresAuth' => !empty($reflection->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class)), - 'preAuthorize' => $reflection->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class) - ]; + public function addResponse(string $method, string $statusCode, OpenAPI\ResponseObj|string $response): WebService { + $method = strtoupper($method); + + if (!isset($this->responsesByMethod[$method])) { + $this->responsesByMethod[$method] = new OpenAPI\ResponsesObj(); + } + + $this->responsesByMethod[$method]->addResponse($statusCode, $response); + + return $this; + } + + public final function addResponseDescription(string $description) { + $trimmed = trim($description); + + if (strlen($trimmed) != 0) { + $this->responses[] = $trimmed; + } } - + /** * Check method-level authorization before processing. */ public function checkMethodAuthorization(): bool { $reflection = new \ReflectionClass($this); $method = $this->getCurrentProcessingMethod() ?: $this->getTargetMethod(); - + if (!$method) { return $this->isAuthorized(); } - + $reflectionMethod = $reflection->getMethod($method); - + // Check for conflicting annotations - $hasAllowAnonymous = !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class)); - $hasRequiresAuth = !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class)); - + $hasAllowAnonymous = !empty($reflectionMethod->getAttributes(Annotations\AllowAnonymous::class)); + $hasRequiresAuth = !empty($reflectionMethod->getAttributes(Annotations\RequiresAuth::class)); + if ($hasAllowAnonymous && $hasRequiresAuth) { throw new \InvalidArgumentException( "Method '$method' has conflicting annotations: #[AllowAnonymous] and #[RequiresAuth] cannot be used together" ); } - + // Check AllowAnonymous first if ($hasAllowAnonymous) { return true; } - + // Check RequiresAuth if ($hasRequiresAuth) { // First call isAuthorized() if (!$this->isAuthorized()) { return false; } - + // Then check for PreAuthorize - $preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class); + $preAuthAttributes = $reflectionMethod->getAttributes(Annotations\PreAuthorize::class); + if (!empty($preAuthAttributes)) { $preAuth = $preAuthAttributes[0]->newInstance(); + return SecurityContext::evaluateExpression($preAuth->expression); } - + // If no PreAuthorize, continue based on isAuthorized (already passed) return true; } - + // Check PreAuthorize without RequiresAuth - $preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class); + $preAuthAttributes = $reflectionMethod->getAttributes(Annotations\PreAuthorize::class); + if (!empty($preAuthAttributes)) { $preAuth = $preAuthAttributes[0]->newInstance(); + return SecurityContext::evaluateExpression($preAuth->expression); } - + return $this->isAuthorized(); } - + /** - * Check if the method has any authorization annotations. + * Configure parameters dynamically for a specific method. + * + * @param string $methodName The method name to configure parameters for */ - public function hasMethodAuthorizationAnnotations(): bool { - $reflection = new \ReflectionClass($this); - $method = $this->getCurrentProcessingMethod() ?: $this->getTargetMethod(); - - if (!$method) { - return false; - } - - $reflectionMethod = $reflection->getMethod($method); - - return !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class)) || - !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class)) || - !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class)); + public function configureParametersForMethod(string $methodName): void { + try { + $reflection = new \ReflectionMethod($this, $methodName); + $this->configureParametersFromMethod($reflection); + } catch (\ReflectionException $e) { + // Method doesn't exist, ignore + } } - + /** - * Get the current processing method name (to be overridden by subclasses if needed). + * Gets all responses mapped by HTTP method. + * + * @return array Map of methods to responses. */ - protected function getCurrentProcessingMethod(): ?string { - return null; // Default implementation + public function getAllResponses(): array { + return $this->responsesByMethod; + } + /** + * Returns an object that contains the value of the header 'authorization'. + * + * @return AuthHeader|null The object will have two primary attributes, the first is + * the 'scheme' and the second one is 'credentials'. The 'scheme' + * will contain the name of the scheme which is used to authenticate + * ('basic', 'bearer', 'digest', etc...). The 'credentials' will contain + * the credentials which can be used to authenticate the client. + * + */ + public function getAuthHeader() { + if ($this->request !== null) { + return $this->request->getAuthHeader(); + } + + return null; + } + /** + * Returns the description of the service. + * + * @return string The description of the service. Default is empty string. + * + */ + public final function getDescription() : string { + return $this->serviceDesc; + } + /** + * Returns an associative array or an object of type Json of filtered request inputs. + * + * The indices of the array will represent request parameters and the + * values of each index will represent the value which was set in + * request body. The values will be filtered and might not be exactly the same as + * the values passed in request body. Note that if a parameter is optional and not + * provided in request body, its value will be set to 'null'. Note that + * if request content type is 'application/json', only basic filtering will + * be applied. Also, parameters in this case don't apply. + * + * @return array|Json|null An array of filtered request inputs. This also can + * be an object of type 'Json' if request content type was 'application/json'. + * If no manager was associated with the service, the method will return null. + * + */ + public function getInputs() { + $manager = $this->getManager(); + + if ($manager !== null) { + return $manager->getInputs(); + } + + return null; + } + /** + * Returns the manager which is used to manage the web service. + * + * @return WebServicesManager|null If set, it is returned as an object. + * Other than that, null is returned. + */ + public function getManager() { + return $this->owner; + } + /** + * Returns the name of the service. + * + * @return string The name of the service. + * + */ + public final function getName() : string { + return $this->name; + } + /** + * Map service parameter to specific instance of a class. + * + * This method assumes that every parameter in the request has a method + * that can be called to set attribute value. For example, if a parameter + * has the name 'user-last-name', the mapping method should have the name + * 'setUserLastName' for mapping to work correctly. + * + * @param string $clazz The class that service parameters will be mapped + * to. + * + * @param array $settersMap An optional array that can have custom + * setters map. The indices of the array should be parameters names + * and the values are the names of setter methods in the class. + * + * @return object The Method will return an instance of the class with + * all its attributes set to request parameter's values. + */ + public function getObject(string $clazz, array $settersMap = []) { + $mapper = new ObjectMapper($clazz, $this); + + foreach ($settersMap as $param => $method) { + $mapper->addSetterMap($param, $method); + } + + return $mapper->map($this->getInputs()); + } + /** + * Returns one of the parameters of the service given its name. + * + * @param string $paramName The name of the parameter. + * + * @return RequestParameter|null Returns an objects of type RequestParameter if + * a parameter with the given name was found. null if nothing is found. + * + */ + public final function getParameterByName(string $paramName, ?string $httpMethod = null) { + // Configure parameters if HTTP method specified + if ($httpMethod !== null) { + $this->configureParametersForHttpMethod($httpMethod); + } else { + // Configure parameters for all methods with annotations + $this->configureAllAnnotatedParameters(); + } + + $trimmed = trim($paramName); + + if (strlen($trimmed) != 0) { + foreach ($this->parameters as $param) { + if ($param->getName() == $trimmed) { + return $param; + } + } + } + + return null; + } + /** + * Returns the value of request parameter given its name. + * + * @param string $paramName The name of request parameter as specified when + * it was added to the service. + * + * @return mixed|null If the parameter is found and its value is set, the + * method will return its value. Other than that, the method will return null. + * For optional parameters, if a default value is set for it, the method will + * return that value. + * + */ + public function getParamVal(string $paramName) { + $inputs = $this->getInputs(); + $trimmed = trim($paramName); + + if ($inputs !== null) { + if ($inputs instanceof Json) { + return $inputs->get($trimmed); + } else { + return $inputs[$trimmed] ?? null; + } + } + + return null; + } + /** + * Returns an indexed array that contains information about possible responses. + * + * It is used to describe the API for front-end developers and help them + * identify possible responses if they call the API using the specified service. + * + * @return array An array that contains information about possible responses. + * + */ + public final function getResponsesDescriptions() : array { + return $this->responses; + } + public function getResponsesForMethod(string $method): ?OpenAPI\ResponsesObj { + $method = strtoupper($method); + + return $this->responsesByMethod[$method] ?? null; + } + /** + * Returns version number or name at which the service was added to the API. + * + * Version number is set based on the version number which was set in the + * class WebAPI. + * + * @return string The version number at which the service was added to the API. + * Default is '1.0.0'. + * + */ + public final function getSince() : string { + return $this->sinceVersion; } - + /** * Get the target method name based on current HTTP request. * @@ -411,13 +563,15 @@ public function getTargetMethod(): ?string { $httpMethod = $this->getManager() ? $this->getManager()->getRequest()->getMethod() : ($_SERVER['REQUEST_METHOD'] ?? 'GET'); - + // First try to get method from getCurrentProcessingMethod (if implemented) $currentMethod = $this->getCurrentProcessingMethod(); + if ($currentMethod) { $reflection = new \ReflectionClass($this); try { $method = $reflection->getMethod($currentMethod); + if ($this->methodHandlesHttpMethod($method, $httpMethod)) { return $currentMethod; } @@ -425,647 +579,77 @@ public function getTargetMethod(): ?string { // Method doesn't exist, continue with discovery } } - + // Fall back to finding first method that matches HTTP method $reflection = new \ReflectionClass($this); + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { if ($this->methodHandlesHttpMethod($method, $httpMethod)) { return $method->getName(); } } - + return null; } - + /** - * Check if a method handles the specified HTTP method. + * Check if the method has any authorization annotations. + */ + public function hasMethodAuthorizationAnnotations(): bool { + $reflection = new \ReflectionClass($this); + $method = $this->getCurrentProcessingMethod() ?: $this->getTargetMethod(); + + if (!$method) { + return false; + } + + $reflectionMethod = $reflection->getMethod($method); + + return !empty($reflectionMethod->getAttributes(Annotations\AllowAnonymous::class)) || + !empty($reflectionMethod->getAttributes(Annotations\RequiresAuth::class)) || + !empty($reflectionMethod->getAttributes(Annotations\PreAuthorize::class)); + } + /** + * Checks if the service has a specific request parameter given its name. + * + * Note that the name of the parameter is case-sensitive. This means that + * 'get-profile' is not the same as 'Get-Profile'. + * + * @param string $name The name of the parameter. + * + * @return bool If a request parameter which has the given name is added + * to the service, the method will return true. Otherwise, the method will return + * false. * - * @param \ReflectionMethod $method The method to check - * @param string $httpMethod The HTTP method (GET, POST, etc.) - * @return bool True if the method handles this HTTP method */ - private function methodHandlesHttpMethod(\ReflectionMethod $method, string $httpMethod): bool { - $methodMappings = [ - GetMapping::class => RequestMethod::GET, - PostMapping::class => RequestMethod::POST, - PutMapping::class => RequestMethod::PUT, - DeleteMapping::class => RequestMethod::DELETE - ]; - - foreach ($methodMappings as $annotationClass => $mappedMethod) { - if ($httpMethod === $mappedMethod && !empty($method->getAttributes($annotationClass))) { - return true; + public function hasParameter(string $name) : bool { + $trimmed = trim($name); + + if (strlen($name) != 0) { + foreach ($this->getParameters() as $param) { + if ($param->getName() == $trimmed) { + return true; + } } } - + return false; } - + /** - * Get method parameters by extracting values from request. - * - * @param string $methodName The method name - * @return array Array of parameter values in correct order - */ - private function getMethodParameters(string $methodName): array { - $reflection = new \ReflectionMethod($this, $methodName); - $params = []; - - // Check for MapEntity attribute - $mapEntityAttrs = $reflection->getAttributes(\WebFiori\Http\Annotations\MapEntity::class); - - if (!empty($mapEntityAttrs)) { - $mapEntity = $mapEntityAttrs[0]->newInstance(); - $mappedObject = $this->getObject($mapEntity->entityClass, $mapEntity->setters); - $params[] = $mappedObject; - } else { - // Original parameter handling - foreach ($reflection->getParameters() as $param) { - $paramName = $param->getName(); - $value = $this->getParamVal($paramName); - - // Handle optional parameters with defaults - if ($value === null && $param->isDefaultValueAvailable()) { - $value = $param->getDefaultValue(); - } - - $params[] = $value; - } - } - - return $params; - } - - /** - * Handle method response by auto-converting return values to HTTP responses. - * - * @param mixed $result The return value from the method - * @param string $methodName The name of the method that was called - * @return void - */ - protected function handleMethodResponse(mixed $result, string $methodName): void { - $reflection = new \ReflectionMethod($this, $methodName); - $responseBodyAttrs = $reflection->getAttributes(ResponseBody::class); - - if (empty($responseBodyAttrs)) { - return; // No auto-processing, method should handle response manually - } - - $responseBody = $responseBodyAttrs[0]->newInstance(); - $contentType = $responseBody->contentType; - // Handle custom content types - if ($contentType !== 'application/json') { - // For non-JSON content types, send raw result - if (is_array($result)) { - $content = new Json(); - $content->addArray('data', $result); - $contentType = 'application/json'; - } else if (is_object($result)) { - $content = new Json(); - $content->addObject('data', $result); - $contentType = 'application/json'; - } else { - $content = (string)$result; - } - - $this->send($contentType, $content, $responseBody->status); - return; - } - - // Auto-convert return value to JSON response - if ($result === null) { - // Null return = empty response with configured status - $this->sendResponse('', $responseBody->status, $responseBody->type); - } else if (is_array($result) || is_object($result)) { - // Array/object = JSON response - if ($result instanceof Json) { - $json = $result; - } else if ($result instanceof JsonI) { - $json = $result->toJSON(); - } else { - $json = new Json(); - $json->add('data', $result); - } - $this->send($responseBody->contentType, $json, $responseBody->status); - } else { - // String/scalar = plain response - $this->send($responseBody->contentType, $result, $responseBody->status); - } - } - - /** - * Configure allowed HTTP methods from method annotations. - */ - private function configureMethodMappings(): void { - $reflection = new \ReflectionClass($this); - $httpMethodToMethods = []; - - foreach ($reflection->getMethods() as $method) { - $methodMappings = [ - GetMapping::class => RequestMethod::GET, - PostMapping::class => RequestMethod::POST, - PutMapping::class => RequestMethod::PUT, - DeleteMapping::class => RequestMethod::DELETE - ]; - - foreach ($methodMappings as $annotationClass => $httpMethod) { - $attributes = $method->getAttributes($annotationClass); - if (!empty($attributes)) { - if (!isset($httpMethodToMethods[$httpMethod])) { - $httpMethodToMethods[$httpMethod] = []; - } - $httpMethodToMethods[$httpMethod][] = $method->getName(); - } - } - } - - // Check for duplicates only if getCurrentProcessingMethod is not overridden - $hasCustomRouting = $reflection->getMethod('getCurrentProcessingMethod')->getDeclaringClass()->getName() !== self::class; - - if (!$hasCustomRouting) { - foreach ($httpMethodToMethods as $httpMethod => $methods) { - if (count($methods) > 1) { - throw new Exceptions\DuplicateMappingException( - "HTTP method $httpMethod is mapped to multiple methods: " . implode(', ', $methods) - ); - } - } - } - - if (!empty($httpMethodToMethods)) { - $this->setRequestMethods(array_keys($httpMethodToMethods)); - } - } - - /** - * Configure parameters from method RequestParam annotations. - */ - private function configureParametersFromMethod(\ReflectionMethod $method): void { - $paramAttributes = $method->getAttributes(\WebFiori\Http\Annotations\RequestParam::class); - - foreach ($paramAttributes as $attribute) { - $param = $attribute->newInstance(); - - $options = [ - \WebFiori\Http\ParamOption::TYPE => $this->mapParamType($param->type), - \WebFiori\Http\ParamOption::OPTIONAL => $param->optional, - \WebFiori\Http\ParamOption::DEFAULT => $param->default, - \WebFiori\Http\ParamOption::DESCRIPTION => $param->description - ]; - - if ($param->filter !== null) { - $options[\WebFiori\Http\ParamOption::FILTER] = $param->filter; - } - - $this->addParameters([ - $param->name => $options - ]); - } - } - - /** - * Map string type to ParamType constant. - */ - private function mapParamType(string $type): string { - return match(strtolower($type)) { - 'int', 'integer' => \WebFiori\Http\ParamType::INT, - 'float', 'double' => \WebFiori\Http\ParamType::DOUBLE, - 'bool', 'boolean' => \WebFiori\Http\ParamType::BOOL, - 'email' => \WebFiori\Http\ParamType::EMAIL, - 'url' => \WebFiori\Http\ParamType::URL, - 'array' => \WebFiori\Http\ParamType::ARR, - 'json' => \WebFiori\Http\ParamType::JSON_OBJ, - default => \WebFiori\Http\ParamType::STRING - }; - } /** - * Returns an array that contains all possible requests methods at which the - * service can be called with. - * - * The array will contain strings like 'GET' or 'POST'. If no request methods - * where added, the array will be empty. - * - * @return array An array that contains all possible requests methods at which the - * service can be called using. - * - */ - public function &getRequestMethods() : array { - return $this->reqMethods; - } - /** - * Returns an array that contains an objects of type RequestParameter. - * - * @return array an array that contains an objects of type RequestParameter. - * - */ - public final function &getParameters() : array { - return $this->parameters; - } - /** - * - * @return string - * - */ - public function __toString() { - return $this->toJSON().''; - } - /** - * Adds new request parameter to the service. - * - * The parameter will only be added if no parameter which has the same - * name as the given one is added before. - * - * @param RequestParameter|array $param The parameter that will be added. It - * can be an object of type 'RequestParameter' or an associative array of - * options. The array can have the following indices: - *
    - *
  • name: The name of the parameter. It must be provided.
  • - *
  • type: The datatype of the parameter. If not provided, 'string' is used.
  • - *
  • optional: A boolean. If set to true, it means the parameter is - * optional. If not provided, 'false' is used.
  • - *
  • min: Minimum value of the parameter. Applicable only for - * numeric types.
  • - *
  • max: Maximum value of the parameter. Applicable only for - * numeric types.
  • - *
  • allow-empty: A boolean. If the type of the parameter is string or string-like - * type and this is set to true, then empty strings will be allowed. If - * not provided, 'false' is used.
  • - *
  • custom-filter: A PHP function that can be used to filter the - * parameter even further
  • - *
  • default: An optional default value to use if the parameter is - * not provided and is optional.
  • - *
  • description: The description of the attribute.
  • - *
- * - * @return bool If the given request parameter is added, the method will - * return true. If it was not added for any reason, the method will return - * false. - * - */ - public function addParameter($param) : bool { - if (gettype($param) == 'array') { - $param = RequestParameter::create($param); - } - - if ($param instanceof RequestParameter && !$this->hasParameter($param->getName())) { - // Additional validation for reserved parameter names - if (in_array(strtolower($param->getName()), \WebFiori\Http\RequestParameter::RESERVED_NAMES)) { - throw new \InvalidArgumentException("Cannot add parameter '" . $param->getName() . "' to service '" . $this->getName() . "': parameter name is reserved. Reserved names: " . implode(', ', \WebFiori\Http\RequestParameter::RESERVED_NAMES)); - } - - $this->parameters[] = $param; - - return true; - } - - return false; - } - /** - * Adds multiple parameters to the web service in one batch. - * - * @param array $params An associative or indexed array. If the array is indexed, - * each index should hold an object of type 'RequestParameter'. If it is associative, - * then the key will represent the name of the web service and the value of the - * key should be a sub-associative array that holds parameter options. - * - */ - public function addParameters(array $params) { - foreach ($params as $paramIndex => $param) { - if ($param instanceof RequestParameter) { - $this->addParameter($param); - } else if (gettype($param) == 'array') { - $param['name'] = $paramIndex; - $this->addParameter(RequestParameter::create($param)); - } - } - } - /** - * Adds new request method. - * - * The value that will be passed to this method can be any string - * that represents HTTP request method (e.g. 'get', 'post', 'options' ...). It - * can be in upper case or lower case. - * - * @param string $method The request method. - * - * @return bool true in case the request method is added. If the given - * request method is already added or the method is unknown, the method - * will return false. - * - */ - public final function addRequestMethod(string $method) : bool { - $uMethod = strtoupper(trim($method)); - - if (in_array($uMethod, RequestMethod::getAll()) && !in_array($uMethod, $this->reqMethods)) { - $this->reqMethods[] = $uMethod; - - return true; - } - - return false; - } - /** - * Adds response description. - * - * It is used to describe the API for front-end developers and help them - * identify possible responses if they call the API using the specified service. - * - * @param string $description A paragraph that describes one of - * the possible responses due to calling the service. - */ - public function addResponse(string $method, string $statusCode, OpenAPI\ResponseObj|string $response): WebService { - $method = strtoupper($method); - - if (!isset($this->responsesByMethod[$method])) { - $this->responsesByMethod[$method] = new OpenAPI\ResponsesObj(); - } - - $this->responsesByMethod[$method]->addResponse($statusCode, $response); - return $this; - } - - public final function addResponseDescription(string $description) { - $trimmed = trim($description); - - if (strlen($trimmed) != 0) { - $this->responses[] = $trimmed; - } - } - public function getResponsesForMethod(string $method): ?OpenAPI\ResponsesObj { - $method = strtoupper($method); - return $this->responsesByMethod[$method] ?? null; - } - /** - * Sets all responses for a specific HTTP method. - * - * @param string $method HTTP method. - * @param OpenAPI\ResponsesObj $responses Responses object. - * - * @return WebService Returns self for method chaining. - */ - public function setResponsesForMethod(string $method, OpenAPI\ResponsesObj $responses): WebService { - $this->responsesByMethod[strtoupper($method)] = $responses; - return $this; - } - - /** - * Gets all responses mapped by HTTP method. - * - * @return array Map of methods to responses. - */ - public function getAllResponses(): array { - return $this->responsesByMethod; - } - - /** - * Converts this web service to an OpenAPI PathItemObj. - * - * Each HTTP method supported by this service becomes an operation in the path item. - * - * @return OpenAPI\PathItemObj The PathItemObj representation of this service. - */ - public function toPathItemObj(): OpenAPI\PathItemObj { - $pathItem = new OpenAPI\PathItemObj(); - - foreach ($this->getRequestMethods() as $method) { - $responses = $this->getResponsesForMethod($method); - - if ($responses === null) { - $responses = new OpenAPI\ResponsesObj(); - $responses->addResponse('200', 'Successful operation'); - } - - $operation = new OpenAPI\OperationObj($responses); - - switch ($method) { - case RequestMethod::GET: - $pathItem->setGet($operation); - break; - case RequestMethod::POST: - $pathItem->setPost($operation); - break; - case RequestMethod::PUT: - $pathItem->setPut($operation); - break; - case RequestMethod::DELETE: - $pathItem->setDelete($operation); - break; - case RequestMethod::PATCH: - $pathItem->setPatch($operation); - break; - } - - - }return $pathItem;} - /** - * Returns an object that contains the value of the header 'authorization'. - * - * @return AuthHeader|null The object will have two primary attributes, the first is - * the 'scheme' and the second one is 'credentials'. The 'scheme' - * will contain the name of the scheme which is used to authenticate - * ('basic', 'bearer', 'digest', etc...). The 'credentials' will contain - * the credentials which can be used to authenticate the client. - * - */ - public function getAuthHeader() { - if ($this->request !== null) { - return $this->request->getAuthHeader(); - } - return null; - } - - /** - * Sets the request instance for the service. - * - * @param mixed $request The request instance (Request, etc.) - */ - public function setRequest($request) { - $this->request = $request; - } - /** - * Returns the description of the service. - * - * @return string The description of the service. Default is empty string. - * - */ - public final function getDescription() : string { - return $this->serviceDesc; - } - /** - * Returns an associative array or an object of type Json of filtered request inputs. - * - * The indices of the array will represent request parameters and the - * values of each index will represent the value which was set in - * request body. The values will be filtered and might not be exactly the same as - * the values passed in request body. Note that if a parameter is optional and not - * provided in request body, its value will be set to 'null'. Note that - * if request content type is 'application/json', only basic filtering will - * be applied. Also, parameters in this case don't apply. - * - * @return array|Json|null An array of filtered request inputs. This also can - * be an object of type 'Json' if request content type was 'application/json'. - * If no manager was associated with the service, the method will return null. - * - */ - public function getInputs() { - $manager = $this->getManager(); - - if ($manager !== null) { - return $manager->getInputs(); - } - - return null; - } - /** - * Returns the manager which is used to manage the web service. - * - * @return WebServicesManager|null If set, it is returned as an object. - * Other than that, null is returned. - */ - public function getManager() { - return $this->owner; - } - /** - * Returns the name of the service. - * - * @return string The name of the service. - * - */ - public final function getName() : string { - return $this->name; - } - /** - * Map service parameter to specific instance of a class. - * - * This method assumes that every parameter in the request has a method - * that can be called to set attribute value. For example, if a parameter - * has the name 'user-last-name', the mapping method should have the name - * 'setUserLastName' for mapping to work correctly. - * - * @param string $clazz The class that service parameters will be mapped - * to. - * - * @param array $settersMap An optional array that can have custom - * setters map. The indices of the array should be parameters names - * and the values are the names of setter methods in the class. - * - * @return object The Method will return an instance of the class with - * all its attributes set to request parameter's values. - */ - public function getObject(string $clazz, array $settersMap = []) { - $mapper = new ObjectMapper($clazz, $this); - - foreach ($settersMap as $param => $method) { - $mapper->addSetterMap($param, $method); - } - - return $mapper->map($this->getInputs()); - } - /** - * Returns one of the parameters of the service given its name. - * - * @param string $paramName The name of the parameter. - * - * @return RequestParameter|null Returns an objects of type RequestParameter if - * a parameter with the given name was found. null if nothing is found. - * - */ - public final function getParameterByName(string $paramName, ?string $httpMethod = null) { - // Configure parameters if HTTP method specified - if ($httpMethod !== null) { - $this->configureParametersForHttpMethod($httpMethod); - } else { - // Configure parameters for all methods with annotations - $this->configureAllAnnotatedParameters(); - } - - $trimmed = trim($paramName); - - if (strlen($trimmed) != 0) { - foreach ($this->parameters as $param) { - if ($param->getName() == $trimmed) { - return $param; - } - } - } - - return null; - } - /** - * Returns the value of request parameter given its name. - * - * @param string $paramName The name of request parameter as specified when - * it was added to the service. - * - * @return mixed|null If the parameter is found and its value is set, the - * method will return its value. Other than that, the method will return null. - * For optional parameters, if a default value is set for it, the method will - * return that value. - * - */ - public function getParamVal(string $paramName) { - $inputs = $this->getInputs(); - $trimmed = trim($paramName); - - if ($inputs !== null) { - if ($inputs instanceof Json) { - return $inputs->get($trimmed); - } else { - return $inputs[$trimmed] ?? null; - } - } - - return null; - } - /** - * Returns an indexed array that contains information about possible responses. - * - * It is used to describe the API for front-end developers and help them - * identify possible responses if they call the API using the specified service. - * - * @return array An array that contains information about possible responses. - * - */ - public final function getResponsesDescriptions() : array { - return $this->responses; - } - /** - * Returns version number or name at which the service was added to the API. - * - * Version number is set based on the version number which was set in the - * class WebAPI. - * - * @return string The version number at which the service was added to the API. - * Default is '1.0.0'. - * - */ - public final function getSince() : string { - return $this->sinceVersion; - } - /** - * Checks if the service has a specific request parameter given its name. - * - * Note that the name of the parameter is case-sensitive. This means that - * 'get-profile' is not the same as 'Get-Profile'. - * - * @param string $name The name of the parameter. - * - * @return bool If a request parameter which has the given name is added - * to the service, the method will return true. Otherwise, the method will return - * false. + * Check if a method has the ResponseBody annotation. * + * @param string $methodName The method name to check + * @return bool True if the method has ResponseBody annotation */ - public function hasParameter(string $name) : bool { - $trimmed = trim($name); + public function hasResponseBodyAnnotation(string $methodName): bool { + try { + $reflection = new \ReflectionMethod($this, $methodName); - if (strlen($name) != 0) { - foreach ($this->getParameters() as $param) { - if ($param->getName() == $trimmed) { - return true; - } - } + return !empty($reflection->getAttributes(ResponseBody::class)); + } catch (\ReflectionException $e) { + return false; } - - return false; } /** * Checks if the client is authorized to use the service or not. @@ -1080,7 +664,9 @@ public function hasParameter(string $name) : bool { * @return bool True if the user is allowed to perform the action. False otherwise. * */ - public function isAuthorized() : bool {return false;} + public function isAuthorized() : bool { + return false; + } /** * Returns the value of the property 'requireAuth'. * @@ -1123,7 +709,41 @@ public static function isValidName(string $name): bool { /** * Process client's request. */ - public function processRequest() {} + public function processRequest() { + } + + /** + * Process the web service request with auto-processing support. + * This method should be called instead of processRequest() for auto-processing. + */ + public function processWithAutoHandling(): void { + $targetMethod = $this->getTargetMethod(); + + if ($targetMethod && $this->hasResponseBodyAnnotation($targetMethod)) { + // Check method-level authorization first + if (!$this->checkMethodAuthorization()) { + $this->sendResponse('Access denied', 403, 'error'); + + return; + } + + try { + // Inject parameters into method call + $params = $this->getMethodParameters($targetMethod); + $result = $this->$targetMethod(...$params); + $this->handleMethodResponse($result, $targetMethod); + } catch (HttpException $e) { + // Handle HTTP exceptions automatically + $this->handleException($e); + } catch (\Exception $e) { + // Handle other exceptions as 500 Internal Server Error + $this->sendResponse($e->getMessage(), 500, 'error'); + } + } else { + // Fall back to traditional processRequest() approach + $this->processRequest(); + } + } /** * Removes a request parameter from the service given its name. * @@ -1245,110 +865,512 @@ public function send(string $contentType, $data, int $code = 200) { * will be not included in response. Default is empty string. Default is null. * */ - public function sendResponse(string $message, int $code = 200, string $type = '', mixed $otherInfo = '') { - $manager = $this->getManager(); + public function sendResponse(string $message, int $code = 200, string $type = '', mixed $otherInfo = '') { + $manager = $this->getManager(); + + if ($manager !== null) { + $manager->sendResponse($message, $code, $type, $otherInfo); + } + } + /** + * Sets the description of the service. + * + * Used to help front-end to identify the use of the service. + * + * @param string $desc Action description. + * + */ + public final function setDescription(string $desc) { + $this->serviceDesc = trim($desc); + } + /** + * Sets the value of the property 'requireAuth'. + * + * The property is used to tell if the authorization step will be skipped + * or not when the service is called. + * + * @param bool $bool True to make authorization step required. False to + * skip the authorization step. + * + */ + public function setIsAuthRequired(bool $bool) { + $this->requireAuth = $bool; + } + /** + * Associate the web service with a manager. + * + * The developer does not have to use this method. It is used when a + * service is added to a manager. + * + * @param WebServicesManager|null $manager The manager at which the service + * will be associated with. If null is given, the association will be removed if + * the service was associated with a manager. + * + */ + public function setManager(?WebServicesManager $manager) { + if ($manager === null) { + $this->owner = null; + } else { + $this->owner = $manager; + } + } + /** + * Sets the name of the service. + * + * A valid service name must follow the following rules: + *
    + *
  • It can contain the letters [A-Z] and [a-z].
  • + *
  • It can contain the numbers [0-9].
  • + *
  • It can have the character '-' and the character '_'.
  • + *
+ * + * @param string $name The name of the web service. + * + * @return bool If the given name is valid, the method will return + * true once the name is set. false is returned if the given + * name is invalid. + * + */ + public final function setName(string $name) : bool { + if (self::isValidName($name)) { + $this->name = trim($name); + + return true; + } + + return false; + } + + /** + * Sets the request instance for the service. + * + * @param mixed $request The request instance (Request, etc.) + */ + public function setRequest($request) { + $this->request = $request; + } + /** + * Adds multiple request methods as one group. + * + * @param array $methods + */ + public function setRequestMethods(array $methods) { + foreach ($methods as $m) { + $this->addRequestMethod($m); + } + } + /** + * Sets all responses for a specific HTTP method. + * + * @param string $method HTTP method. + * @param OpenAPI\ResponsesObj $responses Responses object. + * + * @return WebService Returns self for method chaining. + */ + public function setResponsesForMethod(string $method, OpenAPI\ResponsesObj $responses): WebService { + $this->responsesByMethod[strtoupper($method)] = $responses; + + return $this; + } + /** + * Sets version number or name at which the service was added to a manager. + * + * This method is called automatically when the service is added to any services manager. + * The developer does not have to use this method. + * + * @param string $sinceAPIv The version number at which the service was added to the API. + * + */ + public final function setSince(string $sinceAPIv) { + $this->sinceVersion = $sinceAPIv; + } + /** + * Returns a Json object that represents the service. + * + * @return Json an object of type Json. + * + */ + public function toJSON() : Json { + return $this->toPathItemObj()->toJSON(); + } + + /** + * Converts this web service to an OpenAPI PathItemObj. + * + * Each HTTP method supported by this service becomes an operation in the path item. + * + * @return OpenAPI\PathItemObj The PathItemObj representation of this service. + */ + public function toPathItemObj(): OpenAPI\PathItemObj { + $pathItem = new OpenAPI\PathItemObj(); + + foreach ($this->getRequestMethods() as $method) { + $responses = $this->getResponsesForMethod($method); + + if ($responses === null) { + $responses = new OpenAPI\ResponsesObj(); + $responses->addResponse('200', 'Successful operation'); + } + + $operation = new OpenAPI\OperationObj($responses); + + switch ($method) { + case RequestMethod::GET: + $pathItem->setGet($operation); + break; + case RequestMethod::POST: + $pathItem->setPost($operation); + break; + case RequestMethod::PUT: + $pathItem->setPut($operation); + break; + case RequestMethod::DELETE: + $pathItem->setDelete($operation); + break; + case RequestMethod::PATCH: + $pathItem->setPatch($operation); + break; + } + } + +return $pathItem; + } + + /** + * Configure parameters for all methods with RequestParam annotations. + */ + private function configureAllAnnotatedParameters(): void { + $reflection = new \ReflectionClass($this); + + foreach ($reflection->getMethods() as $method) { + $paramAttributes = $method->getAttributes(Annotations\RequestParam::class); + + if (!empty($paramAttributes)) { + $this->configureParametersFromMethod($method); + } + } + } + + /** + * Configure authentication from annotations. + */ + private function configureAuthentication(): void { + $reflection = new \ReflectionClass($this); + + // Check class-level authentication + $classAuth = $this->getAuthenticationFromClass($reflection); + + // If class has AllowAnonymous, disable auth requirement + if ($classAuth['allowAnonymous']) { + $this->setIsAuthRequired(false); + } else if ($classAuth['requiresAuth'] || $classAuth['preAuthorize']) { + $this->setIsAuthRequired(true); + } + } + + /** + * Configure service from annotations if present. + */ + private function configureFromAnnotations(string $fallbackName): void { + $reflection = new \ReflectionClass($this); + $attributes = $reflection->getAttributes(Annotations\RestController::class); + + if (!empty($attributes)) { + $restController = $attributes[0]->newInstance(); + $serviceName = $restController->name ?: $fallbackName; + $description = $restController->description; + } else { + $serviceName = $fallbackName; + $description = ''; + } + + if (!$this->setName($serviceName)) { + $this->setName('new-service'); + } + + if ($description) { + $this->setDescription($description); + } + + $this->configureMethodMappings(); + $this->configureAuthentication(); + } + + /** + * Configure allowed HTTP methods from method annotations. + */ + private function configureMethodMappings(): void { + $reflection = new \ReflectionClass($this); + $httpMethodToMethods = []; + + foreach ($reflection->getMethods() as $method) { + $methodMappings = [ + GetMapping::class => RequestMethod::GET, + PostMapping::class => RequestMethod::POST, + PutMapping::class => RequestMethod::PUT, + DeleteMapping::class => RequestMethod::DELETE + ]; + + foreach ($methodMappings as $annotationClass => $httpMethod) { + $attributes = $method->getAttributes($annotationClass); + + if (!empty($attributes)) { + if (!isset($httpMethodToMethods[$httpMethod])) { + $httpMethodToMethods[$httpMethod] = []; + } + $httpMethodToMethods[$httpMethod][] = $method->getName(); + } + } + } + + // Check for duplicates only if getCurrentProcessingMethod is not overridden + $hasCustomRouting = $reflection->getMethod('getCurrentProcessingMethod')->getDeclaringClass()->getName() !== self::class; + + if (!$hasCustomRouting) { + foreach ($httpMethodToMethods as $httpMethod => $methods) { + if (count($methods) > 1) { + throw new Exceptions\DuplicateMappingException( + "HTTP method $httpMethod is mapped to multiple methods: ".implode(', ', $methods) + ); + } + } + } + + if (!empty($httpMethodToMethods)) { + $this->setRequestMethods(array_keys($httpMethodToMethods)); + } + } + + /** + * Configure parameters for methods with specific HTTP method mapping. + * + * @param string $httpMethod HTTP method (GET, POST, PUT, DELETE, etc.) + */ + private function configureParametersForHttpMethod(string $httpMethod): void { + $reflection = new \ReflectionClass($this); + $httpMethod = strtoupper($httpMethod); - if ($manager !== null) { - $manager->sendResponse($message, $code, $type, $otherInfo); + foreach ($reflection->getMethods() as $method) { + // Check if method has HTTP method mapping annotation + $mappingFound = false; + + // Check for specific HTTP method annotations + $annotations = [ + 'GET' => GetMapping::class, + 'POST' => PostMapping::class, + 'PUT' => PutMapping::class, + 'DELETE' => DeleteMapping::class, + 'PATCH' => Annotations\PatchMapping::class, + ]; + + if (isset($annotations[$httpMethod])) { + $mappingFound = !empty($method->getAttributes($annotations[$httpMethod])); + } + + if ($mappingFound) { + $this->configureParametersFromMethod($method); + } } } + /** - * Sets the description of the service. - * - * Used to help front-end to identify the use of the service. - * - * @param string $desc Action description. - * + * Configure parameters from method RequestParam annotations. */ - public final function setDescription(string $desc) { - $this->serviceDesc = trim($desc); + private function configureParametersFromMethod(\ReflectionMethod $method): void { + $paramAttributes = $method->getAttributes(Annotations\RequestParam::class); + + foreach ($paramAttributes as $attribute) { + $param = $attribute->newInstance(); + + $options = [ + ParamOption::TYPE => $this->mapParamType($param->type), + ParamOption::OPTIONAL => $param->optional, + ParamOption::DEFAULT => $param->default, + ParamOption::DESCRIPTION => $param->description + ]; + + if ($param->filter !== null) { + $options[ParamOption::FILTER] = $param->filter; + } + + $this->addParameters([ + $param->name => $options + ]); + } } + /** - * Sets the value of the property 'requireAuth'. - * - * The property is used to tell if the authorization step will be skipped - * or not when the service is called. - * - * @param bool $bool True to make authorization step required. False to - * skip the authorization step. - * + * Get authentication configuration from class annotations. */ - public function setIsAuthRequired(bool $bool) { - $this->requireAuth = $bool; + private function getAuthenticationFromClass(\ReflectionClass $reflection): array { + return [ + 'allowAnonymous' => !empty($reflection->getAttributes(Annotations\AllowAnonymous::class)), + 'requiresAuth' => !empty($reflection->getAttributes(Annotations\RequiresAuth::class)), + 'preAuthorize' => $reflection->getAttributes(Annotations\PreAuthorize::class) + ]; } + /** - * Associate the web service with a manager. - * - * The developer does not have to use this method. It is used when a - * service is added to a manager. - * - * @param WebServicesManager|null $manager The manager at which the service - * will be associated with. If null is given, the association will be removed if - * the service was associated with a manager. + * Get method parameters by extracting values from request. * + * @param string $methodName The method name + * @return array Array of parameter values in correct order */ - public function setManager(?WebServicesManager $manager) { - if ($manager === null) { - $this->owner = null; + private function getMethodParameters(string $methodName): array { + $reflection = new \ReflectionMethod($this, $methodName); + $params = []; + + // Check for MapEntity attribute + $mapEntityAttrs = $reflection->getAttributes(Annotations\MapEntity::class); + + if (!empty($mapEntityAttrs)) { + $mapEntity = $mapEntityAttrs[0]->newInstance(); + $mappedObject = $this->getObject($mapEntity->entityClass, $mapEntity->setters); + $params[] = $mappedObject; } else { - $this->owner = $manager; + // Original parameter handling + foreach ($reflection->getParameters() as $param) { + $paramName = $param->getName(); + $value = $this->getParamVal($paramName); + + // Handle optional parameters with defaults + if ($value === null && $param->isDefaultValueAvailable()) { + $value = $param->getDefaultValue(); + } + + $params[] = $value; + } } + + return $params; } + /** - * Sets the name of the service. + * Map string type to ParamType constant. + */ + private function mapParamType(string $type): string { + return match (strtolower($type)) { + 'int', 'integer' => ParamType::INT, + 'float', 'double' => ParamType::DOUBLE, + 'bool', 'boolean' => ParamType::BOOL, + 'email' => ParamType::EMAIL, + 'url' => ParamType::URL, + 'array' => ParamType::ARR, + 'json' => ParamType::JSON_OBJ, + default => ParamType::STRING + }; + } /** + * Returns an array that contains all possible requests methods at which the + * service can be called with. * - * A valid service name must follow the following rules: - *
    - *
  • It can contain the letters [A-Z] and [a-z].
  • - *
  • It can contain the numbers [0-9].
  • - *
  • It can have the character '-' and the character '_'.
  • - *
+ * The array will contain strings like 'GET' or 'POST'. If no request methods + * where added, the array will be empty. * - * @param string $name The name of the web service. + * @return array An array that contains all possible requests methods at which the + * service can be called using. * - * @return bool If the given name is valid, the method will return - * true once the name is set. false is returned if the given - * name is invalid. + */ + + /** + * Check if a method handles the specified HTTP method. * + * @param \ReflectionMethod $method The method to check + * @param string $httpMethod The HTTP method (GET, POST, etc.) + * @return bool True if the method handles this HTTP method */ - public final function setName(string $name) : bool { - if (self::isValidName($name)) { - $this->name = trim($name); + private function methodHandlesHttpMethod(\ReflectionMethod $method, string $httpMethod): bool { + $methodMappings = [ + GetMapping::class => RequestMethod::GET, + PostMapping::class => RequestMethod::POST, + PutMapping::class => RequestMethod::PUT, + DeleteMapping::class => RequestMethod::DELETE + ]; - return true; + foreach ($methodMappings as $annotationClass => $mappedMethod) { + if ($httpMethod === $mappedMethod && !empty($method->getAttributes($annotationClass))) { + return true; + } } return false; } + /** - * Adds multiple request methods as one group. - * - * @param array $methods + * Get the current processing method name (to be overridden by subclasses if needed). */ - public function setRequestMethods(array $methods) { - foreach ($methods as $m) { - $this->addRequestMethod($m); - } + protected function getCurrentProcessingMethod(): ?string { + return null; // Default implementation } + /** - * Sets version number or name at which the service was added to a manager. - * - * This method is called automatically when the service is added to any services manager. - * The developer does not have to use this method. - * - * @param string $sinceAPIv The version number at which the service was added to the API. + * Handle HTTP exceptions by converting them to appropriate responses. * + * @param HttpException $exception The HTTP exception to handle */ - public final function setSince(string $sinceAPIv) { - $this->sinceVersion = $sinceAPIv; + protected function handleException(HttpException $exception): void { + $this->sendResponse( + $exception->getMessage(), + $exception->getStatusCode(), + $exception->getResponseType() + ); } + /** - * Returns a Json object that represents the service. - * - * @return Json an object of type Json. + * Handle method response by auto-converting return values to HTTP responses. * + * @param mixed $result The return value from the method + * @param string $methodName The name of the method that was called + * @return void */ - public function toJSON() : Json { - return $this->toPathItemObj()->toJSON(); + protected function handleMethodResponse(mixed $result, string $methodName): void { + $reflection = new \ReflectionMethod($this, $methodName); + $responseBodyAttrs = $reflection->getAttributes(ResponseBody::class); + + if (empty($responseBodyAttrs)) { + return; // No auto-processing, method should handle response manually + } + + $responseBody = $responseBodyAttrs[0]->newInstance(); + $contentType = $responseBody->contentType; + + // Handle custom content types + if ($contentType !== 'application/json') { + // For non-JSON content types, send raw result + if (is_array($result)) { + $content = new Json(); + $content->addArray('data', $result); + $contentType = 'application/json'; + } else if (is_object($result)) { + $content = new Json(); + $content->addObject('data', $result); + $contentType = 'application/json'; + } else { + $content = (string)$result; + } + + $this->send($contentType, $content, $responseBody->status); + + return; + } + + // Auto-convert return value to JSON response + if ($result === null) { + // Null return = empty response with configured status + $this->sendResponse('', $responseBody->status, $responseBody->type); + } else if (is_array($result) || is_object($result)) { + // Array/object = JSON response + if ($result instanceof Json) { + $json = $result; + } else if ($result instanceof JsonI) { + $json = $result->toJSON(); + } else { + $json = new Json(); + $json->add('data', $result); + } + $this->send($responseBody->contentType, $json, $responseBody->status); + } else { + // String/scalar = plain response + $this->send($responseBody->contentType, $result, $responseBody->status); + } } } diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index 56acb85..f3146b7 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -1,4 +1,5 @@ request = $request ?? Request::createFromGlobals(); $this->response = new Response(); } - public function setRequest(Request $request) : WebServicesManager { - $this->request = $request; - return $this; - } - /** - * Returns the response object used by the manager. - * - * @return Response - */ - public function getResponse() : Response { - return $this->response; - } /** * Adds new web service to the set of web services. * @@ -172,26 +161,27 @@ public function autoDiscoverServices(?string $path = null) : WebServicesManager $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); $path = dirname($trace[0]['file']); } - - $files = glob($path . '/*.php'); + + $files = glob($path.'/*.php'); $beforeClasses = get_declared_classes(); - + foreach ($files as $file) { require_once $file; } - + $afterClasses = get_declared_classes(); $newClasses = array_diff($afterClasses, $beforeClasses); - + foreach ($newClasses as $class) { if (is_subclass_of($class, WebService::class)) { $reflection = new \ReflectionClass($class); + if (!$reflection->isAbstract()) { $this->addService(new $class()); } } } - + return $this; } /** @@ -217,6 +207,14 @@ public function contentTypeNotSupported(string $cType = '') { $j->add('request-content-type', $cType); $this->sendResponse(ResponseMessage::get('415'), 415, WebService::E, $j); } + /** + * Returns the base path for all services. + * + * @return string The base path. + */ + public function getBasePath(): string { + return $this->basePath; + } /** * Returns the name of the service which is being called. * @@ -243,29 +241,6 @@ public function getCalledServiceName() { public function getDescription() { return $this->apiDesc; } - /** - * Sets the base path for all services in this manager. - * - * The base path will be prepended to each service name when generating paths. - * For example, if base path is "/api/v1" and service name is "user", - * the final path will be "/api/v1/user". - * - * @param string $basePath The base path (e.g., "/api/v1"). Leading/trailing slashes are handled automatically. - * - * @return WebServicesManager Returns self for method chaining. - */ - public function setBasePath(string $basePath): WebServicesManager { - $this->basePath = rtrim($basePath, '/'); - return $this; - } - /** - * Returns the base path for all services. - * - * @return string The base path. - */ - public function getBasePath(): string { - return $this->basePath; - } /** * Returns an associative array or an object of type Json of filtered request inputs. * @@ -339,6 +314,17 @@ public function getOutputStream() { public function getOutputStreamPath() { return $this->outputStreamPath; } + public function getRequest() : Request { + return $this->request; + } + /** + * Returns the response object used by the manager. + * + * @return Response + */ + public function getResponse() : Response { + return $this->response; + } /** * Returns a web service given its name. * @@ -409,9 +395,6 @@ public function invParams() { 'invalid' => $paramsNamesArr ])); } - public function getRequest() : Request { - return $this->request; - } /** * Checks if request content type is supported by the service or not (For 'POST' * and PUT requests only). @@ -433,6 +416,7 @@ public final function isContentTypeSupported() : bool { return true; } } + return false; } else if ($c === null && ($rm == RequestMethod::POST || $rm == RequestMethod::PUT)) { return false; @@ -521,21 +505,21 @@ public final function process() { if ($this->isContentTypeSupported()) { if ($this->_checkAction()) { $actionObj = $this->getServiceByName($this->getCalledServiceName()); - + // Configure parameters for ResponseBody services before getting them if ($this->serviceHasResponseBodyMethods($actionObj)) { $this->configureServiceParameters($actionObj); } - + $params = $actionObj->getParameters(); $params = $actionObj->getParameters(); $this->filter->clearParametersDef(); $this->filter->clearInputs(); $requestMethod = $this->getRequest()->getRequestMethod(); - + foreach ($params as $param) { $paramMethods = $param->getMethods(); - + if (count($paramMethods) == 0 || in_array($requestMethod, $paramMethods)) { $this->filter->addRequestParameter($param); } @@ -745,6 +729,22 @@ public function serviceNotImplemented() { public function serviceNotSupported() { $this->sendResponse(ResponseMessage::get('404-5'), 404, WebService::E); } + /** + * Sets the base path for all services in this manager. + * + * The base path will be prepended to each service name when generating paths. + * For example, if base path is "/api/v1" and service name is "user", + * the final path will be "/api/v1/user". + * + * @param string $basePath The base path (e.g., "/api/v1"). Leading/trailing slashes are handled automatically. + * + * @return WebServicesManager Returns self for method chaining. + */ + public function setBasePath(string $basePath): WebServicesManager { + $this->basePath = rtrim($basePath, '/'); + + return $this; + } /** * Sets the description of the web services set. * @@ -810,6 +810,11 @@ public function setOutputStream($stream, bool $new = false): bool { return false; } + public function setRequest(Request $request) : WebServicesManager { + $this->request = $request; + + return $this; + } /** * Sets version number of the set. * @@ -840,32 +845,6 @@ public final function setVersion(string $val) : bool { return false; } - /** - * Converts the services manager to an OpenAPI document. - * - * This method generates a complete OpenAPI 3.1.0 specification document - * from the registered services. Each service becomes a path in the document. - * - * @return OpenAPI\OpenAPIObj The OpenAPI document. - */ - public function toOpenAPI(): OpenAPI\OpenAPIObj { - $info = new OpenAPI\InfoObj( - $this->getDescription(), - $this->getVersion() - ); - - $openapi = new OpenAPI\OpenAPIObj($info); - - $paths = new OpenAPI\PathsObj(); - foreach ($this->getServices() as $service) { - $path = $this->basePath . '/' . $service->getName(); - $paths->addPath($path, $service->toPathItemObj()); - } - - $openapi->setPaths($paths); - - return $openapi; - } /** * Returns Json object that represents services set. * @@ -899,6 +878,33 @@ public function toJSON() : Json { return $json; } + /** + * Converts the services manager to an OpenAPI document. + * + * This method generates a complete OpenAPI 3.1.0 specification document + * from the registered services. Each service becomes a path in the document. + * + * @return OpenAPI\OpenAPIObj The OpenAPI document. + */ + public function toOpenAPI(): OpenAPI\OpenAPIObj { + $info = new OpenAPI\InfoObj( + $this->getDescription(), + $this->getVersion() + ); + + $openapi = new OpenAPI\OpenAPIObj($info); + + $paths = new OpenAPI\PathsObj(); + + foreach ($this->getServices() as $service) { + $path = $this->basePath.'/'.$service->getName(); + $paths->addPath($path, $service->toPathItemObj()); + } + + $openapi->setPaths($paths); + + return $openapi; + } private function _AfterParamsCheck($processReq) { if ($processReq) { $service = $this->getServiceByName($this->getCalledServiceName()); @@ -1007,9 +1013,25 @@ private function _processNonJson($params) { private function addAction(WebService $service) : WebServicesManager { $this->services[$service->getName()] = $service; $service->setManager($this); + return $this; } + /** + * Configure parameters for the target method of a service. + */ + private function configureServiceParameters(WebService $service): void { + if (method_exists($service, 'getTargetMethod')) { + $targetMethod = $service->getTargetMethod(); + + if ($targetMethod && method_exists($service, 'configureParametersForMethod')) { + $reflection = new \ReflectionMethod($service, 'configureParametersForMethod'); + $reflection->setAccessible(true); + $reflection->invoke($service, $targetMethod); + } + } + } + /** * @throws Exception */ @@ -1046,7 +1068,7 @@ private function filterInputsHelper() { */ private function getAction() { $services = $this->getServices(); - + if (count($services) == 1) { return $services[array_keys($services)[0]]->getName(); } @@ -1087,6 +1109,7 @@ private function getAction() { $retVal = filter_var($_POST[$serviceNameIndex]); } else if ($reqMeth == RequestMethod::PUT || $reqMeth == RequestMethod::PATCH) { $this->populatePutData($contentType); + if (isset($_POST[$serviceNameIndex])) { $retVal = filter_var($_POST[$serviceNameIndex]); } @@ -1095,8 +1118,29 @@ private function getAction() { return $retVal; } - private function populatePutData(string $contentType) { + private function isAuth(WebService $service) { + $isAuth = false; + if ($service->isAuthRequired()) { + // Check if method has authorization annotations + if ($service->hasMethodAuthorizationAnnotations()) { + // Use annotation-based authorization + return $service->checkMethodAuthorization(); + } + + // Fall back to legacy HTTP-method-specific authorization + $isAuthCheck = 'isAuthorized'.$this->getRequest()->getMethod(); + + if (!method_exists($service, $isAuthCheck)) { + return $service->isAuthorized() === null || $service->isAuthorized(); + } + + return $service->$isAuthCheck() === null || $service->$isAuthCheck(); + } + + return true; + } + private function populatePutData(string $contentType) { $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; $input = file_get_contents('php://input'); @@ -1107,6 +1151,7 @@ private function populatePutData(string $contentType) { // Handle application/x-www-form-urlencoded if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { parse_str($input, $_POST); + return; } @@ -1114,11 +1159,12 @@ private function populatePutData(string $contentType) { if (strpos($contentType, 'multipart/form-data') === 0) { // Extract boundary from content type preg_match('/boundary=(.+)$/', $contentType, $matches); + if (!isset($matches[1])) { return; } - $boundary = '--' . $matches[1]; + $boundary = '--'.$matches[1]; $parts = explode($boundary, $input); foreach ($parts as $part) { @@ -1128,6 +1174,7 @@ private function populatePutData(string $contentType) { // Split headers and content $sections = explode("\r\n\r\n", $part, 2); + if (count($sections) !== 2) { continue; } @@ -1146,6 +1193,7 @@ private function populatePutData(string $contentType) { // Extract content type if present $fileType = 'application/octet-stream'; + if (preg_match('/Content-Type:\s*(.+)/i', $headers, $typeMatch)) { $fileType = trim($typeMatch[1]); } @@ -1169,37 +1217,16 @@ private function populatePutData(string $contentType) { } } } - private function isAuth(WebService $service) { - $isAuth = false; - - if ($service->isAuthRequired()) { - // Check if method has authorization annotations - if ($service->hasMethodAuthorizationAnnotations()) { - // Use annotation-based authorization - return $service->checkMethodAuthorization(); - } - - // Fall back to legacy HTTP-method-specific authorization - $isAuthCheck = 'isAuthorized'.$this->getRequest()->getMethod(); - - if (!method_exists($service, $isAuthCheck)) { - return $service->isAuthorized() === null || $service->isAuthorized(); - } - - return $service->$isAuthCheck() === null || $service->$isAuthCheck(); - } - - return true; - } private function processService(WebService $service) { // Try auto-processing only if service has ResponseBody methods if ($this->serviceHasResponseBodyMethods($service)) { // Configure parameters for the target method before processing $this->configureServiceParameters($service); $service->processWithAutoHandling(); + return; } - + $processMethod = 'process'.$this->getRequest()->getMethod(); if (!method_exists($service, $processMethod)) { @@ -1213,30 +1240,17 @@ private function processService(WebService $service) { */ private function serviceHasResponseBodyMethods(WebService $service): bool { $reflection = new \ReflectionClass($service); - + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - $attributes = $method->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class); + $attributes = $method->getAttributes(Annotations\ResponseBody::class); + if (!empty($attributes)) { return true; } } - + return false; } - - /** - * Configure parameters for the target method of a service. - */ - private function configureServiceParameters(WebService $service): void { - if (method_exists($service, 'getTargetMethod')) { - $targetMethod = $service->getTargetMethod(); - if ($targetMethod && method_exists($service, 'configureParametersForMethod')) { - $reflection = new \ReflectionMethod($service, 'configureParametersForMethod'); - $reflection->setAccessible(true); - $reflection->invoke($service, $targetMethod); - } - } - } private function setOutputStreamHelper($trimmed, $mode) : bool { $tempStream = fopen($trimmed, $mode); diff --git a/examples/00-basic/01-hello-world/HelloService.php b/examples/00-basic/01-hello-world/HelloService.php index 8ad02e5..d6c8179 100644 --- a/examples/00-basic/01-hello-world/HelloService.php +++ b/examples/00-basic/01-hello-world/HelloService.php @@ -2,18 +2,17 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * A minimal web service that responds with "Hello World!" */ #[RestController('hello', 'A simple hello world service')] class HelloService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/00-basic/02-with-parameters/GreetingService.php b/examples/00-basic/02-with-parameters/GreetingService.php index 0ca8698..9978494 100644 --- a/examples/00-basic/02-with-parameters/GreetingService.php +++ b/examples/00-basic/02-with-parameters/GreetingService.php @@ -2,19 +2,18 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * A service that accepts a name parameter and returns a personalized greeting */ #[RestController('greeting', 'A greeting service with name parameter')] class GreetingService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/00-basic/03-multiple-methods/TaskService.php b/examples/00-basic/03-multiple-methods/TaskService.php index db80826..fbb13fb 100644 --- a/examples/00-basic/03-multiple-methods/TaskService.php +++ b/examples/00-basic/03-multiple-methods/TaskService.php @@ -2,32 +2,21 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\DeleteMapping; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\PutMapping; -use WebFiori\Http\Annotations\DeleteMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * A service that demonstrates handling multiple HTTP methods for CRUD operations */ #[RestController('tasks', 'Task management service with CRUD operations')] class TaskService extends WebService { - - #[GetMapping] - #[ResponseBody] - #[AllowAnonymous] - public function getTasks(): array { - return [ - 'tasks' => [], - 'count' => 0 - ]; - } - #[PostMapping] #[ResponseBody(status: 201)] #[AllowAnonymous] @@ -35,45 +24,55 @@ public function getTasks(): array { #[RequestParam('description', 'string', true, null, 'Task description (max 500 chars)')] public function createTask(string $title, ?string $description = null): array { if (!$title) { - throw new \InvalidArgumentException('Title is required for creating a task'); + throw new InvalidArgumentException('Title is required for creating a task'); } - + return [ 'id' => 1, 'title' => $title, 'description' => $description ?: '' ]; } - - #[PutMapping] + + #[DeleteMapping] #[ResponseBody] #[AllowAnonymous] - #[RequestParam('id', 'int', false, null, 'Task ID')] - #[RequestParam('title', 'string', true, null, 'Updated task title')] - public function updateTask(int $id, ?string $title = null): array { + #[RequestParam('id', 'int', false, null, 'Task ID to delete')] + public function deleteTask(int $id): array { if (!$id) { - throw new \InvalidArgumentException('ID is required for updating a task'); + throw new InvalidArgumentException('ID is required for deleting a task'); } - + return [ 'id' => $id, - 'title' => $title, - 'updated_at' => date('Y-m-d H:i:s') + 'deleted_at' => date('Y-m-d H:i:s') ]; } - - #[DeleteMapping] + + #[GetMapping] #[ResponseBody] #[AllowAnonymous] - #[RequestParam('id', 'int', false, null, 'Task ID to delete')] - public function deleteTask(int $id): array { + public function getTasks(): array { + return [ + 'tasks' => [], + 'count' => 0 + ]; + } + + #[PutMapping] + #[ResponseBody] + #[AllowAnonymous] + #[RequestParam('id', 'int', false, null, 'Task ID')] + #[RequestParam('title', 'string', true, null, 'Updated task title')] + public function updateTask(int $id, ?string $title = null): array { if (!$id) { - throw new \InvalidArgumentException('ID is required for deleting a task'); + throw new InvalidArgumentException('ID is required for updating a task'); } - + return [ 'id' => $id, - 'deleted_at' => date('Y-m-d H:i:s') + 'title' => $title, + 'updated_at' => date('Y-m-d H:i:s') ]; } } diff --git a/examples/00-basic/04-simple-manager/InfoService.php b/examples/00-basic/04-simple-manager/InfoService.php index 1744acb..1ee1f40 100644 --- a/examples/00-basic/04-simple-manager/InfoService.php +++ b/examples/00-basic/04-simple-manager/InfoService.php @@ -2,18 +2,17 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * API information service that provides metadata about the API */ #[RestController('info', 'API information and metadata')] class InfoService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/00-basic/04-simple-manager/ProductService.php b/examples/00-basic/04-simple-manager/ProductService.php index 520272f..be83129 100644 --- a/examples/00-basic/04-simple-manager/ProductService.php +++ b/examples/00-basic/04-simple-manager/ProductService.php @@ -2,18 +2,17 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * A simple product management service */ #[RestController('products', 'Product catalog operations')] class ProductService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/00-basic/04-simple-manager/UserService.php b/examples/00-basic/04-simple-manager/UserService.php index 87ad758..cd7524f 100644 --- a/examples/00-basic/04-simple-manager/UserService.php +++ b/examples/00-basic/04-simple-manager/UserService.php @@ -2,18 +2,17 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * A simple user management service */ #[RestController('users', 'User management operations')] class UserService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/01-core/01-parameter-validation/ValidationService.php b/examples/01-core/01-parameter-validation/ValidationService.php index 46af84f..8da6b45 100644 --- a/examples/01-core/01-parameter-validation/ValidationService.php +++ b/examples/01-core/01-parameter-validation/ValidationService.php @@ -2,20 +2,19 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; use WebFiori\Http\APIFilter; +use WebFiori\Http\WebService; /** * Service demonstrating comprehensive parameter validation */ #[RestController('validate', 'Parameter validation demonstration')] class ValidationService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] @@ -44,7 +43,7 @@ public function validateData(string $name, ?int $age = 25, string $email, ?strin ] ]; } - + /** * Custom validation function for username */ @@ -53,11 +52,11 @@ public static function validateUsername($original, $filtered, $param) { if (strlen($filtered) < 3 || strlen($filtered) > 20) { return APIFilter::INVALID; } - + if (!ctype_alnum($filtered)) { return APIFilter::INVALID; } - + return strtolower($filtered); // Normalize to lowercase } } diff --git a/examples/01-core/02-error-handling/ErrorService.php b/examples/01-core/02-error-handling/ErrorService.php index 1817daf..dc36191 100644 --- a/examples/01-core/02-error-handling/ErrorService.php +++ b/examples/01-core/02-error-handling/ErrorService.php @@ -2,19 +2,18 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * Service demonstrating comprehensive error handling */ #[RestController('error-demo', 'Error handling demonstration service')] class ErrorService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] @@ -31,47 +30,47 @@ public function handleAction(string $operation, ?int $age = null, ?float $a = nu case 'divide': return $this->handleDivision($a, $b); case 'not-found': - throw new \Exception('The requested resource was not found', 404); + throw new Exception('The requested resource was not found', 404); case 'server-error': - throw new \Exception('Simulated server error for testing purposes', 500); + throw new Exception('Simulated server error for testing purposes', 500); case 'unauthorized': - throw new \Exception('Access denied: insufficient permissions', 403); + throw new Exception('Access denied: insufficient permissions', 403); default: - throw new \InvalidArgumentException("Unknown operation: $operation. Available: success, validate, divide, not-found, server-error, unauthorized"); + throw new InvalidArgumentException("Unknown operation: $operation. Available: success, validate, divide, not-found, server-error, unauthorized"); } } - + + private function handleDivision(?float $a, ?float $b): array { + if ($a === null || $b === null) { + throw new InvalidArgumentException('Both parameters a and b are required for division'); + } + + if ($b == 0) { + throw new InvalidArgumentException('Division by zero is not allowed'); + } + + return [ + 'operands' => ['a' => $a, 'b' => $b], + 'result' => $a / $b + ]; + } + private function handleSuccess(): array { return [ 'timestamp' => date('Y-m-d H:i:s'), 'status' => 'OK' ]; } - + private function handleValidation(?int $age): array { if ($age === null) { - throw new \InvalidArgumentException('Age parameter is required for validation test'); + throw new InvalidArgumentException('Age parameter is required for validation test'); } - + if ($age < 18) { - throw new \InvalidArgumentException("Age must be 18 or older. Provided: $age"); + throw new InvalidArgumentException("Age must be 18 or older. Provided: $age"); } - + return ['validated_age' => $age]; } - - private function handleDivision(?float $a, ?float $b): array { - if ($a === null || $b === null) { - throw new \InvalidArgumentException('Both parameters a and b are required for division'); - } - - if ($b == 0) { - throw new \InvalidArgumentException('Division by zero is not allowed'); - } - - return [ - 'operands' => ['a' => $a, 'b' => $b], - 'result' => $a / $b - ]; - } } diff --git a/examples/01-core/03-json-requests/JsonService.php b/examples/01-core/03-json-requests/JsonService.php index 2c7600b..a15df59 100644 --- a/examples/01-core/03-json-requests/JsonService.php +++ b/examples/01-core/03-json-requests/JsonService.php @@ -2,13 +2,13 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\PutMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; use WebFiori\Json\Json; /** @@ -16,7 +16,6 @@ */ #[RestController('json-data', 'JSON request processing service')] class JsonService extends WebService { - #[PostMapping] #[ResponseBody] #[AllowAnonymous] @@ -24,7 +23,7 @@ class JsonService extends WebService { public function processJsonPost(?string $operation = 'create'): array { return $this->processJsonRequest($operation); } - + #[PutMapping] #[ResponseBody] #[AllowAnonymous] @@ -32,31 +31,11 @@ public function processJsonPost(?string $operation = 'create'): array { public function processJsonPut(?string $operation = 'update'): array { return $this->processJsonRequest($operation); } - - private function processJsonRequest(string $operation): array { - $inputs = $this->getInputs(); - - // Check if we received JSON data - if (!($inputs instanceof Json)) { - throw new \InvalidArgumentException('This service expects JSON data'); - } - - switch ($operation) { - case 'create': - return $this->handleCreate($inputs); - case 'update': - return $this->handleUpdate($inputs); - case 'validate': - return $this->handleValidate($inputs); - default: - return $this->handleGeneric($inputs, $operation); - } - } - + private function handleCreate(Json $jsonData): array { $user = $jsonData->get('user'); $preferences = $jsonData->get('preferences'); - + return [ 'operation' => 'create', 'user_data' => $user, @@ -64,11 +43,20 @@ private function handleCreate(Json $jsonData): array { 'created_at' => date('Y-m-d H:i:s') ]; } - + + private function handleGeneric(Json $jsonData, string $operation): array { + return [ + 'operation' => $operation, + 'received_json' => $jsonData->toArray(), + 'json_keys' => array_keys($jsonData->toArray()), + 'processed_at' => date('Y-m-d H:i:s') + ]; + } + private function handleUpdate(Json $jsonData): array { $name = $jsonData->get('name'); $email = $jsonData->get('email'); - + return [ 'operation' => 'update', 'updated_fields' => [ @@ -78,32 +66,43 @@ private function handleUpdate(Json $jsonData): array { 'updated_at' => date('Y-m-d H:i:s') ]; } - + private function handleValidate(Json $jsonData): array { $errors = []; - + // Validate required fields if (!$jsonData->hasKey('name') || empty($jsonData->get('name'))) { $errors[] = 'Name is required'; } - + if (!$jsonData->hasKey('email') || !filter_var($jsonData->get('email'), FILTER_VALIDATE_EMAIL)) { $errors[] = 'Valid email is required'; } - + if (!empty($errors)) { - throw new \InvalidArgumentException('Validation failed: ' . implode(', ', $errors)); + throw new InvalidArgumentException('Validation failed: '.implode(', ', $errors)); } - + return ['validated_data' => $jsonData->toArray()]; } - - private function handleGeneric(Json $jsonData, string $operation): array { - return [ - 'operation' => $operation, - 'received_json' => $jsonData->toArray(), - 'json_keys' => array_keys($jsonData->toArray()), - 'processed_at' => date('Y-m-d H:i:s') - ]; + + private function processJsonRequest(string $operation): array { + $inputs = $this->getInputs(); + + // Check if we received JSON data + if (!($inputs instanceof Json)) { + throw new InvalidArgumentException('This service expects JSON data'); + } + + switch ($operation) { + case 'create': + return $this->handleCreate($inputs); + case 'update': + return $this->handleUpdate($inputs); + case 'validate': + return $this->handleValidate($inputs); + default: + return $this->handleGeneric($inputs, $operation); + } } } diff --git a/examples/01-core/04-file-uploads/UploadService.php b/examples/01-core/04-file-uploads/UploadService.php index cd25400..bd12e27 100644 --- a/examples/01-core/04-file-uploads/UploadService.php +++ b/examples/01-core/04-file-uploads/UploadService.php @@ -2,32 +2,32 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\PostMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * Service demonstrating file upload handling */ #[RestController('upload', 'File upload handling service')] class UploadService extends WebService { - - private const UPLOAD_DIR = 'uploads/'; - private const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB private const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'text/plain', 'application/pdf']; - + private const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + + private const UPLOAD_DIR = 'uploads/'; + public function __construct() { parent::__construct(); - + // Ensure upload directory exists if (!is_dir(self::UPLOAD_DIR)) { mkdir(self::UPLOAD_DIR, 0755, true); } } - + #[PostMapping] #[ResponseBody] #[AllowAnonymous] @@ -44,39 +44,19 @@ public function handleUpload(?string $operation = 'single', ?string $description case 'with-metadata': return $this->handleUploadWithMetadata($title, $category); default: - throw new \InvalidArgumentException('Unknown upload operation'); + throw new InvalidArgumentException('Unknown upload operation'); } } - - private function handleSingleUpload(?string $description): array { - if (!isset($_FILES['file'])) { - throw new \InvalidArgumentException('No file uploaded. Expected field: file'); - } - - $file = $_FILES['file']; - - $result = $this->processFile($file); - - if (!$result['success']) { - throw new \InvalidArgumentException($result['error']); - } - - return [ - 'file_info' => $result['data'], - 'description' => $description, - 'uploaded_at' => date('Y-m-d H:i:s') - ]; - } - + private function handleMultipleUpload(): array { if (!isset($_FILES['files'])) { - throw new \InvalidArgumentException('No files uploaded. Expected field: files[]'); + throw new InvalidArgumentException('No files uploaded. Expected field: files[]'); } - + $files = $_FILES['files']; $results = []; $errors = []; - + for ($i = 0; $i < count($files['name']); $i++) { $file = [ 'name' => $files['name'][$i], @@ -85,9 +65,9 @@ private function handleMultipleUpload(): array { 'error' => $files['error'][$i], 'size' => $files['size'][$i] ]; - + $result = $this->processFile($file); - + if ($result['success']) { $results[] = $result['data']; } else { @@ -97,7 +77,7 @@ private function handleMultipleUpload(): array { ]; } } - + return [ 'uploaded_files' => $results, 'errors' => $errors, @@ -105,20 +85,40 @@ private function handleMultipleUpload(): array { 'total_errors' => count($errors) ]; } - + + private function handleSingleUpload(?string $description): array { + if (!isset($_FILES['file'])) { + throw new InvalidArgumentException('No file uploaded. Expected field: file'); + } + + $file = $_FILES['file']; + + $result = $this->processFile($file); + + if (!$result['success']) { + throw new InvalidArgumentException($result['error']); + } + + return [ + 'file_info' => $result['data'], + 'description' => $description, + 'uploaded_at' => date('Y-m-d H:i:s') + ]; + } + private function handleUploadWithMetadata(?string $title, ?string $category): array { if (!isset($_FILES['file'])) { - throw new \InvalidArgumentException('No file uploaded'); + throw new InvalidArgumentException('No file uploaded'); } - + $file = $_FILES['file']; - + $result = $this->processFile($file); - + if (!$result['success']) { - throw new \InvalidArgumentException($result['error']); + throw new InvalidArgumentException($result['error']); } - + return [ 'title' => $title ?: $file['name'], 'category' => $category ?: 'general', @@ -127,7 +127,7 @@ private function handleUploadWithMetadata(?string $title, ?string $category): ar 'file_info' => $result['data'] ]; } - + private function processFile(array $file): array { // Check for upload errors if ($file['error'] !== UPLOAD_ERR_OK) { @@ -137,7 +137,7 @@ private function processFile(array $file): array { 'details' => ['upload_error_code' => $file['error']] ]; } - + // Validate file size if ($file['size'] > self::MAX_FILE_SIZE) { return [ @@ -149,7 +149,7 @@ private function processFile(array $file): array { ] ]; } - + // Validate file type if (!in_array($file['type'], self::ALLOWED_TYPES)) { return [ @@ -161,13 +161,13 @@ private function processFile(array $file): array { ] ]; } - + // Generate unique filename $extension = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = pathinfo($file['name'], PATHINFO_FILENAME); - $uniqueFilename = $filename . '_' . date('Ymd_His') . '.' . $extension; - $uploadPath = self::UPLOAD_DIR . $uniqueFilename; - + $uniqueFilename = $filename.'_'.date('Ymd_His').'.'.$extension; + $uploadPath = self::UPLOAD_DIR.$uniqueFilename; + // Move uploaded file if (move_uploaded_file($file['tmp_name'], $uploadPath)) { return [ @@ -178,7 +178,7 @@ private function processFile(array $file): array { 'size' => $file['size'], 'type' => $file['type'], 'upload_path' => $uploadPath, - 'url' => '/' . $uploadPath + 'url' => '/'.$uploadPath ] ]; } else { diff --git a/examples/01-core/05-response-formats/JsonResponseService.php b/examples/01-core/05-response-formats/JsonResponseService.php index a9dad77..0b3051b 100644 --- a/examples/01-core/05-response-formats/JsonResponseService.php +++ b/examples/01-core/05-response-formats/JsonResponseService.php @@ -2,15 +2,14 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; #[RestController('json-response', 'JSON response service')] class JsonResponseService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/01-core/05-response-formats/ResponseService.php b/examples/01-core/05-response-formats/ResponseService.php index c32123a..df2f3a5 100644 --- a/examples/01-core/05-response-formats/ResponseService.php +++ b/examples/01-core/05-response-formats/ResponseService.php @@ -2,19 +2,18 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * Service demonstrating different response formats */ #[RestController('response', 'Multiple response format demonstration')] class ResponseService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] @@ -22,193 +21,203 @@ class ResponseService extends WebService { #[RequestParam('data', 'string', true, 'sample', 'Data to include in response')] public function handleResponse(?string $format = 'json', ?string $data = 'sample'): mixed { $sampleData = $this->getSampleData($data, $format); - + // Dynamically set content type based on format - $reflection = new \ReflectionMethod($this, 'handleResponse'); - $attrs = $reflection->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class); - + $reflection = new ReflectionMethod($this, 'handleResponse'); + $attrs = $reflection->getAttributes(ResponseBody::class); + switch ($format) { case 'xml': // Override response to XML $xml = $this->arrayToXml($sampleData, 'response'); $this->send('application/xml', $xml, 200); + return null; case 'text': // Override response to text $text = $this->arrayToText($sampleData); $this->send('text/plain', $text, 200); + return null; default: // Return array for JSON (handled by ResponseBody) return $sampleData; } } - - private function getSampleData(string $type, string $format): array { - switch ($type) { - case 'users': - return [ - ['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com'], - ['id' => 2, 'name' => 'Jane Smith', 'email' => 'jane@example.com'], - ['id' => 3, 'name' => 'Bob Johnson', 'email' => 'bob@example.com'] - ]; - case 'products': - return [ - ['id' => 1, 'name' => 'Laptop', 'price' => 999.99, 'category' => 'Electronics'], - ['id' => 2, 'name' => 'Book', 'price' => 19.99, 'category' => 'Education'], - ['id' => 3, 'name' => 'Coffee Mug', 'price' => 12.50, 'category' => 'Kitchen'] - ]; - default: - return [ - 'message' => 'Hello from WebFiori HTTP', - 'timestamp' => date('Y-m-d H:i:s'), - 'format_requested' => $format, - 'server_info' => [ - 'php_version' => PHP_VERSION, - 'server_time' => time() - ] - ]; + + private function arrayToCsv(array $data): string { + if (empty($data)) { + return ''; } - } - - private function arrayToXml(array $data, string $rootElement = 'root'): string { - $xml = "\n"; - $xml .= "<$rootElement>\n"; - $xml .= $this->arrayToXmlRecursive($data, 1); - $xml .= ""; - return $xml; - } - - private function arrayToXmlRecursive(array $data, int $indent = 0): string { - $xml = ''; - $spaces = str_repeat(' ', $indent); - - foreach ($data as $key => $value) { - $key = is_numeric($key) ? 'item' : $key; - - if (is_array($value)) { - $xml .= "$spaces<$key>\n"; - $xml .= $this->arrayToXmlRecursive($value, $indent + 1); - $xml .= "$spaces\n"; - } else { - $xml .= "$spaces<$key>" . htmlspecialchars($value) . "\n"; + + $csv = ''; + + // Check if it's a list of objects/arrays + if (is_array($data[0] ?? null)) { + // Get headers + $headers = array_keys($data[0]); + $csv .= implode(',', $headers)."\n"; + + // Add data rows + foreach ($data as $row) { + $csvRow = []; + + foreach ($headers as $header) { + $value = $row[$header] ?? ''; + $csvRow[] = '"'.str_replace('"', '""', $value).'"'; + } + $csv .= implode(',', $csvRow)."\n"; } - } - - return $xml; - } - - private function arrayToText(array $data): string { - return $this->arrayToTextRecursive($data, 0); - } - - private function arrayToTextRecursive(array $data, int $indent = 0): string { - $text = ''; - $spaces = str_repeat(' ', $indent); - - foreach ($data as $key => $value) { - if (is_array($value)) { - $text .= "$spaces$key:\n"; - $text .= $this->arrayToTextRecursive($value, $indent + 1); - } else { - $text .= "$spaces$key: $value\n"; + } else { + // Simple key-value pairs + $csv .= "Key,Value\n"; + + foreach ($data as $key => $value) { + $csv .= '"'.str_replace('"', '""', $key).'","'.str_replace('"', '""', $value)."\"\n"; } } - - return $text; + + return $csv; } - + private function arrayToHtml(array $data): string { $html = "\n\n\n"; $html .= "WebFiori HTTP Response\n"; $html .= "\n"; $html .= "\n\n"; $html .= "

WebFiori HTTP Response

\n"; - $html .= "

Generated: " . date('Y-m-d H:i:s') . "

\n"; + $html .= "

Generated: ".date('Y-m-d H:i:s')."

\n"; $html .= $this->arrayToHtmlTable($data); $html .= "\n"; + return $html; } - + private function arrayToHtmlTable(array $data): string { if (empty($data)) { return '

No data available

'; } - + // Check if it's a list of objects/arrays if (is_array($data[0] ?? null)) { return $this->createHtmlTable($data); } - + // Simple key-value pairs $html = "\n"; + foreach ($data as $key => $value) { - $html .= ""; - $html .= "\n"; + $html .= ""; + $html .= "\n"; } $html .= "
" . htmlspecialchars($key) . "" . htmlspecialchars(is_array($value) ? json_encode($value) : $value) . "
".htmlspecialchars($key)."".htmlspecialchars(is_array($value) ? json_encode($value) : $value)."
\n"; - + return $html; } - + + private function arrayToText(array $data): string { + return $this->arrayToTextRecursive($data, 0); + } + + private function arrayToTextRecursive(array $data, int $indent = 0): string { + $text = ''; + $spaces = str_repeat(' ', $indent); + + foreach ($data as $key => $value) { + if (is_array($value)) { + $text .= "$spaces$key:\n"; + $text .= $this->arrayToTextRecursive($value, $indent + 1); + } else { + $text .= "$spaces$key: $value\n"; + } + } + + return $text; + } + + private function arrayToXml(array $data, string $rootElement = 'root'): string { + $xml = "\n"; + $xml .= "<$rootElement>\n"; + $xml .= $this->arrayToXmlRecursive($data, 1); + $xml .= ""; + + return $xml; + } + + private function arrayToXmlRecursive(array $data, int $indent = 0): string { + $xml = ''; + $spaces = str_repeat(' ', $indent); + + foreach ($data as $key => $value) { + $key = is_numeric($key) ? 'item' : $key; + + if (is_array($value)) { + $xml .= "$spaces<$key>\n"; + $xml .= $this->arrayToXmlRecursive($value, $indent + 1); + $xml .= "$spaces\n"; + } else { + $xml .= "$spaces<$key>".htmlspecialchars($value)."\n"; + } + } + + return $xml; + } + private function createHtmlTable(array $data): string { if (empty($data)) { return '

No data available

'; } - + $html = "\n\n"; - + // Get headers from first row $headers = array_keys($data[0]); + foreach ($headers as $header) { - $html .= ""; + $html .= ""; } $html .= "\n\n\n"; - + // Add data rows foreach ($data as $row) { $html .= ""; + foreach ($headers as $header) { $value = $row[$header] ?? ''; - $html .= ""; + $html .= ""; } $html .= "\n"; } - + $html .= "\n
" . htmlspecialchars($header) . "".htmlspecialchars($header)."
" . htmlspecialchars($value) . "".htmlspecialchars($value)."
\n"; + return $html; } - - private function arrayToCsv(array $data): string { - if (empty($data)) { - return ''; - } - - $csv = ''; - - // Check if it's a list of objects/arrays - if (is_array($data[0] ?? null)) { - // Get headers - $headers = array_keys($data[0]); - $csv .= implode(',', $headers) . "\n"; - - // Add data rows - foreach ($data as $row) { - $csvRow = []; - foreach ($headers as $header) { - $value = $row[$header] ?? ''; - $csvRow[] = '"' . str_replace('"', '""', $value) . '"'; - } - $csv .= implode(',', $csvRow) . "\n"; - } - } else { - // Simple key-value pairs - $csv .= "Key,Value\n"; - foreach ($data as $key => $value) { - $csv .= '"' . str_replace('"', '""', $key) . '","' . str_replace('"', '""', $value) . "\"\n"; - } + + private function getSampleData(string $type, string $format): array { + switch ($type) { + case 'users': + return [ + ['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com'], + ['id' => 2, 'name' => 'Jane Smith', 'email' => 'jane@example.com'], + ['id' => 3, 'name' => 'Bob Johnson', 'email' => 'bob@example.com'] + ]; + case 'products': + return [ + ['id' => 1, 'name' => 'Laptop', 'price' => 999.99, 'category' => 'Electronics'], + ['id' => 2, 'name' => 'Book', 'price' => 19.99, 'category' => 'Education'], + ['id' => 3, 'name' => 'Coffee Mug', 'price' => 12.50, 'category' => 'Kitchen'] + ]; + default: + return [ + 'message' => 'Hello from WebFiori HTTP', + 'timestamp' => date('Y-m-d H:i:s'), + 'format_requested' => $format, + 'server_info' => [ + 'php_version' => PHP_VERSION, + 'server_time' => time() + ] + ]; } - - return $csv; } } diff --git a/examples/01-core/05-response-formats/TextResponseService.php b/examples/01-core/05-response-formats/TextResponseService.php index d86f417..1c96f52 100644 --- a/examples/01-core/05-response-formats/TextResponseService.php +++ b/examples/01-core/05-response-formats/TextResponseService.php @@ -2,15 +2,14 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; #[RestController('text-response', 'Plain text response service')] class TextResponseService extends WebService { - #[GetMapping] #[ResponseBody(contentType: 'text/plain')] #[AllowAnonymous] @@ -22,11 +21,13 @@ public function getTextData(): string { 'php_version' => PHP_VERSION, 'server_time' => time() ]; - + $text = ''; + foreach ($data as $key => $value) { $text .= "$key: $value\n"; } + return $text; } } diff --git a/examples/01-core/05-response-formats/XmlResponseService.php b/examples/01-core/05-response-formats/XmlResponseService.php index 9701650..19749d5 100644 --- a/examples/01-core/05-response-formats/XmlResponseService.php +++ b/examples/01-core/05-response-formats/XmlResponseService.php @@ -2,15 +2,14 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; #[RestController('xml-response', 'XML response service')] class XmlResponseService extends WebService { - #[GetMapping] #[ResponseBody(contentType: 'application/xml')] #[AllowAnonymous] @@ -24,24 +23,27 @@ public function getXmlData(): string { 'server_time' => time() ] ]; - + return $this->arrayToXml($data); } - + private function arrayToXml(array $data, string $root = 'response'): string { $xml = "\n<$root>\n"; + foreach ($data as $key => $value) { if (is_array($value)) { $xml .= " <$key>\n"; + foreach ($value as $k => $v) { - $xml .= " <$k>" . htmlspecialchars($v) . "\n"; + $xml .= " <$k>".htmlspecialchars($v)."\n"; } $xml .= " \n"; } else { - $xml .= " <$key>" . htmlspecialchars($value) . "\n"; + $xml .= " <$key>".htmlspecialchars($value)."\n"; } } $xml .= ""; + return $xml; } } diff --git a/examples/02-security/01-basic-auth/BasicAuthService.php b/examples/02-security/01-basic-auth/BasicAuthService.php index a6c0202..fb99389 100644 --- a/examples/02-security/01-basic-auth/BasicAuthService.php +++ b/examples/02-security/01-basic-auth/BasicAuthService.php @@ -2,25 +2,24 @@ require_once '../../../vendor/autoload.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequiresAuth; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * Service demonstrating HTTP Basic authentication */ #[RestController('secure', 'Secure service with Basic authentication')] class BasicAuthService extends WebService { - // Predefined users (in real app, use database) private const USERS = [ 'admin' => 'password123', 'user' => 'userpass', 'guest' => 'guestpass' ]; - + #[GetMapping] #[ResponseBody] #[RequiresAuth] @@ -29,7 +28,7 @@ public function getSecureData(): array { $authHeader = $this->getAuthHeader(); $credentials = base64_decode($authHeader->getCredentials()); [$username] = explode(':', $credentials, 2); - + return [ 'user' => $username, 'authenticated_at' => date('Y-m-d H:i:s'), @@ -41,54 +40,42 @@ public function getSecureData(): array { ] ]; } - + public function isAuthorized(): bool { $authHeader = $this->getAuthHeader(); - + if ($authHeader === null) { return false; } - + $scheme = $authHeader->getScheme(); $credentials = $authHeader->getCredentials(); - + // Check if it's Basic authentication if ($scheme !== 'basic') { return false; } - + // Decode base64 credentials $decoded = base64_decode($credentials); - + if ($decoded === false) { return false; } - + // Split username and password $parts = explode(':', $decoded, 2); - + if (count($parts) !== 2) { return false; } - + [$username, $password] = $parts; - + // Validate credentials return $this->validateUser($username, $password); } - - private function validateUser(string $username, string $password): bool { - if (!isset(self::USERS[$username])) { - return false; - } - - if (self::USERS[$username] !== $password) { - return false; - } - - return true; - } - + private function getUserAccessLevel(string $username): string { switch ($username) { case 'admin': @@ -101,4 +88,16 @@ private function getUserAccessLevel(string $username): string { return 'unknown'; } } + + private function validateUser(string $username, string $password): bool { + if (!isset(self::USERS[$username])) { + return false; + } + + if (self::USERS[$username] !== $password) { + return false; + } + + return true; + } } diff --git a/examples/02-security/02-bearer-tokens/LoginService.php b/examples/02-security/02-bearer-tokens/LoginService.php index 141e75f..d9ccc24 100644 --- a/examples/02-security/02-bearer-tokens/LoginService.php +++ b/examples/02-security/02-bearer-tokens/LoginService.php @@ -3,23 +3,21 @@ require_once '../../../vendor/autoload.php'; require_once 'TokenHelper.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; -use WebFiori\Http\Annotations\PostMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Json\Json; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; #[RestController('login', 'Login service to get Bearer token')] class LoginService extends WebService { - private const USERS = [ 'admin' => ['password' => 'password123', 'role' => 'admin'], 'user' => ['password' => 'userpass', 'role' => 'user'], 'guest' => ['password' => 'guestpass', 'role' => 'guest'] ]; - + #[PostMapping] #[ResponseBody] #[AllowAnonymous] @@ -28,23 +26,22 @@ class LoginService extends WebService { public function login(string $username, string $password): array { // Validate credentials if (!isset(self::USERS[$username]) || self::USERS[$username]['password'] !== $password) { - throw new \InvalidArgumentException('Invalid credentials'); + throw new InvalidArgumentException('Invalid credentials'); } - + // Generate token $userData = [ 'username' => $username, 'role' => self::USERS[$username]['role'], 'login_time' => date('Y-m-d H:i:s') ]; - + $token = TokenHelper::generateToken($userData); - + return [ 'token' => $token, 'user' => $userData, 'expires_in' => 3600 ]; } - } diff --git a/examples/02-security/02-bearer-tokens/ProfileService.php b/examples/02-security/02-bearer-tokens/ProfileService.php index 351d97b..1ba9950 100644 --- a/examples/02-security/02-bearer-tokens/ProfileService.php +++ b/examples/02-security/02-bearer-tokens/ProfileService.php @@ -3,15 +3,14 @@ require_once '../../../vendor/autoload.php'; require_once 'TokenHelper.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequiresAuth; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; #[RestController('profile', 'Get user profile with Bearer token')] class ProfileService extends WebService { - #[GetMapping] #[ResponseBody] #[RequiresAuth] @@ -20,27 +19,27 @@ public function getProfile(): array { $token = $authHeader->getCredentials(); $tokenData = TokenHelper::validateToken($token); $user = $tokenData['user']; - + return [ 'user' => $user, 'permissions' => $this->getUserPermissions($user['role']), 'last_access' => date('Y-m-d H:i:s') ]; } - + public function isAuthorized(): bool { $authHeader = $this->getAuthHeader(); - + if ($authHeader === null || $authHeader->getScheme() !== 'bearer') { return false; } - + $token = $authHeader->getCredentials(); $tokenData = TokenHelper::validateToken($token); - + return $tokenData !== null; } - + private function getUserPermissions(string $role): array { switch ($role) { case 'admin': diff --git a/examples/02-security/02-bearer-tokens/TokenAuthService.php b/examples/02-security/02-bearer-tokens/TokenAuthService.php index e1f804f..b5a3b57 100644 --- a/examples/02-security/02-bearer-tokens/TokenAuthService.php +++ b/examples/02-security/02-bearer-tokens/TokenAuthService.php @@ -3,14 +3,14 @@ require_once '../../../vendor/autoload.php'; require_once 'TokenHelper.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\PostMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequestParam; -use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\RequiresAuth; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; use WebFiori\Json\Json; /** @@ -18,54 +18,14 @@ */ #[RestController('auth', 'Bearer token authentication service')] class TokenAuthService extends WebService { - private const USERS = [ 'admin' => ['password' => 'password123', 'role' => 'admin'], 'user' => ['password' => 'userpass', 'role' => 'user'], 'guest' => ['password' => 'guestpass', 'role' => 'guest'] ]; - + private ?array $currentUser = null; - - #[PostMapping] - #[ResponseBody] - #[AllowAnonymous] - #[RequestParam('operation', 'string', false, null, 'Operation: login')] - public function login(?string $operation = 'login'): array { - $inputs = $this->getInputs(); - - if (!($inputs instanceof Json)) { - throw new \InvalidArgumentException('JSON body required for login'); - } - - $username = $inputs->get('username'); - $password = $inputs->get('password'); - - if (!$username || !$password) { - throw new \InvalidArgumentException('Username and password required'); - } - - // Validate credentials - if (!isset(self::USERS[$username]) || self::USERS[$username]['password'] !== $password) { - throw new \InvalidArgumentException('Invalid credentials'); - } - - // Generate token - $userData = [ - 'username' => $username, - 'role' => self::USERS[$username]['role'], - 'login_time' => date('Y-m-d H:i:s') - ]; - - $token = TokenHelper::generateToken($userData); - - return [ - 'token' => $token, - 'user' => $userData, - 'expires_in' => 3600 - ]; - } - + #[GetMapping] #[ResponseBody] #[RequiresAuth] @@ -76,78 +36,117 @@ public function handleAuthenticatedAction(?string $operation = 'profile'): array $token = $authHeader->getCredentials(); $tokenData = TokenHelper::validateToken($token); $this->currentUser = $tokenData['user']; - + switch ($operation) { case 'profile': return $this->handleProfile(); case 'refresh': return $this->handleRefresh(); default: - throw new \InvalidArgumentException('Unknown operation'); + throw new InvalidArgumentException('Unknown operation'); } } - + public function isAuthorized(): bool { // All actions require Bearer token (login uses AllowAnonymous) $authHeader = $this->getAuthHeader(); - + if ($authHeader === null) { return false; } - + $scheme = $authHeader->getScheme(); $token = $authHeader->getCredentials(); - + if ($scheme !== 'bearer') { return false; } - + // Validate token $tokenData = TokenHelper::validateToken($token); - + return $tokenData !== null; } - + + #[PostMapping] + #[ResponseBody] + #[AllowAnonymous] + #[RequestParam('operation', 'string', false, null, 'Operation: login')] + public function login(?string $operation = 'login'): array { + $inputs = $this->getInputs(); + + if (!($inputs instanceof Json)) { + throw new InvalidArgumentException('JSON body required for login'); + } + + $username = $inputs->get('username'); + $password = $inputs->get('password'); + + if (!$username || !$password) { + throw new InvalidArgumentException('Username and password required'); + } + + // Validate credentials + if (!isset(self::USERS[$username]) || self::USERS[$username]['password'] !== $password) { + throw new InvalidArgumentException('Invalid credentials'); + } + + // Generate token + $userData = [ + 'username' => $username, + 'role' => self::USERS[$username]['role'], + 'login_time' => date('Y-m-d H:i:s') + ]; + + $token = TokenHelper::generateToken($userData); + + return [ + 'token' => $token, + 'user' => $userData, + 'expires_in' => 3600 + ]; + } + + private function getUserPermissions(string $role): array { + switch ($role) { + case 'admin': + return ['read', 'write', 'delete', 'admin']; + case 'user': + return ['read', 'write']; + case 'guest': + return ['read']; + default: + return []; + } + } + private function handleProfile(): array { if (!$this->currentUser) { - throw new \RuntimeException('User context not available'); + throw new RuntimeException('User context not available'); } - + return [ 'user' => $this->currentUser, 'permissions' => $this->getUserPermissions($this->currentUser['role']), 'last_access' => date('Y-m-d H:i:s') ]; } - + private function handleRefresh(): array { if (!$this->currentUser) { - throw new \RuntimeException('User context not available'); + throw new RuntimeException('User context not available'); } - + // Generate new token with updated timestamp $userData = $this->currentUser; $userData['refresh_time'] = date('Y-m-d H:i:s'); - + $newToken = TokenHelper::generateToken($userData); - + return [ 'token' => $newToken, 'user' => $userData, 'expires_in' => 3600 ]; } - - private function getUserPermissions(string $role): array { - switch ($role) { - case 'admin': - return ['read', 'write', 'delete', 'admin']; - case 'user': - return ['read', 'write']; - case 'guest': - return ['read']; - default: - return []; - } - } } diff --git a/examples/02-security/02-bearer-tokens/TokenHelper.php b/examples/02-security/02-bearer-tokens/TokenHelper.php index fecff73..9ab8c51 100644 --- a/examples/02-security/02-bearer-tokens/TokenHelper.php +++ b/examples/02-security/02-bearer-tokens/TokenHelper.php @@ -6,64 +6,63 @@ * Helper class for token generation and validation */ class TokenHelper { - private const SECRET_KEY = 'your-secret-key-here'; private const TOKEN_EXPIRY = 3600; // 1 hour - + public static function generateToken(array $userData): string { $payload = [ 'user' => $userData, 'iat' => time(), 'exp' => time() + self::TOKEN_EXPIRY ]; - + $header = base64_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256'])); $payload = base64_encode(json_encode($payload)); $signature = base64_encode(hash_hmac('sha256', "$header.$payload", self::SECRET_KEY, true)); - + return "$header.$payload.$signature"; } - + + public static function isTokenExpired(string $token): bool { + $parts = explode('.', $token); + + if (count($parts) !== 3) { + return true; + } + + $payloadData = json_decode(base64_decode($parts[1]), true); + + return isset($payloadData['exp']) && $payloadData['exp'] < time(); + } + public static function validateToken(string $token): ?array { $parts = explode('.', $token); - + if (count($parts) !== 3) { return null; } - + [$header, $payload, $signature] = $parts; - + // Verify signature $expectedSignature = base64_encode(hash_hmac('sha256', "$header.$payload", self::SECRET_KEY, true)); - + if ($signature !== $expectedSignature) { return null; } - + // Decode payload $payloadData = json_decode(base64_decode($payload), true); - + if (!$payloadData) { return null; } - + // Check expiration if (isset($payloadData['exp']) && $payloadData['exp'] < time()) { return null; // Token expired } - + return $payloadData; } - - public static function isTokenExpired(string $token): bool { - $parts = explode('.', $token); - - if (count($parts) !== 3) { - return true; - } - - $payloadData = json_decode(base64_decode($parts[1]), true); - - return isset($payloadData['exp']) && $payloadData['exp'] < time(); - } } diff --git a/examples/02-security/04-role-based-access/AdminService.php b/examples/02-security/04-role-based-access/AdminService.php index 5d91a5f..a344957 100644 --- a/examples/02-security/04-role-based-access/AdminService.php +++ b/examples/02-security/04-role-based-access/AdminService.php @@ -3,19 +3,18 @@ require_once '../../../vendor/autoload.php'; require_once 'DemoUser.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\PreAuthorize; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; use WebFiori\Http\SecurityContext; +use WebFiori\Http\WebService; /** * Admin panel service - requires ADMIN role */ #[RestController('admin', 'Administrative panel service')] class AdminService extends WebService { - #[GetMapping] #[ResponseBody] #[PreAuthorize("hasRole('ADMIN')")] @@ -35,7 +34,7 @@ public function getAdminPanel(): array { ] ]; } - + public function isAuthorized(): bool { $demoUser = new DemoUser( id: 1, @@ -43,8 +42,9 @@ public function isAuthorized(): bool { roles: ['USER', 'ADMIN'], authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE'] ); - + SecurityContext::setCurrentUser($demoUser); + return true; } } diff --git a/examples/02-security/04-role-based-access/DemoUser.php b/examples/02-security/04-role-based-access/DemoUser.php index af07032..44022c5 100644 --- a/examples/02-security/04-role-based-access/DemoUser.php +++ b/examples/02-security/04-role-based-access/DemoUser.php @@ -8,31 +8,31 @@ * Demo user implementation */ class DemoUser implements SecurityPrincipal { - public function __construct( private int|string $id, private string $name, private array $roles = [], private array $authorities = [], private bool $active = true - ) {} - + ) { + } + + public function getAuthorities(): array { + return $this->authorities; + } + public function getId(): int|string { return $this->id; } - + public function getName(): string { return $this->name; } - + public function getRoles(): array { return $this->roles; } - - public function getAuthorities(): array { - return $this->authorities; - } - + public function isActive(): bool { return $this->active; } diff --git a/examples/02-security/04-role-based-access/PublicService.php b/examples/02-security/04-role-based-access/PublicService.php index e2b3229..294b0b0 100644 --- a/examples/02-security/04-role-based-access/PublicService.php +++ b/examples/02-security/04-role-based-access/PublicService.php @@ -3,19 +3,18 @@ require_once '../../../vendor/autoload.php'; require_once 'DemoUser.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\GetMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; use WebFiori\Http\SecurityContext; +use WebFiori\Http\WebService; /** * Public information service - no authentication required */ #[RestController('public', 'Public information service')] class PublicService extends WebService { - #[GetMapping] #[ResponseBody] #[AllowAnonymous] @@ -29,7 +28,7 @@ public function getPublicInfo(): array { ] ]; } - + public function isAuthorized(): bool { return true; // Public service, no auth needed } diff --git a/examples/02-security/04-role-based-access/UserManagerService.php b/examples/02-security/04-role-based-access/UserManagerService.php index 028e00c..83cde44 100644 --- a/examples/02-security/04-role-based-access/UserManagerService.php +++ b/examples/02-security/04-role-based-access/UserManagerService.php @@ -3,19 +3,31 @@ require_once '../../../vendor/autoload.php'; require_once 'DemoUser.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\PreAuthorize; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; use WebFiori\Http\SecurityContext; +use WebFiori\Http\WebService; /** * User management service - requires USER_MANAGE authority */ #[RestController('user-manager', 'User management operations service')] class UserManagerService extends WebService { - + public function isAuthorized(): bool { + $demoUser = new DemoUser( + id: 1, + name: 'Demo User', + roles: ['USER', 'ADMIN'], + authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE'] + ); + + SecurityContext::setCurrentUser($demoUser); + + return true; + } + #[GetMapping] #[ResponseBody] #[PreAuthorize("hasAuthority('USER_MANAGE')")] @@ -35,16 +47,4 @@ public function manageUsers(): array { ] ]; } - - public function isAuthorized(): bool { - $demoUser = new DemoUser( - id: 1, - name: 'Demo User', - roles: ['USER', 'ADMIN'], - authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE'] - ); - - SecurityContext::setCurrentUser($demoUser); - return true; - } } diff --git a/examples/02-security/04-role-based-access/UserService.php b/examples/02-security/04-role-based-access/UserService.php index e1ccb49..9f3a132 100644 --- a/examples/02-security/04-role-based-access/UserService.php +++ b/examples/02-security/04-role-based-access/UserService.php @@ -3,19 +3,18 @@ require_once '../../../vendor/autoload.php'; require_once 'DemoUser.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; use WebFiori\Http\Annotations\GetMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\RequiresAuth; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; use WebFiori\Http\SecurityContext; +use WebFiori\Http\WebService; /** * User profile service - requires authentication */ #[RestController('user', 'User profile and information service')] class UserService extends WebService { - #[GetMapping] #[ResponseBody] #[RequiresAuth] @@ -28,7 +27,7 @@ public function getUserInfo(): array { 'access_time' => date('Y-m-d H:i:s') ]; } - + public function isAuthorized(): bool { $demoUser = new DemoUser( id: 1, @@ -36,8 +35,9 @@ public function isAuthorized(): bool { roles: ['USER', 'ADMIN'], authorities: ['USER_CREATE', 'USER_UPDATE', 'USER_DELETE', 'USER_READ', 'USER_MANAGE'] ); - + SecurityContext::setCurrentUser($demoUser); + return true; } } diff --git a/examples/04-advanced/01-object-mapping/GetObjectMappingService.php b/examples/04-advanced/01-object-mapping/GetObjectMappingService.php index 454918a..8839f19 100644 --- a/examples/04-advanced/01-object-mapping/GetObjectMappingService.php +++ b/examples/04-advanced/01-object-mapping/GetObjectMappingService.php @@ -3,24 +3,23 @@ require_once '../../../vendor/autoload.php'; require_once 'User.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * getObject() mapping service */ #[RestController('getobject', 'getObject() mapping')] class GetObjectMappingService extends WebService { - #[PostMapping] #[ResponseBody] #[AllowAnonymous] public function create(): array { $user = $this->getObject(User::class); - + return [ 'message' => 'User created with getObject mapping', 'user' => $user->toArray(), diff --git a/examples/04-advanced/01-object-mapping/ManualMappingService.php b/examples/04-advanced/01-object-mapping/ManualMappingService.php index 2e54ac5..c39f6f6 100644 --- a/examples/04-advanced/01-object-mapping/ManualMappingService.php +++ b/examples/04-advanced/01-object-mapping/ManualMappingService.php @@ -3,31 +3,38 @@ require_once '../../../vendor/autoload.php'; require_once 'User.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\ResponseBody; -use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * Manual object mapping service */ #[RestController('manual', 'Manual object mapping')] class ManualMappingService extends WebService { - #[PostMapping] #[ResponseBody] #[AllowAnonymous] public function create(): array { $inputs = $this->getInputs(); $user = new User(); - - if ($inputs instanceof \WebFiori\Json\Json) { - if ($inputs->hasKey('name')) $user->setName($inputs->get('name')); - if ($inputs->hasKey('email')) $user->setEmail($inputs->get('email')); - if ($inputs->hasKey('age')) $user->setAge($inputs->get('age')); + + if ($inputs instanceof WebFiori\Json\Json) { + if ($inputs->hasKey('name')) { + $user->setName($inputs->get('name')); + } + + if ($inputs->hasKey('email')) { + $user->setEmail($inputs->get('email')); + } + + if ($inputs->hasKey('age')) { + $user->setAge($inputs->get('age')); + } } - + return [ 'message' => 'User created with manual mapping', 'user' => $user->toArray(), diff --git a/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php b/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php index b671623..f2857d9 100644 --- a/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php +++ b/examples/04-advanced/01-object-mapping/MapEntityCustomMappingService.php @@ -3,19 +3,18 @@ require_once '../../../vendor/autoload.php'; require_once 'User.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; -use WebFiori\Http\Annotations\PostMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\MapEntity; +use WebFiori\Http\Annotations\PostMapping; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * MapEntity with custom setters mapping service */ #[RestController('mapentity-custom', 'MapEntity with custom setters')] class MapEntityCustomMappingService extends WebService { - #[PostMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/04-advanced/01-object-mapping/MapEntityMappingService.php b/examples/04-advanced/01-object-mapping/MapEntityMappingService.php index aa1a5b5..b65b061 100644 --- a/examples/04-advanced/01-object-mapping/MapEntityMappingService.php +++ b/examples/04-advanced/01-object-mapping/MapEntityMappingService.php @@ -3,19 +3,18 @@ require_once '../../../vendor/autoload.php'; require_once 'User.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; -use WebFiori\Http\Annotations\PostMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\AllowAnonymous; use WebFiori\Http\Annotations\MapEntity; +use WebFiori\Http\Annotations\PostMapping; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * MapEntity attribute mapping service */ #[RestController('mapentity', 'MapEntity attribute mapping')] class MapEntityMappingService extends WebService { - #[PostMapping] #[ResponseBody] #[AllowAnonymous] diff --git a/examples/04-advanced/01-object-mapping/TraditionalMappingService.php b/examples/04-advanced/01-object-mapping/TraditionalMappingService.php index ff219a3..db95cee 100644 --- a/examples/04-advanced/01-object-mapping/TraditionalMappingService.php +++ b/examples/04-advanced/01-object-mapping/TraditionalMappingService.php @@ -3,19 +3,18 @@ require_once '../../../vendor/autoload.php'; require_once 'User.php'; -use WebFiori\Http\WebService; -use WebFiori\Http\Annotations\RestController; -use WebFiori\Http\Annotations\PostMapping; -use WebFiori\Http\Annotations\ResponseBody; use WebFiori\Http\Annotations\AllowAnonymous; +use WebFiori\Http\Annotations\PostMapping; use WebFiori\Http\Annotations\RequestParam; +use WebFiori\Http\Annotations\ResponseBody; +use WebFiori\Http\Annotations\RestController; +use WebFiori\Http\WebService; /** * Traditional parameter mapping service */ #[RestController('traditional', 'Traditional parameter mapping')] class TraditionalMappingService extends WebService { - #[PostMapping] #[ResponseBody] #[AllowAnonymous] @@ -27,7 +26,7 @@ public function create(string $name, string $email, int $age): array { $user->setName($name); $user->setEmail($email); $user->setAge($age); - + return [ 'message' => 'User created with traditional parameters', 'user' => $user->toArray(), diff --git a/examples/04-advanced/01-object-mapping/User.php b/examples/04-advanced/01-object-mapping/User.php index 71e83a4..4a51b9e 100644 --- a/examples/04-advanced/01-object-mapping/User.php +++ b/examples/04-advanced/01-object-mapping/User.php @@ -4,106 +4,106 @@ * User data model class */ class User { - private ?string $name = null; - private ?string $email = null; + private ?string $address = null; private ?int $age = null; + private ?string $email = null; + private ?string $name = null; private ?string $phone = null; - private ?string $address = null; - - public function setName(string $name): void { - $this->name = trim($name); - } - - public function getName(): ?string { - return $this->name; + + public function getAddress(): ?string { + return $this->address; } - - public function setEmail(string $email): void { - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - throw new \InvalidArgumentException('Invalid email format'); - } - $this->email = strtolower($email); + + public function getAge(): ?int { + return $this->age; } - + public function getEmail(): ?string { return $this->email; } - - public function setAge(int $age): void { - if ($age < 0 || $age > 150) { - throw new \InvalidArgumentException('Age must be between 0 and 150'); - } - $this->age = $age; - } - - public function getAge(): ?int { - return $this->age; - } - - public function setPhone(?string $phone): void { - if ($phone !== null) { - $this->phone = preg_replace('/[^0-9+\-\s]/', '', $phone); - } + + public function getName(): ?string { + return $this->name; } - + public function getPhone(): ?string { return $this->phone; } - + public function setAddress(?string $address): void { if ($address !== null) { $this->address = trim($address); } } - - public function getAddress(): ?string { - return $this->address; + + public function setAge(int $age): void { + if ($age < 0 || $age > 150) { + throw new InvalidArgumentException('Age must be between 0 and 150'); + } + $this->age = $age; } - + + public function setEmail(string $email): void { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException('Invalid email format'); + } + $this->email = strtolower($email); + } + + public function setEmailAddress(?string $email): void { + if ($email !== null) { + $this->setEmail($email); + } + } + // Custom setters for alternative parameter names public function setFullName(?string $name): void { if ($name !== null) { $this->setName($name); } } - - public function setEmailAddress(?string $email): void { - if ($email !== null) { - $this->setEmail($email); + + public function setName(string $name): void { + $this->name = trim($name); + } + + public function setPhone(?string $phone): void { + if ($phone !== null) { + $this->phone = preg_replace('/[^0-9+\-\s]/', '', $phone); } } - + public function setUserAge(?int $age): void { if ($age !== null) { $this->setAge($age); } } - + + public function toArray(): array { + return [ + 'name' => $this->name, + 'email' => $this->email, + 'age' => $this->age, + 'phone' => $this->phone, + 'address' => $this->address + ]; + } + public function validate(): array { $errors = []; - + if (empty($this->name)) { $errors[] = 'Name is required'; } - + if (empty($this->email)) { $errors[] = 'Email is required'; } - + if ($this->age === null) { $errors[] = 'Age is required'; } - + return $errors; } - - public function toArray(): array { - return [ - 'name' => $this->name, - 'email' => $this->email, - 'age' => $this->age, - 'phone' => $this->phone, - 'address' => $this->address - ]; - } } diff --git a/examples/04-advanced/03-manual-openapi/OpenAPIService.php b/examples/04-advanced/03-manual-openapi/OpenAPIService.php index 3c644c8..7d89712 100644 --- a/examples/04-advanced/03-manual-openapi/OpenAPIService.php +++ b/examples/04-advanced/03-manual-openapi/OpenAPIService.php @@ -1,19 +1,18 @@ setRequestMethods([RequestMethod::GET]); } - + public function processRequest() { $openApiObj = $this->getManager()->toOpenAPI(); $info = $openApiObj->getInfo(); diff --git a/examples/04-advanced/03-manual-openapi/ProductService.php b/examples/04-advanced/03-manual-openapi/ProductService.php index 1962b9c..b5fc4d1 100644 --- a/examples/04-advanced/03-manual-openapi/ProductService.php +++ b/examples/04-advanced/03-manual-openapi/ProductService.php @@ -1,19 +1,35 @@ 3, 'name' => $name, 'price' => $price]; + } + + #[DeleteMapping] + #[ResponseBody] + #[AllowAnonymous] + #[Param('id', ParamType::INT, 'Product ID')] + public function deleteProduct(int $id): array { + return ['deleted' => $id]; + } + #[GetMapping] #[ResponseBody] #[AllowAnonymous] @@ -24,16 +40,7 @@ public function listProducts(?string $category): array { ['id' => 2, 'name' => 'Product B', 'category' => $category ?? 'Electronics'] ]; } - - #[PostMapping] - #[ResponseBody] - #[AllowAnonymous] - #[Param('name', ParamType::STRING, 'Product name')] - #[Param('price', ParamType::DOUBLE, 'Product price', min: 0)] - public function createProduct(string $name, float $price): array { - return ['id' => 3, 'name' => $name, 'price' => $price]; - } - + #[PutMapping] #[ResponseBody] #[AllowAnonymous] @@ -42,12 +49,4 @@ public function createProduct(string $name, float $price): array { public function updateProduct(int $id, string $name): array { return ['id' => $id, 'name' => $name]; } - - #[DeleteMapping] - #[ResponseBody] - #[AllowAnonymous] - #[Param('id', ParamType::INT, 'Product ID')] - public function deleteProduct(int $id): array { - return ['deleted' => $id]; - } } diff --git a/examples/04-advanced/03-manual-openapi/UserService.php b/examples/04-advanced/03-manual-openapi/UserService.php index 8edeb75..26c5ce2 100644 --- a/examples/04-advanced/03-manual-openapi/UserService.php +++ b/examples/04-advanced/03-manual-openapi/UserService.php @@ -1,29 +1,16 @@ $id ?? 1, - 'name' => 'John Doe', - 'email' => 'john@example.com' - ]; - } - #[PostMapping] #[ResponseBody] #[AllowAnonymous] @@ -36,4 +23,16 @@ public function createUser(string $name, string $email): array { 'email' => $email ]; } + + #[GetMapping] + #[ResponseBody] + #[AllowAnonymous] + #[Param('id', ParamType::INT, 'User ID', min: 1)] + public function getUser(?int $id): array { + return [ + 'id' => $id ?? 1, + 'name' => 'John Doe', + 'email' => 'john@example.com' + ]; + } } diff --git a/php_cs.php.dist b/php_cs.php.dist index ac0a570..0b4f088 100644 --- a/php_cs.php.dist +++ b/php_cs.php.dist @@ -11,7 +11,7 @@ return $config->setRules([ 'align_multiline_comment' => [ 'comment_type' => 'phpdocs_only' ], - 'array_indentation' => [], + 'array_indentation' => true, 'array_syntax' => [ 'syntax' => 'short' ],