From 05b1b045a120f3d04c8611428db31fb699638469 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 27 Nov 2025 00:13:04 +0300 Subject: [PATCH 01/50] feat: Add Support for Parameter Request Methods --- WebFiori/Http/ParamOption.php | 6 +- WebFiori/Http/RequestParameter.php | 96 +++++++++++++++++++++------- WebFiori/Http/WebServicesManager.php | 7 +- 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/WebFiori/Http/ParamOption.php b/WebFiori/Http/ParamOption.php index fa53a1f..355918c 100644 --- a/WebFiori/Http/ParamOption.php +++ b/WebFiori/Http/ParamOption.php @@ -20,6 +20,10 @@ class ParamOption { * not provided. */ const DEFAULT = 'default'; + /** + * An option which is used to set the methods at which the parameter must exist. + */ + const METHODS = 'methods'; /** * An option which is used to set a description for the parameter */ @@ -36,7 +40,7 @@ class ParamOption { * An option which is used to set maximum allowed value. Applicable to numerical * types only. */ - const MAX = 'maxt'; + const MAX = 'max'; /** * An option which is used to set minimum allowed length. Applicable to string types only. */ diff --git a/WebFiori/Http/RequestParameter.php b/WebFiori/Http/RequestParameter.php index 50837df..87e5924 100644 --- a/WebFiori/Http/RequestParameter.php +++ b/WebFiori/Http/RequestParameter.php @@ -107,6 +107,13 @@ class RequestParameter implements JsonI { * */ private $type; + /** + * An array of request methods at which the parameter must exist. + * + * @var array + * + */ + private $methods; /** * Creates new instance of the class. * @@ -149,6 +156,7 @@ public function __construct(string $name, string $type = 'string', bool $isOptio } $this->applyBasicFilter = true; $this->isEmptyStrAllowed = false; + $this->methods = []; } /** * Returns a string that represents the object. @@ -225,10 +233,10 @@ public function __toString() { * If it was not created for any reason, the method will return null. * */ - public static function create(array $options) { - if (isset($options['name'])) { - $paramType = $options['type'] ?? 'string'; - $param = new RequestParameter($options['name'], $paramType); + public static function create(array $options) : ?RequestParameter { + if (isset($options[ParamOption::NAME])) { + $paramType = $options[ParamOption::TYPE] ?? 'string'; + $param = new RequestParameter($options[ParamOption::NAME], $paramType); self::checkParamAttrs($param, $options); return $param; @@ -714,43 +722,85 @@ public function toJSON() : Json { * @param array $options */ private static function checkParamAttrs(RequestParameter $param, array $options) { - $isOptional = $options['optional'] ?? false; + $isOptional = $options[ParamOption::OPTIONAL] ?? false; $param->setIsOptional($isOptional); - if (isset($options['custom-filter'])) { - $param->setCustomFilterFunction($options['custom-filter']); + if (isset($options[ParamOption::FILTER])) { + $param->setCustomFilterFunction($options[ParamOption::FILTER]); } - if (isset($options['min'])) { - $param->setMinValue($options['min']); + if (isset($options[ParamOption::MIN])) { + $param->setMinValue($options[ParamOption::MIN]); } - if (isset($options['max'])) { - $param->setMaxValue($options['max']); + if (isset($options[ParamOption::MAX])) { + $param->setMaxValue($options[ParamOption::MAX]); } - if (isset($options['min-length'])) { - $param->setMinLength($options['min-length']); + if (isset($options[ParamOption::MIN_LENGTH])) { + $param->setMinLength($options[ParamOption::MIN_LENGTH]); } - if (isset($options['max-length'])) { - $param->setMaxLength($options['max-length']); + if (isset($options[ParamOption::MAX_LENGTH])) { + $param->setMaxLength($options[ParamOption::MAX_LENGTH]); } - if (isset($options['allow-empty'])) { - $param->setIsEmptyStringAllowed($options['allow-empty']); + if (isset($options[ParamOption::EMPTY])) { + $param->setIsEmptyStringAllowed($options[ParamOption::EMPTY]); } - if (isset($options['custom-filter'])) { - $param->setCustomFilterFunction($options['custom-filter']); + if (isset($options[ParamOption::METHODS])) { + $type = gettype($options[ParamOption::METHODS]); + if ($type == 'string') { + $param->addMethod($options[ParamOption::METHODS]); + } else if ($type == 'array') { + $param->addMethods($options[ParamOption::METHODS]); + } } - if (isset($options['default'])) { - $param->setDefault($options['default']); + if (isset($options[ParamOption::DEFAULT])) { + $param->setDefault($options[ParamOption::DEFAULT]); } - if (isset($options['description'])) { - $param->setDescription($options['description']); + if (isset($options[ParamOption::DESCRIPTION])) { + $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; } } diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index c504092..c3b39f7 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -455,9 +455,14 @@ public final function process() { $params = $actionObj->getParameters(); $this->filter->clearParametersDef(); $this->filter->clearInputs(); + $requestMethod = $this->getRequest()->getRequestMethod(); foreach ($params as $param) { - $this->filter->addRequestParameter($param); + $paramMethods = $param->getMethods(); + + if (count($paramMethods) == 0 || in_array($requestMethod, $paramMethods)) { + $this->filter->addRequestParameter($param); + } } $this->filterInputsHelper(); $i = $this->getInputs(); From 063c34a32545cdf1179ee314f814bff9db8bff67 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 27 Nov 2025 00:56:29 +0300 Subject: [PATCH 02/50] test: Added Test Cases --- WebFiori/Http/APIFilter.php | 6 ++ WebFiori/Http/WebServicesManager.php | 6 +- .../Tests/Http/RequestParameterTest.php | 12 ++++ .../CreateUserProfileServiceV2.php | 69 +++++++++++++++++++ .../TestServices/SampleServicesManager.php | 1 + tests/WebFiori/Tests/Http/WebServiceTest.php | 4 +- .../Tests/Http/WebServicesManagerTest.php | 57 ++++++++++++++- 7 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php diff --git a/WebFiori/Http/APIFilter.php b/WebFiori/Http/APIFilter.php index ea9551a..19fddfa 100644 --- a/WebFiori/Http/APIFilter.php +++ b/WebFiori/Http/APIFilter.php @@ -21,6 +21,7 @@ * */ class APIFilter { + private $requestParameters = []; /** * A constant that indicates a given value is invalid. * @@ -103,6 +104,10 @@ public function addRequestParameter(RequestParameter $reqParam) { $attribute[$filterIdx][] = FILTER_DEFAULT; } $this->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. @@ -118,6 +123,7 @@ public function clearInputs() { */ public function clearParametersDef() { $this->paramDefs = []; + $this->requestParameters = []; } /** * Filter the values of an associative array. diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index c3b39f7..ec3b53d 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -456,7 +456,7 @@ public final function process() { $this->filter->clearParametersDef(); $this->filter->clearInputs(); $requestMethod = $this->getRequest()->getRequestMethod(); - + foreach ($params as $param) { $paramMethods = $param->getMethods(); @@ -468,9 +468,9 @@ public final function process() { $i = $this->getInputs(); if (!($i instanceof Json)) { - $this->_processNonJson($params); + $this->_processNonJson($this->filter->getParameters()); } else { - $this->_processJson($params); + $this->_processJson($this->filter->getParameters()); } } } else { diff --git a/tests/WebFiori/Tests/Http/RequestParameterTest.php b/tests/WebFiori/Tests/Http/RequestParameterTest.php index 1962c57..93c719d 100644 --- a/tests/WebFiori/Tests/Http/RequestParameterTest.php +++ b/tests/WebFiori/Tests/Http/RequestParameterTest.php @@ -662,5 +662,17 @@ public function testToString01() { ." Minimum Length => 'null',\n" ." Maximum Length => 'null'\n" ."]\n",$rp.''); + + } + /** + * @test + */ + public function testRequestMethod00() { + $rp = new RequestParameter('user-id','integer'); + $this->assertEquals([], $rp->getMethods()); + $rp->addMethod('get'); + $this->assertEquals(['GET'], $rp->getMethods()); + $rp->addMethods(['geT', 'PoSt ']); + $this->assertEquals(['GET', 'POST'], $rp->getMethods()); } } diff --git a/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php new file mode 100644 index 0000000..e0eb4d3 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php @@ -0,0 +1,69 @@ +addRequestMethod(RequestMethod::POST); + $this->addRequestMethod(RequestMethod::GET); + $this->addParameters([ + 'id' => [ + ParamOption::TYPE => ParamType::INT, + ParamOption::METHODS => [RequestMethod::GET] + ], + 'name' => [ + ParamOption::TYPE => ParamType::STRING, + ParamOption::METHODS => [RequestMethod::POST] + ], + 'username' => [ + ParamOption::TYPE => ParamType::STRING, + ParamOption::METHODS => [RequestMethod::POST] + ], + 'x' => [ + ParamOption::TYPE => ParamType::INT, + ParamOption::OPTIONAL => true, + ParamOption::DEFAULT => 3 + ] + ]); + } + public function processRequest() { + + } + public function processGET() { + $j = new Json(); + $userObj = new TestUserObj(); + $userObj->setFullName('Ibx'); + $userObj->setId($this->getParamVal('id')); + $j->addObject('user', $userObj); + $this->send('application/json', $j); + } + public function processPOST() { + try { + $userObj = $this->getObject('not\\Exist', [ + 'name' => 'setFullName' + ]); + } catch (Exception $ex) { + $userObj = $this->getObject(TestUserObj::class, [ + 'name' => 'setFullName', + 'x' => 'setId' + ]); + } + $j = new Json(); + $j->addObject('user', $userObj); + $this->send('application/json', $j); + } + +} diff --git a/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php b/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php index 4916228..75b3cb3 100644 --- a/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php +++ b/tests/WebFiori/Tests/Http/TestServices/SampleServicesManager.php @@ -17,6 +17,7 @@ public function __construct() { $this->addService(new SumNumbersService()); $this->addService(new GetUserProfileService()); + $this->addService(new CreateUserProfileServiceV2()); $this->addService(new CreateUserProfileService()); $this->addService(new MulNubmersService()); } diff --git a/tests/WebFiori/Tests/Http/WebServiceTest.php b/tests/WebFiori/Tests/Http/WebServiceTest.php index 8f3a393..e9bdc6d 100644 --- a/tests/WebFiori/Tests/Http/WebServiceTest.php +++ b/tests/WebFiori/Tests/Http/WebServiceTest.php @@ -108,7 +108,8 @@ public function testAddParameters02() { 'password' => [ ParamOption::OPTIONAL => true, ParamOption::DEFAULT => 1234, - ParamOption::TYPE => 'integer' + ParamOption::TYPE => 'integer', + ParamOption::METHODS => 'get' ] ]); $this->assertEquals(2,count($action->getParameters())); @@ -119,6 +120,7 @@ public function testAddParameters02() { $this->assertEquals('integer', $param2->getType()); $this->assertTrue($param2->isOptional()); $this->assertEquals(1234, $param2->getDefault()); + $this->assertEquals(['GET'], $param2->getMethods()); } /** * @test diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php index 3a819ad..1e3ebe5 100644 --- a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php +++ b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php @@ -160,7 +160,7 @@ public function testConstructor00() { $this->assertEquals('1.0.1',$api->getVersion()); $this->assertEquals('NO DESCRIPTION',$api->getDescription()); $api->setDescription('Test API.'); - $this->assertEquals(5,count($api->getServices())); + $this->assertEquals(6,count($api->getServices())); $this->assertEquals('Test API.',$api->getDescription()); $this->assertTrue($api->getServiceByName('sum-array') instanceof AbstractWebService); $this->assertNull($api->getServiceByName('request-info')); @@ -300,6 +300,61 @@ public function testCreateUser01() { $manager->process(); $this->assertEquals('{"user":{"Id":3,"FullName":"Me","Username":"Cpool"}}', $manager->readOutputStream()); } + /** + * @test + */ + public function testCreateUser02() { + $this->clrearVars(); + putenv('REQUEST_METHOD=GET'); + $_GET['service'] = 'user-profile'; + $_GET['id'] = '99'; + $api = new SampleServicesManager(); + $api->setOutputStream($this->outputStreamName); + $api->process(); + $this->assertEquals('{"user":{"Id":99,"FullName":"Ibrahim"}}', $api->readOutputStream()); + } + /** + * @test + */ + public function testCreateUser03() { + $this->clrearVars(); + putenv('REQUEST_METHOD=POST'); + $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + $_POST['service'] = 'user-profile'; + $_POST['id'] = '99'; + $api = new SampleServicesManager(); + $api->setOutputStream($this->outputStreamName); + $api->process(); + $this->assertEquals('{"message":"The following required parameter(s) where missing from the request body: \'name\', \'username\'.","type":"error","http-code":404,"more-info":{"missing":["name","username"]}}', $api->readOutputStream()); + } + /** + * @test + */ + public function testCreateUser04() { + $this->clrearVars(); + putenv('REQUEST_METHOD=POST'); + $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + $_POST['service'] = 'user-profile'; + $_POST['name'] = '99'; + $_POST['username'] = 'Cool'; + $api = new SampleServicesManager(); + $api->setOutputStream($this->outputStreamName); + $api->process(); + $this->assertEquals('{"user":{"Id":3,"FullName":"99","Username":"Cool"}}', $api->readOutputStream()); + } + /** + * @test + */ + public function testCreateUser05() { + $this->clrearVars(); + putenv('REQUEST_METHOD=GET'); + $_GET['service'] = 'user-profile'; + + $api = new SampleServicesManager(); + $api->setOutputStream($this->outputStreamName); + $api->process(); + $this->assertEquals('{"message":"The following required parameter(s) where missing from the request body: \'id\'.","type":"error","http-code":404,"more-info":{"missing":["id"]}}', $api->readOutputStream()); + } /** * @test */ From 2d034754b2ef05b8bcdea71c6b81075c1a7a509c Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 27 Nov 2025 01:01:07 +0300 Subject: [PATCH 03/50] Update WebServicesManagerTest.php --- tests/WebFiori/Tests/Http/WebServicesManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php index 1e3ebe5..91d430c 100644 --- a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php +++ b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php @@ -311,7 +311,7 @@ public function testCreateUser02() { $api = new SampleServicesManager(); $api->setOutputStream($this->outputStreamName); $api->process(); - $this->assertEquals('{"user":{"Id":99,"FullName":"Ibrahim"}}', $api->readOutputStream()); + $this->assertEquals('{"user":{"Id":99,"FullName":"Ibx"}}', $api->readOutputStream()); } /** * @test From 0ea983a99f3deaefe932961721af64256e8f7dfb Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 2 Dec 2025 18:21:18 +0300 Subject: [PATCH 04/50] chore: Rename `AbstractWebService` to `WebService` --- WebFiori/Http/APITestCase.php | 2 +- WebFiori/Http/AbstractWebService.php | 840 +---------------- WebFiori/Http/ManagerInfoService.php | 4 +- WebFiori/Http/ObjectMapper.php | 4 +- WebFiori/Http/RequestParameter.php | 2 +- WebFiori/Http/WebService.php | 844 ++++++++++++++++++ WebFiori/Http/WebServicesManager.php | 34 +- examples/GetRandomService.php | 4 +- examples/HelloWithAuthService.php | 4 +- examples/HelloWorldService.php | 4 +- .../TestServices/AbstractNumbersService.php | 4 +- .../CreateUserProfileServiceV2.php | 4 +- .../Http/TestServices/MulNubmersService.php | 4 +- .../Tests/Http/TestServices/NoAuthService.php | 4 +- .../Http/TestServices/NotImplService.php | 4 +- .../Http/TestServices/TestServiceObj.php | 4 +- .../Tests/Http/WebServicesManagerTest.php | 6 +- 17 files changed, 890 insertions(+), 882 deletions(-) create mode 100644 WebFiori/Http/WebService.php diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php index 30c9208..cf9bf22 100644 --- a/WebFiori/Http/APITestCase.php +++ b/WebFiori/Http/APITestCase.php @@ -113,7 +113,7 @@ public function callEndpoint(WebServicesManager $manager, string $requestMethod, if (class_exists($apiEndpointName)) { $service = new $apiEndpointName(); - if ($service instanceof AbstractWebService) { + if ($service instanceof WebService) { $apiEndpointName = $service->getName(); } } diff --git a/WebFiori/Http/AbstractWebService.php b/WebFiori/Http/AbstractWebService.php index f6b97bb..516be9a 100644 --- a/WebFiori/Http/AbstractWebService.php +++ b/WebFiori/Http/AbstractWebService.php @@ -1,844 +1,8 @@ - *
  • 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 '_'.
  • - * - * If The given name is invalid, the name of the service will be set to 'new-service'. - * - * @param string $name The name of the web service. - * - * @param WebServicesManager|null $owner The manager which is used to - * manage the web service. - */ - public function __construct(string $name) { - if (!$this->setName($name)) { - $this->setName('new-service'); - } - $this->reqMethods = []; - $this->parameters = []; - $this->responses = []; - $this->requireAuth = true; - $this->sinceVersion = '1.0.0'; - $this->serviceDesc = ''; - $this->request = Request::createFromGlobals(); - } - /** - * 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() { - $retVal = "APIAction[\n"; - $retVal .= " Name => '".$this->getName()."',\n"; - $retVal .= " Description => '".$this->getDescription()."',\n"; - $since = $this->getSince() === null ? 'null' : $this->getSince(); - $retVal .= " Since => '$since',\n"; - $reqMethodsStr = "[\n"; - $comma = ','; - - for ($x = 0, $count = count($this->getRequestMethods()) ; $x < $count ; $x++) { - $meth = $this->getRequestMethods()[$x]; - - if ($x + 1 == $count) { - $comma = ''; - } - $reqMethodsStr .= " $meth$comma\n"; - } - $reqMethodsStr .= " ],\n"; - $retVal .= " Request Methods => $reqMethodsStr"; - $paramsStr = "[\n"; - - $comma = ','; - - for ($x = 0 , $count = count($this->getParameters()); $x < $count ; $x++) { - $param = $this->getParameters()[$x]; - $paramsStr .= " ".$param->getName()." => [\n"; - $paramsStr .= " Type => '".$param->getType()."',\n"; - $descStr = $param->getDescription() === null ? 'null' : $param->getDescription(); - $paramsStr .= " Description => '$descStr',\n"; - $isOptional = $param->isOptional() ? 'true' : 'false'; - $paramsStr .= " Is Optional => '$isOptional',\n"; - $defaultStr = $param->getDefault() === null ? 'null' : $param->getDefault(); - $paramsStr .= " Default => '$defaultStr',\n"; - $min = $param->getMinValue() === null ? 'null' : $param->getMinValue(); - $paramsStr .= " Minimum Value => '$min',\n"; - $max = $param->getMaxValue() === null ? 'null' : $param->getMaxValue(); - - if ($x + 1 == $count) { - $comma = ''; - } - $paramsStr .= " Maximum Value => '$max'\n ]$comma\n"; - } - $paramsStr .= " ],\n"; - $retVal .= " Parameters => $paramsStr"; - $responsesStr = "[\n"; - $count = count($this->getResponsesDescriptions()); - $comma = ','; - - for ($x = 0 ; $x < $count ; $x++) { - if ($x + 1 == $count) { - $comma = ''; - } - $responsesStr .= " Response #$x => '".$this->getResponsesDescriptions()[$x]."'".$comma."\n"; - } - $responsesStr .= " ]\n"; - - return $retVal." Responses Descriptions => $responsesStr]\n"; - } - /** - * 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())) { - $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 final function addResponseDescription(string $description) { - $trimmed = trim($description); - - if (strlen($trimmed) != 0) { - $this->responses[] = $trimmed; - } - } - /** - * 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) { - $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. - * - */ - 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; - } - /** - * Checks if the client is authorized to use the service or not. - * - * The developer should implement this method in a way it returns a boolean. - * If the method returns true, it means the client is allowed to use the service. - * If the method returns false, then he is not authorized and a 401 error - * code will be sent back. If the method returned nothing, then it means the - * user is authorized to call the API. If WebFiori framework is used, it is - * possible to perform the functionality of this method using middleware. - * - */ - public function isAuthorized() { - } - /** - * Returns 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. - * - * @return bool The method will return true if authorization step required. - * False if the authorization step will be skipped. Default return value is true. - * - */ - public function isAuthRequired() : bool { - return $this->requireAuth; - } - - /** - * Validates the name of a web service or request parameter. - * - * @param string $name The name of the service or parameter. - * - * @return bool If valid, true is returned. Other than that, false is returned. - */ - public static function isValidName(string $name): bool { - $trimmedName = trim($name); - $len = strlen($trimmedName); - - if ($len != 0) { - for ($x = 0 ; $x < $len ; $x++) { - $ch = $trimmedName[$x]; - - if (!($ch == '_' || $ch == '-' || ($ch >= 'a' && $ch <= 'z') || ($ch >= 'A' && $ch <= 'Z') || ($ch >= '0' && $ch <= '9'))) { - return false; - } - } - - return true; - } - - return false; - } - /** - * Process client's request. - * - * This method must be implemented in a way it sends back a response after - * processing the request. - * - */ - abstract function processRequest(); - /** - * Removes a request parameter from the service given its name. - * - * @param string $paramName The name of the parameter (case-sensitive). - * - * @return null|RequestParameter If a parameter which has the given name - * was removed, the method will return an object of type 'RequestParameter' - * that represents the removed parameter. If nothing is removed, the - * method will return null. - * - */ - public function removeParameter(string $paramName) { - $trimmed = trim($paramName); - $params = &$this->getParameters(); - $index = -1; - $count = count($params); - - for ($x = 0 ; $x < $count ; $x++) { - if ($params[$x]->getName() == $trimmed) { - $index = $x; - break; - } - } - $retVal = null; - - if ($index != -1) { - if ($count == 1) { - $retVal = $params[0]; - unset($params[0]); - } else { - $retVal = $params[$index]; - $params[$index] = $params[$count - 1]; - unset($params[$count - 1]); - } - } - - return $retVal; - } - /** - * Removes a request method from the previously added ones. - * - * @param string $method The request method (e.g. 'get', 'post', 'options' ...). It - * can be in upper case or lower case. - * - * @return bool If the given request method is remove, the method will - * return true. Other than that, the method will return true. - * - */ - public function removeRequestMethod(string $method): bool { - $uMethod = strtoupper(trim($method)); - $allowedMethods = &$this->getRequestMethods(); - - if (in_array($uMethod, $allowedMethods)) { - $count = count($allowedMethods); - $methodIndex = -1; - - for ($x = 0 ; $x < $count ; $x++) { - if ($this->getRequestMethods()[$x] == $uMethod) { - $methodIndex = $x; - break; - } - } - - if ($count == 1) { - unset($allowedMethods[0]); - } else { - $allowedMethods[$methodIndex] = $allowedMethods[$count - 1]; - unset($allowedMethods[$count - 1]); - } - - return true; - } - - return false; - } - /** - * Sends Back a data using specific content type and specific response code. - * - * @param string $contentType Response content type (such as 'application/json') - * - * @param mixed $data Any data to send back. Mostly, it will be a string. - * - * @param int $code HTTP response code that will be used to send the data. - * Default is HTTP code 200 - Ok. - * - */ - public function send(string $contentType, $data, int $code = 200) { - $manager = $this->getManager(); - - if ($manager !== null) { - $manager->send($contentType, $data, $code); - } - } - /** - * Sends a JSON response to the client. - * - * The basic format of the message will be as follows: - *

    - * {
    - *   "message":"Action is not set.",
    - *   "type":"error"
    - *   "http-code":404
    - *   "more-info":EXTRA_INFO
    - * } - *

    - * Where EXTRA_INFO can be a simple string or any JSON data. - * - * @param string $message The message to send back. - * - * @param string $type A string that tells the client what is the type of - * the message. The developer can specify his own message types such as - * 'debug', 'info' or any string. If it is empty string, it will be not - * included in response payload. - * - * @param int $code Response code (such as 404 or 200). Default is 200. - * - * @param mixed $otherInfo Any other data to send back it can be a simple - * string, an object... . If null is given, the parameter 'more-info' - * 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(); - - 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; - } - /** - * Adds multiple request methods as one group. - * - * @param array $methods - */ - public function setRequestMethods(array $methods) { - foreach ($methods as $m) { - $this->addRequestMethod($m); - } - } - /** - * 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. - * - * The generated JSON string from the returned Json object will have - * the following format: - *

    - * {
    - *   "name":"",
    - *   "since":"",
    - *   "description":"",
    - *   "request-methods":[],
    - *   "parameters":[],
    - *   "responses":[]
    - * } - *

    - * - * @return Json an object of type Json. - * - */ - public function toJSON() : Json { - $json = new Json(); - $json->add('name', $this->getName()); - $json->add('since', $this->getSince()); - $json->add('description', $this->getDescription()); - $json->add('request-methods', $this->reqMethods); - $json->add('parameters', $this->parameters); - $json->add('responses', $this->getResponsesDescriptions()); - - return $json; - } +abstract class AbstractWebService extends WebService { } diff --git a/WebFiori/Http/ManagerInfoService.php b/WebFiori/Http/ManagerInfoService.php index 0260f24..3af3bff 100644 --- a/WebFiori/Http/ManagerInfoService.php +++ b/WebFiori/Http/ManagerInfoService.php @@ -13,12 +13,12 @@ * A service which can be used to display information about services manager. * * The developer must extend this class and complete the implementation of - * the method AbstractWebService::isAuthorized() in order to use it. + * the method WebService::isAuthorized() in order to use it. * * @author Ibrahim * */ -abstract class ManagerInfoService extends AbstractWebService { +abstract class ManagerInfoService extends WebService { /** * Creates new instance of the class. * diff --git a/WebFiori/Http/ObjectMapper.php b/WebFiori/Http/ObjectMapper.php index 4ab7380..bdb46ad 100644 --- a/WebFiori/Http/ObjectMapper.php +++ b/WebFiori/Http/ObjectMapper.php @@ -39,12 +39,12 @@ class ObjectMapper { * @param string $clazz The name of the class that API request will be mapped * to. Usually obtained using the syntax 'Class::class'. * - * @param AbstractWebService $service The service at which its parameters + * @param WebService $service The service at which its parameters * will be mapped to the object. * * @throws Exception */ - public function __construct(string $clazz, AbstractWebService $service) { + public function __construct(string $clazz, WebService $service) { $this->settersMap = []; $this->setClass($clazz); $this->extractMethodsNames($service->getInputs()); diff --git a/WebFiori/Http/RequestParameter.php b/WebFiori/Http/RequestParameter.php index 87e5924..d97329a 100644 --- a/WebFiori/Http/RequestParameter.php +++ b/WebFiori/Http/RequestParameter.php @@ -635,7 +635,7 @@ public function setMinValue(float $val) : bool { public function setName(string $name) : bool { $nameTrimmed = trim($name); - if (AbstractWebService::isValidName($nameTrimmed)) { + if (WebService::isValidName($nameTrimmed)) { $this->name = $nameTrimmed; return true; diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php new file mode 100644 index 0000000..9ee0ef9 --- /dev/null +++ b/WebFiori/Http/WebService.php @@ -0,0 +1,844 @@ + + *
  • 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 '_'.
  • + * + * If The given name is invalid, the name of the service will be set to 'new-service'. + * + * @param string $name The name of the web service. + * + * @param WebServicesManager|null $owner The manager which is used to + * manage the web service. + */ + public function __construct(string $name) { + if (!$this->setName($name)) { + $this->setName('new-service'); + } + $this->reqMethods = []; + $this->parameters = []; + $this->responses = []; + $this->requireAuth = true; + $this->sinceVersion = '1.0.0'; + $this->serviceDesc = ''; + $this->request = Request::createFromGlobals(); + } + /** + * 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() { + $retVal = "APIAction[\n"; + $retVal .= " Name => '".$this->getName()."',\n"; + $retVal .= " Description => '".$this->getDescription()."',\n"; + $since = $this->getSince() === null ? 'null' : $this->getSince(); + $retVal .= " Since => '$since',\n"; + $reqMethodsStr = "[\n"; + $comma = ','; + + for ($x = 0, $count = count($this->getRequestMethods()) ; $x < $count ; $x++) { + $meth = $this->getRequestMethods()[$x]; + + if ($x + 1 == $count) { + $comma = ''; + } + $reqMethodsStr .= " $meth$comma\n"; + } + $reqMethodsStr .= " ],\n"; + $retVal .= " Request Methods => $reqMethodsStr"; + $paramsStr = "[\n"; + + $comma = ','; + + for ($x = 0 , $count = count($this->getParameters()); $x < $count ; $x++) { + $param = $this->getParameters()[$x]; + $paramsStr .= " ".$param->getName()." => [\n"; + $paramsStr .= " Type => '".$param->getType()."',\n"; + $descStr = $param->getDescription() === null ? 'null' : $param->getDescription(); + $paramsStr .= " Description => '$descStr',\n"; + $isOptional = $param->isOptional() ? 'true' : 'false'; + $paramsStr .= " Is Optional => '$isOptional',\n"; + $defaultStr = $param->getDefault() === null ? 'null' : $param->getDefault(); + $paramsStr .= " Default => '$defaultStr',\n"; + $min = $param->getMinValue() === null ? 'null' : $param->getMinValue(); + $paramsStr .= " Minimum Value => '$min',\n"; + $max = $param->getMaxValue() === null ? 'null' : $param->getMaxValue(); + + if ($x + 1 == $count) { + $comma = ''; + } + $paramsStr .= " Maximum Value => '$max'\n ]$comma\n"; + } + $paramsStr .= " ],\n"; + $retVal .= " Parameters => $paramsStr"; + $responsesStr = "[\n"; + $count = count($this->getResponsesDescriptions()); + $comma = ','; + + for ($x = 0 ; $x < $count ; $x++) { + if ($x + 1 == $count) { + $comma = ''; + } + $responsesStr .= " Response #$x => '".$this->getResponsesDescriptions()[$x]."'".$comma."\n"; + } + $responsesStr .= " ]\n"; + + return $retVal." Responses Descriptions => $responsesStr]\n"; + } + /** + * 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())) { + $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 final function addResponseDescription(string $description) { + $trimmed = trim($description); + + if (strlen($trimmed) != 0) { + $this->responses[] = $trimmed; + } + } + /** + * 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) { + $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. + * + */ + 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; + } + /** + * Checks if the client is authorized to use the service or not. + * + * The developer should implement this method in a way it returns a boolean. + * If the method returns true, it means the client is allowed to use the service. + * If the method returns false, then he is not authorized and a 401 error + * code will be sent back. If the method returned nothing, then it means the + * user is authorized to call the API. If WebFiori framework is used, it is + * possible to perform the functionality of this method using middleware. + * + */ + public function isAuthorized() { + } + /** + * Returns 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. + * + * @return bool The method will return true if authorization step required. + * False if the authorization step will be skipped. Default return value is true. + * + */ + public function isAuthRequired() : bool { + return $this->requireAuth; + } + + /** + * Validates the name of a web service or request parameter. + * + * @param string $name The name of the service or parameter. + * + * @return bool If valid, true is returned. Other than that, false is returned. + */ + public static function isValidName(string $name): bool { + $trimmedName = trim($name); + $len = strlen($trimmedName); + + if ($len != 0) { + for ($x = 0 ; $x < $len ; $x++) { + $ch = $trimmedName[$x]; + + if (!($ch == '_' || $ch == '-' || ($ch >= 'a' && $ch <= 'z') || ($ch >= 'A' && $ch <= 'Z') || ($ch >= '0' && $ch <= '9'))) { + return false; + } + } + + return true; + } + + return false; + } + /** + * Process client's request. + * + * This method must be implemented in a way it sends back a response after + * processing the request. + * + */ + abstract function processRequest(); + /** + * Removes a request parameter from the service given its name. + * + * @param string $paramName The name of the parameter (case-sensitive). + * + * @return null|RequestParameter If a parameter which has the given name + * was removed, the method will return an object of type 'RequestParameter' + * that represents the removed parameter. If nothing is removed, the + * method will return null. + * + */ + public function removeParameter(string $paramName) { + $trimmed = trim($paramName); + $params = &$this->getParameters(); + $index = -1; + $count = count($params); + + for ($x = 0 ; $x < $count ; $x++) { + if ($params[$x]->getName() == $trimmed) { + $index = $x; + break; + } + } + $retVal = null; + + if ($index != -1) { + if ($count == 1) { + $retVal = $params[0]; + unset($params[0]); + } else { + $retVal = $params[$index]; + $params[$index] = $params[$count - 1]; + unset($params[$count - 1]); + } + } + + return $retVal; + } + /** + * Removes a request method from the previously added ones. + * + * @param string $method The request method (e.g. 'get', 'post', 'options' ...). It + * can be in upper case or lower case. + * + * @return bool If the given request method is remove, the method will + * return true. Other than that, the method will return true. + * + */ + public function removeRequestMethod(string $method): bool { + $uMethod = strtoupper(trim($method)); + $allowedMethods = &$this->getRequestMethods(); + + if (in_array($uMethod, $allowedMethods)) { + $count = count($allowedMethods); + $methodIndex = -1; + + for ($x = 0 ; $x < $count ; $x++) { + if ($this->getRequestMethods()[$x] == $uMethod) { + $methodIndex = $x; + break; + } + } + + if ($count == 1) { + unset($allowedMethods[0]); + } else { + $allowedMethods[$methodIndex] = $allowedMethods[$count - 1]; + unset($allowedMethods[$count - 1]); + } + + return true; + } + + return false; + } + /** + * Sends Back a data using specific content type and specific response code. + * + * @param string $contentType Response content type (such as 'application/json') + * + * @param mixed $data Any data to send back. Mostly, it will be a string. + * + * @param int $code HTTP response code that will be used to send the data. + * Default is HTTP code 200 - Ok. + * + */ + public function send(string $contentType, $data, int $code = 200) { + $manager = $this->getManager(); + + if ($manager !== null) { + $manager->send($contentType, $data, $code); + } + } + /** + * Sends a JSON response to the client. + * + * The basic format of the message will be as follows: + *

    + * {
    + *   "message":"Action is not set.",
    + *   "type":"error"
    + *   "http-code":404
    + *   "more-info":EXTRA_INFO
    + * } + *

    + * Where EXTRA_INFO can be a simple string or any JSON data. + * + * @param string $message The message to send back. + * + * @param string $type A string that tells the client what is the type of + * the message. The developer can specify his own message types such as + * 'debug', 'info' or any string. If it is empty string, it will be not + * included in response payload. + * + * @param int $code Response code (such as 404 or 200). Default is 200. + * + * @param mixed $otherInfo Any other data to send back it can be a simple + * string, an object... . If null is given, the parameter 'more-info' + * 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(); + + 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; + } + /** + * Adds multiple request methods as one group. + * + * @param array $methods + */ + public function setRequestMethods(array $methods) { + foreach ($methods as $m) { + $this->addRequestMethod($m); + } + } + /** + * 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. + * + * The generated JSON string from the returned Json object will have + * the following format: + *

    + * {
    + *   "name":"",
    + *   "since":"",
    + *   "description":"",
    + *   "request-methods":[],
    + *   "parameters":[],
    + *   "responses":[]
    + * } + *

    + * + * @return Json an object of type Json. + * + */ + public function toJSON() : Json { + $json = new Json(); + $json->add('name', $this->getName()); + $json->add('since', $this->getSince()); + $json->add('description', $this->getDescription()); + $json->add('request-methods', $this->reqMethods); + $json->add('parameters', $this->parameters); + $json->add('responses', $this->getResponsesDescriptions()); + + return $json; + } +} diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index ec3b53d..00fd635 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -147,11 +147,11 @@ public function getResponse() : Response { /** * Adds new web service to the set of web services. * - * @param AbstractWebService $service The web service that will be added. + * @param WebService $service The web service that will be added. * * */ - public function addService(AbstractWebService $service) : WebServicesManager { + public function addService(WebService $service) : WebServicesManager { return $this->addAction($service); } /** @@ -175,7 +175,7 @@ public function addService(AbstractWebService $service) : WebServicesManager { public function contentTypeNotSupported(string $cType = '') { $j = new Json(); $j->add('request-content-type', $cType); - $this->sendResponse(ResponseMessage::get('415'), 415, AbstractWebService::E, $j); + $this->sendResponse(ResponseMessage::get('415'), 415, WebService::E, $j); } /** * Returns the name of the service which is being called. @@ -281,7 +281,7 @@ public function getOutputStreamPath() { * * @param string $serviceName The name of the service. * - * @return AbstractWebService|null The method will return an object of type 'WebService' + * @return WebService|null The method will return an object of type 'WebService' * if the service is found. If no service was found which has the given name, * The method will return null. * @@ -342,7 +342,7 @@ public function invParams() { } $i++; } - $this->sendResponse(ResponseMessage::get('404-1').$val.'.', 404, AbstractWebService::E, new Json([ + $this->sendResponse(ResponseMessage::get('404-1').$val.'.', 404, WebService::E, new Json([ 'invalid' => $paramsNamesArr ])); } @@ -398,7 +398,7 @@ public function missingParams() { } $i++; } - $this->sendResponse(ResponseMessage::get('404-2').$val.'.', 404, AbstractWebService::E, new Json([ + $this->sendResponse(ResponseMessage::get('404-2').$val.'.', 404, WebService::E, new Json([ 'missing' => $paramsNamesArr ])); } @@ -417,7 +417,7 @@ public function missingParams() { * */ public function missingServiceName() { - $this->sendResponse(ResponseMessage::get('404-3'), 404, AbstractWebService::E); + $this->sendResponse(ResponseMessage::get('404-3'), 404, WebService::E); } /** * Sends a response message to indicate that a user is not authorized call a @@ -434,7 +434,7 @@ public function missingServiceName() { * */ public function notAuth() { - $this->sendResponse(ResponseMessage::get('401'), 401, AbstractWebService::E); + $this->sendResponse(ResponseMessage::get('401'), 401, WebService::E); } /** @@ -506,7 +506,7 @@ public function readOutputStream() { * * @param string $name The name of the service. * - * @return AbstractWebService|null If a web service which has the given name was found + * @return WebService|null If a web service which has the given name was found * and removed, the method will return an object that represent the removed * service. Other than that, the method will return null. * @@ -547,7 +547,7 @@ public function removeServices() { * */ public function requestMethodNotAllowed() { - $this->sendResponse(ResponseMessage::get('405'), 405, AbstractWebService::E); + $this->sendResponse(ResponseMessage::get('405'), 405, WebService::E); } /** * Sends Back a data using specific content type and specific response code. @@ -651,7 +651,7 @@ public function sendResponse(string $message, int $code = 200, string $type = '' * */ public function serviceNotImplemented() { - $this->sendResponse(ResponseMessage::get('404-4'), 404, AbstractWebService::E); + $this->sendResponse(ResponseMessage::get('404-4'), 404, WebService::E); } /** * Sends a response message to indicate that called web service is not supported by the API. @@ -667,7 +667,7 @@ public function serviceNotImplemented() { * */ public function serviceNotSupported() { - $this->sendResponse(ResponseMessage::get('404-5'), 404, AbstractWebService::E); + $this->sendResponse(ResponseMessage::get('404-5'), 404, WebService::E); } /** * Sets the description of the web services set. @@ -848,7 +848,7 @@ private function _checkAction(): bool { return true; } } else { - $this->sendResponse(ResponseMessage::get('404-6'), 404, AbstractWebService::E); + $this->sendResponse(ResponseMessage::get('404-6'), 404, WebService::E); } } else { $this->serviceNotSupported(); @@ -897,12 +897,12 @@ private function _processNonJson($params) { /** * Adds new web service to the set of web services. * - * @param AbstractWebService $service The web service that will be added. + * @param WebService $service The web service that will be added. * * * @deprecated since version 1.4.7 Use WebservicesSet::addService() */ - private function addAction(AbstractWebService $service) : WebServicesManager { + private function addAction(WebService $service) : WebServicesManager { $this->services[$service->getName()] = $service; $service->setManager($this); return $this; @@ -981,7 +981,7 @@ private function getAction() { return $retVal; } - private function isAuth(AbstractWebService $service) { + private function isAuth(WebService $service) { if ($service->isAuthRequired()) { $isAuthCheck = 'isAuthorized'.$this->getRequest()->getMethod(); @@ -994,7 +994,7 @@ private function isAuth(AbstractWebService $service) { return true; } - private function processService(AbstractWebService $service) { + private function processService(WebService $service) { $processMethod = 'process'.$this->getRequest()->getMethod(); if (!method_exists($service, $processMethod)) { diff --git a/examples/GetRandomService.php b/examples/GetRandomService.php index ae48520..4d2686f 100644 --- a/examples/GetRandomService.php +++ b/examples/GetRandomService.php @@ -2,12 +2,12 @@ require 'loader.php'; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\ParamOption; use WebFiori\Http\ParamType; use WebFiori\Http\RequestMethod; -class GetRandomService extends AbstractWebService { +class GetRandomService extends WebService { public function __construct() { parent::__construct('get-random-number'); $this->setRequestMethods([ diff --git a/examples/HelloWithAuthService.php b/examples/HelloWithAuthService.php index 40fdd2a..5295d20 100644 --- a/examples/HelloWithAuthService.php +++ b/examples/HelloWithAuthService.php @@ -2,13 +2,13 @@ require 'loader.php'; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\ParamOption; use WebFiori\Http\ParamType; use WebFiori\Http\RequestMethod; use WebFiori\Http\ResponseMessage; -class HelloWithAuthService extends AbstractWebService { +class HelloWithAuthService extends WebService { public function __construct() { parent::__construct('hello-with-auth'); $this->setRequestMethods([RequestMethod::GET]); diff --git a/examples/HelloWorldService.php b/examples/HelloWorldService.php index 44233e8..25b8a6d 100644 --- a/examples/HelloWorldService.php +++ b/examples/HelloWorldService.php @@ -2,12 +2,12 @@ require 'loader.php'; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\ParamOption; use WebFiori\Http\ParamType; use WebFiori\Http\RequestMethod; -class HelloWorldService extends AbstractWebService { +class HelloWorldService extends WebService { public function __construct() { parent::__construct('hello'); $this->setRequestMethods([RequestMethod::GET]); diff --git a/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php b/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php index ca7f33a..2f9548d 100644 --- a/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php +++ b/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php @@ -1,7 +1,7 @@ addParameter(new RequestParameter('pass','string')); diff --git a/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php index e0eb4d3..e0bd77d 100644 --- a/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php +++ b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php @@ -3,7 +3,7 @@ namespace WebFiori\Tests\Http\TestServices; use Exception; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\ParamOption; use WebFiori\Http\ParamType; use WebFiori\Http\RequestMethod; @@ -14,7 +14,7 @@ * * @author Ibrahim */ -class CreateUserProfileServiceV2 extends AbstractWebService { +class CreateUserProfileServiceV2 extends WebService { public function __construct() { parent::__construct('user-profile'); $this->addRequestMethod(RequestMethod::POST); diff --git a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php index 8080339..e712f2e 100644 --- a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php +++ b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php @@ -2,14 +2,14 @@ namespace WebFiori\Tests\Http\TestServices; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\RequestMethod; use WebFiori\Http\RequestParameter; /** * * @author Ibrahim */ -class MulNubmersService extends AbstractWebService { +class MulNubmersService extends WebService { public function __construct() { parent::__construct('mul-two-integers'); $this->setDescription('Returns a JSON string that has the multiplication of two integers.'); diff --git a/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php b/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php index a109560..68b6c65 100644 --- a/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php +++ b/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php @@ -2,14 +2,14 @@ namespace WebFiori\Tests\Http\TestServices; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\RequestMethod; /** * Description of NoAuthService * * @author Ibrahim */ -class NoAuthService extends AbstractWebService { +class NoAuthService extends WebService { public function __construct() { parent::__construct('ok-service'); $this->setIsAuthRequired(false); diff --git a/tests/WebFiori/Tests/Http/TestServices/NotImplService.php b/tests/WebFiori/Tests/Http/TestServices/NotImplService.php index 4180ab5..2a18caa 100644 --- a/tests/WebFiori/Tests/Http/TestServices/NotImplService.php +++ b/tests/WebFiori/Tests/Http/TestServices/NotImplService.php @@ -2,7 +2,7 @@ namespace WebFiori\Tests\Http\TestServices; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; use WebFiori\Http\RequestMethod; /** @@ -10,7 +10,7 @@ * * @author Ibrahim */ -class NotImplService extends AbstractWebService { +class NotImplService extends WebService { public function __construct() { parent::__construct('not-implemented'); $this->addRequestMethod(RequestMethod::POST); diff --git a/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php b/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php index 4153c8a..75064f9 100644 --- a/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php +++ b/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php @@ -2,13 +2,13 @@ namespace WebFiori\Tests\Http\TestServices; -use WebFiori\Http\AbstractWebService; +use WebFiori\Http\WebService; /** * Description of TestServiceObj * * @author Ibrahim */ -class TestServiceObj extends AbstractWebService { +class TestServiceObj extends WebService { public function __construct($name) { parent::__construct($name); } diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php index 91d430c..5726d76 100644 --- a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php +++ b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php @@ -1,7 +1,7 @@ assertNull($manager->removeService('xyz')); $service = $manager->removeService('ok-service'); - $this->assertTrue($service instanceof AbstractWebService); + $this->assertTrue($service instanceof WebService); $this->assertEquals(0, count($manager->getServices())); $this->assertNull($service->getManager()); } @@ -162,7 +162,7 @@ public function testConstructor00() { $api->setDescription('Test API.'); $this->assertEquals(6,count($api->getServices())); $this->assertEquals('Test API.',$api->getDescription()); - $this->assertTrue($api->getServiceByName('sum-array') instanceof AbstractWebService); + $this->assertTrue($api->getServiceByName('sum-array') instanceof WebService); $this->assertNull($api->getServiceByName('request-info')); $this->assertNull($api->getServiceByName('api-info-2')); From fd5ea74b6a3ce97da2af1f0cfe38faf6795b1e85 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 2 Dec 2025 18:37:14 +0300 Subject: [PATCH 05/50] refactor: Made `isAuthorized` Abstract --- WebFiori/Http/WebService.php | 9 ++--- examples/GetRandomService.php | 39 ++++++++----------- examples/HelloWithAuthService.php | 2 +- examples/HelloWorldService.php | 17 ++++---- .../TestServices/AbstractNumbersService.php | 2 +- .../CreateUserProfileServiceV2.php | 3 ++ .../Http/TestServices/MulNubmersService.php | 4 ++ .../Tests/Http/TestServices/NoAuthService.php | 2 +- .../Http/TestServices/NotImplService.php | 4 +- .../Http/TestServices/TestServiceObj.php | 4 +- tests/WebFiori/Tests/Http/WebServiceTest.php | 2 +- 11 files changed, 44 insertions(+), 44 deletions(-) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 9ee0ef9..39b0b45 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -543,9 +543,10 @@ public function hasParameter(string $name) : bool { * user is authorized to call the API. If WebFiori framework is used, it is * possible to perform the functionality of this method using middleware. * + * @return bool True if the user is allowed to perform the action. False otherwise. + * */ - public function isAuthorized() { - } + abstract function isAuthorized() : bool; /** * Returns the value of the property 'requireAuth'. * @@ -587,10 +588,6 @@ public static function isValidName(string $name): bool { } /** * Process client's request. - * - * This method must be implemented in a way it sends back a response after - * processing the request. - * */ abstract function processRequest(); /** diff --git a/examples/GetRandomService.php b/examples/GetRandomService.php index 4d2686f..7e1cc8d 100644 --- a/examples/GetRandomService.php +++ b/examples/GetRandomService.php @@ -1,43 +1,33 @@ setRequestMethods([ - RequestMethod::GET, - RequestMethod::POST - ]); + parent::__construct('get-random'); + $this->setRequestMethods([RequestMethod::GET, RequestMethod::POST]); + $this->setDescription('Returns a random integer. If no range is specified, the method will return a number between 0 and getrandmax().'); $this->addParameters([ 'min' => [ ParamOption::TYPE => ParamType::INT, - ParamOption::OPTIONAL => true + ParamOption::OPTIONAL => true, + ParamOption::DESCRIPTION => 'Minimum value for the random number.' ], 'max' => [ ParamOption::TYPE => ParamType::INT, - ParamOption::OPTIONAL => true + ParamOption::OPTIONAL => true, + ParamOption::DESCRIPTION => 'Maximum value for the random number.' ] ]); } - public function isAuthorized() { -// $authHeader = $this->getAuthHeader(); -// -// if ($authHeader === null) { -// return false; -// } -// -// $scheme = $authHeader->getScheme(); -// $credentials = $authHeader->getCredentials(); - - //Verify credentials based on auth scheme (e.g. 'Basic', 'Barear' + public function isAuthorized(): bool { + return true; } public function processRequest() { @@ -49,6 +39,9 @@ public function processRequest() { } else { $random = rand(); } - $this->sendResponse($random); + + $this->sendResponse('Random number generated', 'success', 200, [ + 'number' => $random + ]); } } diff --git a/examples/HelloWithAuthService.php b/examples/HelloWithAuthService.php index 5295d20..2585571 100644 --- a/examples/HelloWithAuthService.php +++ b/examples/HelloWithAuthService.php @@ -20,7 +20,7 @@ public function __construct() { ] ]); } - public function isAuthorized() { + public function isAuthorized(): bool { //Change default response message to custom one ResponseMessage::set('401', 'Not authorized to use this API.'); diff --git a/examples/HelloWorldService.php b/examples/HelloWorldService.php index 25b8a6d..23dbd9c 100644 --- a/examples/HelloWorldService.php +++ b/examples/HelloWorldService.php @@ -1,25 +1,27 @@ setRequestMethods([RequestMethod::GET]); + $this->setDescription('Returns a greeting message.'); $this->addParameters([ 'my-name' => [ ParamOption::TYPE => ParamType::STRING, - ParamOption::OPTIONAL => true + ParamOption::OPTIONAL => true, + ParamOption::DESCRIPTION => 'Your name to include in the greeting.' ] ]); } - public function isAuthorized() { + public function isAuthorized(): bool { + return true; } public function processRequest() { @@ -27,7 +29,8 @@ public function processRequest() { if ($name !== null) { $this->sendResponse("Hello '$name'."); + } else { + $this->sendResponse('Hello World!'); } - $this->sendResponse('Hello World!'); } } diff --git a/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php b/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php index 2f9548d..b11abf6 100644 --- a/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php +++ b/tests/WebFiori/Tests/Http/TestServices/AbstractNumbersService.php @@ -14,7 +14,7 @@ public function __construct($name) { parent::__construct($name); $this->addParameter(new RequestParameter('pass','string')); } - public function isAuthorized() { + public function isAuthorized(): bool { $inputs = $this->getInputs(); if ($inputs instanceof \webfiori\json\Json) { $pass = $inputs->get('pass'); diff --git a/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php index e0bd77d..003785f 100644 --- a/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php +++ b/tests/WebFiori/Tests/Http/TestServices/CreateUserProfileServiceV2.php @@ -39,6 +39,9 @@ public function __construct() { ] ]); } + public function isAuthorized(): bool { + return true; + } public function processRequest() { } diff --git a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php index e712f2e..cf8afaf 100644 --- a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php +++ b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php @@ -19,6 +19,10 @@ public function __construct() { $this->addParameter(new RequestParameter('second-number', 'integer')); } + public function isAuthorized(): bool { + return true; + } + public function isAuthorizedGET() { if ($this->getParamVal('first-number') < 0) { return false; diff --git a/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php b/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php index 68b6c65..80f5c36 100644 --- a/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php +++ b/tests/WebFiori/Tests/Http/TestServices/NoAuthService.php @@ -15,7 +15,7 @@ public function __construct() { $this->setIsAuthRequired(false); $this->addRequestMethod(RequestMethod::GET); } - public function isAuthorized() { + public function isAuthorized(): bool { return false; } diff --git a/tests/WebFiori/Tests/Http/TestServices/NotImplService.php b/tests/WebFiori/Tests/Http/TestServices/NotImplService.php index 2a18caa..e88fd17 100644 --- a/tests/WebFiori/Tests/Http/TestServices/NotImplService.php +++ b/tests/WebFiori/Tests/Http/TestServices/NotImplService.php @@ -15,8 +15,8 @@ public function __construct() { parent::__construct('not-implemented'); $this->addRequestMethod(RequestMethod::POST); } - public function isAuthorized() { - + public function isAuthorized(): bool { + return true; } public function processRequest() { diff --git a/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php b/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php index 75064f9..ed43921 100644 --- a/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php +++ b/tests/WebFiori/Tests/Http/TestServices/TestServiceObj.php @@ -13,8 +13,8 @@ public function __construct($name) { parent::__construct($name); } //put your code here - public function isAuthorized() { - return parent::isAuthorized(); + public function isAuthorized(): bool { + return true; } public function processRequest() { diff --git a/tests/WebFiori/Tests/Http/WebServiceTest.php b/tests/WebFiori/Tests/Http/WebServiceTest.php index e9bdc6d..db74891 100644 --- a/tests/WebFiori/Tests/Http/WebServiceTest.php +++ b/tests/WebFiori/Tests/Http/WebServiceTest.php @@ -16,7 +16,7 @@ class WebServiceTest extends TestCase { public function testGetAuthHeaders00() { $service = new TestServiceObj('Hello'); $this->assertNull($service->getAuthHeader()); - $this->assertNull($service->isAuthorized()); + $this->assertTrue($service->isAuthorized()); } /** * From 18cf276ccd2a2b90825470df63d4ab01ac39b877 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 2 Dec 2025 18:40:06 +0300 Subject: [PATCH 06/50] refactor: Rename Default Output Stream --- .gitignore | 1 + WebFiori/Http/APITestCase.php | 2 +- tests/WebFiori/Tests/Http/WebServicesManagerTest.php | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 7742743..7207ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ php-cs-fixer.phar *.Identifier /tests/.phpunit.cache /.vscode +tests/WebFiori/Tests/Http/output-stream.txt diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php index cf9bf22..d33d2d9 100644 --- a/WebFiori/Http/APITestCase.php +++ b/WebFiori/Http/APITestCase.php @@ -23,7 +23,7 @@ */ class APITestCase extends TestCase { const NL = "\r\n"; - const DEFAULT_OUTPUT_STREAM = __DIR__.DIRECTORY_SEPARATOR.'outputStream.txt'; + const DEFAULT_OUTPUT_STREAM = __DIR__.DIRECTORY_SEPARATOR.'output-stream.txt'; /** * The path to the output stream file. * diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php index 5726d76..852fccb 100644 --- a/tests/WebFiori/Tests/Http/WebServicesManagerTest.php +++ b/tests/WebFiori/Tests/Http/WebServicesManagerTest.php @@ -16,7 +16,7 @@ * @author Eng.Ibrahim */ class WebServicesManagerTest extends APITestCase { - private $outputStreamName = __DIR__.DIRECTORY_SEPARATOR.'outputStream.txt'; + private $outputStreamName = __DIR__.DIRECTORY_SEPARATOR.'output-stream.txt'; public function test00() { $manager = new WebServicesManager(); $manager->addService(new NoAuthService()); @@ -650,10 +650,10 @@ public function testSetOutputStream02() { */ public function testSetOutputStream03() { $api = new SampleServicesManager(); - $this->assertTrue($api->setOutputStream(__DIR__.DIRECTORY_SEPARATOR.'outputStream.txt', true)); + $this->assertTrue($api->setOutputStream(__DIR__.DIRECTORY_SEPARATOR.'output-stream.txt', true)); $this->assertNotNull($api->getOutputStream()); $this->assertNotNull($api->getOutputStreamPath()); - $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'outputStream.txt', $api->getOutputStreamPath()); + $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'output-stream.txt', $api->getOutputStreamPath()); } private function clrearVars() { foreach ($_GET as $k => $v) { From 9f4532e9c3f0bcb1151baadbdaed2ed2edc0a9bd Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 2 Dec 2025 23:03:32 +0300 Subject: [PATCH 07/50] refactor: Enhanced API Test Case Class --- WebFiori/Http/APITestCase.php | 229 ++++++++++++++++++++++++---------- 1 file changed, 163 insertions(+), 66 deletions(-) diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php index d33d2d9..0515d6e 100644 --- a/WebFiori/Http/APITestCase.php +++ b/WebFiori/Http/APITestCase.php @@ -30,6 +30,30 @@ class APITestCase extends TestCase { * @var string */ private $outputStreamPath; + /** + * Backup of global variables. + * + * @var array + */ + private $backupGlobals; + + protected function setUp(): void { + parent::setUp(); + $this->backupGlobals = [ + 'GET' => $_GET, + 'POST' => $_POST, + 'FILES' => $_FILES, + 'SERVER' => $_SERVER + ]; + } + + protected function tearDown(): void { + $_GET = $this->backupGlobals['GET']; + $_POST = $this->backupGlobals['POST']; + $_FILES = $this->backupGlobals['FILES']; + $_SERVER = $this->backupGlobals['SERVER']; + parent::tearDown(); + } /** * Sets the path to the file which is used to store API output temporarily. * @@ -106,43 +130,96 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false) * @return string The method will return the output of the endpoint. */ public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = []) : string { - $manager->setOutputStream(fopen($this->getOutputFile(),'w')); $method = strtoupper($requestMethod); - putenv('REQUEST_METHOD='.$method); + $serviceName = $this->resolveServiceName($apiEndpointName); + + $this->setupRequest($method, $serviceName, $parameters, $httpHeaders); - if (class_exists($apiEndpointName)) { - $service = new $apiEndpointName(); + $manager->setOutputStream(fopen($this->getOutputFile(), 'w')); + $manager->setRequest(Request::createFromGlobals()); + $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. + * + * @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 ($service instanceof WebService) { - $apiEndpointName = $service->getName(); + if ($reflection->isSubclassOf(WebService::class)) { + $constructor = $reflection->getConstructor(); + + if ($constructor && $constructor->getNumberOfRequiredParameters() === 0) { + $service = $reflection->newInstance(); + return $service->getName(); + } } } - if ($method == RequestMethod::POST || $method == RequestMethod::PUT || $method == RequestMethod::PATCH) { - foreach ($parameters as $key => $val) { - $_POST[$key] = $this->parseVal($val); - } - $_POST['service'] = $apiEndpointName; - $_SERVER['CONTENT_TYPE'] = 'multipart/form-data'; - $this->unset($_POST, $parameters, $manager, $httpHeaders); + + 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 HTTP headers + */ + 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 { - foreach ($parameters as $key => $val) { - $_GET[$key] = $this->parseVal($val); - } - $_GET['service'] = $apiEndpointName; - $this->unset($_GET, $parameters, $manager, $httpHeaders); + $_GET = $parameters; + $_GET['service'] = $serviceName; } - - $retVal = $manager->readOutputStream(); - unlink($this->getOutputFile()); + 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 + * + * @return string Formatted output + */ + private function formatOutput(string $output): string { try { - $json = Json::decode($retVal); + $json = Json::decode($output); $json->setIsFormatted(true); - return $json.''; + return $json . ''; } catch (JsonException $ex) { - return $retVal; + return $output; } - } /** * Creates a formatted string from calling an API. @@ -166,26 +243,6 @@ public function format(string $output) { echo ". '$expl[$x]]'".$nl; } } - private function parseVal($val) { - $type = gettype($val); - - if ($type == 'array') { - $array = []; - - foreach ($val as $arrVal) { - if (gettype($val) == 'string') { - $array[] = "'".$arrVal."'"; - } else { - $array[] = $arrVal; - } - } - - return implode(',', $array); - } else if ($type == 'boolean') { - return $type === true ? 'y' : 'n'; - } - return $val; - } /** * Sends a DELETE request to specific endpoint. * @@ -203,7 +260,7 @@ private function parseVal($val) { * @return string The method will return the output that was produced by * the endpoint as string. */ - public function deletRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { + public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders); } /** @@ -262,6 +319,66 @@ public function postRequest(WebServicesManager $manager, string $endpoint, array public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders); } + /** + * Sends a PATCH 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. + * + * @return string The method will return the output that was produced by + * the endpoint as string. + */ + public function patchRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { + return $this->callEndpoint($manager, RequestMethod::PATCH, $endpoint, $parameters, $httpHeaders); + } + /** + * Sends an OPTIONS 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. + * + * @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 = []) : string { + return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders); + } + /** + * Sends a HEAD 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. + * + * @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 = []) : string { + return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders); + } private function extractPathAndName($absPath): array { $DS = DIRECTORY_SEPARATOR; $cleanPath = str_replace('\\', $DS, str_replace('/', $DS, trim($absPath))); @@ -286,24 +403,4 @@ private function extractPathAndName($absPath): array { 'path' => '' ]; } - private function unset(array &$arr, array $params, WebServicesManager $m, array $httpHeaders) { - foreach ($httpHeaders as $header => $value) { - $trHeader = trim($header.''); - $trVal = trim($value.''); - if (strlen($trHeader) != 0) { - $_SERVER['HTTP_'.strtoupper($trHeader)] = $trVal; - } - } - $m->setRequest(Request::createFromGlobals()); - $m->process(); - - foreach ($params as $key => $val) { - unset($arr[$key]); - } - - foreach ($httpHeaders as $header => $value) { - $trHeader = trim($header.''); - unset($_SERVER['HTTP_'.strtoupper($trHeader)]); - } - } } From 02ad50399532075388958805b0b68f1d51440b78 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 01:07:00 +0300 Subject: [PATCH 08/50] feat: Added OpenAPI Schema --- WebFiori/Http/OpenAPI/Schema.php | 175 +++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 WebFiori/Http/OpenAPI/Schema.php diff --git a/WebFiori/Http/OpenAPI/Schema.php b/WebFiori/Http/OpenAPI/Schema.php new file mode 100644 index 0000000..1143ef8 --- /dev/null +++ b/WebFiori/Http/OpenAPI/Schema.php @@ -0,0 +1,175 @@ +type = $type; + } + + /** + * Creates a Schema from a RequestParameter. + * + * @param RequestParameter $param The request parameter + * + * @return Schema The schema object + */ + 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(); + $schema->minLength = $param->getMinLength(); + $schema->maxLength = $param->getMaxLength(); + $schema->default = $param->getDefault(); + $schema->description = $param->getDescription(); + + return $schema; + } + + /** + * Converts the schema to JSON representation. + * + * @return Json JSON object + */ + 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'; + } +} From c8c820de9240df8c2db98a64f66fd15ade75939e Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 01:11:28 +0300 Subject: [PATCH 09/50] feat: Add OpenAPI Specs to Request Param --- WebFiori/Http/RequestParameter.php | 45 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/WebFiori/Http/RequestParameter.php b/WebFiori/Http/RequestParameter.php index d97329a..0fb35c5 100644 --- a/WebFiori/Http/RequestParameter.php +++ b/WebFiori/Http/RequestParameter.php @@ -9,6 +9,7 @@ */ namespace WebFiori\Http; +use WebFiori\Http\OpenAPI\Schema; use WebFiori\Json\Json; use WebFiori\Json\JsonI; /** @@ -684,20 +685,6 @@ public function setType(string $type) : bool { /** * Returns a Json object that represents the request parameter. * - * This method is used to help front-end developers in showing the - * documentation of the request parameter. The format of JSON string - * will be as follows: - *

    - * {
    - *   "name":"a-param",
    - *   "type":"string",
    - *   "description":null,
    - *   "is-optional":true,
    - *   "default-value":null,
    - *   "min-val":null,
    - *   "max-val":null
    - * } - *

    * * @return Json An object of type Json. * @@ -705,17 +692,29 @@ public function setType(string $type) : bool { public function toJSON() : Json { $json = new Json(); $json->add('name', $this->getName()); - $json->add('type', $this->getType()); - $json->add('description', $this->getDescription()); - $json->add('is-optional', $this->isOptional()); - $json->add('default-value', $this->getDefault()); - $json->add('min-val', $this->getMinValue()); - $json->add('max-val', $this->getMaxValue()); - $json->add('min-length', $this->getMinLength()); - $json->add('max-length', $this->getMaxLength()); - + + $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 From decf743382e40ebcd2572894b6b52e39fe6a39fc Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 01:31:22 +0300 Subject: [PATCH 10/50] feat: External Doc Obj of Open API --- .gitignore | 1 + WebFiori/Http/OpenAPI/ExternalDocObj.php | 89 ++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 WebFiori/Http/OpenAPI/ExternalDocObj.php diff --git a/.gitignore b/.gitignore index 7207ee6..2a0b5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ php-cs-fixer.phar /tests/.phpunit.cache /.vscode tests/WebFiori/Tests/Http/output-stream.txt +/OpenAPI_files diff --git a/WebFiori/Http/OpenAPI/ExternalDocObj.php b/WebFiori/Http/OpenAPI/ExternalDocObj.php new file mode 100644 index 0000000..bf6cfe0 --- /dev/null +++ b/WebFiori/Http/OpenAPI/ExternalDocObj.php @@ -0,0 +1,89 @@ +setUrl($url); + + if ($description !== null) { + $this->setDescription($description); + } + } + + /** + * Sets the description of the target documentation. + * + * @param string $description A description of the target documentation. + * CommonMark syntax MAY be used for rich text representation. + * + * @return ExternalDocObj + */ + public function setDescription(string $description): ExternalDocObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description of the target documentation. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Sets the URI for the target documentation. + * + * @param string $url The URI for the target documentation. This MUST be in the form of a URI. + * + * @return ExternalDocObj + */ + public function setUrl(string $url): ExternalDocObj { + $this->url = $url; + return $this; + } + + /** + * Returns the URI for the target documentation. + * + * @return string + */ + public function getUrl(): string { + return $this->url; + } + + /** + * Returns a Json object that represents the External Documentation Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + $json->add('url', $this->url); + + return $json; + } +} From 2ac8ce9fec1129327b2dc615e0a0381f2319e5db Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 01:52:47 +0300 Subject: [PATCH 11/50] feat: Open API Obj --- WebFiori/Http/OpenAPI/ContactObj.php | 102 ++++++ WebFiori/Http/OpenAPI/HeaderObj.php | 232 +++++++++++++ WebFiori/Http/OpenAPI/InfoObj.php | 214 ++++++++++++ WebFiori/Http/OpenAPI/LicenseObj.php | 113 +++++++ WebFiori/Http/OpenAPI/OAuthFlowObj.php | 151 +++++++++ WebFiori/Http/OpenAPI/OAuthFlowsObj.php | 128 ++++++++ WebFiori/Http/OpenAPI/ParameterObj.php | 345 ++++++++++++++++++++ WebFiori/Http/OpenAPI/ReferenceObj.php | 111 +++++++ WebFiori/Http/OpenAPI/SecuritySchemeObj.php | 241 ++++++++++++++ WebFiori/Http/OpenAPI/ServerObj.php | 89 +++++ WebFiori/Http/OpenAPI/TagObj.php | 116 +++++++ 11 files changed, 1842 insertions(+) create mode 100644 WebFiori/Http/OpenAPI/ContactObj.php create mode 100644 WebFiori/Http/OpenAPI/HeaderObj.php create mode 100644 WebFiori/Http/OpenAPI/InfoObj.php create mode 100644 WebFiori/Http/OpenAPI/LicenseObj.php create mode 100644 WebFiori/Http/OpenAPI/OAuthFlowObj.php create mode 100644 WebFiori/Http/OpenAPI/OAuthFlowsObj.php create mode 100644 WebFiori/Http/OpenAPI/ParameterObj.php create mode 100644 WebFiori/Http/OpenAPI/ReferenceObj.php create mode 100644 WebFiori/Http/OpenAPI/SecuritySchemeObj.php create mode 100644 WebFiori/Http/OpenAPI/ServerObj.php create mode 100644 WebFiori/Http/OpenAPI/TagObj.php diff --git a/WebFiori/Http/OpenAPI/ContactObj.php b/WebFiori/Http/OpenAPI/ContactObj.php new file mode 100644 index 0000000..cf80ba3 --- /dev/null +++ b/WebFiori/Http/OpenAPI/ContactObj.php @@ -0,0 +1,102 @@ +name = $name; + return $this; + } + + /** + * Returns the contact name. + * + * @return string|null + */ + 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 + */ + public function setUrl(string $url): ContactObj { + $this->url = $url; + return $this; + } + + /** + * Returns the contact URL. + * + * @return string|null + */ + public function getUrl(): ?string { + return $this->url; + } + + /** + * Sets the email address of the contact person/organization. + * + * @param string $email The email address. This MUST be in the form of an email address. + * + * @return ContactObj + */ + public function setEmail(string $email): ContactObj { + $this->email = $email; + return $this; + } + + /** + * Returns the contact email. + * + * @return string|null + */ + public function getEmail(): ?string { + return $this->email; + } + + /** + * Returns a Json object that represents the Contact Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + if ($this->name !== null) { + $json->add('name', $this->name); + } + + if ($this->url !== null) { + $json->add('url', $this->url); + } + + if ($this->email !== null) { + $json->add('email', $this->email); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/HeaderObj.php b/WebFiori/Http/OpenAPI/HeaderObj.php new file mode 100644 index 0000000..2280fe3 --- /dev/null +++ b/WebFiori/Http/OpenAPI/HeaderObj.php @@ -0,0 +1,232 @@ +description = $description; + return $this; + } + + /** + * Returns the description. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Sets whether this header is mandatory. + * + * @param bool $required True if required. + * + * @return HeaderObj + */ + public function setRequired(bool $required): HeaderObj { + $this->required = $required; + return $this; + } + + /** + * Returns whether this header is required. + * + * @return bool + */ + public function isRequired(): bool { + return $this->required; + } + + /** + * Sets whether this header is deprecated. + * + * @param bool $deprecated True if deprecated. + * + * @return HeaderObj + */ + public function setDeprecated(bool $deprecated): HeaderObj { + $this->deprecated = $deprecated; + return $this; + } + + /** + * Returns whether this header is deprecated. + * + * @return bool + */ + public function isDeprecated(): bool { + return $this->deprecated; + } + + /** + * Sets the serialization style. + * + * @param string $style The style value. Default is "simple". + * + * @return HeaderObj + */ + public function setStyle(string $style): HeaderObj { + $this->style = $style; + return $this; + } + + /** + * Returns the style. + * + * @return string|null + */ + public function getStyle(): ?string { + return $this->style; + } + + /** + * Sets the explode value. + * + * @param bool $explode The explode value. + * + * @return HeaderObj + */ + public function setExplode(bool $explode): HeaderObj { + $this->explode = $explode; + return $this; + } + + /** + * Returns the explode value. + * + * @return bool|null + */ + public function getExplode(): ?bool { + return $this->explode; + } + + /** + * Sets the schema defining the type used for the header. + * + * @param mixed $schema Schema Object or any schema definition. + * + * @return HeaderObj + */ + 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 + */ + 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. + * + * @param array $examples Map of example names to Example Objects or Reference Objects. + * + * @return HeaderObj + */ + public function setExamples(array $examples): HeaderObj { + $this->examples = $examples; + return $this; + } + + /** + * Returns the examples. + * + * @return array|null + */ + public function getExamples(): ?array { + return $this->examples; + } + + /** + * Returns a Json object that represents the Header Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + if ($this->required) { + $json->add('required', $this->required); + } + + if ($this->deprecated) { + $json->add('deprecated', $this->deprecated); + } + + if ($this->style !== null) { + $json->add('style', $this->style); + } + + if ($this->explode !== null) { + $json->add('explode', $this->explode); + } + + if ($this->schema !== null) { + $json->add('schema', $this->schema); + } + + if ($this->example !== null) { + $json->add('example', $this->example); + } + + if ($this->examples !== null) { + $json->add('examples', $this->examples); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/InfoObj.php b/WebFiori/Http/OpenAPI/InfoObj.php new file mode 100644 index 0000000..387ee85 --- /dev/null +++ b/WebFiori/Http/OpenAPI/InfoObj.php @@ -0,0 +1,214 @@ +setTitle($title); + $this->setVersion($version); + } + + /** + * Sets the title of the API. + * + * @param string $title The title of the API. + * + * @return InfoObj + */ + public function setTitle(string $title): InfoObj { + $this->title = $title; + return $this; + } + + /** + * Returns the title of the API. + * + * @return string + */ + public function getTitle(): string { + return $this->title; + } + + /** + * Sets the version of the OpenAPI Document. + * + * @param string $version The version of the OpenAPI Document. + * + * @return InfoObj + */ + public function setVersion(string $version): InfoObj { + $this->version = $version; + return $this; + } + + /** + * Returns the version of the OpenAPI Document. + * + * @return string + */ + public function getVersion(): string { + return $this->version; + } + + /** + * Sets a short summary of the API. + * + * @param string $summary A short summary of the API. + * + * @return InfoObj + */ + public function setSummary(string $summary): InfoObj { + $this->summary = $summary; + return $this; + } + + /** + * Returns the summary of the API. + * + * @return string|null + */ + public function getSummary(): ?string { + return $this->summary; + } + + /** + * Sets a description of the API. + * + * @param string $description A description of the API. + * CommonMark syntax MAY be used for rich text representation. + * + * @return InfoObj + */ + public function setDescription(string $description): InfoObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description of the API. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Sets a URI for the Terms of Service for the API. + * + * @param string $termsOfService A URI for the Terms of Service. This MUST be in the form of a URI. + * + * @return InfoObj + */ + public function setTermsOfService(string $termsOfService): InfoObj { + $this->termsOfService = $termsOfService; + return $this; + } + + /** + * Returns the Terms of Service URI. + * + * @return string|null + */ + public function getTermsOfService(): ?string { + return $this->termsOfService; + } + + /** + * Sets the contact information for the exposed API. + * + * @param ContactObj $contact Contact Object. + * + * @return InfoObj + */ + public function setContact(ContactObj $contact): InfoObj { + $this->contact = $contact; + return $this; + } + + /** + * Returns the contact information. + * + * @return ContactObj|null + */ + public function getContact(): ?ContactObj { + return $this->contact; + } + + /** + * Sets the license information for the exposed API. + * + * @param LicenseObj $license License Object. + * + * @return InfoObj + */ + public function setLicense(LicenseObj $license): InfoObj { + $this->license = $license; + return $this; + } + + /** + * Returns the license information. + * + * @return LicenseObj|null + */ + public function getLicense(): ?LicenseObj { + return $this->license; + } + + /** + * Returns a Json object that represents the Info Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('title', $this->title); + + if ($this->summary !== null) { + $json->add('summary', $this->summary); + } + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + if ($this->termsOfService !== null) { + $json->add('termsOfService', $this->termsOfService); + } + + if ($this->contact !== null) { + $json->add('contact', $this->contact); + } + + if ($this->license !== null) { + $json->add('license', $this->license); + } + + $json->add('version', $this->version); + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/LicenseObj.php b/WebFiori/Http/OpenAPI/LicenseObj.php new file mode 100644 index 0000000..2a3c2dd --- /dev/null +++ b/WebFiori/Http/OpenAPI/LicenseObj.php @@ -0,0 +1,113 @@ +setName($name); + } + + /** + * Sets the license name used for the API. + * + * @param string $name The license name. + * + * @return LicenseObj + */ + public function setName(string $name): LicenseObj { + $this->name = $name; + return $this; + } + + /** + * Returns the license name. + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * Sets an SPDX license expression for the API. + * + * @param string $identifier An SPDX license expression. + * The identifier field is mutually exclusive of the url field. + * + * @return LicenseObj + */ + public function setIdentifier(string $identifier): LicenseObj { + $this->identifier = $identifier; + $this->url = null; + return $this; + } + + /** + * Returns the SPDX license identifier. + * + * @return string|null + */ + public function getIdentifier(): ?string { + return $this->identifier; + } + + /** + * Sets a URI for the license used for the API. + * + * @param string $url A URI for the license. This MUST be in the form of a URI. + * The url field is mutually exclusive of the identifier field. + * + * @return LicenseObj + */ + public function setUrl(string $url): LicenseObj { + $this->url = $url; + $this->identifier = null; + return $this; + } + + /** + * Returns the license URL. + * + * @return string|null + */ + public function getUrl(): ?string { + return $this->url; + } + + /** + * Returns a Json object that represents the License Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('name', $this->name); + + if ($this->identifier !== null) { + $json->add('identifier', $this->identifier); + } + + if ($this->url !== null) { + $json->add('url', $this->url); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/OAuthFlowObj.php b/WebFiori/Http/OpenAPI/OAuthFlowObj.php new file mode 100644 index 0000000..e5b2027 --- /dev/null +++ b/WebFiori/Http/OpenAPI/OAuthFlowObj.php @@ -0,0 +1,151 @@ +scopes = $scopes; + } + + /** + * Sets the authorization URL to be used for this flow. + * + * @param string $authorizationUrl The authorization URL. This MUST be in the form of a URL. + * REQUIRED for implicit and authorizationCode flows. + * + * @return OAuthFlowObj + */ + public function setAuthorizationUrl(string $authorizationUrl): OAuthFlowObj { + $this->authorizationUrl = $authorizationUrl; + return $this; + } + + /** + * Returns the authorization URL. + * + * @return string|null + */ + public function getAuthorizationUrl(): ?string { + return $this->authorizationUrl; + } + + /** + * Sets the token URL to be used for this flow. + * + * @param string $tokenUrl The token URL. This MUST be in the form of a URL. + * REQUIRED for password, clientCredentials, and authorizationCode flows. + * + * @return OAuthFlowObj + */ + public function setTokenUrl(string $tokenUrl): OAuthFlowObj { + $this->tokenUrl = $tokenUrl; + return $this; + } + + /** + * Returns the token URL. + * + * @return string|null + */ + public function getTokenUrl(): ?string { + return $this->tokenUrl; + } + + /** + * Sets the URL to be used for obtaining refresh tokens. + * + * @param string $refreshUrl The refresh URL. This MUST be in the form of a URL. + * + * @return OAuthFlowObj + */ + public function setRefreshUrl(string $refreshUrl): OAuthFlowObj { + $this->refreshUrl = $refreshUrl; + return $this; + } + + /** + * Returns the refresh URL. + * + * @return string|null + */ + public function getRefreshUrl(): ?string { + return $this->refreshUrl; + } + + /** + * Sets the available scopes for the OAuth2 security scheme. + * + * @param array $scopes A map between the scope name and a short description for it. + * + * @return OAuthFlowObj + */ + public function setScopes(array $scopes): OAuthFlowObj { + $this->scopes = $scopes; + return $this; + } + + /** + * Adds a scope to the OAuth2 security scheme. + * + * @param string $name The scope name. + * @param string $description A short description for the scope. + * + * @return OAuthFlowObj + */ + public function addScope(string $name, string $description): OAuthFlowObj { + $this->scopes[$name] = $description; + 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. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + if ($this->authorizationUrl !== null) { + $json->add('authorizationUrl', $this->authorizationUrl); + } + + if ($this->tokenUrl !== null) { + $json->add('tokenUrl', $this->tokenUrl); + } + + if ($this->refreshUrl !== null) { + $json->add('refreshUrl', $this->refreshUrl); + } + + $json->add('scopes', $this->scopes); + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php new file mode 100644 index 0000000..294642c --- /dev/null +++ b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php @@ -0,0 +1,128 @@ +implicit = $implicit; + return $this; + } + + /** + * Returns the implicit flow configuration. + * + * @return OAuthFlowObj|null + */ + public function getImplicit(): ?OAuthFlowObj { + return $this->implicit; + } + + /** + * Sets configuration for the OAuth Resource Owner Password flow. + * + * @param OAuthFlowObj $password OAuth Flow Object for password flow. + * + * @return OAuthFlowsObj + */ + public function setPassword(OAuthFlowObj $password): OAuthFlowsObj { + $this->password = $password; + return $this; + } + + /** + * Returns the password flow configuration. + * + * @return OAuthFlowObj|null + */ + public function getPassword(): ?OAuthFlowObj { + return $this->password; + } + + /** + * Sets configuration for the OAuth Client Credentials flow. + * + * @param OAuthFlowObj $clientCredentials OAuth Flow Object for client credentials flow. + * + * @return OAuthFlowsObj + */ + public function setClientCredentials(OAuthFlowObj $clientCredentials): OAuthFlowsObj { + $this->clientCredentials = $clientCredentials; + return $this; + } + + /** + * Returns the client credentials flow configuration. + * + * @return OAuthFlowObj|null + */ + public function getClientCredentials(): ?OAuthFlowObj { + return $this->clientCredentials; + } + + /** + * Sets configuration for the OAuth Authorization Code flow. + * + * @param OAuthFlowObj $authorizationCode OAuth Flow Object for authorization code flow. + * + * @return OAuthFlowsObj + */ + public function setAuthorizationCode(OAuthFlowObj $authorizationCode): OAuthFlowsObj { + $this->authorizationCode = $authorizationCode; + return $this; + } + + /** + * Returns the authorization code flow configuration. + * + * @return OAuthFlowObj|null + */ + public function getAuthorizationCode(): ?OAuthFlowObj { + return $this->authorizationCode; + } + + /** + * Returns a Json object that represents the OAuth Flows Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + if ($this->implicit !== null) { + $json->add('implicit', $this->implicit); + } + + if ($this->password !== null) { + $json->add('password', $this->password); + } + + if ($this->clientCredentials !== null) { + $json->add('clientCredentials', $this->clientCredentials); + } + + if ($this->authorizationCode !== null) { + $json->add('authorizationCode', $this->authorizationCode); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/ParameterObj.php b/WebFiori/Http/OpenAPI/ParameterObj.php new file mode 100644 index 0000000..cc4e019 --- /dev/null +++ b/WebFiori/Http/OpenAPI/ParameterObj.php @@ -0,0 +1,345 @@ +setName($name); + $this->setIn($in); + } + + /** + * Sets the name of the parameter. + * + * @param string $name The name of the parameter. + * + * @return ParameterObj + */ + public function setName(string $name): ParameterObj { + $this->name = $name; + return $this; + } + + /** + * Returns the name of the parameter. + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * Sets the location of the parameter. + * + * @param string $in The location. Possible values: "query", "header", "path", "cookie". + * + * @return ParameterObj + */ + public function setIn(string $in): ParameterObj { + $this->in = $in; + if ($in === 'path') { + $this->required = true; + } + return $this; + } + + /** + * Returns the location of the parameter. + * + * @return string + */ + public function getIn(): string { + return $this->in; + } + + /** + * Sets the description of the parameter. + * + * @param string $description A brief description of the parameter. + * + * @return ParameterObj + */ + public function setDescription(string $description): ParameterObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Sets whether this parameter is mandatory. + * + * @param bool $required True if required. + * + * @return ParameterObj + */ + public function setRequired(bool $required): ParameterObj { + $this->required = $required; + return $this; + } + + /** + * Returns whether this parameter is required. + * + * @return bool + */ + public function isRequired(): bool { + return $this->required; + } + + /** + * Sets whether this parameter is deprecated. + * + * @param bool $deprecated True if deprecated. + * + * @return ParameterObj + */ + public function setDeprecated(bool $deprecated): ParameterObj { + $this->deprecated = $deprecated; + return $this; + } + + /** + * Returns whether this parameter is deprecated. + * + * @return bool + */ + public function isDeprecated(): bool { + return $this->deprecated; + } + + /** + * Sets whether to allow empty value. + * + * @param bool $allowEmptyValue True to allow empty value. + * + * @return ParameterObj + */ + 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; + } + + /** + * Sets the serialization style. + * + * @param string $style The style value. + * + * @return ParameterObj + */ + public function setStyle(string $style): ParameterObj { + $this->style = $style; + return $this; + } + + /** + * Returns the style. + * + * @return string|null + */ + public function getStyle(): ?string { + return $this->style; + } + + /** + * Sets the explode value. + * + * @param bool $explode The explode value. + * + * @return ParameterObj + */ + public function setExplode(bool $explode): ParameterObj { + $this->explode = $explode; + return $this; + } + + /** + * Returns the explode value. + * + * @return bool|null + */ + public function getExplode(): ?bool { + return $this->explode; + } + + /** + * Sets whether to allow reserved characters. + * + * @param bool $allowReserved True to allow reserved characters. + * + * @return ParameterObj + */ + public function setAllowReserved(bool $allowReserved): ParameterObj { + $this->allowReserved = $allowReserved; + return $this; + } + + /** + * Returns whether reserved characters are allowed. + * + * @return bool|null + */ + public function getAllowReserved(): ?bool { + return $this->allowReserved; + } + + /** + * Sets the schema defining the type used for the parameter. + * + * @param mixed $schema Schema Object or any schema definition. + * + * @return ParameterObj + */ + public function setSchema($schema): ParameterObj { + $this->schema = $schema; + return $this; + } + + /** + * Returns the schema. + * + * @return mixed + */ + public function getSchema() { + return $this->schema; + } + + /** + * Sets an example of the parameter's potential value. + * + * @param mixed $example Example value. + * + * @return ParameterObj + */ + public function setExample($example): ParameterObj { + $this->example = $example; + return $this; + } + + /** + * Returns the example. + * + * @return mixed + */ + public function getExample() { + return $this->example; + } + + /** + * Sets examples of the parameter's potential value. + * + * @param array $examples Map of example names to Example Objects or Reference Objects. + * + * @return ParameterObj + */ + public function setExamples(array $examples): ParameterObj { + $this->examples = $examples; + return $this; + } + + /** + * Returns the examples. + * + * @return array|null + */ + public function getExamples(): ?array { + return $this->examples; + } + + /** + * Returns a Json object that represents the Parameter Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('name', $this->name); + $json->add('in', $this->in); + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + if ($this->required) { + $json->add('required', $this->required); + } + + if ($this->deprecated) { + $json->add('deprecated', $this->deprecated); + } + + if ($this->allowEmptyValue) { + $json->add('allowEmptyValue', $this->allowEmptyValue); + } + + if ($this->style !== null) { + $json->add('style', $this->style); + } + + if ($this->explode !== null) { + $json->add('explode', $this->explode); + } + + if ($this->allowReserved !== null) { + $json->add('allowReserved', $this->allowReserved); + } + + if ($this->schema !== null) { + $json->add('schema', $this->schema); + } + + if ($this->example !== null) { + $json->add('example', $this->example); + } + + if ($this->examples !== null) { + $json->add('examples', $this->examples); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/ReferenceObj.php b/WebFiori/Http/OpenAPI/ReferenceObj.php new file mode 100644 index 0000000..b4867bd --- /dev/null +++ b/WebFiori/Http/OpenAPI/ReferenceObj.php @@ -0,0 +1,111 @@ +setRef($ref); + } + + /** + * Sets the reference identifier. + * + * @param string $ref The reference identifier. This MUST be in the form of a URI. + * + * @return ReferenceObj + */ + public function setRef(string $ref): ReferenceObj { + $this->ref = $ref; + return $this; + } + + /** + * Returns the reference identifier. + * + * @return string + */ + 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 + */ + public function setSummary(string $summary): ReferenceObj { + $this->summary = $summary; + return $this; + } + + /** + * Returns the summary. + * + * @return string|null + */ + public function getSummary(): ?string { + return $this->summary; + } + + /** + * Sets a description which by default SHOULD override that of the referenced component. + * + * @param string $description A description. + * CommonMark syntax MAY be used for rich text representation. + * + * @return ReferenceObj + */ + public function setDescription(string $description): ReferenceObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Returns a Json object that represents the Reference Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('$ref', $this->ref); + + if ($this->summary !== null) { + $json->add('summary', $this->summary); + } + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/SecuritySchemeObj.php b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php new file mode 100644 index 0000000..193cc72 --- /dev/null +++ b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php @@ -0,0 +1,241 @@ +setType($type); + } + + /** + * Sets the type of the security scheme. + * + * @param string $type Valid values are "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect". + * + * @return SecuritySchemeObj + */ + 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. + * + * @return SecuritySchemeObj + */ + public function setDescription(string $description): SecuritySchemeObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description. + * + * @return string|null + */ + 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. + * + * @return SecuritySchemeObj + */ + public function setName(string $name): SecuritySchemeObj { + $this->name = $name; + return $this; + } + + /** + * Returns the parameter name. + * + * @return string|null + */ + 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 + */ + public function setIn(string $in): SecuritySchemeObj { + $this->in = $in; + return $this; + } + + /** + * Returns the location of the API key. + * + * @return string|null + */ + public function getIn(): ?string { + return $this->in; + } + + /** + * Sets the name of the HTTP Authentication scheme. + * + * @param string $scheme The HTTP Authentication scheme. REQUIRED for http type. + * + * @return SecuritySchemeObj + */ + public function setScheme(string $scheme): SecuritySchemeObj { + $this->scheme = $scheme; + return $this; + } + + /** + * Returns the HTTP Authentication scheme. + * + * @return string|null + */ + public function getScheme(): ?string { + return $this->scheme; + } + + /** + * Sets a hint to identify how the bearer token is formatted. + * + * @param string $bearerFormat The bearer token format (e.g., "JWT"). + * + * @return SecuritySchemeObj + */ + public function setBearerFormat(string $bearerFormat): SecuritySchemeObj { + $this->bearerFormat = $bearerFormat; + return $this; + } + + /** + * Returns the bearer token format. + * + * @return string|null + */ + public function getBearerFormat(): ?string { + return $this->bearerFormat; + } + + /** + * Sets configuration information for the OAuth2 flow types supported. + * + * @param OAuthFlowsObj $flows OAuth Flows Object. REQUIRED for oauth2 type. + * + * @return SecuritySchemeObj + */ + public function setFlows(OAuthFlowsObj $flows): SecuritySchemeObj { + $this->flows = $flows; + return $this; + } + + /** + * Returns the OAuth flows configuration. + * + * @return OAuthFlowsObj|null + */ + public function getFlows(): ?OAuthFlowsObj { + return $this->flows; + } + + /** + * Sets the OpenID Connect discovery URL. + * + * @param string $openIdConnectUrl Well-known URL to discover the provider metadata. + * REQUIRED for openIdConnect type. + * + * @return SecuritySchemeObj + */ + public function setOpenIdConnectUrl(string $openIdConnectUrl): SecuritySchemeObj { + $this->openIdConnectUrl = $openIdConnectUrl; + return $this; + } + + /** + * Returns the OpenID Connect discovery URL. + * + * @return string|null + */ + public function getOpenIdConnectUrl(): ?string { + return $this->openIdConnectUrl; + } + + /** + * Returns a Json object that represents the Security Scheme Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('type', $this->type); + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + if ($this->name !== null) { + $json->add('name', $this->name); + } + + if ($this->in !== null) { + $json->add('in', $this->in); + } + + if ($this->scheme !== null) { + $json->add('scheme', $this->scheme); + } + + if ($this->bearerFormat !== null) { + $json->add('bearerFormat', $this->bearerFormat); + } + + if ($this->flows !== null) { + $json->add('flows', $this->flows); + } + + if ($this->openIdConnectUrl !== null) { + $json->add('openIdConnectUrl', $this->openIdConnectUrl); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/ServerObj.php b/WebFiori/Http/OpenAPI/ServerObj.php new file mode 100644 index 0000000..cf25a21 --- /dev/null +++ b/WebFiori/Http/OpenAPI/ServerObj.php @@ -0,0 +1,89 @@ +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. + * + * @return ServerObj + */ + public function setUrl(string $url): ServerObj { + $this->url = $url; + return $this; + } + + /** + * Returns the URL to the target host. + * + * @return string + */ + public function getUrl(): string { + return $this->url; + } + + /** + * Sets the description of the host designated by the URL. + * + * @param string $description An optional string describing the host. + * CommonMark syntax MAY be used for rich text representation. + * + * @return ServerObj + */ + public function setDescription(string $description): ServerObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description of the host. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Returns a Json object that represents the Server Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('url', $this->url); + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/TagObj.php b/WebFiori/Http/OpenAPI/TagObj.php new file mode 100644 index 0000000..0522559 --- /dev/null +++ b/WebFiori/Http/OpenAPI/TagObj.php @@ -0,0 +1,116 @@ +setName($name); + + if ($description !== null) { + $this->setDescription($description); + } + } + + /** + * Sets the name of the tag. + * + * @param string $name The name of the tag. + * + * @return TagObj + */ + public function setName(string $name): TagObj { + $this->name = $name; + return $this; + } + + /** + * Returns the name of the tag. + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * Sets the description for the tag. + * + * @param string $description A description for the tag. + * CommonMark syntax MAY be used for rich text representation. + * + * @return TagObj + */ + public function setDescription(string $description): TagObj { + $this->description = $description; + return $this; + } + + /** + * Returns the description for the tag. + * + * @return string|null + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * Sets additional external documentation for this tag. + * + * @param ExternalDocObj $externalDocs External documentation object. + * + * @return TagObj + */ + public function setExternalDocs(ExternalDocObj $externalDocs): TagObj { + $this->externalDocs = $externalDocs; + return $this; + } + + /** + * Returns the external documentation for this tag. + * + * @return ExternalDocObj|null + */ + public function getExternalDocs(): ?ExternalDocObj { + return $this->externalDocs; + } + + /** + * Returns a Json object that represents the Tag Object. + * + * @return Json + */ + public function toJSON(): Json { + $json = new Json(); + + $json->add('name', $this->name); + + if ($this->description !== null) { + $json->add('description', $this->description); + } + + if ($this->externalDocs !== null) { + $json->add('externalDocs', $this->externalDocs); + } + + return $json; + } +} From 2bfdf8ab7f3d79457c975ba5376b9553547289ea Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 02:11:31 +0300 Subject: [PATCH 12/50] chore: OpenAPI --- WebFiori/Http/OpenAPI/ComponentsObj.php | 81 ++++++++++ WebFiori/Http/OpenAPI/ContactObj.php | 39 ++++- WebFiori/Http/OpenAPI/ExternalDocObj.php | 40 ++++- WebFiori/Http/OpenAPI/HeaderObj.php | 97 ++++++++++-- WebFiori/Http/OpenAPI/InfoObj.php | 84 ++++++++-- WebFiori/Http/OpenAPI/LicenseObj.php | 41 ++++- WebFiori/Http/OpenAPI/MediaTypeObj.php | 40 +++++ WebFiori/Http/OpenAPI/OAuthFlowObj.php | 57 ++++++- WebFiori/Http/OpenAPI/OAuthFlowsObj.php | 49 ++++-- WebFiori/Http/OpenAPI/OpenAPIObj.php | 112 ++++++++++++++ WebFiori/Http/OpenAPI/OperationObj.php | 44 ++++++ WebFiori/Http/OpenAPI/ParameterObj.php | 145 +++++++++++++++--- WebFiori/Http/OpenAPI/PathItemObj.php | 99 ++++++++++++ WebFiori/Http/OpenAPI/PathsObj.php | 64 ++++++++ WebFiori/Http/OpenAPI/ReferenceObj.php | 45 +++++- WebFiori/Http/OpenAPI/ResponseObj.php | 47 ++++++ WebFiori/Http/OpenAPI/ResponsesObj.php | 66 ++++++++ .../Http/OpenAPI/SecurityRequirementObj.php | 61 ++++++++ WebFiori/Http/OpenAPI/SecuritySchemeObj.php | 113 ++++++++++++-- WebFiori/Http/OpenAPI/ServerObj.php | 32 +++- WebFiori/Http/OpenAPI/TagObj.php | 46 +++++- 21 files changed, 1285 insertions(+), 117 deletions(-) create mode 100644 WebFiori/Http/OpenAPI/ComponentsObj.php create mode 100644 WebFiori/Http/OpenAPI/MediaTypeObj.php create mode 100644 WebFiori/Http/OpenAPI/OpenAPIObj.php create mode 100644 WebFiori/Http/OpenAPI/OperationObj.php create mode 100644 WebFiori/Http/OpenAPI/PathItemObj.php create mode 100644 WebFiori/Http/OpenAPI/PathsObj.php create mode 100644 WebFiori/Http/OpenAPI/ResponseObj.php create mode 100644 WebFiori/Http/OpenAPI/ResponsesObj.php create mode 100644 WebFiori/Http/OpenAPI/SecurityRequirementObj.php diff --git a/WebFiori/Http/OpenAPI/ComponentsObj.php b/WebFiori/Http/OpenAPI/ComponentsObj.php new file mode 100644 index 0000000..94fb02a --- /dev/null +++ b/WebFiori/Http/OpenAPI/ComponentsObj.php @@ -0,0 +1,81 @@ +schemas[$name] = $schema; + return $this; + } + + /** + * Adds a reusable Security Scheme Object to the components. + * + * The key MUST match the regular expression: ^[a-zA-Z0-9\.\-_]+$ + * + * @param string $name The name/key for the security scheme (e.g., "bearerAuth", "apiKey"). + * @param SecuritySchemeObj $scheme The Security Scheme Object. + * + * @return ComponentsObj Returns self for method chaining. + */ + public function addSecurityScheme(string $name, SecuritySchemeObj $scheme): ComponentsObj { + $this->securitySchemes[$name] = $scheme; + return $this; + } + + /** + * Returns a Json object that represents the Components Object. + * + * The JSON structure follows the OpenAPI 3.1.0 specification. + * + * @return Json A Json object representation following OpenAPI 3.1.0 specification. + */ + $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 cf80ba3..edf843e 100644 --- a/WebFiori/Http/OpenAPI/ContactObj.php +++ b/WebFiori/Http/OpenAPI/ContactObj.php @@ -8,10 +8,35 @@ * Represents a Contact Object in OpenAPI specification. * * Contact information for the exposed API. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#contact-object */ class ContactObj implements JsonI { + /** + * The identifying name of the contact person/organization. + * + * @var string|null + */ private ?string $name = null; + + /** + * The URI for the contact information. + * + * This MUST be in the form of a URI. + * + * @var string|null + */ private ?string $url = null; + + /** + * The email address of the contact person/organization. + * + * This MUST be in the form of an email address. + * + * @var string|null + */ private ?string $email = null; /** @@ -19,7 +44,7 @@ class ContactObj implements JsonI { * * @param string $name The identifying name. * - * @return ContactObj + * @return ContactObj Returns self for method chaining. */ public function setName(string $name): ContactObj { $this->name = $name; @@ -29,7 +54,7 @@ public function setName(string $name): ContactObj { /** * Returns the contact name. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getName(): ?string { return $this->name; @@ -40,7 +65,7 @@ public function getName(): ?string { * * @param string $url The URI for the contact information. This MUST be in the form of a URI. * - * @return ContactObj + * @return ContactObj Returns self for method chaining. */ public function setUrl(string $url): ContactObj { $this->url = $url; @@ -50,7 +75,7 @@ public function setUrl(string $url): ContactObj { /** * Returns the contact URL. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getUrl(): ?string { return $this->url; @@ -61,7 +86,7 @@ public function getUrl(): ?string { * * @param string $email The email address. This MUST be in the form of an email address. * - * @return ContactObj + * @return ContactObj Returns self for method chaining. */ public function setEmail(string $email): ContactObj { $this->email = $email; @@ -71,7 +96,7 @@ public function setEmail(string $email): ContactObj { /** * Returns the contact email. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getEmail(): ?string { return $this->email; @@ -80,7 +105,7 @@ public function getEmail(): ?string { /** * Returns a Json object that represents the Contact Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/ExternalDocObj.php b/WebFiori/Http/OpenAPI/ExternalDocObj.php index bf6cfe0..ece4e47 100644 --- a/WebFiori/Http/OpenAPI/ExternalDocObj.php +++ b/WebFiori/Http/OpenAPI/ExternalDocObj.php @@ -8,13 +8,32 @@ * Represents an External Documentation Object in OpenAPI specification. * * Allows referencing an external resource for extended documentation. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#external-documentation-object */ class ExternalDocObj implements JsonI { + /** + * A description of the target documentation. + * + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; + + /** + * The URI for the target documentation. + * + * This MUST be in the form of a URI. + * + * @var string + */ private string $url; /** - * Creates new instance. + * Creates new instance of External Documentation Object. * * @param string $url The URI for the target documentation. REQUIRED. * @param string|null $description A description of the target documentation. @@ -30,10 +49,11 @@ public function __construct(string $url, ?string $description = null) { /** * Sets the description of the target documentation. * - * @param string $description A description of the target documentation. * CommonMark syntax MAY be used for rich text representation. * - * @return ExternalDocObj + * @param string $description A description of the target documentation. + * + * @return ExternalDocObj Returns self for method chaining. */ public function setDescription(string $description): ExternalDocObj { $this->description = $description; @@ -43,7 +63,7 @@ public function setDescription(string $description): ExternalDocObj { /** * Returns the description of the target documentation. * - * @return string|null + * @return string|null The description, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -52,9 +72,11 @@ public function getDescription(): ?string { /** * Sets the URI for the target documentation. * - * @param string $url The URI for the target documentation. This MUST be in the form of a URI. + * This MUST be in the form of a URI. * - * @return ExternalDocObj + * @param string $url The URI for the target documentation. + * + * @return ExternalDocObj Returns self for method chaining. */ public function setUrl(string $url): ExternalDocObj { $this->url = $url; @@ -64,7 +86,7 @@ public function setUrl(string $url): ExternalDocObj { /** * Returns the URI for the target documentation. * - * @return string + * @return string The URI for the target documentation. */ public function getUrl(): string { return $this->url; @@ -73,7 +95,9 @@ public function getUrl(): string { /** * Returns a Json object that represents the External Documentation Object. * - * @return Json + * The JSON structure follows the OpenAPI 3.1.0 specification. + * + * @return Json A Json object representation of this External Documentation Object. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/HeaderObj.php b/WebFiori/Http/OpenAPI/HeaderObj.php index 2280fe3..041c1d2 100644 --- a/WebFiori/Http/OpenAPI/HeaderObj.php +++ b/WebFiori/Http/OpenAPI/HeaderObj.php @@ -7,16 +7,85 @@ /** * Represents a Header Object in OpenAPI specification. * - * The Header Object follows the structure of the Parameter Object with some differences. + * The Header Object follows the structure of the Parameter Object with the following changes: + * - name MUST NOT be specified, it is given in the corresponding headers map. + * - in MUST NOT be specified, it is implicitly in header. + * - All traits that are affected by the location MUST be applicable to a location of header + * (for example, style). + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#header-object */ class HeaderObj implements JsonI { + /** + * A brief description of the header. + * + * This could contain examples of use. + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; + + /** + * Determines whether this header is mandatory. + * + * The default value is false. + * + * @var bool + */ private bool $required = false; + + /** + * Specifies that the header is deprecated and SHOULD be transitioned out of usage. + * + * Default value is false. + * + * @var bool + */ private bool $deprecated = false; + + /** + * Describes how the header value will be serialized. + * + * The default (and only legal value for headers) is "simple". + * + * @var string|null + */ private ?string $style = null; + + /** + * 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. + * + * @var bool|null + */ private ?bool $explode = null; + + /** + * The schema defining the type used for the header. + * + * @var mixed + */ private $schema = null; + + /** + * Example of the header's potential value. + * + * @var mixed + */ private $example = null; + + /** + * Examples of the header's potential value. + * + * Map of string to Example Object or Reference Object. + * + * @var array|null + */ private ?array $examples = null; /** @@ -24,7 +93,7 @@ class HeaderObj implements JsonI { * * @param string $description A brief description of the header. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setDescription(string $description): HeaderObj { $this->description = $description; @@ -34,7 +103,7 @@ public function setDescription(string $description): HeaderObj { /** * Returns the description. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -45,7 +114,7 @@ public function getDescription(): ?string { * * @param bool $required True if required. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setRequired(bool $required): HeaderObj { $this->required = $required; @@ -66,7 +135,7 @@ public function isRequired(): bool { * * @param bool $deprecated True if deprecated. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setDeprecated(bool $deprecated): HeaderObj { $this->deprecated = $deprecated; @@ -87,7 +156,7 @@ public function isDeprecated(): bool { * * @param string $style The style value. Default is "simple". * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setStyle(string $style): HeaderObj { $this->style = $style; @@ -97,7 +166,7 @@ public function setStyle(string $style): HeaderObj { /** * Returns the style. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getStyle(): ?string { return $this->style; @@ -108,7 +177,7 @@ public function getStyle(): ?string { * * @param bool $explode The explode value. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setExplode(bool $explode): HeaderObj { $this->explode = $explode; @@ -118,7 +187,7 @@ public function setExplode(bool $explode): HeaderObj { /** * Returns the explode value. * - * @return bool|null + * @return bool|null Returns the value, or null if not set. */ public function getExplode(): ?bool { return $this->explode; @@ -129,7 +198,7 @@ public function getExplode(): ?bool { * * @param mixed $schema Schema Object or any schema definition. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setSchema($schema): HeaderObj { $this->schema = $schema; @@ -150,7 +219,7 @@ public function getSchema() { * * @param mixed $example Example value. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setExample($example): HeaderObj { $this->example = $example; @@ -171,7 +240,7 @@ public function getExample() { * * @param array $examples Map of example names to Example Objects or Reference Objects. * - * @return HeaderObj + * @return HeaderObj Returns self for method chaining. */ public function setExamples(array $examples): HeaderObj { $this->examples = $examples; @@ -181,7 +250,7 @@ public function setExamples(array $examples): HeaderObj { /** * Returns the examples. * - * @return array|null + * @return array|null Returns the value, or null if not set. */ public function getExamples(): ?array { return $this->examples; @@ -190,7 +259,7 @@ public function getExamples(): ?array { /** * Returns a Json object that represents the Header Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/InfoObj.php b/WebFiori/Http/OpenAPI/InfoObj.php index 387ee85..9654040 100644 --- a/WebFiori/Http/OpenAPI/InfoObj.php +++ b/WebFiori/Http/OpenAPI/InfoObj.php @@ -8,14 +8,72 @@ * Represents an Info Object in OpenAPI specification. * * The object provides metadata about the API. + * The metadata MAY be used by the clients if needed, and MAY be presented + * in editing or documentation generation tools for convenience. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#info-object */ class InfoObj implements JsonI { + /** + * The title of the API. + * + * REQUIRED. + * + * @var string + */ private string $title; + + /** + * The version of the OpenAPI Document. + * + * This is distinct from the OpenAPI Specification version or the version + * of the API being described or the version of the OpenAPI Description. + * + * REQUIRED. + * + * @var string + */ private string $version; + + /** + * A short summary of the API. + * + * @var string|null + */ private ?string $summary = null; + + /** + * A description of the API. + * + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; + + /** + * A URI for the Terms of Service for the API. + * + * This MUST be in the form of a URI. + * + * @var string|null + */ private ?string $termsOfService = null; + + /** + * The contact information for the exposed API. + * + * @var ContactObj|null + */ private ?ContactObj $contact = null; + + /** + * The license information for the exposed API. + * + * @var LicenseObj|null + */ private ?LicenseObj $license = null; /** @@ -34,7 +92,7 @@ public function __construct(string $title, string $version) { * * @param string $title The title of the API. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setTitle(string $title): InfoObj { $this->title = $title; @@ -55,7 +113,7 @@ public function getTitle(): string { * * @param string $version The version of the OpenAPI Document. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setVersion(string $version): InfoObj { $this->version = $version; @@ -76,7 +134,7 @@ public function getVersion(): string { * * @param string $summary A short summary of the API. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setSummary(string $summary): InfoObj { $this->summary = $summary; @@ -86,7 +144,7 @@ public function setSummary(string $summary): InfoObj { /** * Returns the summary of the API. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getSummary(): ?string { return $this->summary; @@ -98,7 +156,7 @@ public function getSummary(): ?string { * @param string $description A description of the API. * CommonMark syntax MAY be used for rich text representation. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setDescription(string $description): InfoObj { $this->description = $description; @@ -108,7 +166,7 @@ public function setDescription(string $description): InfoObj { /** * Returns the description of the API. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -119,7 +177,7 @@ public function getDescription(): ?string { * * @param string $termsOfService A URI for the Terms of Service. This MUST be in the form of a URI. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setTermsOfService(string $termsOfService): InfoObj { $this->termsOfService = $termsOfService; @@ -129,7 +187,7 @@ public function setTermsOfService(string $termsOfService): InfoObj { /** * Returns the Terms of Service URI. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getTermsOfService(): ?string { return $this->termsOfService; @@ -140,7 +198,7 @@ public function getTermsOfService(): ?string { * * @param ContactObj $contact Contact Object. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setContact(ContactObj $contact): InfoObj { $this->contact = $contact; @@ -150,7 +208,7 @@ public function setContact(ContactObj $contact): InfoObj { /** * Returns the contact information. * - * @return ContactObj|null + * @return ContactObj|null Returns the value, or null if not set. */ public function getContact(): ?ContactObj { return $this->contact; @@ -161,7 +219,7 @@ public function getContact(): ?ContactObj { * * @param LicenseObj $license License Object. * - * @return InfoObj + * @return InfoObj Returns self for method chaining. */ public function setLicense(LicenseObj $license): InfoObj { $this->license = $license; @@ -171,7 +229,7 @@ public function setLicense(LicenseObj $license): InfoObj { /** * Returns the license information. * - * @return LicenseObj|null + * @return LicenseObj|null Returns the value, or null if not set. */ public function getLicense(): ?LicenseObj { return $this->license; @@ -180,7 +238,7 @@ public function getLicense(): ?LicenseObj { /** * Returns a Json object that represents the Info Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/LicenseObj.php b/WebFiori/Http/OpenAPI/LicenseObj.php index 2a3c2dd..927a75d 100644 --- a/WebFiori/Http/OpenAPI/LicenseObj.php +++ b/WebFiori/Http/OpenAPI/LicenseObj.php @@ -8,10 +8,39 @@ * Represents a License Object in OpenAPI specification. * * License information for the exposed API. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#license-object */ class LicenseObj implements JsonI { + /** + * The license name used for the API. + * + * REQUIRED. + * + * @var string + */ private string $name; + + /** + * An SPDX license expression for the API. + * + * The identifier field is mutually exclusive of the url field. + * + * @var string|null + * @see https://spdx.org/licenses/ + */ private ?string $identifier = null; + + /** + * A URI for the license used for the API. + * + * This MUST be in the form of a URI. + * The url field is mutually exclusive of the identifier field. + * + * @var string|null + */ private ?string $url = null; /** @@ -28,7 +57,7 @@ public function __construct(string $name) { * * @param string $name The license name. * - * @return LicenseObj + * @return LicenseObj Returns self for method chaining. */ public function setName(string $name): LicenseObj { $this->name = $name; @@ -50,7 +79,7 @@ public function getName(): string { * @param string $identifier An SPDX license expression. * The identifier field is mutually exclusive of the url field. * - * @return LicenseObj + * @return LicenseObj Returns self for method chaining. */ public function setIdentifier(string $identifier): LicenseObj { $this->identifier = $identifier; @@ -61,7 +90,7 @@ public function setIdentifier(string $identifier): LicenseObj { /** * Returns the SPDX license identifier. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getIdentifier(): ?string { return $this->identifier; @@ -73,7 +102,7 @@ public function getIdentifier(): ?string { * @param string $url A URI for the license. This MUST be in the form of a URI. * The url field is mutually exclusive of the identifier field. * - * @return LicenseObj + * @return LicenseObj Returns self for method chaining. */ public function setUrl(string $url): LicenseObj { $this->url = $url; @@ -84,7 +113,7 @@ public function setUrl(string $url): LicenseObj { /** * Returns the license URL. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getUrl(): ?string { return $this->url; @@ -93,7 +122,7 @@ public function getUrl(): ?string { /** * Returns a Json object that represents the License Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/MediaTypeObj.php b/WebFiori/Http/OpenAPI/MediaTypeObj.php new file mode 100644 index 0000000..2d2ae36 --- /dev/null +++ b/WebFiori/Http/OpenAPI/MediaTypeObj.php @@ -0,0 +1,40 @@ +schema = $schema; + return $this; + } + + public function getSchema() { + return $this->schema; + } + + public function toJSON(): Json { + $json = new Json(); + if ($this->schema !== null) { + $json->add('schema', $this->schema); + } + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/OAuthFlowObj.php b/WebFiori/Http/OpenAPI/OAuthFlowObj.php index e5b2027..10be624 100644 --- a/WebFiori/Http/OpenAPI/OAuthFlowObj.php +++ b/WebFiori/Http/OpenAPI/OAuthFlowObj.php @@ -8,11 +8,52 @@ * Represents an OAuth Flow Object in OpenAPI specification. * * Configuration details for a supported OAuth Flow. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#oauth-flow-object */ class OAuthFlowObj implements JsonI { + /** + * The authorization URL to be used for this flow. + * + * This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + * + * REQUIRED for oauth2 ("implicit", "authorizationCode") flows. + * + * @var string|null + */ private ?string $authorizationUrl = null; + + /** + * The token URL to be used for this flow. + * + * This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + * + * REQUIRED for oauth2 ("password", "clientCredentials", "authorizationCode") flows. + * + * @var string|null + */ private ?string $tokenUrl = null; + + /** + * The URL to be used for obtaining refresh tokens. + * + * This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. + * + * @var string|null + */ private ?string $refreshUrl = null; + + /** + * The available scopes for the OAuth2 security scheme. + * + * A map between the scope name and a short description for it. The map MAY be empty. + * + * REQUIRED. + * + * @var array + */ private array $scopes = []; /** @@ -31,7 +72,7 @@ public function __construct(array $scopes = []) { * @param string $authorizationUrl The authorization URL. This MUST be in the form of a URL. * REQUIRED for implicit and authorizationCode flows. * - * @return OAuthFlowObj + * @return OAuthFlowObj Returns self for method chaining. */ public function setAuthorizationUrl(string $authorizationUrl): OAuthFlowObj { $this->authorizationUrl = $authorizationUrl; @@ -41,7 +82,7 @@ public function setAuthorizationUrl(string $authorizationUrl): OAuthFlowObj { /** * Returns the authorization URL. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getAuthorizationUrl(): ?string { return $this->authorizationUrl; @@ -53,7 +94,7 @@ public function getAuthorizationUrl(): ?string { * @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 setTokenUrl(string $tokenUrl): OAuthFlowObj { $this->tokenUrl = $tokenUrl; @@ -63,7 +104,7 @@ public function setTokenUrl(string $tokenUrl): OAuthFlowObj { /** * Returns the token URL. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getTokenUrl(): ?string { return $this->tokenUrl; @@ -74,7 +115,7 @@ public function getTokenUrl(): ?string { * * @param string $refreshUrl The refresh URL. This MUST be in the form of a URL. * - * @return OAuthFlowObj + * @return OAuthFlowObj Returns self for method chaining. */ public function setRefreshUrl(string $refreshUrl): OAuthFlowObj { $this->refreshUrl = $refreshUrl; @@ -84,7 +125,7 @@ public function setRefreshUrl(string $refreshUrl): OAuthFlowObj { /** * Returns the refresh URL. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getRefreshUrl(): ?string { return $this->refreshUrl; @@ -95,7 +136,7 @@ public function getRefreshUrl(): ?string { * * @param array $scopes A map between the scope name and a short description for it. * - * @return OAuthFlowObj + * @return OAuthFlowObj Returns self for method chaining. */ public function setScopes(array $scopes): OAuthFlowObj { $this->scopes = $scopes; @@ -127,7 +168,7 @@ public function getScopes(): array { /** * Returns a Json object that represents the OAuth Flow Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php index 294642c..05cc5e2 100644 --- a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php +++ b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php @@ -8,11 +8,42 @@ * Represents an OAuth Flows Object in OpenAPI specification. * * Allows configuration of the supported OAuth Flows. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#oauth-flows-object */ class OAuthFlowsObj implements JsonI { + /** + * Configuration for the OAuth Implicit flow. + * + * @var OAuthFlowObj|null + */ private ?OAuthFlowObj $implicit = null; + + /** + * Configuration for the OAuth Resource Owner Password flow. + * + * @var OAuthFlowObj|null + */ private ?OAuthFlowObj $password = null; + + /** + * Configuration for the OAuth Client Credentials flow. + * + * Previously called application in OpenAPI 2.0. + * + * @var OAuthFlowObj|null + */ private ?OAuthFlowObj $clientCredentials = null; + + /** + * Configuration for the OAuth Authorization Code flow. + * + * Previously called accessCode in OpenAPI 2.0. + * + * @var OAuthFlowObj|null + */ private ?OAuthFlowObj $authorizationCode = null; /** @@ -20,7 +51,7 @@ class OAuthFlowsObj implements JsonI { * * @param OAuthFlowObj $implicit OAuth Flow Object for implicit flow. * - * @return OAuthFlowsObj + * @return OAuthFlowsObj Returns self for method chaining. */ public function setImplicit(OAuthFlowObj $implicit): OAuthFlowsObj { $this->implicit = $implicit; @@ -30,7 +61,7 @@ public function setImplicit(OAuthFlowObj $implicit): OAuthFlowsObj { /** * Returns the implicit flow configuration. * - * @return OAuthFlowObj|null + * @return OAuthFlowObj|null Returns the value, or null if not set. */ public function getImplicit(): ?OAuthFlowObj { return $this->implicit; @@ -41,7 +72,7 @@ public function getImplicit(): ?OAuthFlowObj { * * @param OAuthFlowObj $password OAuth Flow Object for password flow. * - * @return OAuthFlowsObj + * @return OAuthFlowsObj Returns self for method chaining. */ public function setPassword(OAuthFlowObj $password): OAuthFlowsObj { $this->password = $password; @@ -51,7 +82,7 @@ public function setPassword(OAuthFlowObj $password): OAuthFlowsObj { /** * Returns the password flow configuration. * - * @return OAuthFlowObj|null + * @return OAuthFlowObj|null Returns the value, or null if not set. */ public function getPassword(): ?OAuthFlowObj { return $this->password; @@ -62,7 +93,7 @@ public function getPassword(): ?OAuthFlowObj { * * @param OAuthFlowObj $clientCredentials OAuth Flow Object for client credentials flow. * - * @return OAuthFlowsObj + * @return OAuthFlowsObj Returns self for method chaining. */ public function setClientCredentials(OAuthFlowObj $clientCredentials): OAuthFlowsObj { $this->clientCredentials = $clientCredentials; @@ -72,7 +103,7 @@ public function setClientCredentials(OAuthFlowObj $clientCredentials): OAuthFlow /** * Returns the client credentials flow configuration. * - * @return OAuthFlowObj|null + * @return OAuthFlowObj|null Returns the value, or null if not set. */ public function getClientCredentials(): ?OAuthFlowObj { return $this->clientCredentials; @@ -83,7 +114,7 @@ public function getClientCredentials(): ?OAuthFlowObj { * * @param OAuthFlowObj $authorizationCode OAuth Flow Object for authorization code flow. * - * @return OAuthFlowsObj + * @return OAuthFlowsObj Returns self for method chaining. */ public function setAuthorizationCode(OAuthFlowObj $authorizationCode): OAuthFlowsObj { $this->authorizationCode = $authorizationCode; @@ -93,7 +124,7 @@ public function setAuthorizationCode(OAuthFlowObj $authorizationCode): OAuthFlow /** * Returns the authorization code flow configuration. * - * @return OAuthFlowObj|null + * @return OAuthFlowObj|null Returns the value, or null if not set. */ public function getAuthorizationCode(): ?OAuthFlowObj { return $this->authorizationCode; @@ -102,7 +133,7 @@ public function getAuthorizationCode(): ?OAuthFlowObj { /** * Returns a Json object that represents the OAuth Flows Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/OpenAPIObj.php b/WebFiori/Http/OpenAPI/OpenAPIObj.php new file mode 100644 index 0000000..d5cc540 --- /dev/null +++ b/WebFiori/Http/OpenAPI/OpenAPIObj.php @@ -0,0 +1,112 @@ +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. + * + * @return OpenAPIObj Returns self for method chaining. + */ + public function setOpenapi(string $openapi): OpenAPIObj { + $this->openapi = $openapi; + return $this; + } + + /** + * Returns the OpenAPI Specification version number. + * + * @return string The OpenAPI Specification version. + */ + public function getOpenapi(): string { + return $this->openapi; + } + + /** + * Sets the Info Object containing API metadata. + * + * @param InfoObj $info The Info Object with API metadata. + * + * @return OpenAPIObj Returns self for method chaining. + */ + public function setInfo(InfoObj $info): OpenAPIObj { + $this->info = $info; + return $this; + } + + /** + * Returns the Info Object containing API metadata. + * + * @return InfoObj The Info Object. + */ + public function getInfo(): InfoObj { + return $this->info; + } + + /** + * Returns a Json object that represents the OpenAPI Object. + * + * The JSON structure follows the OpenAPI 3.1.0 specification and represents + * a complete OpenAPI Description document. + * + * @return Json A Json object representation following OpenAPI 3.1.0 specification. + */ + public function toJSON(): Json { + $json = new Json(); + $json->add('openapi', $this->openapi); + $json->add('info', $this->info); + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/OperationObj.php b/WebFiori/Http/OpenAPI/OperationObj.php new file mode 100644 index 0000000..c309ba8 --- /dev/null +++ b/WebFiori/Http/OpenAPI/OperationObj.php @@ -0,0 +1,44 @@ +responses = $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(); + $json->add('responses', $this->responses); + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/ParameterObj.php b/WebFiori/Http/OpenAPI/ParameterObj.php index cc4e019..e77ebb7 100644 --- a/WebFiori/Http/OpenAPI/ParameterObj.php +++ b/WebFiori/Http/OpenAPI/ParameterObj.php @@ -8,19 +8,128 @@ * Represents a Parameter Object in OpenAPI specification. * * Describes a single operation parameter. + * + * A unique parameter is defined by a combination of a name and location. + * + * Parameter Objects MUST include either a content field or a schema field, but not both. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#parameter-object */ class ParameterObj implements JsonI { + /** + * The name of the parameter. + * + * Parameter names are case sensitive. + * + * REQUIRED. + * + * @var string + */ private string $name; + + /** + * The location of the parameter. + * + * Possible values are "query", "header", "path" or "cookie". + * + * REQUIRED. + * + * @var string + */ private string $in; + + /** + * A brief description of the parameter. + * + * This could contain examples of use. + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; + + /** + * Determines whether this parameter is mandatory. + * + * If the parameter location is "path", this property is REQUIRED and its value MUST be true. + * Otherwise, the property MAY be included and its default value is false. + * + * @var bool + */ private bool $required = false; + + /** + * Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. + * + * Default value is false. + * + * @var bool + */ private bool $deprecated = false; + + /** + * If true, clients MAY pass a zero-length string value in place of parameters + * that would otherwise be omitted entirely. + * + * Default value is false. This field is valid only for query parameters. + * Use of this property is NOT RECOMMENDED. + * + * @var bool + */ private bool $allowEmptyValue = false; + + /** + * Describes how the parameter value will be serialized. + * + * Default values (based on value of in): + * for "query" - "form"; for "path" - "simple"; + * for "header" - "simple"; for "cookie" - "form". + * + * @var string|null + */ private ?string $style = null; + + /** + * When this is true, parameter values of type array or object generate + * separate parameters for each value of the array or key-value pair of the map. + * + * @var bool|null + */ private ?bool $explode = null; + + /** + * When this is true, parameter values are serialized using reserved expansion. + * + * This field only applies to parameters with an in value of query. + * The default value is false. + * + * @var bool|null + */ private ?bool $allowReserved = null; + + /** + * The schema defining the type used for the parameter. + * + * @var mixed + */ private $schema = null; + + /** + * Example of the parameter's potential value. + * + * @var mixed + */ private $example = null; + + /** + * Examples of the parameter's potential value. + * + * Map of string to Example Object or Reference Object. + * + * @var array|null + */ private ?array $examples = null; /** @@ -39,7 +148,7 @@ public function __construct(string $name, string $in) { * * @param string $name The name of the parameter. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setName(string $name): ParameterObj { $this->name = $name; @@ -60,7 +169,7 @@ public function getName(): string { * * @param string $in The location. Possible values: "query", "header", "path", "cookie". * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setIn(string $in): ParameterObj { $this->in = $in; @@ -84,7 +193,7 @@ public function getIn(): string { * * @param string $description A brief description of the parameter. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setDescription(string $description): ParameterObj { $this->description = $description; @@ -94,7 +203,7 @@ public function setDescription(string $description): ParameterObj { /** * Returns the description. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -105,7 +214,7 @@ public function getDescription(): ?string { * * @param bool $required True if required. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setRequired(bool $required): ParameterObj { $this->required = $required; @@ -126,7 +235,7 @@ public function isRequired(): bool { * * @param bool $deprecated True if deprecated. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setDeprecated(bool $deprecated): ParameterObj { $this->deprecated = $deprecated; @@ -147,7 +256,7 @@ public function isDeprecated(): bool { * * @param bool $allowEmptyValue True to allow empty value. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setAllowEmptyValue(bool $allowEmptyValue): ParameterObj { $this->allowEmptyValue = $allowEmptyValue; @@ -168,7 +277,7 @@ public function isAllowEmptyValue(): bool { * * @param string $style The style value. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setStyle(string $style): ParameterObj { $this->style = $style; @@ -178,7 +287,7 @@ public function setStyle(string $style): ParameterObj { /** * Returns the style. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getStyle(): ?string { return $this->style; @@ -189,7 +298,7 @@ public function getStyle(): ?string { * * @param bool $explode The explode value. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setExplode(bool $explode): ParameterObj { $this->explode = $explode; @@ -199,7 +308,7 @@ public function setExplode(bool $explode): ParameterObj { /** * Returns the explode value. * - * @return bool|null + * @return bool|null Returns the value, or null if not set. */ public function getExplode(): ?bool { return $this->explode; @@ -210,7 +319,7 @@ public function getExplode(): ?bool { * * @param bool $allowReserved True to allow reserved characters. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setAllowReserved(bool $allowReserved): ParameterObj { $this->allowReserved = $allowReserved; @@ -220,7 +329,7 @@ public function setAllowReserved(bool $allowReserved): ParameterObj { /** * Returns whether reserved characters are allowed. * - * @return bool|null + * @return bool|null Returns the value, or null if not set. */ public function getAllowReserved(): ?bool { return $this->allowReserved; @@ -231,7 +340,7 @@ public function getAllowReserved(): ?bool { * * @param mixed $schema Schema Object or any schema definition. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setSchema($schema): ParameterObj { $this->schema = $schema; @@ -252,7 +361,7 @@ public function getSchema() { * * @param mixed $example Example value. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setExample($example): ParameterObj { $this->example = $example; @@ -273,7 +382,7 @@ public function getExample() { * * @param array $examples Map of example names to Example Objects or Reference Objects. * - * @return ParameterObj + * @return ParameterObj Returns self for method chaining. */ public function setExamples(array $examples): ParameterObj { $this->examples = $examples; @@ -283,7 +392,7 @@ public function setExamples(array $examples): ParameterObj { /** * Returns the examples. * - * @return array|null + * @return array|null Returns the value, or null if not set. */ public function getExamples(): ?array { return $this->examples; @@ -292,7 +401,7 @@ public function getExamples(): ?array { /** * Returns a Json object that represents the Parameter Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/PathItemObj.php b/WebFiori/Http/OpenAPI/PathItemObj.php new file mode 100644 index 0000000..aba5e06 --- /dev/null +++ b/WebFiori/Http/OpenAPI/PathItemObj.php @@ -0,0 +1,99 @@ +get = $operation; + return $this; + } + + public function setPost(OperationObj $operation): PathItemObj { + $this->post = $operation; + return $this; + } + + public function setPut(OperationObj $operation): PathItemObj { + $this->put = $operation; + return $this; + } + + public function setDelete(OperationObj $operation): PathItemObj { + $this->delete = $operation; + return $this; + } + + public function setPatch(OperationObj $operation): PathItemObj { + $this->patch = $operation; + return $this; + } + + public function toJSON(): Json { + $json = new Json(); + + if ($this->get !== null) { + $json->add('get', $this->get); + } + if ($this->post !== null) { + $json->add('post', $this->post); + } + if ($this->put !== null) { + $json->add('put', $this->put); + } + if ($this->delete !== null) { + $json->add('delete', $this->delete); + } + if ($this->patch !== null) { + $json->add('patch', $this->patch); + } + + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/PathsObj.php b/WebFiori/Http/OpenAPI/PathsObj.php new file mode 100644 index 0000000..401d5bf --- /dev/null +++ b/WebFiori/Http/OpenAPI/PathsObj.php @@ -0,0 +1,64 @@ +paths[$path] = $pathItem; + return $this; + } + + /** + * Returns all paths and their operations. + * + * @return array Map of path strings to Path Item Objects. + */ + public function getPaths(): array { + return $this->paths; + } + + /** + * Returns a Json object that represents the Paths Object. + * + * The JSON structure follows the OpenAPI 3.1.0 specification. + * + * @return Json A Json object representation following OpenAPI 3.1.0 specification. + */ + $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 b4867bd..5877427 100644 --- a/WebFiori/Http/OpenAPI/ReferenceObj.php +++ b/WebFiori/Http/OpenAPI/ReferenceObj.php @@ -9,10 +9,43 @@ * * A simple object to allow referencing other components in the OpenAPI Description, * internally and externally. + * + * The $ref string value contains a URI (RFC3986), which identifies the value being referenced. + * + * This object cannot be extended with additional properties, and any properties + * added SHALL be ignored. + * + * @see https://spec.openapis.org/oas/v3.1.0#reference-object */ class ReferenceObj implements JsonI { + /** + * The reference identifier. + * + * This MUST be in the form of a URI. + * + * REQUIRED. + * + * @var string + */ private string $ref; + + /** + * A short summary which by default SHOULD override that of the referenced component. + * + * If the referenced object-type does not allow a summary field, then this field has no effect. + * + * @var string|null + */ private ?string $summary = null; + + /** + * A description which by default SHOULD override that of the referenced component. + * + * CommonMark syntax MAY be used for rich text representation. + * If the referenced object-type does not allow a description field, then this field has no effect. + * + * @var string|null + */ private ?string $description = null; /** @@ -29,7 +62,7 @@ public function __construct(string $ref) { * * @param string $ref The reference identifier. This MUST be in the form of a URI. * - * @return ReferenceObj + * @return ReferenceObj Returns self for method chaining. */ public function setRef(string $ref): ReferenceObj { $this->ref = $ref; @@ -50,7 +83,7 @@ public function getRef(): string { * * @param string $summary A short summary. * - * @return ReferenceObj + * @return ReferenceObj Returns self for method chaining. */ public function setSummary(string $summary): ReferenceObj { $this->summary = $summary; @@ -60,7 +93,7 @@ public function setSummary(string $summary): ReferenceObj { /** * Returns the summary. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getSummary(): ?string { return $this->summary; @@ -72,7 +105,7 @@ public function getSummary(): ?string { * @param string $description A description. * CommonMark syntax MAY be used for rich text representation. * - * @return ReferenceObj + * @return ReferenceObj Returns self for method chaining. */ public function setDescription(string $description): ReferenceObj { $this->description = $description; @@ -82,7 +115,7 @@ public function setDescription(string $description): ReferenceObj { /** * Returns the description. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -91,7 +124,7 @@ public function getDescription(): ?string { /** * Returns a Json object that represents the Reference Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/ResponseObj.php b/WebFiori/Http/OpenAPI/ResponseObj.php new file mode 100644 index 0000000..1e28554 --- /dev/null +++ b/WebFiori/Http/OpenAPI/ResponseObj.php @@ -0,0 +1,47 @@ +description = $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(); + $json->add('description', $this->description); + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/ResponsesObj.php b/WebFiori/Http/OpenAPI/ResponsesObj.php new file mode 100644 index 0000000..3f96dba --- /dev/null +++ b/WebFiori/Http/OpenAPI/ResponsesObj.php @@ -0,0 +1,66 @@ +responses[$statusCode] = $response; + return $this; + } + + /** + * Returns all responses mapped by status code. + * + * @return array Map of status codes to Response Objects. + */ + public function getResponses(): array { + return $this->responses; + } + + /** + * Returns a Json object that represents the Responses Object. + * + * The JSON structure follows the OpenAPI 3.1.0 specification. + * + * @return Json A Json object representation following OpenAPI 3.1.0 specification. + */ + $json = new Json(); + foreach ($this->responses as $code => $response) { + $json->add($code, $response); + } + return $json; + } +} diff --git a/WebFiori/Http/OpenAPI/SecurityRequirementObj.php b/WebFiori/Http/OpenAPI/SecurityRequirementObj.php new file mode 100644 index 0000000..40b1728 --- /dev/null +++ b/WebFiori/Http/OpenAPI/SecurityRequirementObj.php @@ -0,0 +1,61 @@ +requirements[$name] = $scopes; + return $this; + } + + /** + * Returns a Json object that represents the Security Requirement Object. + * + * The JSON structure follows the OpenAPI 3.1.0 specification. + * + * @return Json A Json object representation following OpenAPI 3.1.0 specification. + */ + $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 193cc72..ddb13fc 100644 --- a/WebFiori/Http/OpenAPI/SecuritySchemeObj.php +++ b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php @@ -8,15 +8,96 @@ * Represents a Security Scheme Object in OpenAPI specification. * * Defines a security scheme that can be used by the operations. + * + * Supported schemes are HTTP authentication, an API key (either as a header, + * a cookie parameter or as a query parameter), mutual TLS (use of a client certificate), + * OAuth2's common flows (implicit, password, client credentials and authorization code) + * as defined in RFC6749, and OpenID Connect Discovery. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#security-scheme-object */ class SecuritySchemeObj implements JsonI { + /** + * The type of the security scheme. + * + * Valid values are "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect". + * + * REQUIRED. + * + * @var string + */ private string $type; + + /** + * A description for security scheme. + * + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; + + /** + * The name of the header, query or cookie parameter to be used. + * + * REQUIRED for apiKey type. + * + * @var string|null + */ private ?string $name = null; + + /** + * The location of the API key. + * + * Valid values are "query", "header", or "cookie". + * + * REQUIRED for apiKey type. + * + * @var string|null + */ private ?string $in = null; + + /** + * The name of the HTTP Authentication scheme. + * + * The values used SHOULD be registered in the IANA Authentication Scheme registry. + * + * REQUIRED for http type. + * + * @var string|null + */ private ?string $scheme = null; + + /** + * A hint to the client to identify how the bearer token is formatted. + * + * Bearer tokens are usually generated by an authorization server, + * so this information is primarily for documentation purposes. + * + * Applies to http ("bearer") type. + * + * @var string|null + */ private ?string $bearerFormat = null; + + /** + * An object containing configuration information for the flow types supported. + * + * REQUIRED for oauth2 type. + * + * @var OAuthFlowsObj|null + */ private ?OAuthFlowsObj $flows = null; + + /** + * Well-known URL to discover the OpenID Connect provider metadata. + * + * REQUIRED for openIdConnect type. + * + * @var string|null + */ private ?string $openIdConnectUrl = null; /** @@ -34,7 +115,7 @@ public function __construct(string $type) { * * @param string $type Valid values are "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect". * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setType(string $type): SecuritySchemeObj { $this->type = $type; @@ -55,7 +136,7 @@ public function getType(): string { * * @param string $description A description for security scheme. * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setDescription(string $description): SecuritySchemeObj { $this->description = $description; @@ -65,7 +146,7 @@ public function setDescription(string $description): SecuritySchemeObj { /** * Returns the description. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -76,7 +157,7 @@ public function getDescription(): ?string { * * @param string $name The parameter name. REQUIRED for apiKey type. * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setName(string $name): SecuritySchemeObj { $this->name = $name; @@ -86,7 +167,7 @@ public function setName(string $name): SecuritySchemeObj { /** * Returns the parameter name. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getName(): ?string { return $this->name; @@ -97,7 +178,7 @@ public function getName(): ?string { * * @param string $in Valid values are "query", "header", or "cookie". REQUIRED for apiKey type. * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setIn(string $in): SecuritySchemeObj { $this->in = $in; @@ -107,7 +188,7 @@ public function setIn(string $in): SecuritySchemeObj { /** * Returns the location of the API key. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getIn(): ?string { return $this->in; @@ -118,7 +199,7 @@ public function getIn(): ?string { * * @param string $scheme The HTTP Authentication scheme. REQUIRED for http type. * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setScheme(string $scheme): SecuritySchemeObj { $this->scheme = $scheme; @@ -128,7 +209,7 @@ public function setScheme(string $scheme): SecuritySchemeObj { /** * Returns the HTTP Authentication scheme. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getScheme(): ?string { return $this->scheme; @@ -139,7 +220,7 @@ public function getScheme(): ?string { * * @param string $bearerFormat The bearer token format (e.g., "JWT"). * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setBearerFormat(string $bearerFormat): SecuritySchemeObj { $this->bearerFormat = $bearerFormat; @@ -149,7 +230,7 @@ public function setBearerFormat(string $bearerFormat): SecuritySchemeObj { /** * Returns the bearer token format. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getBearerFormat(): ?string { return $this->bearerFormat; @@ -160,7 +241,7 @@ public function getBearerFormat(): ?string { * * @param OAuthFlowsObj $flows OAuth Flows Object. REQUIRED for oauth2 type. * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setFlows(OAuthFlowsObj $flows): SecuritySchemeObj { $this->flows = $flows; @@ -170,7 +251,7 @@ public function setFlows(OAuthFlowsObj $flows): SecuritySchemeObj { /** * Returns the OAuth flows configuration. * - * @return OAuthFlowsObj|null + * @return OAuthFlowsObj|null Returns the value, or null if not set. */ public function getFlows(): ?OAuthFlowsObj { return $this->flows; @@ -182,7 +263,7 @@ public function getFlows(): ?OAuthFlowsObj { * @param string $openIdConnectUrl Well-known URL to discover the provider metadata. * REQUIRED for openIdConnect type. * - * @return SecuritySchemeObj + * @return SecuritySchemeObj Returns self for method chaining. */ public function setOpenIdConnectUrl(string $openIdConnectUrl): SecuritySchemeObj { $this->openIdConnectUrl = $openIdConnectUrl; @@ -192,7 +273,7 @@ public function setOpenIdConnectUrl(string $openIdConnectUrl): SecuritySchemeObj /** * Returns the OpenID Connect discovery URL. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getOpenIdConnectUrl(): ?string { return $this->openIdConnectUrl; @@ -201,7 +282,7 @@ public function getOpenIdConnectUrl(): ?string { /** * Returns a Json object that represents the Security Scheme Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/ServerObj.php b/WebFiori/Http/OpenAPI/ServerObj.php index cf25a21..621f56e 100644 --- a/WebFiori/Http/OpenAPI/ServerObj.php +++ b/WebFiori/Http/OpenAPI/ServerObj.php @@ -8,9 +8,33 @@ * Represents a Server Object in OpenAPI specification. * * An object representing a Server. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#server-object */ class ServerObj implements JsonI { + /** + * A URL to the target host. + * + * This URL supports Server Variables and MAY be relative, to indicate that + * the host location is relative to the location where the document containing + * the Server Object is being served. Variable substitutions will be made when + * a variable is named in {braces}. + * + * REQUIRED. + * + * @var string + */ private string $url; + + /** + * An optional string describing the host designated by the URL. + * + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; /** @@ -32,7 +56,7 @@ public function __construct(string $url, ?string $description = null) { * * @param string $url A URL to the target host. This URL supports Server Variables and MAY be relative. * - * @return ServerObj + * @return ServerObj Returns self for method chaining. */ public function setUrl(string $url): ServerObj { $this->url = $url; @@ -54,7 +78,7 @@ public function getUrl(): string { * @param string $description An optional string describing the host. * CommonMark syntax MAY be used for rich text representation. * - * @return ServerObj + * @return ServerObj Returns self for method chaining. */ public function setDescription(string $description): ServerObj { $this->description = $description; @@ -64,7 +88,7 @@ public function setDescription(string $description): ServerObj { /** * Returns the description of the host. * - * @return string|null + * @return string|null Returns the value, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -73,7 +97,7 @@ public function getDescription(): ?string { /** * Returns a Json object that represents the Server Object. * - * @return Json + * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { $json = new Json(); diff --git a/WebFiori/Http/OpenAPI/TagObj.php b/WebFiori/Http/OpenAPI/TagObj.php index 0522559..99e0998 100644 --- a/WebFiori/Http/OpenAPI/TagObj.php +++ b/WebFiori/Http/OpenAPI/TagObj.php @@ -9,10 +9,35 @@ * * Adds metadata to a single tag that is used by the Operation Object. * It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. + * + * This object MAY be extended with Specification Extensions. + * + * @see https://spec.openapis.org/oas/v3.1.0#tag-object */ class TagObj implements JsonI { + /** + * The name of the tag. + * + * REQUIRED. + * + * @var string + */ private string $name; + + /** + * A description for the tag. + * + * CommonMark syntax MAY be used for rich text representation. + * + * @var string|null + */ private ?string $description = null; + + /** + * Additional external documentation for this tag. + * + * @var ExternalDocObj|null + */ private ?ExternalDocObj $externalDocs = null; /** @@ -32,9 +57,11 @@ public function __construct(string $name, ?string $description = null) { /** * Sets the name of the tag. * + * The tag name is used to group operations in the OpenAPI Description. + * * @param string $name The name of the tag. * - * @return TagObj + * @return TagObj Returns self for method chaining. */ public function setName(string $name): TagObj { $this->name = $name; @@ -44,7 +71,7 @@ public function setName(string $name): TagObj { /** * Returns the name of the tag. * - * @return string + * @return string The tag name. */ public function getName(): string { return $this->name; @@ -53,10 +80,11 @@ public function getName(): string { /** * Sets the description for the tag. * - * @param string $description A description for the tag. * CommonMark syntax MAY be used for rich text representation. * - * @return TagObj + * @param string $description A description for the tag. + * + * @return TagObj Returns self for method chaining. */ public function setDescription(string $description): TagObj { $this->description = $description; @@ -66,7 +94,7 @@ public function setDescription(string $description): TagObj { /** * Returns the description for the tag. * - * @return string|null + * @return string|null The description, or null if not set. */ public function getDescription(): ?string { return $this->description; @@ -77,7 +105,7 @@ public function getDescription(): ?string { * * @param ExternalDocObj $externalDocs External documentation object. * - * @return TagObj + * @return TagObj Returns self for method chaining. */ public function setExternalDocs(ExternalDocObj $externalDocs): TagObj { $this->externalDocs = $externalDocs; @@ -87,7 +115,7 @@ public function setExternalDocs(ExternalDocObj $externalDocs): TagObj { /** * Returns the external documentation for this tag. * - * @return ExternalDocObj|null + * @return ExternalDocObj|null The external documentation object, or null if not set. */ public function getExternalDocs(): ?ExternalDocObj { return $this->externalDocs; @@ -96,7 +124,9 @@ public function getExternalDocs(): ?ExternalDocObj { /** * Returns a Json object that represents the Tag Object. * - * @return Json + * The JSON structure follows the OpenAPI 3.1.0 specification. + * + * @return Json A Json object representation of this Tag Object. */ public function toJSON(): Json { $json = new Json(); From 930e883cf4e546c27ee1a8b7f65a1f4422b8bd17 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 23:55:51 +0300 Subject: [PATCH 13/50] refactor: Enhanced `toJSON` --- WebFiori/Http/OpenAPI/ContactObj.php | 12 +++--- WebFiori/Http/OpenAPI/ExternalDocObj.php | 10 ++--- WebFiori/Http/OpenAPI/HeaderObj.php | 32 +++++++------- WebFiori/Http/OpenAPI/InfoObj.php | 28 ++++++------ WebFiori/Http/OpenAPI/LicenseObj.php | 14 +++--- WebFiori/Http/OpenAPI/MediaTypeObj.php | 6 ++- WebFiori/Http/OpenAPI/OAuthFlowObj.php | 18 ++++---- WebFiori/Http/OpenAPI/OAuthFlowsObj.php | 16 +++---- WebFiori/Http/OpenAPI/OpenAPIObj.php | 8 ++-- WebFiori/Http/OpenAPI/OperationObj.php | 6 ++- WebFiori/Http/OpenAPI/ParameterObj.php | 48 ++++++++++----------- WebFiori/Http/OpenAPI/PathItemObj.php | 24 ++++++----- WebFiori/Http/OpenAPI/ReferenceObj.php | 14 +++--- WebFiori/Http/OpenAPI/ResponseObj.php | 6 ++- WebFiori/Http/OpenAPI/SecuritySchemeObj.php | 34 +++++++-------- WebFiori/Http/OpenAPI/ServerObj.php | 10 ++--- WebFiori/Http/OpenAPI/TagObj.php | 14 +++--- 17 files changed, 156 insertions(+), 144 deletions(-) diff --git a/WebFiori/Http/OpenAPI/ContactObj.php b/WebFiori/Http/OpenAPI/ContactObj.php index edf843e..06ddfe6 100644 --- a/WebFiori/Http/OpenAPI/ContactObj.php +++ b/WebFiori/Http/OpenAPI/ContactObj.php @@ -110,16 +110,16 @@ public function getEmail(): ?string { public function toJSON(): Json { $json = new Json(); - if ($this->name !== null) { - $json->add('name', $this->name); + if ($this->getName() !== null) { + $json->add('name', $this->getName()); } - if ($this->url !== null) { - $json->add('url', $this->url); + if ($this->getUrl() !== null) { + $json->add('url', $this->getUrl()); } - if ($this->email !== null) { - $json->add('email', $this->email); + if ($this->getEmail() !== null) { + $json->add('email', $this->getEmail()); } return $json; diff --git a/WebFiori/Http/OpenAPI/ExternalDocObj.php b/WebFiori/Http/OpenAPI/ExternalDocObj.php index ece4e47..43c2193 100644 --- a/WebFiori/Http/OpenAPI/ExternalDocObj.php +++ b/WebFiori/Http/OpenAPI/ExternalDocObj.php @@ -100,14 +100,14 @@ public function getUrl(): string { * @return Json A Json object representation of this External Documentation Object. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'url' => $this->getUrl() + ]); - if ($this->description !== null) { - $json->add('description', $this->description); + if ($this->getDescription() !== null) { + $json->add('description', $this->getDescription()); } - $json->add('url', $this->url); - return $json; } } diff --git a/WebFiori/Http/OpenAPI/HeaderObj.php b/WebFiori/Http/OpenAPI/HeaderObj.php index 041c1d2..d8c163e 100644 --- a/WebFiori/Http/OpenAPI/HeaderObj.php +++ b/WebFiori/Http/OpenAPI/HeaderObj.php @@ -264,36 +264,36 @@ public function getExamples(): ?array { public function toJSON(): Json { $json = new Json(); - if ($this->description !== null) { - $json->add('description', $this->description); + if ($this->getDescription() !== null) { + $json->add('description', $this->getDescription()); } - if ($this->required) { - $json->add('required', $this->required); + if ($this->getRequired()) { + $json->add('required', $this->getRequired()); } - if ($this->deprecated) { - $json->add('deprecated', $this->deprecated); + if ($this->getDeprecated()) { + $json->add('deprecated', $this->getDeprecated()); } - if ($this->style !== null) { - $json->add('style', $this->style); + if ($this->getStyle() !== null) { + $json->add('style', $this->getStyle()); } - if ($this->explode !== null) { - $json->add('explode', $this->explode); + if ($this->getExplode() !== null) { + $json->add('explode', $this->getExplode()); } - if ($this->schema !== null) { - $json->add('schema', $this->schema); + if ($this->getSchema() !== null) { + $json->add('schema', $this->getSchema()); } - if ($this->example !== null) { - $json->add('example', $this->example); + if ($this->getExample() !== null) { + $json->add('example', $this->getExample()); } - if ($this->examples !== null) { - $json->add('examples', $this->examples); + 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 9654040..2e886ef 100644 --- a/WebFiori/Http/OpenAPI/InfoObj.php +++ b/WebFiori/Http/OpenAPI/InfoObj.php @@ -241,32 +241,32 @@ public function getLicense(): ?LicenseObj { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'title' => $this->getTitle(), + 'version' => $this->getVersion() + ]); - $json->add('title', $this->title); - if ($this->summary !== null) { - $json->add('summary', $this->summary); + if ($this->getSummary() !== null) { + $json->add('summary', $this->getSummary()); } - if ($this->description !== null) { - $json->add('description', $this->description); + if ($this->getDescription() !== null) { + $json->add('description', $this->getDescription()); } - if ($this->termsOfService !== null) { - $json->add('termsOfService', $this->termsOfService); + if ($this->getTermsOfService() !== null) { + $json->add('termsOfService', $this->getTermsOfService()); } - if ($this->contact !== null) { - $json->add('contact', $this->contact); + if ($this->getContact() !== null) { + $json->add('contact', $this->getContact()); } - if ($this->license !== null) { - $json->add('license', $this->license); + if ($this->getLicense() !== null) { + $json->add('license', $this->getLicense()); } - $json->add('version', $this->version); - return $json; } } diff --git a/WebFiori/Http/OpenAPI/LicenseObj.php b/WebFiori/Http/OpenAPI/LicenseObj.php index 927a75d..72236ce 100644 --- a/WebFiori/Http/OpenAPI/LicenseObj.php +++ b/WebFiori/Http/OpenAPI/LicenseObj.php @@ -125,16 +125,16 @@ public function getUrl(): ?string { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'name' => $this->getName() + ]); - $json->add('name', $this->name); - - if ($this->identifier !== null) { - $json->add('identifier', $this->identifier); + if ($this->getIdentifier() !== null) { + $json->add('identifier', $this->getIdentifier()); } - if ($this->url !== null) { - $json->add('url', $this->url); + 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 2d2ae36..c6a3356 100644 --- a/WebFiori/Http/OpenAPI/MediaTypeObj.php +++ b/WebFiori/Http/OpenAPI/MediaTypeObj.php @@ -32,9 +32,11 @@ public function getSchema() { public function toJSON(): Json { $json = new Json(); - if ($this->schema !== null) { - $json->add('schema', $this->schema); + + 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 10be624..c7b8192 100644 --- a/WebFiori/Http/OpenAPI/OAuthFlowObj.php +++ b/WebFiori/Http/OpenAPI/OAuthFlowObj.php @@ -171,22 +171,22 @@ public function getScopes(): array { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'scopes' => $this->getScopes() + ]); - if ($this->authorizationUrl !== null) { - $json->add('authorizationUrl', $this->authorizationUrl); + if ($this->getAuthorizationUrl() !== null) { + $json->add('authorizationUrl', $this->getAuthorizationUrl()); } - if ($this->tokenUrl !== null) { - $json->add('tokenUrl', $this->tokenUrl); + if ($this->getTokenUrl() !== null) { + $json->add('tokenUrl', $this->getTokenUrl()); } - if ($this->refreshUrl !== null) { - $json->add('refreshUrl', $this->refreshUrl); + if ($this->getRefreshUrl() !== null) { + $json->add('refreshUrl', $this->getRefreshUrl()); } - $json->add('scopes', $this->scopes); - return $json; } } diff --git a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php index 05cc5e2..269ab3d 100644 --- a/WebFiori/Http/OpenAPI/OAuthFlowsObj.php +++ b/WebFiori/Http/OpenAPI/OAuthFlowsObj.php @@ -138,20 +138,20 @@ public function getAuthorizationCode(): ?OAuthFlowObj { public function toJSON(): Json { $json = new Json(); - if ($this->implicit !== null) { - $json->add('implicit', $this->implicit); + if ($this->getImplicit() !== null) { + $json->add('implicit', $this->getImplicit()); } - if ($this->password !== null) { - $json->add('password', $this->password); + if ($this->getPassword() !== null) { + $json->add('password', $this->getPassword()); } - if ($this->clientCredentials !== null) { - $json->add('clientCredentials', $this->clientCredentials); + if ($this->getClientCredentials() !== null) { + $json->add('clientCredentials', $this->getClientCredentials()); } - if ($this->authorizationCode !== null) { - $json->add('authorizationCode', $this->authorizationCode); + 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 d5cc540..7b93af0 100644 --- a/WebFiori/Http/OpenAPI/OpenAPIObj.php +++ b/WebFiori/Http/OpenAPI/OpenAPIObj.php @@ -104,9 +104,11 @@ public function getInfo(): InfoObj { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); - $json->add('openapi', $this->openapi); - $json->add('info', $this->info); + $json = new Json([ + 'openapi' => $this->getOpenapi(), + 'info' => $this->getInfo() + ]); + return $json; } } diff --git a/WebFiori/Http/OpenAPI/OperationObj.php b/WebFiori/Http/OpenAPI/OperationObj.php index c309ba8..c713377 100644 --- a/WebFiori/Http/OpenAPI/OperationObj.php +++ b/WebFiori/Http/OpenAPI/OperationObj.php @@ -37,8 +37,10 @@ public function getResponses(): ResponsesObj { } public function toJSON(): Json { - $json = new Json(); - $json->add('responses', $this->responses); + $json = new Json([ + 'responses' => $this->getResponses() + ]); + return $json; } } diff --git a/WebFiori/Http/OpenAPI/ParameterObj.php b/WebFiori/Http/OpenAPI/ParameterObj.php index e77ebb7..845164f 100644 --- a/WebFiori/Http/OpenAPI/ParameterObj.php +++ b/WebFiori/Http/OpenAPI/ParameterObj.php @@ -404,49 +404,49 @@ public function getExamples(): ?array { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'name' => $this->getName(), + 'in' => $this->getIn() + ]); - $json->add('name', $this->name); - $json->add('in', $this->in); - - if ($this->description !== null) { - $json->add('description', $this->description); + if ($this->getDescription() !== null) { + $json->add('description', $this->getDescription()); } - if ($this->required) { - $json->add('required', $this->required); + if ($this->getRequired()) { + $json->add('required', $this->getRequired()); } - if ($this->deprecated) { - $json->add('deprecated', $this->deprecated); + if ($this->getDeprecated()) { + $json->add('deprecated', $this->getDeprecated()); } - if ($this->allowEmptyValue) { - $json->add('allowEmptyValue', $this->allowEmptyValue); + if ($this->getAllowEmptyValue()) { + $json->add('allowEmptyValue', $this->getAllowEmptyValue()); } - if ($this->style !== null) { - $json->add('style', $this->style); + if ($this->getStyle() !== null) { + $json->add('style', $this->getStyle()); } - if ($this->explode !== null) { - $json->add('explode', $this->explode); + if ($this->getExplode() !== null) { + $json->add('explode', $this->getExplode()); } - if ($this->allowReserved !== null) { - $json->add('allowReserved', $this->allowReserved); + if ($this->getAllowReserved() !== null) { + $json->add('allowReserved', $this->getAllowReserved()); } - if ($this->schema !== null) { - $json->add('schema', $this->schema); + if ($this->getSchema() !== null) { + $json->add('schema', $this->getSchema()); } - if ($this->example !== null) { - $json->add('example', $this->example); + if ($this->getExample() !== null) { + $json->add('example', $this->getExample()); } - if ($this->examples !== null) { - $json->add('examples', $this->examples); + 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 aba5e06..bf0d124 100644 --- a/WebFiori/Http/OpenAPI/PathItemObj.php +++ b/WebFiori/Http/OpenAPI/PathItemObj.php @@ -78,20 +78,24 @@ public function setPatch(OperationObj $operation): PathItemObj { public function toJSON(): Json { $json = new Json(); - if ($this->get !== null) { - $json->add('get', $this->get); + if ($this->getGet() !== null) { + $json->add('get', $this->getGet()); } - if ($this->post !== null) { - $json->add('post', $this->post); + + if ($this->getPost() !== null) { + $json->add('post', $this->getPost()); } - if ($this->put !== null) { - $json->add('put', $this->put); + + if ($this->getPut() !== null) { + $json->add('put', $this->getPut()); } - if ($this->delete !== null) { - $json->add('delete', $this->delete); + + if ($this->getDelete() !== null) { + $json->add('delete', $this->getDelete()); } - if ($this->patch !== null) { - $json->add('patch', $this->patch); + + if ($this->getPatch() !== null) { + $json->add('patch', $this->getPatch()); } return $json; diff --git a/WebFiori/Http/OpenAPI/ReferenceObj.php b/WebFiori/Http/OpenAPI/ReferenceObj.php index 5877427..6b05062 100644 --- a/WebFiori/Http/OpenAPI/ReferenceObj.php +++ b/WebFiori/Http/OpenAPI/ReferenceObj.php @@ -127,16 +127,16 @@ public function getDescription(): ?string { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + '$ref' => $this->getRef() + ]); - $json->add('$ref', $this->ref); - - if ($this->summary !== null) { - $json->add('summary', $this->summary); + if ($this->getSummary() !== null) { + $json->add('summary', $this->getSummary()); } - if ($this->description !== null) { - $json->add('description', $this->description); + 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 1e28554..55372e0 100644 --- a/WebFiori/Http/OpenAPI/ResponseObj.php +++ b/WebFiori/Http/OpenAPI/ResponseObj.php @@ -40,8 +40,10 @@ public function getDescription(): string { } public function toJSON(): Json { - $json = new Json(); - $json->add('description', $this->description); + $json = new Json([ + 'description' => $this->getDescription() + ]); + return $json; } } diff --git a/WebFiori/Http/OpenAPI/SecuritySchemeObj.php b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php index ddb13fc..ed1b617 100644 --- a/WebFiori/Http/OpenAPI/SecuritySchemeObj.php +++ b/WebFiori/Http/OpenAPI/SecuritySchemeObj.php @@ -285,36 +285,36 @@ public function getOpenIdConnectUrl(): ?string { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'type' => $this->getType() + ]); - $json->add('type', $this->type); - - if ($this->description !== null) { - $json->add('description', $this->description); + if ($this->getDescription() !== null) { + $json->add('description', $this->getDescription()); } - if ($this->name !== null) { - $json->add('name', $this->name); + if ($this->getName() !== null) { + $json->add('name', $this->getName()); } - if ($this->in !== null) { - $json->add('in', $this->in); + if ($this->getIn() !== null) { + $json->add('in', $this->getIn()); } - if ($this->scheme !== null) { - $json->add('scheme', $this->scheme); + if ($this->getScheme() !== null) { + $json->add('scheme', $this->getScheme()); } - if ($this->bearerFormat !== null) { - $json->add('bearerFormat', $this->bearerFormat); + if ($this->getBearerFormat() !== null) { + $json->add('bearerFormat', $this->getBearerFormat()); } - if ($this->flows !== null) { - $json->add('flows', $this->flows); + if ($this->getFlows() !== null) { + $json->add('flows', $this->getFlows()); } - if ($this->openIdConnectUrl !== null) { - $json->add('openIdConnectUrl', $this->openIdConnectUrl); + 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 621f56e..557702a 100644 --- a/WebFiori/Http/OpenAPI/ServerObj.php +++ b/WebFiori/Http/OpenAPI/ServerObj.php @@ -100,12 +100,12 @@ public function getDescription(): ?string { * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'url' => $this->getUrl() + ]); - $json->add('url', $this->url); - - if ($this->description !== null) { - $json->add('description', $this->description); + 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 99e0998..013fc29 100644 --- a/WebFiori/Http/OpenAPI/TagObj.php +++ b/WebFiori/Http/OpenAPI/TagObj.php @@ -129,16 +129,16 @@ public function getExternalDocs(): ?ExternalDocObj { * @return Json A Json object representation of this Tag Object. */ public function toJSON(): Json { - $json = new Json(); + $json = new Json([ + 'name' => $this->getName() + ]); - $json->add('name', $this->name); - - if ($this->description !== null) { - $json->add('description', $this->description); + if ($this->getDescription() !== null) { + $json->add('description', $this->getDescription()); } - if ($this->externalDocs !== null) { - $json->add('externalDocs', $this->externalDocs); + if ($this->getExternalDocs() !== null) { + $json->add('externalDocs', $this->getExternalDocs()); } return $json; From af63ed35076715bf1d8c72b3d325934d52ab5bc6 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Tue, 16 Dec 2025 23:56:52 +0300 Subject: [PATCH 14/50] feat: Open API Classes --- .../Http/OpenAPI/APIResponseDefinition.php | 68 +++++++++++++++++++ WebFiori/Http/OpenAPI/ContentType.php | 52 ++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 WebFiori/Http/OpenAPI/APIResponseDefinition.php create mode 100644 WebFiori/Http/OpenAPI/ContentType.php diff --git a/WebFiori/Http/OpenAPI/APIResponseDefinition.php b/WebFiori/Http/OpenAPI/APIResponseDefinition.php new file mode 100644 index 0000000..6c28712 --- /dev/null +++ b/WebFiori/Http/OpenAPI/APIResponseDefinition.php @@ -0,0 +1,68 @@ +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. + * + * @param string $mediaType Media type (e.g., 'application/json') + * @param Schema $schema The schema for this content type + * + * @return ContentType The created content type object + */ + public function addContent(string $mediaType, Schema $schema): ContentType { + $content = new ContentType($mediaType, $schema); + $this->content[$mediaType] = $content; + return $content; + } + + /** + * Converts the response to JSON representation. + * + * @return Json JSON object + */ + 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/ContentType.php b/WebFiori/Http/OpenAPI/ContentType.php new file mode 100644 index 0000000..4da2c89 --- /dev/null +++ b/WebFiori/Http/OpenAPI/ContentType.php @@ -0,0 +1,52 @@ +mediaType = $mediaType; + $this->schema = $schema; + } + + /** + * Gets the media type. + * + * @return string + */ + public function getMediaType(): string { + return $this->mediaType; + } + + /** + * Gets the schema. + * + * @return Schema + */ + public function getSchema(): Schema { + return $this->schema; + } + + /** + * Converts the content type to JSON representation. + * + * @return Json JSON object + */ + public function toJson(): Json { + return new Json(['schema' => $this->schema->toJson()]); + } +} From 7be79474b55f4d055b2acedd2b829ed68b9f16dc Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:47:19 +0300 Subject: [PATCH 15/50] Update ResponsesObj.php --- WebFiori/Http/OpenAPI/ResponsesObj.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/WebFiori/Http/OpenAPI/ResponsesObj.php b/WebFiori/Http/OpenAPI/ResponsesObj.php index 3f96dba..70b6f3d 100644 --- a/WebFiori/Http/OpenAPI/ResponsesObj.php +++ b/WebFiori/Http/OpenAPI/ResponsesObj.php @@ -32,11 +32,14 @@ class ResponsesObj implements JsonI { * The status code can also be a range using uppercase wildcard character X (e.g., "2XX"). * * @param string $statusCode The HTTP status code (e.g., "200", "404", "2XX"). - * @param ResponseObj $response The Response Object for this status code. + * @param ResponseObj|string $response The Response Object or description string for this status code. * * @return ResponsesObj Returns self for method chaining. */ - public function addResponse(string $statusCode, ResponseObj $response): ResponsesObj { + public function addResponse(string $statusCode, ResponseObj|string $response): ResponsesObj { + if (is_string($response)) { + $response = new ResponseObj($response); + } $this->responses[$statusCode] = $response; return $this; } @@ -57,10 +60,13 @@ public function getResponses(): array { * * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ + public function toJSON(): Json { $json = new Json(); + foreach ($this->responses as $code => $response) { $json->add($code, $response); } + return $json; } } From d0295a456d3cb430eaedf53bb57a81d384e7ea88 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:50:20 +0300 Subject: [PATCH 16/50] Update PathItemObj.php --- WebFiori/Http/OpenAPI/PathItemObj.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/WebFiori/Http/OpenAPI/PathItemObj.php b/WebFiori/Http/OpenAPI/PathItemObj.php index bf0d124..dd2eda7 100644 --- a/WebFiori/Http/OpenAPI/PathItemObj.php +++ b/WebFiori/Http/OpenAPI/PathItemObj.php @@ -55,26 +55,46 @@ public function setGet(OperationObj $operation): PathItemObj { return $this; } + public function getGet(): ?OperationObj { + return $this->get; + } + public function setPost(OperationObj $operation): PathItemObj { $this->post = $operation; return $this; } + 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 setPatch(OperationObj $operation): PathItemObj { $this->patch = $operation; return $this; } + public function getPatch(): ?OperationObj { + return $this->patch; + } + public function toJSON(): Json { $json = new Json(); From 983598f4bead40a051a052a959c4e666141fc507 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:51:18 +0300 Subject: [PATCH 17/50] Update WebService.php --- WebFiori/Http/WebService.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 39b0b45..9b072a9 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -321,6 +321,16 @@ public final function addRequestMethod(string $method) : bool { * * @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) { From f140d8567fd2137643a1a8c36fc5ac2b7979cf1a Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:51:49 +0300 Subject: [PATCH 18/50] Update WebService.php --- WebFiori/Http/WebService.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 9b072a9..2c1f076 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -338,6 +338,10 @@ public final function addResponseDescription(string $description) { if (strlen($trimmed) != 0) { $this->responses[] = $trimmed; + public function getResponsesForMethod(string $method): ?OpenAPI\ResponsesObj { + $method = strtoupper($method); + return $this->responsesByMethod[$method] ?? null; + } } } /** From 71eba0441316e3eb08bca027043798e78fd12d39 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:52:32 +0300 Subject: [PATCH 19/50] Update WebService.php --- WebFiori/Http/WebService.php | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 2c1f076..cc4d118 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -342,7 +342,67 @@ 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'. From 995bb747acc2b77e5ac9b3fc18be36caf413599e Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:53:02 +0300 Subject: [PATCH 20/50] Update WebService.php --- WebFiori/Http/WebService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index cc4d118..1a0e96b 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -93,6 +93,7 @@ abstract class WebService implements JsonI { * */ private $responses; + private array $responsesByMethod = []; /** * An optional description for the service. * From c3c7dd5081ecebeca68e5753dfa80cc96fded0fd Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:53:30 +0300 Subject: [PATCH 21/50] Update WebService.php --- WebFiori/Http/WebService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 1a0e96b..c852095 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -133,6 +133,7 @@ public function __construct(string $name) { $this->reqMethods = []; $this->parameters = []; $this->responses = []; + $this->responsesByMethod = []; $this->requireAuth = true; $this->sinceVersion = '1.0.0'; $this->serviceDesc = ''; From 6dc29e88173933bb096266faae48543476a90669 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 17 Dec 2025 00:58:27 +0300 Subject: [PATCH 22/50] Update WebService.php --- WebFiori/Http/WebService.php | 90 ++++-------------------------------- 1 file changed, 8 insertions(+), 82 deletions(-) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index c852095..807075c 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -168,62 +168,7 @@ public final function &getParameters() : array { * */ public function __toString() { - $retVal = "APIAction[\n"; - $retVal .= " Name => '".$this->getName()."',\n"; - $retVal .= " Description => '".$this->getDescription()."',\n"; - $since = $this->getSince() === null ? 'null' : $this->getSince(); - $retVal .= " Since => '$since',\n"; - $reqMethodsStr = "[\n"; - $comma = ','; - - for ($x = 0, $count = count($this->getRequestMethods()) ; $x < $count ; $x++) { - $meth = $this->getRequestMethods()[$x]; - - if ($x + 1 == $count) { - $comma = ''; - } - $reqMethodsStr .= " $meth$comma\n"; - } - $reqMethodsStr .= " ],\n"; - $retVal .= " Request Methods => $reqMethodsStr"; - $paramsStr = "[\n"; - - $comma = ','; - - for ($x = 0 , $count = count($this->getParameters()); $x < $count ; $x++) { - $param = $this->getParameters()[$x]; - $paramsStr .= " ".$param->getName()." => [\n"; - $paramsStr .= " Type => '".$param->getType()."',\n"; - $descStr = $param->getDescription() === null ? 'null' : $param->getDescription(); - $paramsStr .= " Description => '$descStr',\n"; - $isOptional = $param->isOptional() ? 'true' : 'false'; - $paramsStr .= " Is Optional => '$isOptional',\n"; - $defaultStr = $param->getDefault() === null ? 'null' : $param->getDefault(); - $paramsStr .= " Default => '$defaultStr',\n"; - $min = $param->getMinValue() === null ? 'null' : $param->getMinValue(); - $paramsStr .= " Minimum Value => '$min',\n"; - $max = $param->getMaxValue() === null ? 'null' : $param->getMaxValue(); - - if ($x + 1 == $count) { - $comma = ''; - } - $paramsStr .= " Maximum Value => '$max'\n ]$comma\n"; - } - $paramsStr .= " ],\n"; - $retVal .= " Parameters => $paramsStr"; - $responsesStr = "[\n"; - $count = count($this->getResponsesDescriptions()); - $comma = ','; - - for ($x = 0 ; $x < $count ; $x++) { - if ($x + 1 == $count) { - $comma = ''; - } - $responsesStr .= " Response #$x => '".$this->getResponsesDescriptions()[$x]."'".$comma."\n"; - } - $responsesStr .= " ]\n"; - - return $retVal." Responses Descriptions => $responsesStr]\n"; + return $this->toJSON().''; } /** * Adds new request parameter to the service. @@ -323,6 +268,7 @@ public final function addRequestMethod(string $method) : bool { * * @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); @@ -333,13 +279,14 @@ public function addResponse(string $method, string $statusCode, OpenAPI\Response $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; @@ -404,8 +351,8 @@ public function toPathItemObj(): OpenAPI\PathItemObj { break; } - return $pathItem; - } + + }return $pathItem;} /** * Returns an object that contains the value of the header 'authorization'. * @@ -887,31 +834,10 @@ public final function setSince(string $sinceAPIv) { /** * Returns a Json object that represents the service. * - * The generated JSON string from the returned Json object will have - * the following format: - *

    - * {
    - *   "name":"",
    - *   "since":"",
    - *   "description":"",
    - *   "request-methods":[],
    - *   "parameters":[],
    - *   "responses":[]
    - * } - *

    - * * @return Json an object of type Json. * */ public function toJSON() : Json { - $json = new Json(); - $json->add('name', $this->getName()); - $json->add('since', $this->getSince()); - $json->add('description', $this->getDescription()); - $json->add('request-methods', $this->reqMethods); - $json->add('parameters', $this->parameters); - $json->add('responses', $this->getResponsesDescriptions()); - - return $json; + return $this->toPathItemObj()->toJSON(); } } From fa656b102aabd26312087660acbf723d5dbdee15 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 00:02:20 +0300 Subject: [PATCH 23/50] Update WebServiceTest.php --- tests/WebFiori/Tests/Http/WebServiceTest.php | 128 ++----------------- 1 file changed, 14 insertions(+), 114 deletions(-) diff --git a/tests/WebFiori/Tests/Http/WebServiceTest.php b/tests/WebFiori/Tests/Http/WebServiceTest.php index db74891..78e4651 100644 --- a/tests/WebFiori/Tests/Http/WebServiceTest.php +++ b/tests/WebFiori/Tests/Http/WebServiceTest.php @@ -254,79 +254,27 @@ public function testRemoveRequestMethod($action) { */ public function testToJson00() { $action = new TestServiceObj('login'); - $this->assertEquals('' - .'{"name":"login",' - .'"since":"1.0.0",' - .'"description":"",' - .'"request-methods":[],' - .'"parameters":[],' - .'"responses":[]}',$action->toJSON().''); + $this->assertEquals('{}',$action->toJSON().''); $action->setSince('1.0.1'); $action->setDescription('Allow the user to login to the system.'); - $this->assertEquals('' - .'{"name":"login",' - .'"since":"1.0.1",' - .'"description":"Allow the user to login to the system.",' - .'"request-methods":[],' - .'"parameters":[],' - .'"responses":[]}',$action->toJSON().''); + $this->assertEquals('{}',$action->toJSON().''); $action->setRequestMethods([RequestMethod::GET, RequestMethod::POST, RequestMethod::PUT]); $this->assertEquals('' - .'{"name":"login",' - .'"since":"1.0.1",' - .'"description":"Allow the user to login to the system.",' - .'"request-methods":["GET","POST","PUT"],' - .'"parameters":[],' - .'"responses":[]}',$action->toJSON().''); + .'{"get":{"responses":{"200":{"description":"Successful operation"}}},' + .'"post":{"responses":{"200":{"description":"Successful operation"}}},' + .'"put":{"responses":{"200":{"description":"Successful operation"}}}}',$action->toJSON().''); $action->removeRequestMethod('put'); $action->addParameter(new RequestParameter('username')); $this->assertEquals('' - .'{"name":"login",' - .'"since":"1.0.1",' - .'"description":"Allow the user to login to the system.",' - .'"request-methods":["GET","POST"],' - .'"parameters":[' - .'{"name":"username",' - .'"type":"string",' - .'"description":null,' - .'"is-optional":false,' - .'"default-value":null,' - .'"min-val":null,' - .'"max-val":null,' - .'"min-length":null,' - .'"max-length":null}' - .'],' - .'"responses":[]}',$action->toJSON().''); + .'{"get":{"responses":{"200":{"description":"Successful operation"}}},' + .'"post":{"responses":{"200":{"description":"Successful operation"}}}}',$action->toJSON().''); $action->addParameter(new RequestParameter('password', 'integer')); $action->getParameterByName('password')->setDescription('The password of the user.'); $action->getParameterByName('password')->setMinValue(1000000); $this->assertEquals('' - .'{"name":"login",' - .'"since":"1.0.1",' - .'"description":"Allow the user to login to the system.",' - .'"request-methods":["GET","POST"],' - .'"parameters":[' - .'{"name":"username",' - .'"type":"string",' - .'"description":null,' - .'"is-optional":false,' - .'"default-value":null,' - .'"min-val":null,' - .'"max-val":null,' - .'"min-length":null,' - .'"max-length":null},' - .'{"name":"password",' - .'"type":"integer",' - .'"description":"The password of the user.",' - .'"is-optional":false,' - .'"default-value":null,' - .'"min-val":1000000,' - .'"max-val":'.PHP_INT_MAX.',' - .'"min-length":null,' - .'"max-length":null}' - .'],' - .'"responses":[]}',$action->toJSON().''); + .'{"get":{"responses":{"200":{"description":"Successful operation"}}},' + .'"post":{"responses":{"200":{"description":"Successful operation"}}}}',$action->toJSON().''); } /** * @test @@ -337,26 +285,7 @@ public function testToString00() { $action->addParameter(new RequestParameter('user-id', 'integer')); $action->getParameterByName('user-id')->setDescription('The ID of the user.'); $action->setDescription('Returns a JSON string which holds user profile info.'); - $this->assertEquals("APIAction[\n" - ." Name => 'get-user',\n" - ." Description => 'Returns a JSON string which holds user profile info.',\n" - ." Since => '1.0.0',\n" - ." Request Methods => [\n" - ." GET\n" - ." ],\n" - ." Parameters => [\n" - ." user-id => [\n" - ." Type => 'integer',\n" - ." Description => 'The ID of the user.',\n" - ." Is Optional => 'false',\n" - ." Default => 'null',\n" - ." Minimum Value => '".~PHP_INT_MAX."',\n" - ." Maximum Value => '".PHP_INT_MAX."'\n" - ." ]\n" - ." ],\n" - ." Responses Descriptions => [\n" - ." ]\n" - ."]\n",$action.''); + $this->assertEquals('{"get":{"responses":{"200":{"description":"Successful operation"}}}}',$action.''); } /** * @test @@ -369,38 +298,9 @@ public function testToString01() { $action->getParameterByName('username')->setDescription('The username of the user.'); $action->getParameterByName('email')->setDescription('The email address of the user.'); $action->setDescription('Adds new user profile to the system.'); - $action->addResponseDescription('If the user is added, a 201 HTTP response is send with a JSON string that contains user ID.'); - $action->addResponseDescription('If a user is already exist wich has the given email, a 404 code is sent back.'); - $this->assertEquals("APIAction[\n" - ." Name => 'add-user',\n" - ." Description => 'Adds new user profile to the system.',\n" - ." Since => '1.0.0',\n" - ." Request Methods => [\n" - ." POST,\n" - ." PUT\n" - ." ],\n" - ." Parameters => [\n" - ." username => [\n" - ." Type => 'string',\n" - ." Description => 'The username of the user.',\n" - ." Is Optional => 'false',\n" - ." Default => 'null',\n" - ." Minimum Value => 'null',\n" - ." Maximum Value => 'null'\n" - ." ],\n" - ." email => [\n" - ." Type => 'string',\n" - ." Description => 'The email address of the user.',\n" - ." Is Optional => 'false',\n" - ." Default => 'null',\n" - ." Minimum Value => 'null',\n" - ." Maximum Value => 'null'\n" - ." ]\n" - ." ],\n" - ." Responses Descriptions => [\n" - ." Response #0 => 'If the user is added, a 201 HTTP response is send with a JSON string that contains user ID.',\n" - ." Response #1 => 'If a user is already exist wich has the given email, a 404 code is sent back.'\n" - ." ]\n" - ."]\n",$action.''); + $action->addResponse(RequestMethod::POST, '201', 'User created successfully'); + $action->addResponse(RequestMethod::PUT, '200', 'User updated successfully'); + $this->assertEquals('{"post":{"responses":{"201":{"description":"User created successfully"}}},' + .'"put":{"responses":{"200":{"description":"User updated successfully"}}}}',$action.''); } } From 2e0d30cc324b9599e43f3c11fcb32f8791197fab Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 00:02:33 +0300 Subject: [PATCH 24/50] Update RequestParameterTest.php --- tests/WebFiori/Tests/Http/RequestParameterTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/WebFiori/Tests/Http/RequestParameterTest.php b/tests/WebFiori/Tests/Http/RequestParameterTest.php index 93c719d..4822eab 100644 --- a/tests/WebFiori/Tests/Http/RequestParameterTest.php +++ b/tests/WebFiori/Tests/Http/RequestParameterTest.php @@ -598,8 +598,7 @@ public function testSetMinLength05() { */ public function testToJson00($reqParam) { $reqParam->setDescription('Test Parameter.'); - $this->assertEquals('{"name":"a-parameter","type":"string","description":"Test Parameter.",' - .'"is-optional":false,"default-value":null,"min-val":null,"max-val":null,"min-length":null,"max-length":null}',$reqParam->toJSON().''); + $this->assertEquals('{"name":"a-parameter","in":"query","required":true,"description":"Test Parameter.","schema":{"type":"string"}}',$reqParam->toJSON().''); } /** * @test @@ -608,8 +607,7 @@ public function testToJson00($reqParam) { */ public function testToJson01($reqParam) { $reqParam->setDescription('Test Parameter.'); - $this->assertEquals('{"name":"valid","type":"integer","description":"Test Parameter.",' - .'"is-optional":true,"default-value":null,"min-val":'.~PHP_INT_MAX.',"max-val":'.PHP_INT_MAX.',"min-length":null,"max-length":null}',$reqParam->toJSON().''); + $this->assertEquals('{"name":"valid","in":"query","required":false,"description":"Test Parameter.","schema":{"type":"integer","minimum":'.~PHP_INT_MAX.',"maximum":'.PHP_INT_MAX.'}}',$reqParam->toJSON().''); } /** * From 2239d71aeae182d79aecf43ed2d36177904d1287 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 00:28:23 +0300 Subject: [PATCH 25/50] Update WebServicesManager.php --- WebFiori/Http/WebServicesManager.php | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index 00fd635..72e670a 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -96,6 +96,12 @@ class WebServicesManager implements JsonI { * */ private $services; + /** + * The base path for all services in this manager. + * + * @var string + */ + private string $basePath = ''; private $request; /** * The response object used to send output. @@ -203,6 +209,29 @@ 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. * @@ -764,6 +793,32 @@ 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. * From ffcc4debab466a151da16128c07e5ea104df59b8 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 00:28:32 +0300 Subject: [PATCH 26/50] Update PathsObj.php --- WebFiori/Http/OpenAPI/PathsObj.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WebFiori/Http/OpenAPI/PathsObj.php b/WebFiori/Http/OpenAPI/PathsObj.php index 401d5bf..0831581 100644 --- a/WebFiori/Http/OpenAPI/PathsObj.php +++ b/WebFiori/Http/OpenAPI/PathsObj.php @@ -55,7 +55,9 @@ public function getPaths(): array { * * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ + public function toJSON(): Json { $json = new Json(); + foreach ($this->paths as $path => $pathItem) { $json->add($path, $pathItem); } From 60580875c51a8f36688bc9f6d8aa21706a3102b0 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 00:28:41 +0300 Subject: [PATCH 27/50] Update OpenAPIObj.php --- WebFiori/Http/OpenAPI/OpenAPIObj.php | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/WebFiori/Http/OpenAPI/OpenAPIObj.php b/WebFiori/Http/OpenAPI/OpenAPIObj.php index 7b93af0..587b297 100644 --- a/WebFiori/Http/OpenAPI/OpenAPIObj.php +++ b/WebFiori/Http/OpenAPI/OpenAPIObj.php @@ -37,6 +37,13 @@ class OpenAPIObj implements JsonI { */ private InfoObj $info; + /** + * The available paths and operations for the API. + * + * @var PathsObj|null + */ + private ?PathsObj $paths = null; + /** * Creates a new OpenAPI Object instance. * @@ -95,6 +102,27 @@ public function getInfo(): InfoObj { return $this->info; } + /** + * Sets the Paths Object containing API paths and operations. + * + * @param PathsObj $paths The Paths Object. + * + * @return OpenAPIObj Returns self for method chaining. + */ + 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. * @@ -109,6 +137,10 @@ public function toJSON(): Json { 'info' => $this->getInfo() ]); + if ($this->getPaths() !== null) { + $json->add('paths', $this->getPaths()); + } + return $json; } } From 428948cb53e2ebbb06a61204a091567780b890f8 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 00:28:48 +0300 Subject: [PATCH 28/50] Update ManagerInfoService.php --- WebFiori/Http/ManagerInfoService.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/WebFiori/Http/ManagerInfoService.php b/WebFiori/Http/ManagerInfoService.php index 3af3bff..a5ed130 100644 --- a/WebFiori/Http/ManagerInfoService.php +++ b/WebFiori/Http/ManagerInfoService.php @@ -24,19 +24,11 @@ abstract class ManagerInfoService extends WebService { * */ public function __construct() { - parent::__construct('api-info'); + parent::__construct('api-docs'); $this->setDescription('Returns a JSON string that contains all ' .'needed information about all end points which are registered ' .'under given manager.'); - $this->addParameter([ - ParamOption::NAME => 'version', - ParamOption::TYPE => ParamType::STRING, - ParamOption::OPTIONAL => true, - ParamOption::DESCRIPTION => 'Optional parameter. ' - .'If set, the information that will be returned will be specific ' - .'to the given version number.' - ]); - $this->setRequestMethods(RequestMethod::GET, RequestMethod::POST); + $this->addRequestMethod(RequestMethod::GET); } /** * Sends back JSON response that contains information about the services From 4d602bf98231e39c594a33e8549bf950b0ba52ee Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 01:06:18 +0300 Subject: [PATCH 29/50] test: Added More Test Cases --- WebFiori/Http/OpenAPI/ComponentsObj.php | 20 + WebFiori/Http/OpenAPI/HeaderObj.php | 8 + WebFiori/Http/OpenAPI/ParameterObj.php | 26 + .../Http/OpenAPI/SecurityRequirementObj.php | 12 + .../Tests/Http/ManagerInfoServiceTest.php | 33 ++ tests/WebFiori/Tests/Http/OpenAPITest.php | 525 ++++++++++++++++++ tests/phpunit.xml | 12 +- tests/phpunit10.xml | 16 +- 8 files changed, 636 insertions(+), 16 deletions(-) create mode 100644 tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php create mode 100644 tests/WebFiori/Tests/Http/OpenAPITest.php diff --git a/WebFiori/Http/OpenAPI/ComponentsObj.php b/WebFiori/Http/OpenAPI/ComponentsObj.php index 94fb02a..93801c4 100644 --- a/WebFiori/Http/OpenAPI/ComponentsObj.php +++ b/WebFiori/Http/OpenAPI/ComponentsObj.php @@ -60,6 +60,24 @@ public function addSecurityScheme(string $name, SecuritySchemeObj $scheme): Comp return $this; } + /** + * Returns all schemas. + * + * @return array Map of schema names to schema definitions. + */ + public function getSchemas(): array { + return $this->schemas; + } + + /** + * Returns all security schemes. + * + * @return array Map of security scheme names to SecuritySchemeObj. + */ + public function getSecuritySchemes(): array { + return $this->securitySchemes; + } + /** * Returns a Json object that represents the Components Object. * @@ -67,11 +85,13 @@ public function addSecurityScheme(string $name, SecuritySchemeObj $scheme): Comp * * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ + 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); } diff --git a/WebFiori/Http/OpenAPI/HeaderObj.php b/WebFiori/Http/OpenAPI/HeaderObj.php index d8c163e..6a69678 100644 --- a/WebFiori/Http/OpenAPI/HeaderObj.php +++ b/WebFiori/Http/OpenAPI/HeaderObj.php @@ -130,6 +130,10 @@ public function isRequired(): bool { return $this->required; } + public function getRequired(): bool { + return $this->required; + } + /** * Sets whether this header is deprecated. * @@ -151,6 +155,10 @@ public function isDeprecated(): bool { return $this->deprecated; } + public function getDeprecated(): bool { + return $this->deprecated; + } + /** * Sets the serialization style. * diff --git a/WebFiori/Http/OpenAPI/ParameterObj.php b/WebFiori/Http/OpenAPI/ParameterObj.php index 845164f..741d71b 100644 --- a/WebFiori/Http/OpenAPI/ParameterObj.php +++ b/WebFiori/Http/OpenAPI/ParameterObj.php @@ -230,6 +230,17 @@ public function isRequired(): bool { return $this->required; } + /** + * Returns whether this parameter is required. + * + * Alias for isRequired() for consistency with toJSON(). + * + * @return bool + */ + public function getRequired(): bool { + return $this->required; + } + /** * Sets whether this parameter is deprecated. * @@ -251,6 +262,17 @@ public function isDeprecated(): bool { return $this->deprecated; } + /** + * Returns whether this parameter is deprecated. + * + * Alias for isDeprecated() for consistency with toJSON(). + * + * @return bool + */ + public function getDeprecated(): bool { + return $this->deprecated; + } + /** * Sets whether to allow empty value. * @@ -272,6 +294,10 @@ public function isAllowEmptyValue(): bool { return $this->allowEmptyValue; } + public function getAllowEmptyValue(): bool { + return $this->allowEmptyValue; + } + /** * Sets the serialization style. * diff --git a/WebFiori/Http/OpenAPI/SecurityRequirementObj.php b/WebFiori/Http/OpenAPI/SecurityRequirementObj.php index 40b1728..7ea2886 100644 --- a/WebFiori/Http/OpenAPI/SecurityRequirementObj.php +++ b/WebFiori/Http/OpenAPI/SecurityRequirementObj.php @@ -45,6 +45,15 @@ public function addRequirement(string $name, array $scopes = []): SecurityRequir return $this; } + /** + * Returns all security requirements. + * + * @return array Map of security scheme names to scope arrays. + */ + public function getRequirements(): array { + return $this->requirements; + } + /** * Returns a Json object that represents the Security Requirement Object. * @@ -52,10 +61,13 @@ public function addRequirement(string $name, array $scopes = []): SecurityRequir * * @return Json A Json object representation following OpenAPI 3.1.0 specification. */ + public function toJSON(): Json { $json = new Json(); + foreach ($this->requirements as $name => $scopes) { $json->add($name, $scopes); } + return $json; } } diff --git a/tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php b/tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php new file mode 100644 index 0000000..6e6531b --- /dev/null +++ b/tests/WebFiori/Tests/Http/ManagerInfoServiceTest.php @@ -0,0 +1,33 @@ +addService($service); + + $this->assertEquals('api-docs', $service->getName()); + $this->assertStringContainsString('information about all end points', $service->getDescription()); + $this->assertContains(RequestMethod::GET, $service->getRequestMethods()); + + // Test that processRequest sends JSON + $this->assertNotNull($service->getManager()); + $this->assertSame($manager, $service->getManager()); + } +} + +class TestManagerInfoService extends ManagerInfoService { + public function isAuthorized(): bool { + return true; + } +} diff --git a/tests/WebFiori/Tests/Http/OpenAPITest.php b/tests/WebFiori/Tests/Http/OpenAPITest.php new file mode 100644 index 0000000..5560736 --- /dev/null +++ b/tests/WebFiori/Tests/Http/OpenAPITest.php @@ -0,0 +1,525 @@ +assertEquals('My API', $info->getTitle()); + $this->assertEquals('1.0.0', $info->getVersion()); + + $info->setSummary('API Summary'); + $this->assertEquals('API Summary', $info->getSummary()); + + $info->setDescription('API Description'); + $this->assertEquals('API Description', $info->getDescription()); + + $info->setTermsOfService('https://example.com/terms'); + $this->assertEquals('https://example.com/terms', $info->getTermsOfService()); + + $contact = new ContactObj(); + $info->setContact($contact); + $this->assertSame($contact, $info->getContact()); + + $license = new LicenseObj('MIT'); + $info->setLicense($license); + $this->assertSame($license, $info->getLicense()); + + $json = $info->toJSON(); + $this->assertEquals('My API', $json->get('title')); + $this->assertEquals('1.0.0', $json->get('version')); + $this->assertEquals('API Summary', $json->get('summary')); + } + + /** + * @test + */ + public function testLicenseObj() { + $license = new LicenseObj('Apache 2.0'); + $this->assertEquals('Apache 2.0', $license->getName()); + + $license->setIdentifier('Apache-2.0'); + $this->assertEquals('Apache-2.0', $license->getIdentifier()); + $this->assertNull($license->getUrl()); + + $license->setUrl('https://www.apache.org/licenses/LICENSE-2.0.html'); + $this->assertEquals('https://www.apache.org/licenses/LICENSE-2.0.html', $license->getUrl()); + $this->assertNull($license->getIdentifier()); + + $json = $license->toJSON(); + $this->assertEquals('Apache 2.0', $json->get('name')); + $this->assertEquals('https://www.apache.org/licenses/LICENSE-2.0.html', $json->get('url')); + } + + /** + * @test + */ + public function testContactObj() { + $contact = new ContactObj(); + $this->assertNull($contact->getName()); + + $contact->setName('API Support'); + $this->assertEquals('API Support', $contact->getName()); + + $contact->setUrl('https://example.com/support'); + $this->assertEquals('https://example.com/support', $contact->getUrl()); + + $contact->setEmail('support@example.com'); + $this->assertEquals('support@example.com', $contact->getEmail()); + + $json = $contact->toJSON(); + $this->assertEquals('API Support', $json->get('name')); + $this->assertEquals('https://example.com/support', $json->get('url')); + $this->assertEquals('support@example.com', $json->get('email')); + } + + /** + * @test + */ + public function testOpenAPIObj() { + $info = new InfoObj('Test API', '2.0.0'); + $openapi = new OpenAPIObj($info); + + $this->assertEquals('3.1.0', $openapi->getOpenapi()); + $this->assertSame($info, $openapi->getInfo()); + + $openapi->setOpenapi('3.0.0'); + $this->assertEquals('3.0.0', $openapi->getOpenapi()); + + $paths = new PathsObj(); + $openapi->setPaths($paths); + $this->assertSame($paths, $openapi->getPaths()); + + $json = $openapi->toJSON(); + $this->assertEquals('3.0.0', $json->get('openapi')); + $this->assertNotNull($json->get('info')); + } + + /** + * @test + */ + public function testServerObj() { + $server = new ServerObj('https://api.example.com'); + $this->assertEquals('https://api.example.com', $server->getUrl()); + + $server->setDescription('Production server'); + $this->assertEquals('Production server', $server->getDescription()); + + $json = $server->toJSON(); + $this->assertEquals('https://api.example.com', $json->get('url')); + $this->assertEquals('Production server', $json->get('description')); + } + + /** + * @test + */ + public function testTagObj() { + $tag = new TagObj('users'); + $this->assertEquals('users', $tag->getName()); + + $tag->setDescription('User operations'); + $this->assertEquals('User operations', $tag->getDescription()); + + $externalDocs = new ExternalDocObj('https://docs.example.com'); + $tag->setExternalDocs($externalDocs); + $this->assertSame($externalDocs, $tag->getExternalDocs()); + + $json = $tag->toJSON(); + $this->assertEquals('users', $json->get('name')); + $this->assertEquals('User operations', $json->get('description')); + } + + /** + * @test + */ + public function testExternalDocObj() { + $doc = new ExternalDocObj('https://docs.example.com'); + $this->assertEquals('https://docs.example.com', $doc->getUrl()); + + $doc->setDescription('External documentation'); + $this->assertEquals('External documentation', $doc->getDescription()); + + $json = $doc->toJSON(); + $this->assertEquals('https://docs.example.com', $json->get('url')); + $this->assertEquals('External documentation', $json->get('description')); + } + + /** + * @test + */ + public function testPathsObj() { + $paths = new PathsObj(); + $this->assertEmpty($paths->getPaths()); + + $pathItem = new PathItemObj(); + $paths->addPath('/users', $pathItem); + + $allPaths = $paths->getPaths(); + $this->assertCount(1, $allPaths); + $this->assertSame($pathItem, $allPaths['/users']); + + $json = $paths->toJSON(); + $this->assertNotNull($json->get('/users')); + } + + /** + * @test + */ + public function testPathItemObj() { + $pathItem = new PathItemObj(); + $this->assertNull($pathItem->getGet()); + $this->assertNull($pathItem->getPost()); + + $responses = new ResponsesObj(); + $responses->addResponse('200', 'Success'); + + $getOp = new OperationObj($responses); + $pathItem->setGet($getOp); + $this->assertSame($getOp, $pathItem->getGet()); + + $postOp = new OperationObj($responses); + $pathItem->setPost($postOp); + $this->assertSame($postOp, $pathItem->getPost()); + + $putOp = new OperationObj($responses); + $pathItem->setPut($putOp); + $this->assertSame($putOp, $pathItem->getPut()); + + $deleteOp = new OperationObj($responses); + $pathItem->setDelete($deleteOp); + $this->assertSame($deleteOp, $pathItem->getDelete()); + + $patchOp = new OperationObj($responses); + $pathItem->setPatch($patchOp); + $this->assertSame($patchOp, $pathItem->getPatch()); + + $json = $pathItem->toJSON(); + $this->assertNotNull($json->get('get')); + $this->assertNotNull($json->get('post')); + $this->assertNotNull($json->get('put')); + $this->assertNotNull($json->get('delete')); + $this->assertNotNull($json->get('patch')); + } + + /** + * @test + */ + public function testOperationObj() { + $responses = new ResponsesObj(); + $responses->addResponse('200', 'Success'); + + $operation = new OperationObj($responses); + $this->assertSame($responses, $operation->getResponses()); + + $newResponses = new ResponsesObj(); + $operation->setResponses($newResponses); + $this->assertSame($newResponses, $operation->getResponses()); + + $json = $operation->toJSON(); + $this->assertNotNull($json->get('responses')); + } + + /** + * @test + */ + public function testResponsesObj() { + $responses = new ResponsesObj(); + $this->assertEmpty($responses->getResponses()); + + $responses->addResponse('200', 'Success'); + $responses->addResponse('404', 'Not found'); + + $allResponses = $responses->getResponses(); + $this->assertCount(2, $allResponses); + $this->assertInstanceOf(ResponseObj::class, $allResponses['200']); + $this->assertEquals('Success', $allResponses['200']->getDescription()); + + $json = $responses->toJSON(); + $this->assertNotNull($json->get('200')); + $this->assertNotNull($json->get('404')); + } + + /** + * @test + */ + public function testResponseObj() { + $response = new ResponseObj('Operation successful'); + $this->assertEquals('Operation successful', $response->getDescription()); + + $response->setDescription('Updated description'); + $this->assertEquals('Updated description', $response->getDescription()); + + $json = $response->toJSON(); + $this->assertEquals('Updated description', $json->get('description')); + } + + /** + * @test + */ + public function testParameterObj() { + $param = new ParameterObj('userId', 'path'); + $this->assertEquals('userId', $param->getName()); + $this->assertEquals('path', $param->getIn()); + + $param->setDescription('User ID parameter'); + $this->assertEquals('User ID parameter', $param->getDescription()); + + $param->setRequired(true); + $this->assertTrue($param->getRequired()); + + $param->setDeprecated(true); + $this->assertTrue($param->getDeprecated()); + + $param->setAllowEmptyValue(true); + $this->assertTrue($param->getAllowEmptyValue()); + + $param->setStyle('simple'); + $this->assertEquals('simple', $param->getStyle()); + + $param->setExplode(false); + $this->assertFalse($param->getExplode()); + + $param->setAllowReserved(true); + $this->assertTrue($param->getAllowReserved()); + + $param->setSchema(['type' => 'integer']); + $this->assertEquals(['type' => 'integer'], $param->getSchema()); + + $param->setExample(123); + $this->assertEquals(123, $param->getExample()); + + $param->setExamples(['example1' => ['value' => 123]]); + $this->assertEquals(['example1' => ['value' => 123]], $param->getExamples()); + + $json = $param->toJSON(); + $this->assertEquals('userId', $json->get('name')); + $this->assertEquals('path', $json->get('in')); + $this->assertEquals('User ID parameter', $json->get('description')); + } + + /** + * @test + */ + public function testHeaderObj() { + $header = new HeaderObj(); + $this->assertNull($header->getDescription()); + + $header->setDescription('Custom header'); + $this->assertEquals('Custom header', $header->getDescription()); + + $header->setRequired(true); + $this->assertTrue($header->getRequired()); + + $header->setDeprecated(true); + $this->assertTrue($header->getDeprecated()); + + $header->setStyle('simple'); + $this->assertEquals('simple', $header->getStyle()); + + $header->setExplode(true); + $this->assertTrue($header->getExplode()); + + $header->setSchema(['type' => 'string']); + $this->assertEquals(['type' => 'string'], $header->getSchema()); + + $header->setExample('example-value'); + $this->assertEquals('example-value', $header->getExample()); + + $header->setExamples(['ex1' => ['value' => 'test']]); + $this->assertEquals(['ex1' => ['value' => 'test']], $header->getExamples()); + + $json = $header->toJSON(); + $this->assertEquals('Custom header', $json->get('description')); + } + + /** + * @test + */ + public function testMediaTypeObj() { + $mediaType = new MediaTypeObj(); + $this->assertNull($mediaType->getSchema()); + + $mediaType->setSchema(['type' => 'object']); + $this->assertEquals(['type' => 'object'], $mediaType->getSchema()); + + $json = $mediaType->toJSON(); + $this->assertNotNull($json->get('schema')); + } + + /** + * @test + */ + public function testReferenceObj() { + $ref = new ReferenceObj('#/components/schemas/User'); + $this->assertEquals('#/components/schemas/User', $ref->getRef()); + + $ref->setSummary('User reference'); + $this->assertEquals('User reference', $ref->getSummary()); + + $ref->setDescription('Reference to User schema'); + $this->assertEquals('Reference to User schema', $ref->getDescription()); + + $json = $ref->toJSON(); + $this->assertEquals('#/components/schemas/User', $json->get('$ref')); + $this->assertEquals('User reference', $json->get('summary')); + } + + /** + * @test + */ + public function testSecuritySchemeObj() { + $scheme = new SecuritySchemeObj('http'); + $this->assertEquals('http', $scheme->getType()); + + $scheme->setDescription('HTTP Basic Auth'); + $this->assertEquals('HTTP Basic Auth', $scheme->getDescription()); + + $scheme->setName('Authorization'); + $this->assertEquals('Authorization', $scheme->getName()); + + $scheme->setIn('header'); + $this->assertEquals('header', $scheme->getIn()); + + $scheme->setScheme('basic'); + $this->assertEquals('basic', $scheme->getScheme()); + + $scheme->setBearerFormat('JWT'); + $this->assertEquals('JWT', $scheme->getBearerFormat()); + + $flows = new OAuthFlowsObj(); + $scheme->setFlows($flows); + $this->assertSame($flows, $scheme->getFlows()); + + $scheme->setOpenIdConnectUrl('https://example.com/.well-known/openid-configuration'); + $this->assertEquals('https://example.com/.well-known/openid-configuration', $scheme->getOpenIdConnectUrl()); + + $json = $scheme->toJSON(); + $this->assertEquals('http', $json->get('type')); + $this->assertEquals('HTTP Basic Auth', $json->get('description')); + } + + /** + * @test + */ + public function testOAuthFlowObj() { + $flow = new OAuthFlowObj(); + $this->assertEmpty($flow->getScopes()); + + $flow->setAuthorizationUrl('https://example.com/oauth/authorize'); + $this->assertEquals('https://example.com/oauth/authorize', $flow->getAuthorizationUrl()); + + $flow->setTokenUrl('https://example.com/oauth/token'); + $this->assertEquals('https://example.com/oauth/token', $flow->getTokenUrl()); + + $flow->setRefreshUrl('https://example.com/oauth/refresh'); + $this->assertEquals('https://example.com/oauth/refresh', $flow->getRefreshUrl()); + + $flow->addScope('read', 'Read access'); + $flow->addScope('write', 'Write access'); + + $scopes = $flow->getScopes(); + $this->assertCount(2, $scopes); + $this->assertEquals('Read access', $scopes['read']); + + $json = $flow->toJSON(); + $this->assertNotNull($json->get('scopes')); + } + + /** + * @test + */ + public function testOAuthFlowsObj() { + $flows = new OAuthFlowsObj(); + $this->assertNull($flows->getImplicit()); + + $implicit = new OAuthFlowObj(); + $flows->setImplicit($implicit); + $this->assertSame($implicit, $flows->getImplicit()); + + $password = new OAuthFlowObj(); + $flows->setPassword($password); + $this->assertSame($password, $flows->getPassword()); + + $clientCredentials = new OAuthFlowObj(); + $flows->setClientCredentials($clientCredentials); + $this->assertSame($clientCredentials, $flows->getClientCredentials()); + + $authCode = new OAuthFlowObj(); + $flows->setAuthorizationCode($authCode); + $this->assertSame($authCode, $flows->getAuthorizationCode()); + + $json = $flows->toJSON(); + $this->assertNotNull($json->get('implicit')); + $this->assertNotNull($json->get('password')); + } + + /** + * @test + */ + public function testSecurityRequirementObj() { + $requirement = new SecurityRequirementObj(); + $this->assertEmpty($requirement->getRequirements()); + + $requirement->addRequirement('api_key', []); + $requirement->addRequirement('oauth2', ['read', 'write']); + + $reqs = $requirement->getRequirements(); + $this->assertCount(2, $reqs); + $this->assertEmpty($reqs['api_key']); + $this->assertEquals(['read', 'write'], $reqs['oauth2']); + + $json = $requirement->toJSON(); + $this->assertNotNull($json->get('api_key')); + $this->assertNotNull($json->get('oauth2')); + } + + /** + * @test + */ + public function testComponentsObj() { + $components = new ComponentsObj(); + $this->assertEmpty($components->getSchemas()); + $this->assertEmpty($components->getSecuritySchemes()); + + $components->addSchema('User', ['type' => 'object']); + $schemas = $components->getSchemas(); + $this->assertCount(1, $schemas); + $this->assertEquals(['type' => 'object'], $schemas['User']); + + $securityScheme = new SecuritySchemeObj('http'); + $components->addSecurityScheme('basicAuth', $securityScheme); + $schemes = $components->getSecuritySchemes(); + $this->assertCount(1, $schemes); + $this->assertSame($securityScheme, $schemes['basicAuth']); + + $json = $components->toJSON(); + $this->assertNotNull($json->get('schemas')); + $this->assertNotNull($json->get('securitySchemes')); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 58b4d7e..dbfd68a 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,11 +1,10 @@ - + - - ../WebFiori/Http/AbstractWebService.php - ../WebFiori/Http/APIFilter.php + ../WebFiori/Http/AbstractWebService.php + ../WebFiori/Http/WebService.php../WebFiori/Http/APIFilter.php ../WebFiori/Http/RequestParameter.php ../WebFiori/Http/WebServicesManager.php ../WebFiori/Http/Request.php @@ -17,10 +16,9 @@ ../WebFiori/Http/UriParameter.php ../WebFiori/Http/ObjectMapper.php ../WebFiori/Http/AuthHeader.php - - + ../WebFiori/Http/OpenAPI/ComponentsObj.php../WebFiori/Http/OpenAPI/ContactObj.php../WebFiori/Http/OpenAPI/ExternalDocObj.php../WebFiori/Http/OpenAPI/HeaderObj.php../WebFiori/Http/OpenAPI/InfoObj.php../WebFiori/Http/OpenAPI/LicenseObj.php../WebFiori/Http/OpenAPI/MediaTypeObj.php../WebFiori/Http/OpenAPI/OAuthFlowObj.php../WebFiori/Http/OpenAPI/OAuthFlowsObj.php../WebFiori/Http/OpenAPI/OpenAPIObj.php../WebFiori/Http/OpenAPI/OperationObj.php../WebFiori/Http/OpenAPI/ParameterObj.php../WebFiori/Http/OpenAPI/PathItemObj.php../WebFiori/Http/OpenAPI/PathsObj.php../WebFiori/Http/OpenAPI/ReferenceObj.php../WebFiori/Http/OpenAPI/ResponseObj.php../WebFiori/Http/OpenAPI/ResponsesObj.php../WebFiori/Http/OpenAPI/Schema.php../WebFiori/Http/OpenAPI/SecurityRequirementObj.php../WebFiori/Http/OpenAPI/SecuritySchemeObj.php../WebFiori/Http/OpenAPI/ServerObj.php../WebFiori/Http/OpenAPI/TagObj.php../WebFiori/Http/ManagerInfoService.php../WebFiori/Http/ResponseMessage.php - + diff --git a/tests/phpunit10.xml b/tests/phpunit10.xml index c63e0e4..26e0c22 100644 --- a/tests/phpunit10.xml +++ b/tests/phpunit10.xml @@ -1,22 +1,21 @@ - + - + - + ./WebFiori/Tests/Http - - ../WebFiori/Http/AbstractWebService.php - ../WebFiori/Http/APIFilter.php + ../WebFiori/Http/AbstractWebService.php + ../WebFiori/Http/WebService.php../WebFiori/Http/APIFilter.php ../WebFiori/Http/RequestParameter.php ../WebFiori/Http/WebServicesManager.php ../WebFiori/Http/Request.php @@ -31,6 +30,5 @@ ../WebFiori/Http/HttpMessage.php ../WebFiori/Http/RequestV2.php ../WebFiori/Http/RequestUri.php - - - + ../WebFiori/Http/OpenAPI/ComponentsObj.php../WebFiori/Http/OpenAPI/ContactObj.php../WebFiori/Http/OpenAPI/ExternalDocObj.php../WebFiori/Http/OpenAPI/HeaderObj.php../WebFiori/Http/OpenAPI/InfoObj.php../WebFiori/Http/OpenAPI/LicenseObj.php../WebFiori/Http/OpenAPI/MediaTypeObj.php../WebFiori/Http/OpenAPI/OAuthFlowObj.php../WebFiori/Http/OpenAPI/OAuthFlowsObj.php../WebFiori/Http/OpenAPI/OpenAPIObj.php../WebFiori/Http/OpenAPI/OperationObj.php../WebFiori/Http/OpenAPI/ParameterObj.php../WebFiori/Http/OpenAPI/PathItemObj.php../WebFiori/Http/OpenAPI/PathsObj.php../WebFiori/Http/OpenAPI/ReferenceObj.php../WebFiori/Http/OpenAPI/ResponseObj.php../WebFiori/Http/OpenAPI/ResponsesObj.php../WebFiori/Http/OpenAPI/Schema.php../WebFiori/Http/OpenAPI/SecurityRequirementObj.php../WebFiori/Http/OpenAPI/SecuritySchemeObj.php../WebFiori/Http/OpenAPI/ServerObj.php../WebFiori/Http/OpenAPI/TagObj.php../WebFiori/Http/ManagerInfoService.php../WebFiori/Http/ResponseMessage.php + \ No newline at end of file From 2d0e6fc0bdbbb85d8eef41ecaaec477bc8be35cb Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 17:06:49 +0300 Subject: [PATCH 30/50] feat: Added Annotations to Web Services --- WebFiori/Http/Annotations/RestController.php | 12 ++++ WebFiori/Http/WebService.php | 31 ++++++++-- examples/AnnotatedService.php | 27 +++++++++ .../Tests/Http/RestControllerTest.php | 57 +++++++++++++++++++ .../Http/TestServices/AnnotatedService.php | 16 ++++++ .../Http/TestServices/NonAnnotatedService.php | 19 +++++++ 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 WebFiori/Http/Annotations/RestController.php create mode 100644 examples/AnnotatedService.php create mode 100644 tests/WebFiori/Tests/Http/RestControllerTest.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php diff --git a/WebFiori/Http/Annotations/RestController.php b/WebFiori/Http/Annotations/RestController.php new file mode 100644 index 0000000..a1354b1 --- /dev/null +++ b/WebFiori/Http/Annotations/RestController.php @@ -0,0 +1,12 @@ +setName($name)) { - $this->setName('new-service'); - } + public function __construct(string $name = '') { $this->reqMethods = []; $this->parameters = []; $this->responses = []; @@ -138,8 +135,34 @@ public function __construct(string $name) { $this->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); + } + } /** * Returns an array that contains all possible requests methods at which the * service can be called with. * diff --git a/examples/AnnotatedService.php b/examples/AnnotatedService.php new file mode 100644 index 0000000..e46cdfd --- /dev/null +++ b/examples/AnnotatedService.php @@ -0,0 +1,27 @@ +setRequestMethods([RequestMethod::GET]); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $this->sendResponse('Hello from annotated service!'); + } +} + +// Usage example +$service = new AnnotatedHelloService(); +echo "Service name: " . $service->getName() . "\n"; +echo "Service description: " . $service->getDescription() . "\n"; diff --git a/tests/WebFiori/Tests/Http/RestControllerTest.php b/tests/WebFiori/Tests/Http/RestControllerTest.php new file mode 100644 index 0000000..b3aca02 --- /dev/null +++ b/tests/WebFiori/Tests/Http/RestControllerTest.php @@ -0,0 +1,57 @@ +assertEquals('annotated-service', $service->getName()); + } + + public function testAnnotatedServiceDescription() { + $service = new AnnotatedService(); + $this->assertEquals('A service configured via annotations', $service->getDescription()); + } + + public function testNonAnnotatedService() { + $service = new NonAnnotatedService(); + $this->assertEquals('non-annotated', $service->getName()); + $this->assertEquals('A traditional service', $service->getDescription()); + } + + public function testAnnotationWithEmptyName() { + $service = new class extends \WebFiori\Http\WebService { + public function __construct() { + parent::__construct('fallback-name'); + } + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $this->assertEquals('fallback-name', $service->getName()); + } + + public function testAnnotationWithoutFallback() { + $service = new class extends \WebFiori\Http\WebService { + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $this->assertEquals('new-service', $service->getName()); + } + + public function testAnnotatedServiceWithManager() { + $manager = new WebServicesManager(); + $service = new AnnotatedService(); + $manager->addService($service); + + $retrievedService = $manager->getServiceByName('annotated-service'); + $this->assertNotNull($retrievedService); + $this->assertEquals('A service configured via annotations', $retrievedService->getDescription()); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php new file mode 100644 index 0000000..18de397 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php @@ -0,0 +1,16 @@ +sendResponse('Annotated service response'); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php new file mode 100644 index 0000000..1eec313 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php @@ -0,0 +1,19 @@ +setDescription('A traditional service'); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $this->sendResponse('Non-annotated service response'); + } +} From 0ad4d946c79f30047b2d3d370b067b781efd5b9b Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 17:29:03 +0300 Subject: [PATCH 31/50] feat: Added Support for Annotations --- WebFiori/Http/Annotations/DeleteMapping.php | 8 ++ WebFiori/Http/Annotations/GetMapping.php | 8 ++ WebFiori/Http/Annotations/PostMapping.php | 8 ++ WebFiori/Http/Annotations/PutMapping.php | 8 ++ WebFiori/Http/Annotations/RequestParam.php | 15 +++ WebFiori/Http/WebService.php | 67 ++++++++++ examples/FullAnnotatedController.php | 115 ++++++++++++++++++ examples/UserControllerExample.php | 64 ++++++++++ .../Http/FullAnnotationIntegrationTest.php | 83 +++++++++++++ .../WebFiori/Tests/Http/MethodMappingTest.php | 59 +++++++++ .../Tests/Http/ParameterMappingTest.php | 52 ++++++++ .../Http/TestServices/AllMethodsService.php | 33 +++++ .../TestServices/MappedMethodsService.php | 34 ++++++ .../TestServices/ParameterMappedService.php | 43 +++++++ 14 files changed, 597 insertions(+) create mode 100644 WebFiori/Http/Annotations/DeleteMapping.php create mode 100644 WebFiori/Http/Annotations/GetMapping.php create mode 100644 WebFiori/Http/Annotations/PostMapping.php create mode 100644 WebFiori/Http/Annotations/PutMapping.php create mode 100644 WebFiori/Http/Annotations/RequestParam.php create mode 100644 examples/FullAnnotatedController.php create mode 100644 examples/UserControllerExample.php create mode 100644 tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php create mode 100644 tests/WebFiori/Tests/Http/MethodMappingTest.php create mode 100644 tests/WebFiori/Tests/Http/ParameterMappingTest.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php diff --git a/WebFiori/Http/Annotations/DeleteMapping.php b/WebFiori/Http/Annotations/DeleteMapping.php new file mode 100644 index 0000000..7f65726 --- /dev/null +++ b/WebFiori/Http/Annotations/DeleteMapping.php @@ -0,0 +1,8 @@ +setDescription($description); } + + $this->configureMethodMappings(); + } + + /** + * Configure HTTP methods from method annotations. + */ + private function configureMethodMappings(): void { + $reflection = new \ReflectionClass($this); + $methods = []; + + foreach ($reflection->getMethods() as $method) { + $methodMappings = [ + \WebFiori\Http\Annotations\GetMapping::class => RequestMethod::GET, + \WebFiori\Http\Annotations\PostMapping::class => RequestMethod::POST, + \WebFiori\Http\Annotations\PutMapping::class => RequestMethod::PUT, + \WebFiori\Http\Annotations\DeleteMapping::class => RequestMethod::DELETE + ]; + + foreach ($methodMappings as $annotationClass => $httpMethod) { + $attributes = $method->getAttributes($annotationClass); + if (!empty($attributes)) { + $methods[] = $httpMethod; + $this->configureParametersFromMethod($method); + } + } + } + + if (!empty($methods)) { + $this->setRequestMethods(array_unique($methods)); + } + } + + /** + * 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(); + + $this->addParameters([ + $param->name => [ + \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 + ] + ]); + } + } + + /** + * 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. diff --git a/examples/FullAnnotatedController.php b/examples/FullAnnotatedController.php new file mode 100644 index 0000000..c63cc22 --- /dev/null +++ b/examples/FullAnnotatedController.php @@ -0,0 +1,115 @@ +getParamVal('page'); + $limit = $this->getParamVal('limit'); + + $this->sendResponse('Products retrieved', 'success', 200, [ + 'page' => $page, + 'limit' => $limit, + 'products' => [] + ]); + } + + #[PostMapping] + #[RequestParam('name', 'string', false, null, 'Product name')] + #[RequestParam('price', 'float', false, null, 'Product price')] + #[RequestParam('category', 'string', true, 'General', 'Product category')] + public function createProduct() { + $name = $this->getParamVal('name'); + $price = $this->getParamVal('price'); + $category = $this->getParamVal('category'); + + $this->sendResponse('Product created', 'success', 201, [ + 'id' => 123, + 'name' => $name, + 'price' => $price, + 'category' => $category + ]); + } + + #[PutMapping] + #[RequestParam('id', 'int', false, null, 'Product ID')] + #[RequestParam('name', 'string', true)] + #[RequestParam('price', 'float', true)] + public function updateProduct() { + $id = $this->getParamVal('id'); + $name = $this->getParamVal('name'); + $price = $this->getParamVal('price'); + + $this->sendResponse('Product updated', 'success', 200, [ + 'id' => $id, + 'name' => $name, + 'price' => $price + ]); + } + + #[DeleteMapping] + #[RequestParam('id', 'int', false, null, 'Product ID to delete')] + public function deleteProduct() { + $id = $this->getParamVal('id'); + $this->sendResponse('Product deleted', 'success', 200, ['id' => $id]); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + + switch ($method) { + case \WebFiori\Http\RequestMethod::GET: + $this->getProducts(); + break; + case \WebFiori\Http\RequestMethod::POST: + $this->createProduct(); + break; + case \WebFiori\Http\RequestMethod::PUT: + $this->updateProduct(); + break; + case \WebFiori\Http\RequestMethod::DELETE: + $this->deleteProduct(); + break; + } + } +} + +// Demo usage +echo "=== Product Controller Demo ===\n"; + +$controller = new ProductController(); +echo "Service Name: " . $controller->getName() . "\n"; +echo "Description: " . $controller->getDescription() . "\n"; +echo "HTTP Methods: " . implode(', ', $controller->getRequestMethods()) . "\n"; + +echo "\nParameters:\n"; +foreach ($controller->getParameters() as $param) { + echo "- {$param->getName()}: {$param->getType()}" . + ($param->isOptional() ? ' (optional)' : ' (required)') . + ($param->getDescription() ? " - {$param->getDescription()}" : '') . "\n"; +} + +// Integration with WebServicesManager +echo "\n=== Manager Integration ===\n"; +$manager = new WebServicesManager(); +$manager->addService($controller); + +echo "Service registered successfully!\n"; +echo "Available service: " . $manager->getServiceByName('products')->getName() . "\n"; diff --git a/examples/UserControllerExample.php b/examples/UserControllerExample.php new file mode 100644 index 0000000..b95dc47 --- /dev/null +++ b/examples/UserControllerExample.php @@ -0,0 +1,64 @@ +sendResponse('Retrieved all users', 'success', 200, ['users' => []]); + } + + #[PostMapping] + public function createUser() { + $this->sendResponse('User created', 'success', 201, ['id' => 123]); + } + + #[PutMapping] + public function updateUser() { + $this->sendResponse('User updated', 'success', 200); + } + + #[DeleteMapping] + public function deleteUser() { + $this->sendResponse('User deleted', 'success', 200); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + + switch ($method) { + case \WebFiori\Http\RequestMethod::GET: + $this->getUsers(); + break; + case \WebFiori\Http\RequestMethod::POST: + $this->createUser(); + break; + case \WebFiori\Http\RequestMethod::PUT: + $this->updateUser(); + break; + case \WebFiori\Http\RequestMethod::DELETE: + $this->deleteUser(); + break; + default: + $this->sendResponse('Method not allowed', 'error', 405); + } + } +} + +// Usage example +$service = new UserController(); +echo "Service: " . $service->getName() . "\n"; +echo "Description: " . $service->getDescription() . "\n"; +echo "Supported methods: " . implode(', ', $service->getRequestMethods()) . "\n"; diff --git a/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php b/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php new file mode 100644 index 0000000..471f1fc --- /dev/null +++ b/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php @@ -0,0 +1,83 @@ +setName('manual-service'); + $service->addRequestMethod(\WebFiori\Http\RequestMethod::GET); + $service->addParameters([ + 'test' => [ + \WebFiori\Http\ParamOption::TYPE => ParamType::STRING + ] + ]); + + $this->assertEquals('manual-service', $service->getName()); + $this->assertContains(\WebFiori\Http\RequestMethod::GET, $service->getRequestMethods()); + $this->assertNotNull($service->getParameterByName('test')); + } + + public function testAnnotationOverridesManualConfiguration() { + $service = new #[\WebFiori\Http\Annotations\RestController('annotated-override')] + class extends \WebFiori\Http\WebService { + public function __construct() { + parent::__construct('manual-name'); // This should be overridden + } + + #[\WebFiori\Http\Annotations\GetMapping] + public function getData() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $this->assertEquals('annotated-override', $service->getName()); + $this->assertContains(\WebFiori\Http\RequestMethod::GET, $service->getRequestMethods()); + } + + public function testMixedConfigurationApproach() { + $service = new #[\WebFiori\Http\Annotations\RestController('mixed-service')] + class extends \WebFiori\Http\WebService { + public function __construct() { + parent::__construct(); + // Add manual configuration after annotation processing + $this->addRequestMethod(\WebFiori\Http\RequestMethod::PATCH); + $this->addParameters([ + 'manual_param' => [ + \WebFiori\Http\ParamOption::TYPE => ParamType::STRING + ] + ]); + } + + #[\WebFiori\Http\Annotations\PostMapping] + #[\WebFiori\Http\Annotations\RequestParam('annotated_param', 'int')] + public function createData() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $this->assertEquals('mixed-service', $service->getName()); + + $methods = $service->getRequestMethods(); + $this->assertContains(\WebFiori\Http\RequestMethod::POST, $methods); // From annotation + $this->assertContains(\WebFiori\Http\RequestMethod::PATCH, $methods); // Manual addition + + $this->assertNotNull($service->getParameterByName('annotated_param')); // From annotation + $this->assertNotNull($service->getParameterByName('manual_param')); // Manual addition + } +} diff --git a/tests/WebFiori/Tests/Http/MethodMappingTest.php b/tests/WebFiori/Tests/Http/MethodMappingTest.php new file mode 100644 index 0000000..160bb45 --- /dev/null +++ b/tests/WebFiori/Tests/Http/MethodMappingTest.php @@ -0,0 +1,59 @@ +getRequestMethods(); + + $this->assertContains(RequestMethod::GET, $methods); + $this->assertContains(RequestMethod::POST, $methods); + $this->assertCount(2, $methods); + } + + public function testAllMethodMappings() { + $service = new AllMethodsService(); + $methods = $service->getRequestMethods(); + + $this->assertContains(RequestMethod::GET, $methods); + $this->assertContains(RequestMethod::POST, $methods); + $this->assertContains(RequestMethod::PUT, $methods); + $this->assertContains(RequestMethod::DELETE, $methods); + $this->assertCount(4, $methods); + } + + public function testServiceWithoutMethodAnnotations() { + $service = new class extends \WebFiori\Http\WebService { + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $methods = $service->getRequestMethods(); + $this->assertEmpty($methods); + } + + public function testMixedAnnotationAndManualConfiguration() { + $service = new class extends \WebFiori\Http\WebService { + public function __construct() { + parent::__construct('mixed-service'); + $this->addRequestMethod(RequestMethod::PATCH); // Manual addition + } + + #[\WebFiori\Http\Annotations\GetMapping] + public function getData() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $methods = $service->getRequestMethods(); + $this->assertContains(RequestMethod::GET, $methods); // From annotation + $this->assertContains(RequestMethod::PATCH, $methods); // Manual addition + } +} diff --git a/tests/WebFiori/Tests/Http/ParameterMappingTest.php b/tests/WebFiori/Tests/Http/ParameterMappingTest.php new file mode 100644 index 0000000..af37c59 --- /dev/null +++ b/tests/WebFiori/Tests/Http/ParameterMappingTest.php @@ -0,0 +1,52 @@ +getParameters(); + + $this->assertCount(4, $parameters); // id, name, email, age + + // Check 'id' parameter + $idParam = $service->getParameterByName('id'); + $this->assertNotNull($idParam); + $this->assertEquals(ParamType::INT, $idParam->getType()); + $this->assertFalse($idParam->isOptional()); + $this->assertEquals('User ID', $idParam->getDescription()); + + // Check 'name' parameter + $nameParam = $service->getParameterByName('name'); + $this->assertNotNull($nameParam); + $this->assertEquals(ParamType::STRING, $nameParam->getType()); + $this->assertTrue($nameParam->isOptional()); + $this->assertEquals('Anonymous', $nameParam->getDefault()); + + // Check 'email' parameter + $emailParam = $service->getParameterByName('email'); + $this->assertNotNull($emailParam); + $this->assertEquals(ParamType::EMAIL, $emailParam->getType()); + $this->assertFalse($emailParam->isOptional()); + + // Check 'age' parameter + $ageParam = $service->getParameterByName('age'); + $this->assertNotNull($ageParam); + $this->assertEquals(ParamType::INT, $ageParam->getType()); + $this->assertTrue($ageParam->isOptional()); + $this->assertEquals(18, $ageParam->getDefault()); + } + + public function testHttpMethodsFromParameterAnnotations() { + $service = new ParameterMappedService(); + $methods = $service->getRequestMethods(); + + $this->assertContains(\WebFiori\Http\RequestMethod::GET, $methods); + $this->assertContains(\WebFiori\Http\RequestMethod::POST, $methods); + $this->assertCount(2, $methods); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php b/tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php new file mode 100644 index 0000000..ba2d1df --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/AllMethodsService.php @@ -0,0 +1,33 @@ +sendResponse('All methods service'); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php b/tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php new file mode 100644 index 0000000..d23bf4b --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/MappedMethodsService.php @@ -0,0 +1,34 @@ +sendResponse('GET users'); + } + + #[PostMapping] + public function createUser() { + $this->sendResponse('POST user'); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $method = $this->getManager()->getRequestMethod(); + if ($method === \WebFiori\Http\RequestMethod::GET) { + $this->getUsers(); + } elseif ($method === \WebFiori\Http\RequestMethod::POST) { + $this->createUser(); + } + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php b/tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php new file mode 100644 index 0000000..2ba39af --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/ParameterMappedService.php @@ -0,0 +1,43 @@ +getParamVal('id'); + $name = $this->getParamVal('name'); + $this->sendResponse("User $id: $name"); + } + + #[PostMapping] + #[RequestParam('email', 'email', false)] + #[RequestParam('age', 'int', true, 18)] + public function createUser() { + $email = $this->getParamVal('email'); + $age = $this->getParamVal('age'); + $this->sendResponse("Created user: $email, age: $age"); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + if ($method === \WebFiori\Http\RequestMethod::GET) { + $this->getUser(); + } elseif ($method === \WebFiori\Http\RequestMethod::POST) { + $this->createUser(); + } + } +} From 4241fdf42ecf73c0a9448c66b904376e093b5fc9 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 18 Dec 2025 17:52:15 +0300 Subject: [PATCH 32/50] feat: Annotated Auth --- WebFiori/Http/Annotations/AllowAnonymous.php | 8 + WebFiori/Http/Annotations/PreAuthorize.php | 11 ++ WebFiori/Http/Annotations/RequiresAuth.php | 8 + WebFiori/Http/SecurityContext.php | 50 ++++++ WebFiori/Http/WebService.php | 98 ++++++++++++ examples/AuthenticatedController.php | 143 ++++++++++++++++++ .../Http/AuthenticationAnnotationTest.php | 94 ++++++++++++ .../TestServices/ClassLevelAuthService.php | 19 +++ .../Tests/Http/TestServices/SecureService.php | 80 ++++++++++ 9 files changed, 511 insertions(+) create mode 100644 WebFiori/Http/Annotations/AllowAnonymous.php create mode 100644 WebFiori/Http/Annotations/PreAuthorize.php create mode 100644 WebFiori/Http/Annotations/RequiresAuth.php create mode 100644 WebFiori/Http/SecurityContext.php create mode 100644 examples/AuthenticatedController.php create mode 100644 tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/SecureService.php diff --git a/WebFiori/Http/Annotations/AllowAnonymous.php b/WebFiori/Http/Annotations/AllowAnonymous.php new file mode 100644 index 0000000..0694f3a --- /dev/null +++ b/WebFiori/Http/Annotations/AllowAnonymous.php @@ -0,0 +1,8 @@ +configureMethodMappings(); + $this->configureAuthentication(); + } + + /** + * 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); + } elseif ($classAuth['requiresAuth'] || $classAuth['preAuthorize']) { + $this->setIsAuthRequired(true); + } + } + + /** + * Get authentication configuration from class annotations. + */ + 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) + ]; + } + + /** + * Check method-level authorization before processing. + */ + public function checkMethodAuthorization(): bool { + $reflection = new \ReflectionClass($this); + $method = $this->getCurrentProcessingMethod(); + + if (!$method) { + return $this->isAuthorized(); + } + + $reflectionMethod = $reflection->getMethod($method); + + // Check AllowAnonymous first + if (!empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\AllowAnonymous::class))) { + return true; + } + + // Check RequiresAuth + if (!empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class))) { + if (!SecurityContext::isAuthenticated()) { + return false; + } + } + + // Check PreAuthorize + $preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class); + if (!empty($preAuthAttributes)) { + $preAuth = $preAuthAttributes[0]->newInstance(); + return $this->evaluateSecurityExpression($preAuth->expression); + } + + return $this->isAuthorized(); + } + + /** + * Evaluate security expression (simplified version). + */ + private function evaluateSecurityExpression(string $expression): bool { + // Handle hasRole('ROLE_NAME') + if (preg_match("/hasRole\('([^']+)'\)/", $expression, $matches)) { + return SecurityContext::hasRole($matches[1]); + } + + // Handle hasAuthority('AUTHORITY_NAME') + if (preg_match("/hasAuthority\('([^']+)'\)/", $expression, $matches)) { + return SecurityContext::hasAuthority($matches[1]); + } + + // Handle isAuthenticated() + if ($expression === 'isAuthenticated()') { + return SecurityContext::isAuthenticated(); + } + + // Handle permitAll() + if ($expression === 'permitAll()') { + return true; + } + + return false; + } + + /** + * Get the current processing method name (to be overridden by subclasses if needed). + */ + protected function getCurrentProcessingMethod(): ?string { + return null; // Default implementation } /** diff --git a/examples/AuthenticatedController.php b/examples/AuthenticatedController.php new file mode 100644 index 0000000..353fd8b --- /dev/null +++ b/examples/AuthenticatedController.php @@ -0,0 +1,143 @@ +sendResponse('This is public information - no authentication required'); + } + + #[GetMapping] + #[RequiresAuth] + public function getProfile() { + $user = SecurityContext::getCurrentUser(); + $this->sendResponse('User profile', 200, 'success', [ + 'user' => $user, + 'roles' => SecurityContext::getRoles(), + 'authorities' => SecurityContext::getAuthorities() + ]); + } + + #[PostMapping] + #[PreAuthorize("hasRole('ADMIN')")] + public function adminOperation() { + $this->sendResponse('Admin operation completed successfully'); + } + + #[PostMapping] + #[PreAuthorize("hasAuthority('USER_MANAGE')")] + public function manageUsers() { + $this->sendResponse('User management operation completed'); + } + + public function isAuthorized(): bool { + // This is the fallback authorization check + // In a real application, you might check JWT tokens, session, etc. + return true; + } + + public function processRequest() { + // Check method-level authorization first + if (!$this->checkMethodAuthorization()) { + $this->sendResponse('Access denied', 403, 'error'); + return; + } + + $action = $_GET['action'] ?? 'public'; + + switch ($action) { + case 'public': + $this->getPublicInfo(); + break; + case 'profile': + $this->getProfile(); + break; + case 'admin': + $this->adminOperation(); + break; + case 'manage': + $this->manageUsers(); + break; + default: + $this->sendResponse('Unknown action', 400, 'error'); + } + } + + protected function getCurrentProcessingMethod(): ?string { + $action = $_GET['action'] ?? 'public'; + return match($action) { + 'public' => 'getPublicInfo', + 'profile' => 'getProfile', + 'admin' => 'adminOperation', + 'manage' => 'manageUsers', + default => null + }; + } +} + +// Demo usage +echo "=== Authentication Demo ===\n"; + +$controller = new AuthenticatedController(); + +// Test 1: Public access (no auth required) +echo "\n1. Testing public access:\n"; +$_GET['action'] = 'public'; +$controller->processRequest(); + +// Test 2: Private access without authentication +echo "\n2. Testing private access without auth:\n"; +$_GET['action'] = 'profile'; +$controller->processRequest(); + +// Test 3: Set up authentication +echo "\n3. Setting up authentication:\n"; +SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']); +SecurityContext::setRoles(['USER']); +SecurityContext::setAuthorities(['USER_READ']); + +echo "User authenticated: " . (SecurityContext::isAuthenticated() ? 'Yes' : 'No') . "\n"; +echo "Roles: " . implode(', ', SecurityContext::getRoles()) . "\n"; +echo "Authorities: " . implode(', ', SecurityContext::getAuthorities()) . "\n"; + +// Test 4: Private access with authentication +echo "\n4. Testing private access with auth:\n"; +$_GET['action'] = 'profile'; +$controller->processRequest(); + +// Test 5: Admin access without admin role +echo "\n5. Testing admin access without admin role:\n"; +$_GET['action'] = 'admin'; +$controller->processRequest(); + +// Test 6: Grant admin role and try again +echo "\n6. Granting admin role and testing admin access:\n"; +SecurityContext::setRoles(['USER', 'ADMIN']); +$controller->processRequest(); + +// Test 7: Authority-based access +echo "\n7. Testing authority-based access:\n"; +$_GET['action'] = 'manage'; +$controller->processRequest(); + +// Test 8: Grant required authority +echo "\n8. Granting USER_MANAGE authority:\n"; +SecurityContext::setAuthorities(['USER_READ', 'USER_MANAGE']); +$controller->processRequest(); + +// Cleanup +SecurityContext::clear(); +unset($_GET['action']); diff --git a/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php new file mode 100644 index 0000000..ded4c12 --- /dev/null +++ b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php @@ -0,0 +1,94 @@ +assertFalse($service->isAuthRequired()); + } + + public function testSecurityContextAuthentication() { + // Test unauthenticated state + $this->assertFalse(SecurityContext::isAuthenticated()); + + // Set user and roles + SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John']); + SecurityContext::setRoles(['ADMIN', 'USER']); + SecurityContext::setAuthorities(['USER_CREATE', 'USER_READ']); + + $this->assertTrue(SecurityContext::isAuthenticated()); + $this->assertTrue(SecurityContext::hasRole('ADMIN')); + $this->assertTrue(SecurityContext::hasAuthority('USER_CREATE')); + $this->assertFalse(SecurityContext::hasRole('GUEST')); + } + + public function testMethodLevelAuthorization() { + $service = new SecureService(); + + // Test public method (AllowAnonymous) + $_GET['action'] = 'public'; + $this->assertTrue($service->checkMethodAuthorization()); + + // Test private method without auth (RequiresAuth) + $_GET['action'] = 'private'; + $this->assertFalse($service->checkMethodAuthorization()); + + // Test private method with auth + SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John']); + $this->assertTrue($service->checkMethodAuthorization()); + + // Test admin method without admin role + $_GET['action'] = 'admin'; + SecurityContext::setRoles(['USER']); + $this->assertFalse($service->checkMethodAuthorization()); + + // Test admin method with admin role + SecurityContext::setRoles(['ADMIN']); + $this->assertTrue($service->checkMethodAuthorization()); + + // Test authority-based method + $_GET['action'] = 'create'; + SecurityContext::setAuthorities(['USER_READ']); + $this->assertFalse($service->checkMethodAuthorization()); + + SecurityContext::setAuthorities(['USER_CREATE']); + $this->assertTrue($service->checkMethodAuthorization()); + } + + public function testSecurityExpressions() { + $service = new SecureService(); + $reflection = new \ReflectionClass($service); + $method = $reflection->getMethod('evaluateSecurityExpression'); + $method->setAccessible(true); + + // Test without authentication + $this->assertFalse($method->invoke($service, "hasRole('ADMIN')")); + $this->assertFalse($method->invoke($service, 'isAuthenticated()')); + $this->assertTrue($method->invoke($service, 'permitAll()')); + + // Test with authentication and roles + SecurityContext::setCurrentUser(['id' => 1]); + SecurityContext::setRoles(['ADMIN']); + SecurityContext::setAuthorities(['USER_CREATE']); + + $this->assertTrue($method->invoke($service, "hasRole('ADMIN')")); + $this->assertFalse($method->invoke($service, "hasRole('GUEST')")); + $this->assertTrue($method->invoke($service, "hasAuthority('USER_CREATE')")); + $this->assertTrue($method->invoke($service, 'isAuthenticated()')); + } + + protected function tearDown(): void { + SecurityContext::clear(); + unset($_GET['action']); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php b/tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php new file mode 100644 index 0000000..2a4ad81 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/ClassLevelAuthService.php @@ -0,0 +1,19 @@ +sendResponse('Public service - no auth required'); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/SecureService.php b/tests/WebFiori/Tests/Http/TestServices/SecureService.php new file mode 100644 index 0000000..5e98b18 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/SecureService.php @@ -0,0 +1,80 @@ +sendResponse('Public data - no auth required'); + } + + #[GetMapping] + #[RequiresAuth] + public function getPrivateData() { + $user = SecurityContext::getCurrentUser(); + $this->sendResponse('Private data for: ' . ($user['name'] ?? 'unknown')); + } + + #[PostMapping] + #[PreAuthorize("hasRole('ADMIN')")] + public function adminOnly() { + $this->sendResponse('Admin-only operation'); + } + + #[PostMapping] + #[PreAuthorize("hasAuthority('USER_CREATE')")] + public function createUser() { + $this->sendResponse('User created'); + } + + public function isAuthorized(): bool { + return true; // Default fallback + } + + public function processRequest() { + if (!$this->checkMethodAuthorization()) { + $this->sendResponse('Unauthorized', 'error', 401); + return; + } + + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + $action = $_GET['action'] ?? 'public'; + + switch ($action) { + case 'public': + $this->getPublicData(); + break; + case 'private': + $this->getPrivateData(); + break; + case 'admin': + $this->adminOnly(); + break; + case 'create': + $this->createUser(); + break; + } + } + + protected function getCurrentProcessingMethod(): ?string { + $action = $_GET['action'] ?? 'public'; + return match($action) { + 'public' => 'getPublicData', + 'private' => 'getPrivateData', + 'admin' => 'adminOnly', + 'create' => 'createUser', + default => null + }; + } +} From 71c38ecaeb739deeae40e7d16d69f5ae277a7553 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 24 Dec 2025 00:29:07 +0300 Subject: [PATCH 33/50] feat: Added Exceptions --- .../Http/Exceptions/BadRequestException.php | 14 +++++++++ .../Http/Exceptions/ForbiddenException.php | 14 +++++++++ WebFiori/Http/Exceptions/HttpException.php | 29 +++++++++++++++++++ .../Http/Exceptions/NotFoundException.php | 14 +++++++++ .../Http/Exceptions/UnauthorizedException.php | 14 +++++++++ 5 files changed, 85 insertions(+) create mode 100644 WebFiori/Http/Exceptions/BadRequestException.php create mode 100644 WebFiori/Http/Exceptions/ForbiddenException.php create mode 100644 WebFiori/Http/Exceptions/HttpException.php create mode 100644 WebFiori/Http/Exceptions/NotFoundException.php create mode 100644 WebFiori/Http/Exceptions/UnauthorizedException.php diff --git a/WebFiori/Http/Exceptions/BadRequestException.php b/WebFiori/Http/Exceptions/BadRequestException.php new file mode 100644 index 0000000..1f2386a --- /dev/null +++ b/WebFiori/Http/Exceptions/BadRequestException.php @@ -0,0 +1,14 @@ +statusCode = $statusCode; + $this->responseType = $responseType; + } + + public function getStatusCode(): int { + return $this->statusCode; + } + + public function getResponseType(): string { + return $this->responseType; + } +} diff --git a/WebFiori/Http/Exceptions/NotFoundException.php b/WebFiori/Http/Exceptions/NotFoundException.php new file mode 100644 index 0000000..f236d60 --- /dev/null +++ b/WebFiori/Http/Exceptions/NotFoundException.php @@ -0,0 +1,14 @@ + Date: Wed, 24 Dec 2025 00:30:51 +0300 Subject: [PATCH 34/50] chore: Added Samples --- WebFiori/Http/Annotations/ResponseBody.php | 26 +++ WebFiori/Http/WebService.php | 178 +++++++++++++++++- WebFiori/Http/WebServicesManager.php | 50 ++++- examples/AnnotatedService.php | 5 - examples/AuthTestService.php | 19 ++ examples/AuthenticatedController.php | 81 ++++---- examples/CompleteApiDemo.php | 124 ++++++++++++ ...edController.php => ProductController.php} | 26 +-- ...ntrollerExample.php => UserController.php} | 6 - examples/index.php | 18 ++ examples/loader.php | 2 +- .../Tests/Http/ExceptionHandlingTest.php | 109 +++++++++++ .../WebFiori/Tests/Http/ResponseBodyTest.php | 153 +++++++++++++++ .../TestServices/ExceptionTestService.php | 79 ++++++++ .../TestServices/IntegrationTestService.php | 26 +++ .../Tests/Http/TestServices/LegacyService.php | 24 +++ .../TestServices/MixedResponseService.php | 60 ++++++ .../TestServices/ResponseBodyTestService.php | 87 +++++++++ .../WebServicesManagerIntegrationTest.php | 109 +++++++++++ 19 files changed, 1098 insertions(+), 84 deletions(-) create mode 100644 WebFiori/Http/Annotations/ResponseBody.php create mode 100644 examples/AuthTestService.php create mode 100644 examples/CompleteApiDemo.php rename examples/{FullAnnotatedController.php => ProductController.php} (76%) rename examples/{UserControllerExample.php => UserController.php} (87%) create mode 100644 tests/WebFiori/Tests/Http/ExceptionHandlingTest.php create mode 100644 tests/WebFiori/Tests/Http/ResponseBodyTest.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/LegacyService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php create mode 100644 tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php create mode 100644 tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php diff --git a/WebFiori/Http/Annotations/ResponseBody.php b/WebFiori/Http/Annotations/ResponseBody.php new file mode 100644 index 0000000..24ab040 --- /dev/null +++ b/WebFiori/Http/Annotations/ResponseBody.php @@ -0,0 +1,26 @@ +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 { + // Call the target method and process its return value + $result = $this->$targetMethod(); + $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(); + } + } + + /** + * 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 hasResponseBodyAnnotation(string $methodName): bool { + try { + $reflection = new \ReflectionMethod($this, $methodName); + return !empty($reflection->getAttributes(ResponseBody::class)); + } catch (\ReflectionException $e) { + return false; + } + } + + /** + * Handle HTTP exceptions by converting them to appropriate responses. + * + * @param HttpException $exception The HTTP exception to handle + */ + protected function handleException(HttpException $exception): void { + $this->sendResponse( + $exception->getMessage(), + $exception->getStatusCode(), + $exception->getResponseType() + ); + } + + /** + * Configure parameters dynamically for a specific method. + * + * @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 + } + } + /** * Configure authentication from annotations. */ @@ -200,7 +279,7 @@ private function getAuthenticationFromClass(\ReflectionClass $reflection): array */ public function checkMethodAuthorization(): bool { $reflection = new \ReflectionClass($this); - $method = $this->getCurrentProcessingMethod(); + $method = $this->getCurrentProcessingMethod() ?: $this->getTargetMethod(); if (!$method) { return $this->isAuthorized(); @@ -264,6 +343,95 @@ protected function getCurrentProcessingMethod(): ?string { return null; // Default implementation } + /** + * Get the target method name based on current HTTP request. + * + * @return string|null The method name that should handle this request, or null if none found + */ + 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; + } + } catch (\ReflectionException $e) { + // 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. + * + * @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; + } + } + + return false; + } + + /** + * 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(); + + // Auto-convert return value to response + if ($result === null) { + // Null return = empty response with configured status + $this->sendResponse('', $responseBody->status, $responseBody->type); + } elseif (is_array($result) || is_object($result)) { + // Array/object = JSON response + $this->sendResponse('Success', $responseBody->status, $responseBody->type, $result); + } else { + // String/scalar = plain response + $this->sendResponse($result, $responseBody->status, $responseBody->type); + } + } + /** * Configure HTTP methods from method annotations. */ @@ -283,7 +451,7 @@ private function configureMethodMappings(): void { $attributes = $method->getAttributes($annotationClass); if (!empty($attributes)) { $methods[] = $httpMethod; - $this->configureParametersFromMethod($method); + // Don't configure parameters here - do it dynamically per request } } } @@ -757,7 +925,7 @@ public function hasParameter(string $name) : bool { * @return bool True if the user is allowed to perform the action. False otherwise. * */ - abstract function isAuthorized() : bool; + public function isAuthorized() : bool {return false;} /** * Returns the value of the property 'requireAuth'. * @@ -800,7 +968,7 @@ public static function isValidName(string $name): bool { /** * Process client's request. */ - abstract function processRequest(); + public function processRequest() {} /** * Removes a request parameter from the service given its name. * diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index 72e670a..f7d98a9 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -481,6 +481,13 @@ public final function process() { if ($this->isContentTypeSupported()) { if ($this->_checkAction()) { $actionObj = $this->getServiceByName($this->getCalledServiceName()); + + // Configure parameters for ResponseBody services before getting them + if (method_exists($actionObj, 'processWithAutoHandling') && $this->serviceHasResponseBodyMethods($actionObj)) { + $this->configureServiceParameters($actionObj); + } + + $params = $actionObj->getParameters(); $params = $actionObj->getParameters(); $this->filter->clearParametersDef(); $this->filter->clearInputs(); @@ -1038,6 +1045,11 @@ private function getAction() { } private function isAuth(WebService $service) { if ($service->isAuthRequired()) { + // Check method-level authorization first (handles AllowAnonymous, etc.) + if (method_exists($service, 'checkMethodAuthorization')) { + return $service->checkMethodAuthorization(); + } + $isAuthCheck = 'isAuthorized'.$this->getRequest()->getMethod(); if (!method_exists($service, $isAuthCheck)) { @@ -1050,6 +1062,14 @@ private function isAuth(WebService $service) { return true; } private function processService(WebService $service) { + // Try auto-processing only if service has ResponseBody methods + if (method_exists($service, 'processWithAutoHandling') && $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)) { @@ -1058,7 +1078,35 @@ private function processService(WebService $service) { $service->$processMethod(); } } - + /** + * Check if service has any methods with ResponseBody annotation. + */ + 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); + 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/AnnotatedService.php b/examples/AnnotatedService.php index e46cdfd..542d83b 100644 --- a/examples/AnnotatedService.php +++ b/examples/AnnotatedService.php @@ -1,5 +1,4 @@ getName() . "\n"; -echo "Service description: " . $service->getDescription() . "\n"; diff --git a/examples/AuthTestService.php b/examples/AuthTestService.php new file mode 100644 index 0000000..4103d18 --- /dev/null +++ b/examples/AuthTestService.php @@ -0,0 +1,19 @@ + 'You have super admin access!']; + } +} diff --git a/examples/AuthenticatedController.php b/examples/AuthenticatedController.php index 353fd8b..93562a9 100644 --- a/examples/AuthenticatedController.php +++ b/examples/AuthenticatedController.php @@ -1,5 +1,4 @@ processRequest(); +// // Test 1: Public access (no auth required) +// echo "\n1. Testing public access:\n"; +// $_GET['action'] = 'public'; +// $controller->processRequest(); -// Test 2: Private access without authentication -echo "\n2. Testing private access without auth:\n"; -$_GET['action'] = 'profile'; -$controller->processRequest(); +// // Test 2: Private access without authentication +// echo "\n2. Testing private access without auth:\n"; +// $_GET['action'] = 'profile'; +// $controller->processRequest(); // Test 3: Set up authentication -echo "\n3. Setting up authentication:\n"; -SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']); -SecurityContext::setRoles(['USER']); -SecurityContext::setAuthorities(['USER_READ']); +// echo "\n3. Setting up authentication:\n"; +// SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']); +// SecurityContext::setRoles(['USER']); +// SecurityContext::setAuthorities(['USER_READ']); -echo "User authenticated: " . (SecurityContext::isAuthenticated() ? 'Yes' : 'No') . "\n"; -echo "Roles: " . implode(', ', SecurityContext::getRoles()) . "\n"; -echo "Authorities: " . implode(', ', SecurityContext::getAuthorities()) . "\n"; +// echo "User authenticated: " . (SecurityContext::isAuthenticated() ? 'Yes' : 'No') . "\n"; +// echo "Roles: " . implode(', ', SecurityContext::getRoles()) . "\n"; +// echo "Authorities: " . implode(', ', SecurityContext::getAuthorities()) . "\n"; -// Test 4: Private access with authentication -echo "\n4. Testing private access with auth:\n"; -$_GET['action'] = 'profile'; -$controller->processRequest(); +// // Test 4: Private access with authentication +// echo "\n4. Testing private access with auth:\n"; +// $_GET['action'] = 'profile'; +// $controller->processRequest(); -// Test 5: Admin access without admin role -echo "\n5. Testing admin access without admin role:\n"; -$_GET['action'] = 'admin'; -$controller->processRequest(); +// // Test 5: Admin access without admin role +// echo "\n5. Testing admin access without admin role:\n"; +// $_GET['action'] = 'admin'; +// $controller->processRequest(); -// Test 6: Grant admin role and try again -echo "\n6. Granting admin role and testing admin access:\n"; -SecurityContext::setRoles(['USER', 'ADMIN']); -$controller->processRequest(); +// // Test 6: Grant admin role and try again +// echo "\n6. Granting admin role and testing admin access:\n"; +// SecurityContext::setRoles(['USER', 'ADMIN']); +// $controller->processRequest(); -// Test 7: Authority-based access -echo "\n7. Testing authority-based access:\n"; -$_GET['action'] = 'manage'; -$controller->processRequest(); +// // Test 7: Authority-based access +// echo "\n7. Testing authority-based access:\n"; +// $_GET['action'] = 'manage'; +// $controller->processRequest(); -// Test 8: Grant required authority -echo "\n8. Granting USER_MANAGE authority:\n"; -SecurityContext::setAuthorities(['USER_READ', 'USER_MANAGE']); -$controller->processRequest(); +// // Test 8: Grant required authority +// echo "\n8. Granting USER_MANAGE authority:\n"; +// SecurityContext::setAuthorities(['USER_READ', 'USER_MANAGE']); +// $controller->processRequest(); -// Cleanup -SecurityContext::clear(); -unset($_GET['action']); +// // Cleanup +// SecurityContext::clear(); +// unset($_GET['action']); diff --git a/examples/CompleteApiDemo.php b/examples/CompleteApiDemo.php new file mode 100644 index 0000000..16f4e4a --- /dev/null +++ b/examples/CompleteApiDemo.php @@ -0,0 +1,124 @@ +getParamVal('id'); + + if ($id) { + // Get specific user + if ($id <= 0) { + throw new BadRequestException('Invalid user ID'); + } + + if ($id === 404) { + throw new NotFoundException('User not found'); + } + + return [ + 'user' => [ + 'id' => $id, + 'name' => 'John Doe', + 'email' => 'john@example.com' + ] + ]; + } else { + // Get all users + return [ + 'users' => [ + ['id' => 1, 'name' => 'John Doe'], + ['id' => 2, 'name' => 'Jane Smith'] + ], + 'total' => 2 + ]; + } + } + + #[PostMapping] + #[ResponseBody(status: 201)] + #[RequestParam('name', 'string')] + #[RequestParam('email', 'email')] + #[PreAuthorize("hasAuthority('USER_CREATE')")] + public function createUser(): array { + $name = $this->getParamVal('name'); + $email = $this->getParamVal('email'); + + if (empty($name)) { + throw new BadRequestException('Name is required'); + } + + return [ + 'message' => 'User created successfully', + 'user' => [ + 'id' => rand(1000, 9999), + 'name' => $name, + 'email' => $email, + 'created_at' => date('Y-m-d H:i:s') + ] + ]; + } + + #[PutMapping] + #[ResponseBody] + #[RequestParam('id', 'int')] + #[RequestParam('name', 'string', true)] + #[RequestParam('email', 'email', true)] + #[PreAuthorize("hasAuthority('USER_UPDATE')")] + public function updateUser(): array { + $id = $this->getParamVal('id'); + $name = $this->getParamVal('name'); + $email = $this->getParamVal('email'); + + if ($id === 404) { + throw new NotFoundException('User not found'); + } + + $updates = array_filter([ + 'name' => $name, + 'email' => $email + ]); + + return [ + 'message' => 'User updated successfully', + 'user' => [ + 'id' => $id, + 'updates' => $updates, + 'updated_at' => date('Y-m-d H:i:s') + ] + ]; + } + + #[DeleteMapping] + #[ResponseBody(status: 204)] + #[RequestParam('id', 'int')] + #[PreAuthorize("hasRole('ADMIN')")] + public function deleteUser(): null { + $id = $this->getParamVal('id'); + + if ($id === 404) { + throw new NotFoundException('User not found'); + } + + // Simulate deletion + return null; // Auto-converts to 204 No Content + } +} diff --git a/examples/FullAnnotatedController.php b/examples/ProductController.php similarity index 76% rename from examples/FullAnnotatedController.php rename to examples/ProductController.php index c63cc22..760edaf 100644 --- a/examples/FullAnnotatedController.php +++ b/examples/ProductController.php @@ -1,5 +1,4 @@ getName() . "\n"; -echo "Description: " . $controller->getDescription() . "\n"; -echo "HTTP Methods: " . implode(', ', $controller->getRequestMethods()) . "\n"; - -echo "\nParameters:\n"; -foreach ($controller->getParameters() as $param) { - echo "- {$param->getName()}: {$param->getType()}" . - ($param->isOptional() ? ' (optional)' : ' (required)') . - ($param->getDescription() ? " - {$param->getDescription()}" : '') . "\n"; -} - -// Integration with WebServicesManager -echo "\n=== Manager Integration ===\n"; -$manager = new WebServicesManager(); -$manager->addService($controller); - -echo "Service registered successfully!\n"; -echo "Available service: " . $manager->getServiceByName('products')->getName() . "\n"; +} \ No newline at end of file diff --git a/examples/UserControllerExample.php b/examples/UserController.php similarity index 87% rename from examples/UserControllerExample.php rename to examples/UserController.php index b95dc47..391766c 100644 --- a/examples/UserControllerExample.php +++ b/examples/UserController.php @@ -56,9 +56,3 @@ public function processRequest() { } } } - -// Usage example -$service = new UserController(); -echo "Service: " . $service->getName() . "\n"; -echo "Description: " . $service->getDescription() . "\n"; -echo "Supported methods: " . implode(', ', $service->getRequestMethods()) . "\n"; diff --git a/examples/index.php b/examples/index.php index 0bbe98e..537e386 100644 --- a/examples/index.php +++ b/examples/index.php @@ -4,14 +4,32 @@ require 'HelloWorldService.php'; require 'GetRandomService.php'; require 'HelloWithAuthService.php'; +require 'CompleteApiDemo.php'; +require 'ProductController.php'; +require 'AuthenticatedController.php'; +require 'UserController.php'; +require 'AuthTestService.php'; use HelloWorldService; use GetRandomService; use HelloWithAuthService; use WebFiori\Http\WebServicesManager; +use WebFiori\Http\SecurityContext; + +// Set up authentication context +SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Demo User']); +SecurityContext::setRoles(['USER', 'ADMIN']); +SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Demo User']); +SecurityContext::setRoles(['USER', 'ADMIN']); +SecurityContext::setAuthorities(['USER_CREATE', 'USER_UPDATE', 'USER_DELETE']); $manager = new WebServicesManager(); $manager->addService(new HelloWorldService()); $manager->addService(new GetRandomService()); $manager->addService(new HelloWithAuthService()); +$manager->addService(new CompleteApiDemo()); +$manager->addService(new ProductController()); +$manager->addService(new AuthenticatedController()); +$manager->addService(new UserController()); +$manager->addService(new AuthTestService()); $manager->process(); diff --git a/examples/loader.php b/examples/loader.php index 3290d0c..a83718c 100644 --- a/examples/loader.php +++ b/examples/loader.php @@ -4,4 +4,4 @@ ini_set('display_errors', 1); error_reporting(-1); -require_once '../vendor/autoload.php'; +require_once __DIR__ . '/../vendor/autoload.php'; diff --git a/tests/WebFiori/Tests/Http/ExceptionHandlingTest.php b/tests/WebFiori/Tests/Http/ExceptionHandlingTest.php new file mode 100644 index 0000000..7087c63 --- /dev/null +++ b/tests/WebFiori/Tests/Http/ExceptionHandlingTest.php @@ -0,0 +1,109 @@ +assertEquals(404, $notFound->getStatusCode()); + $this->assertEquals('error', $notFound->getResponseType()); + $this->assertEquals('Resource not found', $notFound->getMessage()); + + $badRequest = new BadRequestException('Invalid input'); + $this->assertEquals(400, $badRequest->getStatusCode()); + $this->assertEquals('error', $badRequest->getResponseType()); + + $unauthorized = new UnauthorizedException('Login required'); + $this->assertEquals(401, $unauthorized->getStatusCode()); + $this->assertEquals('error', $unauthorized->getResponseType()); + + $forbidden = new ForbiddenException('Access denied'); + $this->assertEquals(403, $forbidden->getStatusCode()); + $this->assertEquals('error', $forbidden->getResponseType()); + } + + public function testExceptionDefaults() { + $notFound = new NotFoundException(); + $this->assertEquals('Not Found', $notFound->getMessage()); + + $badRequest = new BadRequestException(); + $this->assertEquals('Bad Request', $badRequest->getMessage()); + + $unauthorized = new UnauthorizedException(); + $this->assertEquals('Unauthorized', $unauthorized->getMessage()); + + $forbidden = new ForbiddenException(); + $this->assertEquals('Forbidden', $forbidden->getMessage()); + } + + public function testServiceExceptionHandling() { + $service = new ExceptionTestService(); + + // Test that method has ResponseBody annotation + $this->assertTrue($service->hasResponseBodyAnnotation('getUser')); + + // Test exception throwing with test parameter + $_GET['test_id'] = 404; + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('User not found'); + $service->getUser(); + } + + public function testDifferentExceptionTypes() { + $service = new ExceptionTestService(); + + // Test BadRequestException + $_GET['test_id'] = 400; + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Invalid user ID'); + $service->getUser(); + } + + public function testUnauthorizedException() { + $service = new ExceptionTestService(); + + $this->expectException(UnauthorizedException::class); + $this->expectExceptionMessage('Authentication required'); + $service->createUser(); + } + + public function testGenericException() { + $service = new ExceptionTestService(); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Generic error'); + $service->getError(); + } + + public function testHandleExceptionMethod() { + $service = new ExceptionTestService(); + $exception = new NotFoundException('Test not found'); + + // Test the handleException method directly + $reflection = new \ReflectionClass($service); + $method = $reflection->getMethod('handleException'); + $method->setAccessible(true); + + // The method should not throw an exception + $this->expectNotToPerformAssertions(); + $method->invoke($service, $exception); + } + + protected function tearDown(): void { + unset($_GET['action']); + unset($_GET['test_id']); + unset($_SERVER['REQUEST_METHOD']); + } +} diff --git a/tests/WebFiori/Tests/Http/ResponseBodyTest.php b/tests/WebFiori/Tests/Http/ResponseBodyTest.php new file mode 100644 index 0000000..20a4831 --- /dev/null +++ b/tests/WebFiori/Tests/Http/ResponseBodyTest.php @@ -0,0 +1,153 @@ +getTargetMethod(); + $this->assertEquals('getArrayData', $targetMethod); + + // Test ResponseBody annotation detection + $this->assertTrue($service->hasResponseBodyAnnotation('getArrayData')); + + // Test return value processing + $result = $service->getArrayData(); + $this->assertIsArray($result); + $this->assertArrayHasKey('users', $result); + } + + public function testStringReturnValue() { + $service = new ResponseBodyTestService(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + + // Test specific method + $this->assertTrue($service->hasResponseBodyAnnotation('getStringData')); + + $result = $service->getStringData(); + $this->assertIsString($result); + $this->assertEquals('Resource created successfully', $result); + } + + public function testNullReturnValue() { + $service = new ResponseBodyTestService(); + $_SERVER['REQUEST_METHOD'] = 'POST'; + + // Test method discovery for POST + $targetMethod = $service->getTargetMethod(); + $this->assertEquals('deleteData', $targetMethod); + + $result = $service->deleteData(); + $this->assertNull($result); + } + + public function testObjectReturnValue() { + $service = new ResponseBodyTestService(); + + // Test specific method + $this->assertTrue($service->hasResponseBodyAnnotation('getObjectData')); + + $result = $service->getObjectData(); + $this->assertIsObject($result); + $this->assertObjectHasProperty('message', $result); + } + + public function testMethodWithoutResponseBody() { + $service = new ResponseBodyTestService(); + + // Should not have ResponseBody annotation + $this->assertFalse($service->hasResponseBodyAnnotation('getManualData')); + } + + public function testMethodWithParameters() { + $service = new ResponseBodyTestService(); + + // Test that method has ResponseBody annotation + $this->assertTrue($service->hasResponseBodyAnnotation('createUser')); + + // Test method has POST mapping + $_SERVER['REQUEST_METHOD'] = 'POST'; + $targetMethod = $service->getTargetMethod(); + $this->assertContains($targetMethod, ['deleteData', 'createUser']); // Either POST method + } + + public function testMixedServiceWithAuthentication() { + $service = new MixedResponseService(); + + // Test methods have correct annotations + $this->assertTrue($service->hasResponseBodyAnnotation('getSecureData')); + $this->assertTrue($service->hasResponseBodyAnnotation('getPublicData')); + $this->assertFalse($service->hasResponseBodyAnnotation('traditionalMethod')); + + // Test with authentication + SecurityContext::setCurrentUser(['id' => 1]); + SecurityContext::setRoles(['USER']); + + // The service should be authorized since we set up proper authentication + $this->assertTrue($service->checkMethodAuthorization()); + } + + public function testLegacyServiceCompatibility() { + $service = new LegacyService(); + + // Should find the GET method + $_SERVER['REQUEST_METHOD'] = 'GET'; + $targetMethod = $service->getTargetMethod(); + $this->assertEquals('getData', $targetMethod); + + // Method should not have ResponseBody annotation + $this->assertFalse($service->hasResponseBodyAnnotation('getData')); + } + + public function testProcessWithAutoHandling() { + $service = new ResponseBodyTestService(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + + // Test the auto-processing logic + $targetMethod = $service->getTargetMethod(); + $hasResponseBody = $service->hasResponseBodyAnnotation($targetMethod); + + $this->assertTrue($hasResponseBody); + $this->assertEquals('getArrayData', $targetMethod); + } + + public function testResponseBodyAnnotationConfiguration() { + $service = new ResponseBodyTestService(); + + // Test default ResponseBody annotation + $reflection = new \ReflectionMethod($service, 'getArrayData'); + $attributes = $reflection->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class); + $this->assertNotEmpty($attributes); + + $responseBody = $attributes[0]->newInstance(); + $this->assertEquals(200, $responseBody->status); + $this->assertEquals('success', $responseBody->type); + + // Test custom ResponseBody annotation + $reflection = new \ReflectionMethod($service, 'getStringData'); + $attributes = $reflection->getAttributes(\WebFiori\Http\Annotations\ResponseBody::class); + $responseBody = $attributes[0]->newInstance(); + $this->assertEquals(201, $responseBody->status); + $this->assertEquals('created', $responseBody->type); + } + + protected function tearDown(): void { + SecurityContext::clear(); + unset($_GET['action']); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php b/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php new file mode 100644 index 0000000..42ccd87 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/ExceptionTestService.php @@ -0,0 +1,79 @@ +getParamVal('id'); + + // For testing, check if id is set via test + if (!$id && isset($_GET['test_id'])) { + $id = (int)$_GET['test_id']; + } + + if ($id === 404) { + throw new NotFoundException('User not found'); + } + + if ($id === 400) { + throw new BadRequestException('Invalid user ID'); + } + + return ['user' => ['id' => $id, 'name' => 'Test User']]; + } + + #[PostMapping] + #[ResponseBody] + public function createUser(): array { + throw new UnauthorizedException('Authentication required'); + } + + #[GetMapping] + #[ResponseBody] + public function getError(): array { + throw new \Exception('Generic error'); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $action = $_GET['action'] ?? 'get'; + switch ($action) { + case 'get': + $this->getUser(); + break; + case 'create': + $this->createUser(); + break; + case 'error': + $this->getError(); + break; + } + } + + protected function getCurrentProcessingMethod(): ?string { + $action = $_GET['action'] ?? 'get'; + return match($action) { + 'get' => 'getUser', + 'create' => 'createUser', + 'error' => 'getError', + default => null + }; + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php b/tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php new file mode 100644 index 0000000..aa4b8cd --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/IntegrationTestService.php @@ -0,0 +1,26 @@ + 'Auto-processing via WebServicesManager']; + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + // This should not be called when using auto-processing + $this->sendResponse('Manual processing fallback', 200, 'info'); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/LegacyService.php b/tests/WebFiori/Tests/Http/TestServices/LegacyService.php new file mode 100644 index 0000000..9861305 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/LegacyService.php @@ -0,0 +1,24 @@ +sendResponse('Legacy service response', 200, 'success', ['legacy' => true]); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $this->getData(); + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php b/tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php new file mode 100644 index 0000000..878fc5e --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/MixedResponseService.php @@ -0,0 +1,60 @@ + 'data', 'user' => 'authenticated']; + } + + // ResponseBody method without authentication + #[GetMapping] + #[ResponseBody] + #[AllowAnonymous] + public function getPublicData(): array { + return ['public' => 'data', 'access' => 'open']; + } + + // Traditional method (no ResponseBody) + #[PostMapping] + #[AllowAnonymous] + public function traditionalMethod(): void { + $this->sendResponse('Traditional method response', 200, 'success', ['method' => 'traditional']); + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + $action = $_GET['action'] ?? 'traditional'; + if ($action === 'traditional') { + $this->traditionalMethod(); + } else { + $this->sendResponse('Unknown action', 400, 'error'); + } + } + + protected function getCurrentProcessingMethod(): ?string { + $action = $_GET['action'] ?? 'traditional'; + return match($action) { + 'secure' => 'getSecureData', + 'public' => 'getPublicData', + 'traditional' => 'traditionalMethod', + default => null + }; + } +} diff --git a/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php new file mode 100644 index 0000000..8ec2580 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php @@ -0,0 +1,87 @@ + [['id' => 1, 'name' => 'John'], ['id' => 2, 'name' => 'Jane']]]; + } + + // Test 2: Return string with custom status + #[GetMapping] + #[ResponseBody(status: 201, type: 'created')] + public function getStringData(): string { + return 'Resource created successfully'; + } + + // Test 3: Return null (should be empty response) + #[PostMapping] + #[ResponseBody(status: 204)] + public function deleteData(): null { + // Simulate deletion + return null; + } + + // Test 4: Return object + #[GetMapping] + #[ResponseBody] + public function getObjectData(): object { + return (object)['message' => 'Hello World', 'timestamp' => time()]; + } + + // Test 5: Method without ResponseBody (manual handling) + #[GetMapping] + #[RequestParam('manual', 'string', true, 'false')] + public function getManualData(): void { + $manual = $this->getParamVal('manual'); + $this->sendResponse('Manual response: ' . $manual, 200, 'success'); + } + + // Test 6: Method with parameters and ResponseBody + #[PostMapping] + #[ResponseBody] + #[RequestParam('name', 'string')] + #[RequestParam('age', 'int', true, 25)] + public function createUser(): array { + return [ + 'user' => [ + 'name' => $this->getParamVal('name'), + 'age' => $this->getParamVal('age'), + 'created_at' => date('Y-m-d H:i:s') + ] + ]; + } + + public function isAuthorized(): bool { + return true; + } + + public function processRequest() { + // This should not be called for ResponseBody methods + $this->sendResponse('Fallback processRequest called', 200, 'info'); + } + + protected function getCurrentProcessingMethod(): ?string { + $action = $_GET['action'] ?? 'array'; + return match($action) { + 'array' => 'getArrayData', + 'string' => 'getStringData', + 'null' => 'deleteData', + 'object' => 'getObjectData', + 'manual' => 'getManualData', + 'create' => 'createUser', + default => null + }; + } +} diff --git a/tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php b/tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php new file mode 100644 index 0000000..b8e74f5 --- /dev/null +++ b/tests/WebFiori/Tests/Http/WebServicesManagerIntegrationTest.php @@ -0,0 +1,109 @@ +addService($service); + + // Test that service has processWithAutoHandling method + $this->assertTrue(method_exists($service, 'processWithAutoHandling')); + + // Test that service has ResponseBody methods + $this->assertTrue($service->hasResponseBodyAnnotation('getData')); + } + + public function testManagerProcessesAutoHandlingServices() { + $service = new IntegrationTestService(); + + // Test that service has ResponseBody methods + $this->assertTrue($service->hasResponseBodyAnnotation('getData')); + + // Test method return value + $result = $service->getData(); + $this->assertIsArray($result); + $this->assertEquals('Auto-processing via WebServicesManager', $result['message']); + } + + public function testLegacyServiceStillWorks() { + $service = new LegacyService(); + + // Legacy service should not have ResponseBody methods + $this->assertFalse($service->hasResponseBodyAnnotation('getData')); + + // Should have traditional methods + $this->assertTrue(method_exists($service, 'processRequest')); + } + + public function testMixedServiceTypes() { + $manager = new WebServicesManager(); + + // Add both new and legacy services + $manager->addService(new IntegrationTestService()); + $manager->addService(new LegacyService()); + + // Both should be registered + $this->assertNotNull($manager->getServiceByName('integration-test')); + $this->assertNotNull($manager->getServiceByName('legacy-service')); + } + + public function testResponseBodyWithExceptionHandling() { + $service = new ResponseBodyTestService(); + + // Test that service has ResponseBody methods + $_SERVER['REQUEST_METHOD'] = 'GET'; + $targetMethod = $service->getTargetMethod(); + $this->assertTrue($service->hasResponseBodyAnnotation($targetMethod)); + + // Test method return value + $result = $service->getArrayData(); + $this->assertIsArray($result); + $this->assertArrayHasKey('users', $result); + } + + public function testServiceMethodDiscovery() { + $service = new ResponseBodyTestService(); + + // Test GET method discovery + $_SERVER['REQUEST_METHOD'] = 'GET'; + $targetMethod = $service->getTargetMethod(); + $this->assertEquals('getArrayData', $targetMethod); + + // Test POST method discovery + $_SERVER['REQUEST_METHOD'] = 'POST'; + $targetMethod = $service->getTargetMethod(); + $this->assertContains($targetMethod, ['deleteData', 'createUser']); + } + + public function testBackwardCompatibility() { + // Test that all existing functionality still works + $legacyService = new LegacyService(); + $newService = new IntegrationTestService(); + + // Both should have processRequest method + $this->assertTrue(method_exists($legacyService, 'processRequest')); + $this->assertTrue(method_exists($newService, 'processRequest')); + + // Both should have processWithAutoHandling (inherited from WebService) + $this->assertTrue(method_exists($legacyService, 'processWithAutoHandling')); + $this->assertTrue(method_exists($newService, 'processWithAutoHandling')); + } + + protected function tearDown(): void { + unset($_GET['service']); + unset($_SERVER['REQUEST_METHOD']); + } +} From df0e1fabab29a86b38eb0a48c8277350c278d9fc Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 24 Dec 2025 13:45:18 +0300 Subject: [PATCH 35/50] test: Updated Test Cases --- tests/WebFiori/Tests/Http/RestControllerTest.php | 4 ---- .../WebFiori/Tests/Http/TestServices/AnnotatedService.php | 6 ------ .../Tests/Http/TestServices/NonAnnotatedService.php | 8 -------- 3 files changed, 18 deletions(-) diff --git a/tests/WebFiori/Tests/Http/RestControllerTest.php b/tests/WebFiori/Tests/Http/RestControllerTest.php index b3aca02..2268a30 100644 --- a/tests/WebFiori/Tests/Http/RestControllerTest.php +++ b/tests/WebFiori/Tests/Http/RestControllerTest.php @@ -29,8 +29,6 @@ public function testAnnotationWithEmptyName() { public function __construct() { parent::__construct('fallback-name'); } - public function isAuthorized(): bool { return true; } - public function processRequest() {} }; $this->assertEquals('fallback-name', $service->getName()); @@ -38,8 +36,6 @@ public function processRequest() {} public function testAnnotationWithoutFallback() { $service = new class extends \WebFiori\Http\WebService { - public function isAuthorized(): bool { return true; } - public function processRequest() {} }; $this->assertEquals('new-service', $service->getName()); diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php index 18de397..852a192 100644 --- a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php +++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php @@ -6,11 +6,5 @@ #[RestController('annotated-service', 'A service configured via annotations')] class AnnotatedService extends WebService { - public function isAuthorized(): bool { - return true; - } - public function processRequest() { - $this->sendResponse('Annotated service response'); - } } diff --git a/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php index 1eec313..13679bb 100644 --- a/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php +++ b/tests/WebFiori/Tests/Http/TestServices/NonAnnotatedService.php @@ -8,12 +8,4 @@ public function __construct() { parent::__construct('non-annotated'); $this->setDescription('A traditional service'); } - - public function isAuthorized(): bool { - return true; - } - - public function processRequest() { - $this->sendResponse('Non-annotated service response'); - } } From 0fb0fecbc474372b890c01dada7015f3849b57e4 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 24 Dec 2025 17:53:51 +0300 Subject: [PATCH 36/50] test: Updated Test Cases --- WebFiori/Http/SecurityContext.php | 93 ++++++++++++++ WebFiori/Http/WebService.php | 42 ++----- WebFiori/Http/WebServicesManager.php | 12 +- .../Tests/Http/RestControllerTest.php | 113 +++++++++++++++++- .../Http/TestServices/AnnotatedService.php | 28 ++++- 5 files changed, 243 insertions(+), 45 deletions(-) diff --git a/WebFiori/Http/SecurityContext.php b/WebFiori/Http/SecurityContext.php index 3f55dab..4afc91c 100644 --- a/WebFiori/Http/SecurityContext.php +++ b/WebFiori/Http/SecurityContext.php @@ -1,50 +1,143 @@ 123, 'name' => 'John Doe', 'email' => 'john@example.com'] + */ public static function setCurrentUser(?array $user): void { self::$currentUser = $user; } + /** + * Get the current authenticated user. + * + * @return array|null User data or null if not authenticated + */ public static function getCurrentUser(): ?array { return self::$currentUser; } + /** + * Set user roles. + * + * @param array $roles Array of role names + * Example: ['USER', 'ADMIN', 'MODERATOR'] + */ public static function setRoles(array $roles): void { self::$roles = $roles; } + /** + * Get user roles. + * + * @return array Array of role names + */ 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. + * + * @return array Array of authority names + */ public static function getAuthorities(): array { return self::$authorities; } + /** + * Check if user has a specific role. + * + * @param string $role Role name to check + * Example: 'ADMIN', 'USER', 'MODERATOR' + * @return bool True if user has the role + */ 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. + * + * @return bool True if user is authenticated + */ public static function isAuthenticated(): bool { return self::$currentUser !== null; } + /** + * Clear all security context data. + */ public static function clear(): void { self::$currentUser = null; self::$roles = []; self::$authorities = []; } + + /** + * Evaluate security expression. + * + * @param string $expression Security expression to evaluate + * Example: "hasRole('ADMIN')", "hasAuthority('USER_CREATE')", "isAuthenticated()" + * @return bool True if expression evaluates to true + */ + public static function evaluateExpression(string $expression): bool { + $evalResult = false; + // Handle hasRole('ROLE_NAME') + if (preg_match("/hasRole\('([^']+)'\)/", $expression, $matches)) { + $evalResult = self::hasRole($matches[1]); + } + + // Handle hasAuthority('AUTHORITY_NAME') + if (preg_match("/hasAuthority\('([^']+)'\)/", $expression, $matches)) { + $evalResult &= self::hasAuthority($matches[1]); + } + + // Handle isAuthenticated() + if ($expression === 'isAuthenticated()') { + $evalResult &= self::isAuthenticated(); + } + + return $evalResult; + } } diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index e11c11a..41f1281 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -258,7 +258,7 @@ private function configureAuthentication(): void { // If class has AllowAnonymous, disable auth requirement if ($classAuth['allowAnonymous']) { $this->setIsAuthRequired(false); - } elseif ($classAuth['requiresAuth'] || $classAuth['preAuthorize']) { + } else if ($classAuth['requiresAuth'] || $classAuth['preAuthorize']) { $this->setIsAuthRequired(true); } } @@ -303,39 +303,13 @@ public function checkMethodAuthorization(): bool { $preAuthAttributes = $reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class); if (!empty($preAuthAttributes)) { $preAuth = $preAuthAttributes[0]->newInstance(); - return $this->evaluateSecurityExpression($preAuth->expression); + + return SecurityContext::evaluateExpression($preAuth->expression); } return $this->isAuthorized(); } - /** - * Evaluate security expression (simplified version). - */ - private function evaluateSecurityExpression(string $expression): bool { - // Handle hasRole('ROLE_NAME') - if (preg_match("/hasRole\('([^']+)'\)/", $expression, $matches)) { - return SecurityContext::hasRole($matches[1]); - } - - // Handle hasAuthority('AUTHORITY_NAME') - if (preg_match("/hasAuthority\('([^']+)'\)/", $expression, $matches)) { - return SecurityContext::hasAuthority($matches[1]); - } - - // Handle isAuthenticated() - if ($expression === 'isAuthenticated()') { - return SecurityContext::isAuthenticated(); - } - - // Handle permitAll() - if ($expression === 'permitAll()') { - return true; - } - - return false; - } - /** * Get the current processing method name (to be overridden by subclasses if needed). */ @@ -433,7 +407,7 @@ protected function handleMethodResponse(mixed $result, string $methodName): void } /** - * Configure HTTP methods from method annotations. + * Configure allowed HTTP methods from method annotations. */ private function configureMethodMappings(): void { $reflection = new \ReflectionClass($this); @@ -441,10 +415,10 @@ private function configureMethodMappings(): void { foreach ($reflection->getMethods() as $method) { $methodMappings = [ - \WebFiori\Http\Annotations\GetMapping::class => RequestMethod::GET, - \WebFiori\Http\Annotations\PostMapping::class => RequestMethod::POST, - \WebFiori\Http\Annotations\PutMapping::class => RequestMethod::PUT, - \WebFiori\Http\Annotations\DeleteMapping::class => RequestMethod::DELETE + GetMapping::class => RequestMethod::GET, + PostMapping::class => RequestMethod::POST, + PutMapping::class => RequestMethod::PUT, + DeleteMapping::class => RequestMethod::DELETE ]; foreach ($methodMappings as $annotationClass => $httpMethod) { diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index f7d98a9..93485d8 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -483,7 +483,7 @@ public final function process() { $actionObj = $this->getServiceByName($this->getCalledServiceName()); // Configure parameters for ResponseBody services before getting them - if (method_exists($actionObj, 'processWithAutoHandling') && $this->serviceHasResponseBodyMethods($actionObj)) { + if ($this->serviceHasResponseBodyMethods($actionObj)) { $this->configureServiceParameters($actionObj); } @@ -1044,12 +1044,14 @@ private function getAction() { return $retVal; } private function isAuth(WebService $service) { + $isAuth = false; + if ($service->isAuthRequired()) { // Check method-level authorization first (handles AllowAnonymous, etc.) - if (method_exists($service, 'checkMethodAuthorization')) { - return $service->checkMethodAuthorization(); + $isAuth = $service->checkMethodAuthorization(); + if ($isAuth) { + return true; } - $isAuthCheck = 'isAuthorized'.$this->getRequest()->getMethod(); if (!method_exists($service, $isAuthCheck)) { @@ -1063,7 +1065,7 @@ private function isAuth(WebService $service) { } private function processService(WebService $service) { // Try auto-processing only if service has ResponseBody methods - if (method_exists($service, 'processWithAutoHandling') && $this->serviceHasResponseBodyMethods($service)) { + if ($this->serviceHasResponseBodyMethods($service)) { // Configure parameters for the target method before processing $this->configureServiceParameters($service); $service->processWithAutoHandling(); diff --git a/tests/WebFiori/Tests/Http/RestControllerTest.php b/tests/WebFiori/Tests/Http/RestControllerTest.php index 2268a30..13e6487 100644 --- a/tests/WebFiori/Tests/Http/RestControllerTest.php +++ b/tests/WebFiori/Tests/Http/RestControllerTest.php @@ -2,20 +2,19 @@ namespace WebFiori\Tests\Http; use PHPUnit\Framework\TestCase; +use WebFiori\Http\APITestCase; +use WebFiori\Http\SecurityContext; use WebFiori\Http\WebServicesManager; use WebFiori\Tests\Http\TestServices\AnnotatedService; use WebFiori\Tests\Http\TestServices\NonAnnotatedService; -class RestControllerTest extends TestCase { +class RestControllerTest extends APITestCase { public function testAnnotatedServiceName() { $service = new AnnotatedService(); $this->assertEquals('annotated-service', $service->getName()); - } - - public function testAnnotatedServiceDescription() { - $service = new AnnotatedService(); $this->assertEquals('A service configured via annotations', $service->getDescription()); + $this->assertEquals(['GET', 'DELETE'], $service->getRequestMethods()); } public function testNonAnnotatedService() { @@ -49,5 +48,109 @@ public function testAnnotatedServiceWithManager() { $retrievedService = $manager->getServiceByName('annotated-service'); $this->assertNotNull($retrievedService); $this->assertEquals('A service configured via annotations', $retrievedService->getDescription()); + + $this->assertEquals('{'.self::NL + . ' "message":"Method Not Allowed.",'.self::NL + . ' "type":"error",'.self::NL + . ' "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('{'.self::NL + . ' "message":"Hi Ibrahim!",'.self::NL + . ' "type":"success",'.self::NL + . ' "http-code":200'.self::NL + . '}', $this->getRequest($manager, 'annotated-service', [ + 'name' => 'Ibrahim' + ])); + } + public function testAnnotatedServiceMethodNotAllowed() { + $manager = new WebServicesManager(); + $service = new AnnotatedService(); + $manager->addService($service); + + $this->assertEquals('{'.self::NL + . ' "message":"Method Not Allowed.",'.self::NL + . ' "type":"error",'.self::NL + . ' "http-code":405'.self::NL + . '}', $this->postRequest($manager, 'annotated-service')); + + } + public function testAnnotatedGet() { + $manager = new WebServicesManager(); + $service = new AnnotatedService(); + $manager->addService($service); + + $retrievedService = $manager->getServiceByName('annotated-service'); + $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('{'.self::NL + . ' "message":"Hi Ibrahim!",'.self::NL + . ' "type":"success",'.self::NL + . ' "http-code":200'.self::NL + . '}', $this->getRequest($manager, 'annotated-service', [ + 'name' => 'Ibrahim' + ])); + } + public function testAnnotatedDelete() { + $manager = new WebServicesManager(); + $service = new AnnotatedService(); + $manager->addService($service); + + $this->assertEquals('{'.self::NL + . ' "message":"The following required parameter(s) where missing from the request body: \'id\'.",'.self::NL + . ' "type":"error",'.self::NL + . ' "http-code":404,'.self::NL + . ' "more-info":{'.self::NL + . ' "missing":['.self::NL + . ' "id"'.self::NL + . ' ]'.self::NL + . ' }'.self::NL + . '}', $this->deleteRequest($manager, 'annotated-service')); + + $this->assertEquals('{'.self::NL + . ' "message":"Not Authorized.",'.self::NL + . ' "type":"error",'.self::NL + . ' "http-code":401'.self::NL + . '}', $this->deleteRequest($manager, 'annotated-service', [ + 'id' => 1 + ])); + + SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Ibrahim']); + $this->assertEquals('{'.self::NL + . ' "message":"Not Authorized.",'.self::NL + . ' "type":"error",'.self::NL + . ' "http-code":401'.self::NL + . '}', $this->deleteRequest($manager, 'annotated-service', [ + 'id' => 1 + ])); + SecurityContext::setRoles(['ADMIN']); + $this->assertEquals('{'.self::NL + . ' "message":"Not Authorized.",'.self::NL + . ' "type":"error",'.self::NL + . ' "http-code":401'.self::NL + . '}', $this->deleteRequest($manager, 'annotated-service', [ + 'id' => 1 + ])); + SecurityContext::setAuthorities(['USER_DELETE']); + $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', [ + 'id' => 1 + ])); } } diff --git a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php index 852a192..098cec6 100644 --- a/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php +++ b/tests/WebFiori/Tests/Http/TestServices/AnnotatedService.php @@ -1,10 +1,36 @@ getParamVal('name'); + + if ($name !== null) { + return "Hi ".$name.'!'; + } + return "Hi user!"; + } + #[DeleteMapping] + #[ResponseBody] + #[RequestParam('id', ParamType::INT)] + #[PreAuthorize("isAuthenticated() && hasRole('ADMIN') && hasAuthority('USER_DELETE')")] + public function delete() { + $id = $this->getParamVal('id'); + return "Delete user with ID: ".$id; + } } From 9cd90b870c71ffc4412285b2ed1b24e1d7e47607 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 24 Dec 2025 18:00:17 +0300 Subject: [PATCH 37/50] feat: Enhanced Security Context --- WebFiori/Http/SecurityContext.php | 126 ++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/WebFiori/Http/SecurityContext.php b/WebFiori/Http/SecurityContext.php index 4afc91c..7db0b78 100644 --- a/WebFiori/Http/SecurityContext.php +++ b/WebFiori/Http/SecurityContext.php @@ -118,26 +118,140 @@ public static function clear(): void { * Evaluate security expression. * * @param string $expression Security expression to evaluate - * Example: "hasRole('ADMIN')", "hasAuthority('USER_CREATE')", "isAuthenticated()" + * + * 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 + * * @return bool True if expression evaluates to true + * @throws \InvalidArgumentException If expression is invalid */ public static function evaluateExpression(string $expression): bool { - $evalResult = false; + $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); + } + + /** + * 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. + */ + private static function evaluateSingleExpression(string $expression): bool { // Handle hasRole('ROLE_NAME') if (preg_match("/hasRole\('([^']+)'\)/", $expression, $matches)) { - $evalResult = self::hasRole($matches[1]); + 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)) { - $evalResult &= self::hasAuthority($matches[1]); + 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()') { - $evalResult &= self::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 $evalResult; + return $result; } } From 04610d0f45c51a40a38b56f9767c4dd44573d12e Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Wed, 24 Dec 2025 18:36:03 +0300 Subject: [PATCH 38/50] refactor: Add `TestUser` class --- WebFiori/Http/APITestCase.php | 66 ++++++++++++++----- WebFiori/Http/SecurityContext.php | 36 ++++++---- WebFiori/Http/UserInterface.php | 37 +++++++++++ .../Http/AuthenticationAnnotationTest.php | 30 ++++----- .../WebFiori/Tests/Http/ResponseBodyTest.php | 4 +- .../Tests/Http/RestControllerTest.php | 25 ++++--- tests/WebFiori/Tests/Http/TestUser.php | 37 +++++++++++ 7 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 WebFiori/Http/UserInterface.php create mode 100644 tests/WebFiori/Tests/Http/TestUser.php diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php index 0515d6e..06cb908 100644 --- a/WebFiori/Http/APITestCase.php +++ b/WebFiori/Http/APITestCase.php @@ -52,6 +52,7 @@ protected function tearDown(): void { $_POST = $this->backupGlobals['POST']; $_FILES = $this->backupGlobals['FILES']; $_SERVER = $this->backupGlobals['SERVER']; + SecurityContext::clear(); parent::tearDown(); } /** @@ -126,10 +127,14 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false) * @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 UserInterface|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. * * @return string The method will return the output of the endpoint. */ - public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = []) : string { + public function callEndpoint(WebServicesManager $manager, string $requestMethod, string $apiEndpointName, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { $method = strtoupper($requestMethod); $serviceName = $this->resolveServiceName($apiEndpointName); @@ -137,6 +142,7 @@ public function callEndpoint(WebServicesManager $manager, string $requestMethod, $manager->setOutputStream(fopen($this->getOutputFile(), 'w')); $manager->setRequest(Request::createFromGlobals()); + SecurityContext::setCurrentUser($user); $manager->process(); $result = $manager->readOutputStream(); @@ -178,7 +184,11 @@ private function resolveServiceName(string $nameOrClass): string { * @param string $method HTTP method * @param string $serviceName Service name * @param array $parameters Request parameters - * @param array $httpHeaders HTTP headers + * @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 UserInterface|null $user Optional user to authenticate the request with. */ private function setupRequest(string $method, string $serviceName, array $parameters, array $httpHeaders) { putenv('REQUEST_METHOD=' . $method); @@ -257,11 +267,15 @@ public function format(string $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. * + * @param UserInterface|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. + * * @return string The method will return the output that was produced by * the endpoint as string. */ - public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { - return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders); + public function deleteRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::DELETE, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends a GET request to specific endpoint. @@ -276,8 +290,8 @@ public function deleteRequest(WebServicesManager $manager, string $endpoint, arr * @return string The method will return the output that was produced by * the endpoint as string. */ - public function getRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { - return $this->callEndpoint($manager, RequestMethod::GET, $endpoint, $parameters, $httpHeaders); + public function getRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::GET, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends a POST request to specific endpoint. @@ -293,11 +307,15 @@ public function getRequest(WebServicesManager $manager, string $endpoint, array * 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 UserInterface|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. + * * @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 = []) : string { - return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders); + public function postRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::POST, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends a PUT request to specific endpoint. @@ -313,11 +331,15 @@ public function postRequest(WebServicesManager $manager, string $endpoint, array * 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 UserInterface|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. + * * @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 = []) : string { - return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders); + public function putRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::PUT, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends a PATCH request to specific endpoint. @@ -333,11 +355,15 @@ public function putRequest(WebServicesManager $manager, string $endpoint, array * 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 UserInterface|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. + * * @return string The method will return the output that was produced by * the endpoint as string. */ - public function patchRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = []) : string { - return $this->callEndpoint($manager, RequestMethod::PATCH, $endpoint, $parameters, $httpHeaders); + public function patchRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::PATCH, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends an OPTIONS request to specific endpoint. @@ -353,11 +379,15 @@ public function patchRequest(WebServicesManager $manager, string $endpoint, arra * 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 UserInterface|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. + * * @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 = []) : string { - return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders); + public function optionsRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::OPTIONS, $endpoint, $parameters, $httpHeaders, $user); } /** * Sends a HEAD request to specific endpoint. @@ -373,11 +403,15 @@ public function optionsRequest(WebServicesManager $manager, string $endpoint, ar * 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 UserInterface|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. + * * @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 = []) : string { - return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders); + public function headRequest(WebServicesManager $manager, string $endpoint, array $parameters = [], array $httpHeaders = [], ?UserInterface $user = null) : string { + return $this->callEndpoint($manager, RequestMethod::HEAD, $endpoint, $parameters, $httpHeaders, $user); } private function extractPathAndName($absPath): array { $DS = DIRECTORY_SEPARATOR; diff --git a/WebFiori/Http/SecurityContext.php b/WebFiori/Http/SecurityContext.php index 7db0b78..3be0d5a 100644 --- a/WebFiori/Http/SecurityContext.php +++ b/WebFiori/Http/SecurityContext.php @@ -6,33 +6,43 @@ * * Provides static methods to manage the current user's authentication status, * roles, and authorities for request-level security checks. + * + * @author Ibrahim */ class SecurityContext { - /** @var array|null Current authenticated user data */ - private static ?array $currentUser = null; + /** @var UserInterface|null Current authenticated user */ + private static ?UserInterface $currentUser = null; - /** @var array User roles (e.g., ['USER', 'ADMIN']) */ + /** @var array User roles (e.g., ['USER', 'ADMIN']) - deprecated, use user object */ private static array $roles = []; - /** @var array User authorities/permissions (e.g., ['USER_CREATE', 'USER_DELETE']) */ + /** @var array User authorities/permissions (e.g., ['USER_CREATE', 'USER_DELETE']) - deprecated, use user object */ private static array $authorities = []; /** * Set the current authenticated user. * - * @param array|null $user User data array or null for unauthenticated - * Example: ['id' => 123, 'name' => 'John Doe', 'email' => 'john@example.com'] + * @param UserInterface|null $user User object or null for unauthenticated */ - public static function setCurrentUser(?array $user): void { + public static function setCurrentUser(?UserInterface $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 = []; + } } /** * Get the current authenticated user. * - * @return array|null User data or null if not authenticated + * @return UserInterface|null User object or null if not authenticated */ - public static function getCurrentUser(): ?array { + public static function getCurrentUser(): ?UserInterface { return self::$currentUser; } @@ -99,10 +109,10 @@ public static function hasAuthority(string $authority): bool { /** * Check if a user is currently authenticated. * - * @return bool True if user is authenticated + * @return bool True if user is authenticated and active */ public static function isAuthenticated(): bool { - return self::$currentUser !== null; + return self::$currentUser !== null && self::$currentUser->isActive(); } /** @@ -138,6 +148,10 @@ public static function clear(): void { * @throws \InvalidArgumentException If expression is invalid */ public static function evaluateExpression(string $expression): bool { + + if (self::getCurrentUser() === null || !self::getCurrentUser()->isActive()) { + return false; + } $expression = trim($expression); if (empty($expression)) { diff --git a/WebFiori/Http/UserInterface.php b/WebFiori/Http/UserInterface.php new file mode 100644 index 0000000..982468c --- /dev/null +++ b/WebFiori/Http/UserInterface.php @@ -0,0 +1,37 @@ +assertFalse(SecurityContext::isAuthenticated()); // Set user and roles - SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John']); - SecurityContext::setRoles(['ADMIN', 'USER']); - SecurityContext::setAuthorities(['USER_CREATE', 'USER_READ']); + SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN', 'USER'], ['USER_CREATE', 'USER_READ'])); $this->assertTrue(SecurityContext::isAuthenticated()); $this->assertTrue(SecurityContext::hasRole('ADMIN')); @@ -44,7 +43,7 @@ public function testMethodLevelAuthorization() { $this->assertFalse($service->checkMethodAuthorization()); // Test private method with auth - SecurityContext::setCurrentUser(['id' => 1, 'name' => 'John']); + SecurityContext::setCurrentUser(new TestUser(1)); $this->assertTrue($service->checkMethodAuthorization()); // Test admin method without admin role @@ -66,25 +65,20 @@ public function testMethodLevelAuthorization() { } public function testSecurityExpressions() { - $service = new SecureService(); - $reflection = new \ReflectionClass($service); - $method = $reflection->getMethod('evaluateSecurityExpression'); - $method->setAccessible(true); + SecurityContext::clear(); // Test without authentication - $this->assertFalse($method->invoke($service, "hasRole('ADMIN')")); - $this->assertFalse($method->invoke($service, 'isAuthenticated()')); - $this->assertTrue($method->invoke($service, 'permitAll()')); + $this->assertFalse(SecurityContext::evaluateExpression("hasRole('ADMIN')")); + $this->assertFalse(SecurityContext::evaluateExpression('isAuthenticated()')); + $this->assertTrue(SecurityContext::evaluateExpression('permitAll()')); // Test with authentication and roles - SecurityContext::setCurrentUser(['id' => 1]); - SecurityContext::setRoles(['ADMIN']); - SecurityContext::setAuthorities(['USER_CREATE']); + SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN'], ['USER_CREATE'])); - $this->assertTrue($method->invoke($service, "hasRole('ADMIN')")); - $this->assertFalse($method->invoke($service, "hasRole('GUEST')")); - $this->assertTrue($method->invoke($service, "hasAuthority('USER_CREATE')")); - $this->assertTrue($method->invoke($service, 'isAuthenticated()')); + $this->assertTrue(SecurityContext::evaluateExpression("hasRole('ADMIN')")); + $this->assertFalse(SecurityContext::evaluateExpression("hasRole('GUEST')")); + $this->assertTrue(SecurityContext::evaluateExpression("hasAuthority('USER_CREATE')")); + $this->assertTrue(SecurityContext::evaluateExpression('isAuthenticated()')); } protected function tearDown(): void { diff --git a/tests/WebFiori/Tests/Http/ResponseBodyTest.php b/tests/WebFiori/Tests/Http/ResponseBodyTest.php index 20a4831..f918d3c 100644 --- a/tests/WebFiori/Tests/Http/ResponseBodyTest.php +++ b/tests/WebFiori/Tests/Http/ResponseBodyTest.php @@ -3,6 +3,7 @@ use PHPUnit\Framework\TestCase; use WebFiori\Http\SecurityContext; +use WebFiori\Tests\Http\TestUser; use WebFiori\Tests\Http\TestServices\ResponseBodyTestService; use WebFiori\Tests\Http\TestServices\MixedResponseService; use WebFiori\Tests\Http\TestServices\LegacyService; @@ -95,8 +96,7 @@ public function testMixedServiceWithAuthentication() { $this->assertFalse($service->hasResponseBodyAnnotation('traditionalMethod')); // Test with authentication - SecurityContext::setCurrentUser(['id' => 1]); - SecurityContext::setRoles(['USER']); + SecurityContext::setCurrentUser(new TestUser(1, ['USER'])); // The service should be authorized since we set up proper authentication $this->assertTrue($service->checkMethodAuthorization()); diff --git a/tests/WebFiori/Tests/Http/RestControllerTest.php b/tests/WebFiori/Tests/Http/RestControllerTest.php index 13e6487..9ffcc7a 100644 --- a/tests/WebFiori/Tests/Http/RestControllerTest.php +++ b/tests/WebFiori/Tests/Http/RestControllerTest.php @@ -108,7 +108,7 @@ public function testAnnotatedDelete() { $manager = new WebServicesManager(); $service = new AnnotatedService(); $manager->addService($service); - + //Missing param $this->assertEquals('{'.self::NL . ' "message":"The following required parameter(s) where missing from the request body: \'id\'.",'.self::NL . ' "type":"error",'.self::NL @@ -119,7 +119,7 @@ public function testAnnotatedDelete() { . ' ]'.self::NL . ' }'.self::NL . '}', $this->deleteRequest($manager, 'annotated-service')); - + //No auth user $this->assertEquals('{'.self::NL . ' "message":"Not Authorized.",'.self::NL . ' "type":"error",'.self::NL @@ -127,30 +127,37 @@ public function testAnnotatedDelete() { . '}', $this->deleteRequest($manager, 'annotated-service', [ 'id' => 1 ])); - - SecurityContext::setCurrentUser(['id' => 1, 'name' => 'Ibrahim']); + //User with no roles $this->assertEquals('{'.self::NL . ' "message":"Not Authorized.",'.self::NL . ' "type":"error",'.self::NL . ' "http-code":401'.self::NL . '}', $this->deleteRequest($manager, 'annotated-service', [ 'id' => 1 - ])); - SecurityContext::setRoles(['ADMIN']); + ], [], new TestUser(1, [''], [''], true))); + //user with no authorites $this->assertEquals('{'.self::NL . ' "message":"Not Authorized.",'.self::NL . ' "type":"error",'.self::NL . ' "http-code":401'.self::NL . '}', $this->deleteRequest($manager, 'annotated-service', [ 'id' => 1 - ])); - SecurityContext::setAuthorities(['USER_DELETE']); + ], [], new TestUser(1, ['ADMIN'], [''], true))); + // Inactive user + $this->assertEquals('{'.self::NL + . ' "message":"Not Authorized.",'.self::NL + . ' "type":"error",'.self::NL + . ' "http-code":401'.self::NL + . '}', $this->deleteRequest($manager, 'annotated-service', [ + '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', [ 'id' => 1 - ])); + ], [], new TestUser(1, ['ADMIN'], ['USER_DELETE'], true))); } } diff --git a/tests/WebFiori/Tests/Http/TestUser.php b/tests/WebFiori/Tests/Http/TestUser.php new file mode 100644 index 0000000..51190e4 --- /dev/null +++ b/tests/WebFiori/Tests/Http/TestUser.php @@ -0,0 +1,37 @@ +id = $id; + $this->roles = $roles; + $this->authorities = $authorities; + $this->active = $active; + } + + public function getId(): int|string { + return $this->id; + } + + public function getRoles(): array { + return $this->roles; + } + + public function getAuthorities(): array { + return $this->authorities; + } + + public function isActive(): bool { + return $this->active; + } +} From 1479aa4acdf39808ae8dcb66b87a9c8d6e6bcf3a Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:28:55 +0300 Subject: [PATCH 39/50] Update AuthenticationAnnotationTest.php --- tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php index 8afa7ba..9345a3f 100644 --- a/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php +++ b/tests/WebFiori/Tests/Http/AuthenticationAnnotationTest.php @@ -23,7 +23,7 @@ public function testSecurityContextAuthentication() { $this->assertFalse(SecurityContext::isAuthenticated()); // Set user and roles - SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN', 'USER'], ['USER_CREATE', 'USER_READ'])); + SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN', 'USER'], ['USER_CREATE', 'USER_READ'], true)); $this->assertTrue(SecurityContext::isAuthenticated()); $this->assertTrue(SecurityContext::hasRole('ADMIN')); @@ -43,7 +43,7 @@ public function testMethodLevelAuthorization() { $this->assertFalse($service->checkMethodAuthorization()); // Test private method with auth - SecurityContext::setCurrentUser(new TestUser(1)); + SecurityContext::setCurrentUser(new TestUser(1, [], [], true)); $this->assertTrue($service->checkMethodAuthorization()); // Test admin method without admin role @@ -73,7 +73,7 @@ public function testSecurityExpressions() { $this->assertTrue(SecurityContext::evaluateExpression('permitAll()')); // Test with authentication and roles - SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN'], ['USER_CREATE'])); + SecurityContext::setCurrentUser(new TestUser(1, ['ADMIN'], ['USER_CREATE'], true)); $this->assertTrue(SecurityContext::evaluateExpression("hasRole('ADMIN')")); $this->assertFalse(SecurityContext::evaluateExpression("hasRole('GUEST')")); From ab62a75d4afe1c7222cadb0a5ea46275b2b1a1fb Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:29:06 +0300 Subject: [PATCH 40/50] Update FullAnnotationIntegrationTest.php --- tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php b/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php index 471f1fc..3b85632 100644 --- a/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php +++ b/tests/WebFiori/Tests/Http/FullAnnotationIntegrationTest.php @@ -77,7 +77,7 @@ public function processRequest() {} $this->assertContains(\WebFiori\Http\RequestMethod::POST, $methods); // From annotation $this->assertContains(\WebFiori\Http\RequestMethod::PATCH, $methods); // Manual addition - $this->assertNotNull($service->getParameterByName('annotated_param')); // From annotation + $this->assertNotNull($service->getParameterByName('annotated_param', 'POST')); // From annotation $this->assertNotNull($service->getParameterByName('manual_param')); // Manual addition } } From 9edb176ff2e39774dd8f4cbf04a6113a1fe45ab1 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:29:24 +0300 Subject: [PATCH 41/50] Update HttpCookieTest.php --- tests/WebFiori/Tests/Http/HttpCookieTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/WebFiori/Tests/Http/HttpCookieTest.php b/tests/WebFiori/Tests/Http/HttpCookieTest.php index 4e8402a..6304a03 100644 --- a/tests/WebFiori/Tests/Http/HttpCookieTest.php +++ b/tests/WebFiori/Tests/Http/HttpCookieTest.php @@ -96,7 +96,8 @@ public function testRemainingTime00() { $cookie->setExpires(1); $this->assertEquals(60, $cookie->getRemainingTime()); sleep(3); - $this->assertEquals(57, $cookie->getRemainingTime()); + $this->assertGreaterThanOrEqual(55, $cookie->getRemainingTime()); + $this->assertLessThanOrEqual(57, $cookie->getRemainingTime()); } /** * @test From f5d478e03c2c0acf50d307761dded4cf20dd617f Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:29:50 +0300 Subject: [PATCH 42/50] Update ParameterMappingTest.php --- tests/WebFiori/Tests/Http/ParameterMappingTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/WebFiori/Tests/Http/ParameterMappingTest.php b/tests/WebFiori/Tests/Http/ParameterMappingTest.php index af37c59..860cb01 100644 --- a/tests/WebFiori/Tests/Http/ParameterMappingTest.php +++ b/tests/WebFiori/Tests/Http/ParameterMappingTest.php @@ -9,6 +9,11 @@ class ParameterMappingTest extends TestCase { public function testParametersFromAnnotations() { $service = new ParameterMappedService(); + + // Trigger parameter configuration for both HTTP methods + $service->getParameterByName('id', 'GET'); + $service->getParameterByName('email', 'POST'); + $parameters = $service->getParameters(); $this->assertCount(4, $parameters); // id, name, email, age From 9e37b45e42440088a99b67410fb0bba542c28440 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:30:01 +0300 Subject: [PATCH 43/50] Update MulNubmersService.php --- tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php index cf8afaf..0386417 100644 --- a/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php +++ b/tests/WebFiori/Tests/Http/TestServices/MulNubmersService.php @@ -27,6 +27,7 @@ public function isAuthorizedGET() { if ($this->getParamVal('first-number') < 0) { return false; } + return true; } public function processGet() { From c996ffb79f62297cd66059e1718ba39800fedcb6 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:30:41 +0300 Subject: [PATCH 44/50] Update WebService.php --- WebFiori/Http/WebService.php | 73 +++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/WebFiori/Http/WebService.php b/WebFiori/Http/WebService.php index 41f1281..19fcdd9 100644 --- a/WebFiori/Http/WebService.php +++ b/WebFiori/Http/WebService.php @@ -245,6 +245,51 @@ public function configureParametersForMethod(string $methodName): void { // Method doesn't exist, ignore } } + + /** + * 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); + } + } + } + + /** + * 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); + + 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); + } + } + } /** * Configure authentication from annotations. @@ -310,6 +355,24 @@ public function checkMethodAuthorization(): bool { return $this->isAuthorized(); } + /** + * 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(\WebFiori\Http\Annotations\AllowAnonymous::class)) || + !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\RequiresAuth::class)) || + !empty($reflectionMethod->getAttributes(\WebFiori\Http\Annotations\PreAuthorize::class)); + } + /** * Get the current processing method name (to be overridden by subclasses if needed). */ @@ -796,7 +859,15 @@ public function getObject(string $clazz, array $settersMap = []) { * a parameter with the given name was found. null if nothing is found. * */ - public final function getParameterByName(string $paramName) { + 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) { From 218e5fd84e69df58db7440825ff81e3c43e06fc5 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:31:02 +0300 Subject: [PATCH 45/50] Update WebServicesManager.php --- WebFiori/Http/WebServicesManager.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/WebFiori/Http/WebServicesManager.php b/WebFiori/Http/WebServicesManager.php index 93485d8..1fe6c5f 100644 --- a/WebFiori/Http/WebServicesManager.php +++ b/WebFiori/Http/WebServicesManager.php @@ -1047,11 +1047,13 @@ private function isAuth(WebService $service) { $isAuth = false; if ($service->isAuthRequired()) { - // Check method-level authorization first (handles AllowAnonymous, etc.) - $isAuth = $service->checkMethodAuthorization(); - if ($isAuth) { - return true; + // 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)) { From 5a2ad486294db0151c1296b743de6364b7fbee97 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Thu, 25 Dec 2025 00:31:24 +0300 Subject: [PATCH 46/50] Update SecurityContext.php --- WebFiori/Http/SecurityContext.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/WebFiori/Http/SecurityContext.php b/WebFiori/Http/SecurityContext.php index 3be0d5a..1cb8165 100644 --- a/WebFiori/Http/SecurityContext.php +++ b/WebFiori/Http/SecurityContext.php @@ -148,10 +148,6 @@ public static function clear(): void { * @throws \InvalidArgumentException If expression is invalid */ public static function evaluateExpression(string $expression): bool { - - if (self::getCurrentUser() === null || !self::getCurrentUser()->isActive()) { - return false; - } $expression = trim($expression); if (empty($expression)) { From bd4119013f203ca584fcd6e6602c22fe53bab9c6 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Mon, 29 Dec 2025 16:58:36 +0300 Subject: [PATCH 47/50] fix: Rename Variable to Remove Conflect --- WebFiori/Http/APITestCase.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/WebFiori/Http/APITestCase.php b/WebFiori/Http/APITestCase.php index 06cb908..e64ffc6 100644 --- a/WebFiori/Http/APITestCase.php +++ b/WebFiori/Http/APITestCase.php @@ -35,11 +35,11 @@ class APITestCase extends TestCase { * * @var array */ - private $backupGlobals; + private $globalsBackup; protected function setUp(): void { parent::setUp(); - $this->backupGlobals = [ + $this->globalsBackup = [ 'GET' => $_GET, 'POST' => $_POST, 'FILES' => $_FILES, @@ -48,10 +48,10 @@ protected function setUp(): void { } protected function tearDown(): void { - $_GET = $this->backupGlobals['GET']; - $_POST = $this->backupGlobals['POST']; - $_FILES = $this->backupGlobals['FILES']; - $_SERVER = $this->backupGlobals['SERVER']; + $_GET = $this->globalsBackup['GET']; + $_POST = $this->globalsBackup['POST']; + $_FILES = $this->globalsBackup['FILES']; + $_SERVER = $this->globalsBackup['SERVER']; SecurityContext::clear(); parent::tearDown(); } @@ -99,7 +99,7 @@ public function addFile(string $fileIdx, string $filePath, bool $reset = false) $_FILES[$fileIdx]['error'] = []; } $info = $this->extractPathAndName($filePath); - $path = $info['path'].DS.$info['name']; + $path = $info['path'].DIRECTORY_SEPARATOR.$info['name']; $_FILES[$fileIdx]['name'][] = $info['name']; $_FILES[$fileIdx]['type'][] = mime_content_type($path); From 68e54b5f7b9160b843bcf594f9ca6c6f6afd9421 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Mon, 29 Dec 2025 17:39:48 +0300 Subject: [PATCH 48/50] fix: Use of Null as Standalone Type --- .../Tests/Http/TestServices/ResponseBodyTestService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php index 8ec2580..31053ef 100644 --- a/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php +++ b/tests/WebFiori/Tests/Http/TestServices/ResponseBodyTestService.php @@ -28,7 +28,7 @@ public function getStringData(): string { // Test 3: Return null (should be empty response) #[PostMapping] #[ResponseBody(status: 204)] - public function deleteData(): null { + public function deleteData(): ?object { // Simulate deletion return null; } From e83112ccc48a3ba2be521bf90119bba2b5d245f4 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Mon, 29 Dec 2025 17:52:21 +0300 Subject: [PATCH 49/50] fix: PHPUnit 9 Missing Method --- tests/WebFiori/Tests/Http/ResponseBodyTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/WebFiori/Tests/Http/ResponseBodyTest.php b/tests/WebFiori/Tests/Http/ResponseBodyTest.php index f918d3c..53cb961 100644 --- a/tests/WebFiori/Tests/Http/ResponseBodyTest.php +++ b/tests/WebFiori/Tests/Http/ResponseBodyTest.php @@ -65,7 +65,7 @@ public function testObjectReturnValue() { $result = $service->getObjectData(); $this->assertIsObject($result); - $this->assertObjectHasProperty('message', $result); + $this->assertTrue(property_exists($result, 'message', "The object should have the attribute 'message'.")); } public function testMethodWithoutResponseBody() { From 30da6a9862e8a639480a78ad09bb57947a430721 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Mon, 29 Dec 2025 18:05:56 +0300 Subject: [PATCH 50/50] feat: Detection of Duplicated Mappings --- .../Exceptions/DuplicateMappingException.php | 14 +++ WebFiori/Http/WebService.php | 25 +++- .../WebFiori/Tests/Http/ResponseBodyTest.php | 2 +- tests/WebFiori/Tests/Http/WebServiceTest.php | 110 ++++++++++++++++++ 4 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 WebFiori/Http/Exceptions/DuplicateMappingException.php diff --git a/WebFiori/Http/Exceptions/DuplicateMappingException.php b/WebFiori/Http/Exceptions/DuplicateMappingException.php new file mode 100644 index 0000000..da2209d --- /dev/null +++ b/WebFiori/Http/Exceptions/DuplicateMappingException.php @@ -0,0 +1,14 @@ +getMethods() as $method) { $methodMappings = [ @@ -487,14 +487,29 @@ private function configureMethodMappings(): void { foreach ($methodMappings as $annotationClass => $httpMethod) { $attributes = $method->getAttributes($annotationClass); if (!empty($attributes)) { - $methods[] = $httpMethod; - // Don't configure parameters here - do it dynamically per request + if (!isset($httpMethodToMethods[$httpMethod])) { + $httpMethodToMethods[$httpMethod] = []; + } + $httpMethodToMethods[$httpMethod][] = $method->getName(); } } } - if (!empty($methods)) { - $this->setRequestMethods(array_unique($methods)); + // 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)); } } diff --git a/tests/WebFiori/Tests/Http/ResponseBodyTest.php b/tests/WebFiori/Tests/Http/ResponseBodyTest.php index 53cb961..23734cd 100644 --- a/tests/WebFiori/Tests/Http/ResponseBodyTest.php +++ b/tests/WebFiori/Tests/Http/ResponseBodyTest.php @@ -65,7 +65,7 @@ public function testObjectReturnValue() { $result = $service->getObjectData(); $this->assertIsObject($result); - $this->assertTrue(property_exists($result, 'message', "The object should have the attribute 'message'.")); + $this->assertTrue(property_exists($result, 'message'), "The object should have the attribute 'message'."); } public function testMethodWithoutResponseBody() { diff --git a/tests/WebFiori/Tests/Http/WebServiceTest.php b/tests/WebFiori/Tests/Http/WebServiceTest.php index 78e4651..535f905 100644 --- a/tests/WebFiori/Tests/Http/WebServiceTest.php +++ b/tests/WebFiori/Tests/Http/WebServiceTest.php @@ -303,4 +303,114 @@ public function testToString01() { $this->assertEquals('{"post":{"responses":{"201":{"description":"User created successfully"}}},' .'"put":{"responses":{"200":{"description":"User updated successfully"}}}}',$action.''); } + /** + * @test + */ + public function testDuplicateGetMappingThrowsException() { + $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class); + $this->expectExceptionMessage('HTTP method GET is mapped to multiple methods: getUsers, getUsersAgain'); + + new class extends \WebFiori\Http\WebService { + #[\WebFiori\Http\Annotations\GetMapping] + public function getUsers() {} + + #[\WebFiori\Http\Annotations\GetMapping] + public function getUsersAgain() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + } + + /** + * @test + */ + public function testDuplicatePostMappingThrowsException() { + $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class); + $this->expectExceptionMessage('HTTP method POST is mapped to multiple methods: createUser, addUser'); + + new class extends \WebFiori\Http\WebService { + #[\WebFiori\Http\Annotations\PostMapping] + public function createUser() {} + + #[\WebFiori\Http\Annotations\PostMapping] + public function addUser() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + } + + /** + * @test + */ + public function testDuplicateMixedMappingsThrowsException() { + $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class); + $this->expectExceptionMessage('HTTP method PUT is mapped to multiple methods: updateUser, modifyUser'); + + new class extends \WebFiori\Http\WebService { + #[\WebFiori\Http\Annotations\GetMapping] + public function getUser() {} + + #[\WebFiori\Http\Annotations\PutMapping] + public function updateUser() {} + + #[\WebFiori\Http\Annotations\PutMapping] + public function modifyUser() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + } + + /** + * @test + */ + public function testMultipleDuplicateGetMappingThrowsException() { + $this->expectException(\WebFiori\Http\Exceptions\DuplicateMappingException::class); + $this->expectExceptionMessage('HTTP method GET is mapped to multiple methods: getUsers, getUsersAgain, fetchUsers'); + + new class extends \WebFiori\Http\WebService { + #[\WebFiori\Http\Annotations\GetMapping] + public function getUsers() {} + + #[\WebFiori\Http\Annotations\GetMapping] + public function getUsersAgain() {} + + #[\WebFiori\Http\Annotations\GetMapping] + public function fetchUsers() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + } + + /** + * @test + */ + public function testNoDuplicateMappingsDoesNotThrowException() { + $service = new class extends \WebFiori\Http\WebService { + #[\WebFiori\Http\Annotations\GetMapping] + public function getUser() {} + + #[\WebFiori\Http\Annotations\PostMapping] + public function createUser() {} + + #[\WebFiori\Http\Annotations\PutMapping] + public function updateUser() {} + + #[\WebFiori\Http\Annotations\DeleteMapping] + public function deleteUser() {} + + public function isAuthorized(): bool { return true; } + public function processRequest() {} + }; + + $methods = $service->getRequestMethods(); + $this->assertContains('GET', $methods); + $this->assertContains('POST', $methods); + $this->assertContains('PUT', $methods); + $this->assertContains('DELETE', $methods); + $this->assertCount(4, $methods); + } }