From 0c8104fbc5e3bd85ea204d9dd8c5429a796b621e Mon Sep 17 00:00:00 2001 From: SuperSonic Date: Sat, 17 Jul 2021 02:36:48 +0000 Subject: [PATCH 1/5] Rename as Abigail --- .travis.yml | 12 ++++++------ LICENSE | 1 + README.md | 6 ++---- composer.json | 19 ++++++++++++------- phpunit.xml | 2 +- {RestService => src}/Client.php | 6 +++--- {RestService => src}/InternalClient.php | 2 +- {RestService => src}/Server.php | 10 +++++----- {Test => tests}/Basic/BasicTest.php | 0 {Test => tests}/Controller/MyRoutes.php | 0 {Test => tests}/Synthetic/CollectTest.php | 0 .../Synthetic/CustomRoutesTest.php | 0 {Test => tests}/Synthetic/RouteTest.php | 0 {Test => tests}/bootstrap.php | 0 14 files changed, 31 insertions(+), 27 deletions(-) rename {RestService => src}/Client.php (99%) rename {RestService => src}/InternalClient.php (95%) rename {RestService => src}/Server.php (99%) rename {Test => tests}/Basic/BasicTest.php (100%) rename {Test => tests}/Controller/MyRoutes.php (100%) rename {Test => tests}/Synthetic/CollectTest.php (100%) rename {Test => tests}/Synthetic/CustomRoutesTest.php (100%) rename {Test => tests}/Synthetic/RouteTest.php (100%) rename {Test => tests}/bootstrap.php (100%) diff --git a/.travis.yml b/.travis.yml index ce9f3d3..ffd7974 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,19 @@ language: php php: - - 5.4 - - 5.5 - - 5.6 + - 8.0 + - 7.4 matrix: include: - - php: 5.3 + - php: 7.4 dist: precise before_script: - # Composer + - apt update + - apt install -y git wget unzip - wget http://getcomposer.org/composer.phar - php composer.phar install script: - - phpunit; + - php ./vendor/bin/phpunit diff --git a/LICENSE b/LICENSE index c53b102..2530c31 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ +Copyright (c) 2021 Star Inc. (https://starinc.xyz) Copyright (c) MArc J. Schmidt Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 0375bdf..4553113 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -php-rest-service +Abigail ============== -Php-Rest-Service is a simple and fast PHP class for RESTful JSON APIs. - -[![Build Status](https://travis-ci.org/marcj/php-rest-service.png)](https://travis-ci.org/marcj/php-rest-service) +Abigail is the fork from marcj/php-rest-service, which is a simple and fast PHP class for server side RESTful APIs. Features -------- diff --git a/composer.json b/composer.json index aa90464..f029541 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,13 @@ { - "name": "marcj/php-rest-service", - "description": "PHPRestService is a simple and fast PHP class for server side RESTful APIs.", - "keywords": ["REST", "RESTful", "php", "API"], + "name": "starinc/abigail", + "description": "Abigail is the fork from marcj/php-rest-service, which is a simple and fast PHP class for server side RESTful APIs.", + "keywords": ["REST", "RESTful", "PHP", "API"], "license": "MIT", "authors": [ + { + "name": "SuperSonic", + "email": "supersonic@livemail.tw" + }, { "name": "Marc J. Schmidt", "email": "marc@kryn.org", @@ -11,13 +15,14 @@ } ], "require": { - "php": ">=5.3.0" + "php": ">=7.4.0" }, "autoload": { - "psr-0": { - "RestService": "" - } + "psr-4": { + "Abigail\\": "core/" + } }, + "require-dev": { "phpunit/phpunit": "^4" } diff --git a/phpunit.xml b/phpunit.xml index 448d001..91ce382 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,7 +6,7 @@ - ./Test/ + ./tests/ diff --git a/RestService/Client.php b/src/Client.php similarity index 99% rename from RestService/Client.php rename to src/Client.php index feaa63a..7343da8 100644 --- a/RestService/Client.php +++ b/src/Client.php @@ -1,6 +1,6 @@ controller = new $pClassName($this); } - if (get_parent_class($this->controller) == '\RestService\Server') { + if (get_parent_class($this->controller) == '\Abigail\Server') { $this->controller->setClient($this->getClient()); } } catch (\Exception $e) { diff --git a/Test/Basic/BasicTest.php b/tests/Basic/BasicTest.php similarity index 100% rename from Test/Basic/BasicTest.php rename to tests/Basic/BasicTest.php diff --git a/Test/Controller/MyRoutes.php b/tests/Controller/MyRoutes.php similarity index 100% rename from Test/Controller/MyRoutes.php rename to tests/Controller/MyRoutes.php diff --git a/Test/Synthetic/CollectTest.php b/tests/Synthetic/CollectTest.php similarity index 100% rename from Test/Synthetic/CollectTest.php rename to tests/Synthetic/CollectTest.php diff --git a/Test/Synthetic/CustomRoutesTest.php b/tests/Synthetic/CustomRoutesTest.php similarity index 100% rename from Test/Synthetic/CustomRoutesTest.php rename to tests/Synthetic/CustomRoutesTest.php diff --git a/Test/Synthetic/RouteTest.php b/tests/Synthetic/RouteTest.php similarity index 100% rename from Test/Synthetic/RouteTest.php rename to tests/Synthetic/RouteTest.php diff --git a/Test/bootstrap.php b/tests/bootstrap.php similarity index 100% rename from Test/bootstrap.php rename to tests/bootstrap.php From f7685877a64fd79260f7760d89f34e9168d456b0 Mon Sep 17 00:00:00 2001 From: SuperSonic Date: Sat, 17 Jul 2021 10:54:19 +0800 Subject: [PATCH 2/5] Update --- .gitignore | 2 + composer.json | 56 +++--- src/Client.php | 109 +++++----- src/InternalClient.php | 5 +- src/Server.php | 440 +++++++++++++++++++++++++---------------- 5 files changed, 357 insertions(+), 255 deletions(-) diff --git a/.gitignore b/.gitignore index 48b8bf9..f7015c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ vendor/ +.idea/ +composer.lock diff --git a/composer.json b/composer.json index f029541..82fb26c 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,35 @@ { - "name": "starinc/abigail", - "description": "Abigail is the fork from marcj/php-rest-service, which is a simple and fast PHP class for server side RESTful APIs.", - "keywords": ["REST", "RESTful", "PHP", "API"], - "license": "MIT", - "authors": [ - { - "name": "SuperSonic", - "email": "supersonic@livemail.tw" - }, - { - "name": "Marc J. Schmidt", - "email": "marc@kryn.org", - "homepage": "http://marcjschmidt.de" - } - ], - "require": { - "php": ">=7.4.0" + "name": "starinc/abigail", + "description": "Abigail is the fork from marcj/php-rest-service, which is a simple and fast PHP class for server side RESTful APIs.", + "keywords": [ + "REST", + "RESTful", + "PHP", + "API" + ], + "license": "MIT", + "authors": [ + { + "name": "SuperSonic", + "email": "supersonic@livemail.tw" }, - "autoload": { - "psr-4": { - "Abigail\\": "core/" - } - }, - - "require-dev": { - "phpunit/phpunit": "^4" + { + "name": "Marc J. Schmidt", + "email": "marc@kryn.org", + "homepage": "http://marcjschmidt.de" + } + ], + "require": { + "php": ">=7.4.0", + "ext-json": "*", + "ext-ctype": "*" + }, + "autoload": { + "psr-4": { + "Abigail\\": "src/" } + }, + "require-dev": { + "phpunit/phpunit": "^4" + } } diff --git a/src/Client.php b/src/Client.php index 7343da8..0a32096 100644 --- a/src/Client.php +++ b/src/Client.php @@ -9,14 +9,14 @@ class Client * * @var string */ - private $outputFormat = 'json'; + private string $outputFormat = 'json'; /** * List of possible output formats. * * @var array */ - private $outputFormats = array( + private array $outputFormats = array( 'json' => 'asJSON', 'xml' => 'asXML' ); @@ -25,30 +25,30 @@ class Client * List of possible methods. * @var array */ - public $methods = array('get', 'post', 'put', 'delete', 'head', 'options', 'patch'); + public array $methods = array('get', 'post', 'put', 'delete', 'head', 'options', 'patch'); /** * Current URL. * * @var string */ - private $url; + private string $url = ""; /** * @var Server * */ - private $controller; + private Server $controller; /** * Custom set http method. * * @var string */ - private $method; + private string $method = ""; - private static $statusCodes = array( + private static array $statusCodes = array( 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', @@ -95,7 +95,7 @@ class Client /** * @param Server $pServerController */ - public function __construct($pServerController) + public function __construct(Server $pServerController) { $this->controller = $pServerController; if (isset($_SERVER['PATH_INFO'])) @@ -105,17 +105,17 @@ public function __construct($pServerController) } /** - * @param \Abigail\Server $controller + * @param Server $controller */ - public function setController($controller) + public function setController(Server $controller) { $this->controller = $controller; } /** - * @return \Abigail\Server + * @return Server */ - public function getController() + public function getController(): Server { return $this->controller; } @@ -125,15 +125,15 @@ public function getController() * * @param string $pHttpCode * @param $pMessage + * @return mixed */ - public function sendResponse($pHttpCode = '200', $pMessage) + public function sendResponse(string $pHttpCode, $pMessage) { - $suppressStatusCode = isset($_GET['_suppress_status_code']) ? $_GET['_suppress_status_code'] : false; + $suppressStatusCode = $_GET['_suppress_status_code'] ?? false; if ($this->controller->getHttpStatusCodes() && !$suppressStatusCode && php_sapi_name() !== 'cli' ) { - $status = self::$statusCodes[intval($pHttpCode)]; header('HTTP/1.0 ' . ($status ? $pHttpCode . ' ' . $status : $pHttpCode), true, $pHttpCode); } elseif (php_sapi_name() !== 'cli') { @@ -145,15 +145,20 @@ public function sendResponse($pHttpCode = '200', $pMessage) $pMessage = array_reverse($pMessage, true); $method = $this->getOutputFormatMethod($this->getOutputFormat()); - echo $this->$method($pMessage); - exit; + + if (php_sapi_name() !== 'cli') { + echo $this->$method($pMessage); + exit; + } else { + return $this->$method($pMessage); + } } /** - * @param string $pFormat + * @param string $pFormat * @return string */ - public function getOutputFormatMethod($pFormat) + public function getOutputFormatMethod(string $pFormat): string { return $this->outputFormats[$pFormat]; } @@ -161,7 +166,7 @@ public function getOutputFormatMethod($pFormat) /** * @return string */ - public function getOutputFormat() + public function getOutputFormat(): string { return $this->outputFormat; } @@ -171,7 +176,7 @@ public function getOutputFormat() * * @return string */ - public function getMethod() + public function getMethod(): string { if ($this->method) { return $this->method; @@ -199,10 +204,10 @@ public function getMethod() * Sets a custom http method. It does then not check against * SERVER['REQUEST_METHOD'], $_GET['_method'] etc anymore. * - * @param string $pMethod + * @param string $pMethod * @return Client */ - public function setMethod($pMethod) + public function setMethod(string $pMethod): Client { $this->method = $pMethod; @@ -226,7 +231,7 @@ public function setContentLength($pMessage) * @param $pMessage * @return string */ - public function asJSON($pMessage) + public function asJSON($pMessage): string { if (php_sapi_name() !== 'cli') header('Content-Type: application/json; charset=utf-8'); @@ -242,11 +247,11 @@ public function asJSON($pMessage) * * Original at http://recursive-design.com/blog/2008/03/11/format-json-with-php/ * - * @param string $json The original JSON string to process. + * @param array $json The original JSON string to process. * * @return string Indented version of the original JSON string. */ - public function jsonFormat($json) + public function jsonFormat(array $json): string { if (!is_string($json)) $json = json_encode($json); @@ -260,15 +265,15 @@ public function jsonFormat($json) for ($i = 0; $i <= $strLen; $i++) { -// Grab the next character in the string. + // Grab the next character in the string. $char = substr($json, $i, 1); -// Are we inside a quoted string? + // Are we inside a quoted string? if ($char == '"' && !$inEscapeMode) { $outOfQuotes = !$outOfQuotes; -// If this character is the end of an element, -// output a new line and indent the next line. + // If this character is the end of an element, + // output a new line and indent the next line. } elseif (($char == '}' || $char == ']') && $outOfQuotes) { $result .= $newLine; $pos--; @@ -279,11 +284,11 @@ public function jsonFormat($json) $char .= ' '; } -// Add the character to the result string. + // Add the character to the result string. $result .= $char; -// If the last character was the beginning of an element, -// output a new line and indent the next line. + // If the last character was the beginning of an element, + // output a new line and indent the next line. if (($char == ',' || $char == '{' || $char == '[') && $outOfQuotes) { $result .= $newLine; if ($char == '{' || $char == '[') { @@ -310,7 +315,7 @@ public function jsonFormat($json) * @param $pMessage * @return string */ - public function asXML($pMessage) + public function asXML($pMessage): string { $xml = $this->toXml($pMessage); $xml = "\n\n$xml\n"; @@ -321,12 +326,12 @@ public function asXML($pMessage) } /** - * @param mixed $pData - * @param string $pParentTagName - * @param int $pDepth + * @param mixed $pData + * @param string $pParentTagName + * @param int $pDepth * @return string XML */ - public function toXml($pData, $pParentTagName = '', $pDepth = 1) + public function toXml(mixed $pData, $pParentTagName = '', $pDepth = 1): string { if (is_array($pData)) { $content = ''; @@ -349,11 +354,11 @@ public function toXml($pData, $pParentTagName = '', $pDepth = 1) /** * Add a additional output format. * - * @param string $pCode - * @param string $pMethod + * @param string $pCode + * @param string $pMethod * @return Client $this */ - public function addOutputFormat($pCode, $pMethod) + public function addOutputFormat(string $pCode, string $pMethod): Client { $this->outputFormats[$pCode] = $pMethod; @@ -363,10 +368,10 @@ public function addOutputFormat($pCode, $pMethod) /** * Set the current output format. * - * @param string $pFormat a key of $outputForms + * @param string $pFormat a key of $outputForms * @return Client */ - public function setFormat($pFormat) + public function setFormat(string $pFormat): Client { $this->outputFormat = $pFormat; @@ -378,7 +383,7 @@ public function setFormat($pFormat) * * @return string */ - public function getUrl() + public function getUrl(): string { return $this->url; } @@ -386,10 +391,10 @@ public function getUrl() /** * Set the url. * - * @param string $pUrl + * @param string $pUrl * @return Client $this */ - public function setUrl($pUrl) + public function setUrl(string $pUrl): Client { $this->url = $pUrl; @@ -401,19 +406,19 @@ public function setUrl($pUrl) * * @return Client */ - public function setupFormats() + public function setupFormats(): Client { -//through HTTP_ACCEPT - if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], '*/*') === false) { + // through HTTP_ACCEPT + if (isset($_SERVER['HTTP_ACCEPT']) && !str_contains($_SERVER['HTTP_ACCEPT'], '*/*')) { foreach ($this->outputFormats as $formatCode => $formatMethod) { - if (strpos($_SERVER['HTTP_ACCEPT'], $formatCode) !== false) { + if (str_contains($_SERVER['HTTP_ACCEPT'], $formatCode)) { $this->outputFormat = $formatCode; break; } } } -//through uri suffix + // through uri suffix if (preg_match('/\.(\w+)$/i', $this->getUrl(), $matches)) { if (isset($this->outputFormats[$matches[1]])) { $this->outputFormat = $matches[1]; @@ -422,7 +427,7 @@ public function setupFormats() } } -//through _format parametr + // through _format parameter if (isset($_GET['_format'])) { if (isset($this->outputFormats[$_GET['_format']])) { $this->outputFormat = $_GET['_format']; diff --git a/src/InternalClient.php b/src/InternalClient.php index 6c0e8a0..f06ba14 100644 --- a/src/InternalClient.php +++ b/src/InternalClient.php @@ -10,15 +10,14 @@ */ class InternalClient extends Client { - public function sendResponse($pHttpCode = '200', $pMessage) + public function sendResponse(string $pHttpCode, $pMessage) { $pMessage = array_reverse($pMessage, true); - $pMessage['status'] = $pHttpCode+0; + $pMessage['status'] = intval($pHttpCode); $pMessage = array_reverse($pMessage, true); $method = $this->getOutputFormatMethod($this->getOutputFormat()); return $this->$method($pMessage); } - } diff --git a/src/Server.php b/src/Server.php index 1afdccf..cd95703 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,9 +3,8 @@ namespace Abigail; /** - * \Abigail\Server - A REST server class for RESTful APIs. + * \RestService\Server - A REST server class for RESTful APIs. */ - class Server { /** @@ -18,66 +17,74 @@ class Server * * @var array */ - protected $routes = array(); + protected array $routes = array(); + + /** + * The data fetch from request body. + * + * @var array + */ + protected array $body_data = array(); /** * Blacklisted http get arguments. * * @var array */ - protected $blacklistedGetParameters = array('_method', '_suppress_status_code'); + protected array $blacklistedGetParameters = array('_method', '_suppress_status_code'); /** * Current URL that triggers the controller. * * @var string */ - protected $triggerUrl = ''; + protected string $triggerUrl = ''; /** * Contains the controller object. * - * @var string + * @var object */ - protected $controller = ''; + protected object $controller; /** * List of sub controllers. * * @var array */ - protected $controllers = array(); + protected array $controllers = array(); /** * Parent controller. * - * @var \Abigail\Server + * @var Server|null */ - protected $parentController; + protected Server|null $parentController = null; /** * The client * * @var Client */ - protected $client; + protected Client $client; /** * List of excluded methods. * * @var array|string array('methodOne', 'methodTwo') or * for all methods */ - protected $collectRoutesExclude = array('__construct'); + protected array|string $collectRoutesExclude = array('__construct'); /** * List of possible methods. * @var array */ - public $methods = array('get', 'post', 'put', 'delete', 'head', 'options', 'patch'); + public array $methods = array('get', 'post', 'put', 'delete', 'head', 'options', 'patch'); /** * Check access function/method. Will be fired after the route has been found. - * Arguments: (url, route) + * If it is forbidden, please throw an Exception to stop the processing. + * Arguments: (url, route, params) * * @var callable */ @@ -97,7 +104,7 @@ class Server * * @var boolean */ - protected $debugMode = false; + protected bool $debugMode = false; /** * Sets whether the service should serve route descriptions @@ -105,7 +112,7 @@ class Server * * @var boolean */ - protected $describeRoutes = true; + protected bool $describeRoutes = true; /** * If this controller can not find a route, @@ -113,7 +120,7 @@ class Server * * @var string */ - protected $fallbackMethod = ''; + protected string $fallbackMethod = ''; /** * If the lib should send HTTP status codes. @@ -122,21 +129,27 @@ class Server * * @var boolean */ - protected $withStatusCode = true; + protected bool $withStatusCode = true; /** * @var callable */ protected $controllerFactory; + /** + * @var callable + */ + protected $successResponseWrapper; + /** * Constructor * - * @param string $pTriggerUrl - * @param string|object $pControllerClass - * @param \Abigail\Server $pParentController + * @param string $pTriggerUrl + * @param null $pControllerClass + * @param null $pParentController + * @throws Exception */ - public function __construct($pTriggerUrl, $pControllerClass = null, $pParentController = null) + public function __construct(string $pTriggerUrl, $pControllerClass = null, $pParentController = null) { $this->normalizeUrl($pTriggerUrl); @@ -159,6 +172,9 @@ public function __construct($pTriggerUrl, $pControllerClass = null, $pParentCont if ($pParentController->getControllerFactory()) $this->setControllerFactory($pParentController->getControllerFactory()); + if ($pParentController->getSuccessResponseWrapper()) + $this->setSuccessResponseWrapper($pParentController->getSuccessResponseWrapper()); + $this->setHttpStatusCodes($pParentController->getHttpStatusCodes()); } else { @@ -177,7 +193,7 @@ public function __construct($pTriggerUrl, $pControllerClass = null, $pParentCont * * @return Server $this */ - public static function create($pTriggerUrl, $pControllerClass = '') + public static function create(string $pTriggerUrl, $pControllerClass = ''): Server { $clazz = get_called_class(); @@ -189,7 +205,7 @@ public static function create($pTriggerUrl, $pControllerClass = '') * * @return Server $this */ - public function setControllerFactory(callable $controllerFactory) + public function setControllerFactory(callable $controllerFactory): Server { $this->controllerFactory = $controllerFactory; @@ -197,9 +213,9 @@ public function setControllerFactory(callable $controllerFactory) } /** - * @return callable + * @return callable|null */ - public function getControllerFactory() + public function getControllerFactory(): callable|null { return $this->controllerFactory; } @@ -208,10 +224,10 @@ public function getControllerFactory() * If the lib should send HTTP status codes. * Some Client libs does not support it. * - * @param boolean $pWithStatusCode + * @param boolean $pWithStatusCode * @return Server $this */ - public function setHttpStatusCodes($pWithStatusCode) + public function setHttpStatusCodes(bool $pWithStatusCode): Server { $this->withStatusCode = $pWithStatusCode; @@ -222,7 +238,7 @@ public function setHttpStatusCodes($pWithStatusCode) * * @return boolean */ - public function getHttpStatusCodes() + public function getHttpStatusCodes(): bool { return $this->withStatusCode; } @@ -231,10 +247,10 @@ public function getHttpStatusCodes() * Set the check access function/method. * Will fired with arguments: (url, route) * - * @param callable $pFn + * @param callable $pFn * @return Server $this */ - public function setCheckAccess($pFn) + public function setCheckAccess(callable $pFn): Server { $this->checkAccessFn = $pFn; @@ -243,21 +259,44 @@ public function setCheckAccess($pFn) /** * Getter for checkAccess - * @return callable + * @return callable|null */ - public function getCheckAccess() + public function getCheckAccess(): callable|null { return $this->checkAccessFn; } + /** + * The wrapper for response while it is successful. + * Will fired with arguments: (pData) + * + * @param callable $pFn + * @return Server $this + */ + public function setSuccessResponseWrapper(callable $pFn): Server + { + $this->successResponseWrapper = $pFn; + + return $this; + } + + /** + * Getter for successResponseWrapper + * @return callable|null + */ + public function getSuccessResponseWrapper(): callable|null + { + return $this->successResponseWrapper; + } + /** * If this controller can not find a route, * we fire this method and send the result. * - * @param string $pFn Methodname of current attached class + * @param string $pFn Method name of current attached class * @return Server $this */ - public function setFallbackMethod($pFn) + public function setFallbackMethod(string $pFn): Server { $this->fallbackMethod = $pFn; @@ -268,7 +307,7 @@ public function setFallbackMethod($pFn) * Getter for fallbackMethod * @return string */ - public function fallbackMethod() + public function fallbackMethod(): string { return $this->fallbackMethod; } @@ -277,10 +316,10 @@ public function fallbackMethod() * Sets whether the service should serve route descriptions * through the OPTIONS method. * - * @param boolean $pDescribeRoutes + * @param boolean $pDescribeRoutes * @return Server $this */ - public function setDescribeRoutes($pDescribeRoutes) + public function setDescribeRoutes(bool $pDescribeRoutes): Server { $this->describeRoutes = $pDescribeRoutes; @@ -292,7 +331,7 @@ public function setDescribeRoutes($pDescribeRoutes) * * @return boolean */ - public function getDescribeRoutes() + public function getDescribeRoutes(): bool { return $this->describeRoutes; } @@ -302,10 +341,10 @@ public function getDescribeRoutes() * Please die/exit in your function then. * Arguments: (exception) * - * @param callable $pFn + * @param callable $pFn * @return Server $this */ - public function setExceptionHandler($pFn) + public function setExceptionHandler(callable $pFn): Server { $this->sendExceptionFn = $pFn; @@ -314,9 +353,9 @@ public function setExceptionHandler($pFn) /** * Getter for checkAccess - * @return callable + * @return callable|null */ - public function getExceptionHandler() + public function getExceptionHandler(): callable|null { return $this->sendExceptionFn; } @@ -324,10 +363,10 @@ public function getExceptionHandler() /** * If this is true, we send file, line and backtrace if an exception has been thrown. * - * @param boolean $pDebugMode + * @param boolean $pDebugMode * @return Server $this */ - public function setDebugMode($pDebugMode) + public function setDebugMode(bool $pDebugMode): Server { $this->debugMode = $pDebugMode; @@ -338,7 +377,7 @@ public function setDebugMode($pDebugMode) * Getter for checkAccess * @return boolean */ - public function getDebugMode() + public function getDebugMode(): bool { return $this->debugMode; } @@ -348,7 +387,7 @@ public function getDebugMode() * * @return Server */ - public function done() + public function done(): Server { return $this->getParentController(); } @@ -356,9 +395,9 @@ public function done() /** * Returns the parent controller * - * @return Server $this + * @return Server|null $this */ - public function getParentController() + public function getParentController(): Server|null { return $this->parentController; } @@ -369,7 +408,7 @@ public function getParentController() * @param $pTriggerUrl * @return Server */ - public function setTriggerUrl($pTriggerUrl) + public function setTriggerUrl($pTriggerUrl): Server { $this->triggerUrl = $pTriggerUrl; @@ -381,18 +420,49 @@ public function setTriggerUrl($pTriggerUrl) * * @return string */ - public function getTriggerUrl() + public function getTriggerUrl(): string { return $this->triggerUrl; } + /** + * Read data from request body. + * + * @throws Exception + */ + public function readDataFromBody() + { + $raw_content_type = $_SERVER["CONTENT_TYPE"] ?? "application/x-www-form-urlencoded"; + $raw_content_type_array = explode(";", $raw_content_type); + if (isset($raw_content_type_array[0])) { + $raw_content = file_get_contents("php://input"); + $this->body_data = match ($raw_content_type_array[0]) { + "application/json" => json_decode($raw_content, true) ?? [], + "application/x-www-form-urlencoded" => self::form_decode($raw_content) ?? [], + default => [] + }; + } else { + throw new Exception("No the header content-type configured."); + } + } + + /** + * @param $raw_content + * @return mixed + */ + private static function form_decode($raw_content) + { + parse_str($raw_content, $result); + return $result; + } + /** * Sets the client. * - * @param Client|string $pClient + * @param Client|string $pClient * @return Server $this */ - public function setClient($pClient) + public function setClient(string|Client $pClient): Server { if (is_string($pClient)) { $pClient = new $pClient($this); @@ -409,7 +479,7 @@ public function setClient($pClient) * * @return Client */ - public function getClient() + public function getClient(): Client { return $this->client; } @@ -419,14 +489,14 @@ public function getClient() * * @param $pCode * @param $pMessage - * @throws \Exception * @return string + * @throws Exception */ - public function sendBadRequest($pCode, $pMessage) + public function sendBadRequest($pCode, $pMessage): string { if (is_object($pMessage) && $pMessage->xdebug_message) $pMessage = $pMessage->xdebug_message; $msg = array('error' => $pCode, 'message' => $pMessage); - if (!$this->getClient()) throw new \Exception('client_not_found_in_ServerController'); + if (!$this->getClient()) throw new Exception('client_not_found_in_ServerController'); return $this->getClient()->sendResponse('400', $msg); } @@ -434,21 +504,22 @@ public function sendBadRequest($pCode, $pMessage) * Sends a 'Internal Server Error' response to the client. * @param $pCode * @param $pMessage - * @throws \Exception * @return string + * @throws Exception */ - public function sendError($pCode, $pMessage) + public function sendError($pCode, $pMessage): string { if (is_object($pMessage) && $pMessage->xdebug_message) $pMessage = $pMessage->xdebug_message; $msg = array('error' => $pCode, 'message' => $pMessage); - if (!$this->getClient()) throw new \Exception('client_not_found_in_ServerController'); + if (!$this->getClient()) throw new Exception('client_not_found_in_ServerController'); return $this->getClient()->sendResponse('500', $msg); } /** * Sends a exception response to the client. * @param $pException - * @throws \Exception + * @return mixed + * @throws Exception */ public function sendException($pException) { @@ -467,22 +538,21 @@ public function sendException($pException) $msg['trace'] = $pException->getTraceAsString(); } - if (!$this->getClient()) throw new \Exception('Client not found in ServerController'); + if (!$this->getClient()) throw new Exception('Client not found in ServerController'); return $this->getClient()->sendResponse('500', $msg); - } /** * Adds a new route for all http methods (get, post, put, delete, options, head, patch). * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. - * @param string $pHttpMethod If you want to limit to a HTTP method. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pHttpMethod If you want to limit to a HTTP method. * @return Server */ - public function addRoute($pUri, $pCb, $pHttpMethod = '_all_') + public function addRoute(string $pUri, callable|string $pCb, $pHttpMethod = '_all_'): Server { - $this->routes[$pUri][ $pHttpMethod ] = $pCb; + $this->routes[$pUri][$pHttpMethod] = $pCb; return $this; } @@ -490,11 +560,11 @@ public function addRoute($pUri, $pCb, $pHttpMethod = '_all_') /** * Same as addRoute, but limits to GET. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addGetRoute($pUri, $pCb) + public function addGetRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'get'); @@ -504,11 +574,11 @@ public function addGetRoute($pUri, $pCb) /** * Same as addRoute, but limits to POST. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addPostRoute($pUri, $pCb) + public function addPostRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'post'); @@ -518,11 +588,11 @@ public function addPostRoute($pUri, $pCb) /** * Same as addRoute, but limits to PUT. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addPutRoute($pUri, $pCb) + public function addPutRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'put'); @@ -532,11 +602,11 @@ public function addPutRoute($pUri, $pCb) /** * Same as addRoute, but limits to PATCH. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addPatchRoute($pUri, $pCb) + public function addPatchRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'patch'); @@ -546,11 +616,11 @@ public function addPatchRoute($pUri, $pCb) /** * Same as addRoute, but limits to HEAD. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addHeadRoute($pUri, $pCb) + public function addHeadRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'head'); @@ -560,11 +630,11 @@ public function addHeadRoute($pUri, $pCb) /** * Same as addRoute, but limits to OPTIONS. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addOptionsRoute($pUri, $pCb) + public function addOptionsRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'options'); @@ -574,11 +644,11 @@ public function addOptionsRoute($pUri, $pCb) /** * Same as addRoute, but limits to DELETE. * - * @param string $pUri - * @param callable|string $pCb The method name of the passed controller or a php callable. + * @param string $pUri + * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addDeleteRoute($pUri, $pCb) + public function addDeleteRoute(string $pUri, callable|string $pCb): Server { $this->addRoute($pUri, $pCb, 'delete'); @@ -588,10 +658,10 @@ public function addDeleteRoute($pUri, $pCb) /** * Removes a route. * - * @param string $pUri + * @param string $pUri * @return Server */ - public function removeRoute($pUri) + public function removeRoute(string $pUri): Server { unset($this->routes[$pUri]); @@ -602,8 +672,9 @@ public function removeRoute($pUri) * Sets the controller class. * * @param string|object $pClass + * @throws Exception */ - public function setClass($pClass) + public function setClass(object|string $pClass) { if (is_string($pClass)) { $this->createControllerClass($pClass); @@ -617,10 +688,10 @@ public function setClass($pClass) /** * Setup the controller class. * - * @param string $pClassName - * @throws \Exception + * @param string $pClassName + * @throws Exception */ - protected function createControllerClass($pClassName) + protected function createControllerClass(string $pClassName) { if ($pClassName != '') { try { @@ -632,11 +703,11 @@ protected function createControllerClass($pClassName) } else { $this->controller = new $pClassName($this); } - if (get_parent_class($this->controller) == '\Abigail\Server') { + if (get_parent_class($this->controller) == '\RestService\Server') { $this->controller->setClient($this->getClient()); } - } catch (\Exception $e) { - throw new \Exception('Error during initialisation of '.$pClassName.': '.$e, 0, $e); + } catch (Exception $e) { + throw new Exception('Error during initialisation of ' . $pClassName . ': ' . $e, 0, $e); } } else { $this->controller = $this; @@ -647,11 +718,12 @@ protected function createControllerClass($pClassName) * Attach a sub controller. * * @param string $pTriggerUrl - * @param mixed $pControllerClass A class name (autoloader required) or a instance of a class. + * @param mixed $pControllerClass A class name (autoloader required) or a instance of a class. * * @return Server new created Server. Use done() to switch the context back to the parent. + * @throws Exception */ - public function addSubController($pTriggerUrl, $pControllerClass = '') + public function addSubController(string $pTriggerUrl, $pControllerClass = ''): Server { $this->normalizeUrl($pTriggerUrl); @@ -670,7 +742,7 @@ public function addSubController($pTriggerUrl, $pControllerClass = '') * * @param string $pUrl */ - public function normalizeUrl(&$pUrl) + public function normalizeUrl(string &$pUrl) { if ('/' === $pUrl) return; if (substr($pUrl, -1) == '/') $pUrl = substr($pUrl, 0, -1); @@ -681,17 +753,21 @@ public function normalizeUrl(&$pUrl) * Sends data to the client with 200 http code. * * @param $pData + * @return mixed */ public function send($pData) { - return $this->getClient()->sendResponse(200, array('data' => $pData)); + if ($this->successResponseWrapper) { + $pData = call_user_func_array($this->successResponseWrapper, [$pData]); + } + return $this->getClient()->sendResponse('200', array('data' => $pData)); } /** - * @param string $pValue + * @param string $pValue * @return string */ - public function camelCase2Dashes($pValue) + public function camelCase2Dashes(string $pValue): string { return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $pValue)); } @@ -700,8 +776,9 @@ public function camelCase2Dashes($pValue) * Setup automatic routes. * * @return Server + * @throws ReflectionException */ - public function collectRoutes() + public function collectRoutes(): Server { if ($this->collectRoutesExclude == '*') return $this; @@ -710,14 +787,14 @@ public function collectRoutes() if (in_array($method, $this->collectRoutesExclude)) continue; $info = explode('/', preg_replace('/([a-z]*)(([A-Z]+)([a-zA-Z0-9_]*))/', '$1/$2', $method)); - $uri = $this->camelCase2Dashes((empty($info[1]) ? '' : $info[1])); + $uri = $this->camelCase2Dashes((empty($info[1]) ? '' : $info[1])); - $httpMethod = $info[0]; + $httpMethod = $info[0]; if ($httpMethod == 'all') { $httpMethod = '_all_'; } - $reflectionMethod = new \ReflectionMethod($this->controller, $method); + $reflectionMethod = new ReflectionMethod($this->controller, $method); if ($reflectionMethod->isPrivate()) continue; $phpDocs = $this->getMethodMetaData($reflectionMethod); @@ -726,7 +803,7 @@ public function collectRoutes() //only one route $this->routes[$phpDocs['url']['url']][$httpMethod] = $method; } else { - foreach($phpDocs['url'] as $urlAnnotation) { + foreach ($phpDocs['url'] as $urlAnnotation) { $this->routes[$urlAnnotation['url']][$httpMethod] = $method; } } @@ -742,14 +819,15 @@ public function collectRoutes() /** * Simulates a HTTP Call. * - * @param string $pUri - * @param string $pMethod The HTTP Method - * @return string + * @param string $pUri + * @param string $pMethod The HTTP Method + * @return bool|string + * @throws ReflectionException */ - public function simulateCall($pUri, $pMethod = 'get') + public function simulateCall(string $pUri, $pMethod = 'get'): bool|string { if (($idx = strpos($pUri, '?')) !== false) { - parse_str(substr($pUri, $idx+1), $_GET); + parse_str(substr($pUri, $idx + 1), $_GET); $pUri = substr($pUri, 0, $idx); } $this->getClient()->setUrl($pUri); @@ -763,9 +841,11 @@ public function simulateCall($pUri, $pMethod = 'get') * * Searches the method and sends the data to the client. * - * @return mixed + * @return bool|string + * @throws ReflectionException + * @throws Exception */ - public function run() + public function run(): bool|string { //check sub controller foreach ($this->controllers as $controller) { @@ -777,7 +857,7 @@ public function run() $requestedUrl = $this->getClient()->getUrl(); $this->normalizeUrl($requestedUrl); //check if its in our area - if (strpos($requestedUrl, $this->triggerUrl) !== 0) return; + if (strpos($requestedUrl, $this->triggerUrl) !== 0) return ""; $endPos = $this->triggerUrl === '/' ? 1 : strlen($this->triggerUrl) + 1; $uri = substr($requestedUrl, $endPos); @@ -788,21 +868,21 @@ public function run() $arguments = array(); $requiredMethod = $this->getClient()->getMethod(); - //does the requested uri exist? - list($callableMethod, $regexArguments, $method, $routeUri) = $this->findRoute($uri, $requiredMethod); + // Does the requested uri exist? + list($callableMethod, $regexArguments, $method) = $this->findRoute($uri, $requiredMethod); if ((!$callableMethod || $method != 'options') && $requiredMethod == 'options') { $description = $this->describe($uri); - $this->send($description); + return $this->send($description); } - if (!$callableMethod) { + if (empty($callableMethod)) { if (!$this->getParentController()) { if ($this->fallbackMethod) { $m = $this->fallbackMethod; - $this->send($this->controller->$m()); + return $this->send($this->controller->$m()); } else { - return $this->sendBadRequest('RouteNotFoundException', "There is no route for '$uri'."); + return $this->sendBadRequest('RouteNotFoundException', "There is no route for '{$uri}'."); } } else { return false; @@ -816,33 +896,38 @@ public function run() $arguments = array_merge($arguments, $regexArguments); } - //open class and scan method + // Open class and scan method if ($this->controller && is_string($callableMethod)) { - $ref = new \ReflectionClass($this->controller); + $ref = new ReflectionClass($this->controller); if (!method_exists($this->controller, $callableMethod)) { - $this->sendBadRequest('MethodNotFoundException', "There is no method '$callableMethod' in ". - get_class($this->controller)."."); + $callableMethodClassName = get_class($this->controller); + return $this->sendBadRequest('MethodNotFoundException', "There is no method '{$callableMethod}' in {$callableMethodClassName}."); } $reflectionMethod = $ref->getMethod($callableMethod); } else if (is_callable($callableMethod)) { - $reflectionMethod = new \ReflectionFunction($callableMethod); + $reflectionMethod = new ReflectionFunction($callableMethod); + } else { + throw new Exception("Unknown"); } $params = $reflectionMethod->getParameters(); if ($method == '_all_') { - //first parameter is $pMethod + // First parameter is $pMethod array_shift($params); } - //remove regex arguments - for ($i=0; $ireadDataFromBody(); + + // Collect arguments foreach ($params as $param) { $name = $this->argumentName($param->getName()); @@ -852,14 +937,16 @@ public function run() if (substr($k, 0, 1) == '_' && $k != '_suppress_status_code') $thisArgs[$k] = $v; } + $arguments[] = $thisArgs; } else { - - if (!$param->isOptional() && !isset($_GET[$name]) && !isset($_POST[$name])) { - return $this->sendBadRequest('MissingRequiredArgumentException', sprintf("Argument '%s' is missing.", $name)); + if (!$param->isOptional() && !isset($_GET[$name]) && !isset($this->body_data[$name])) { + return $this->sendBadRequest('MissingRequiredArgumentException', "Argument '{$name}' is missing."); } - $arguments[] = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : $param->getDefaultValue()); + $arguments[] = isset($_GET[$name]) + ? $_GET[$name] + : ($this->body_data[$name] ?? $param->getDefaultValue()); } } @@ -869,19 +956,17 @@ public function run() $args[] = $arguments; try { call_user_func_array($this->checkAccessFn, $args); - } catch (\Exception $e) { + } catch (Exception $e) { $this->sendException($e); } } - //fire method + // fire method $object = $this->controller; - return $this->fireMethod($callableMethod, $object, $arguments); - } - public function fireMethod($pMethod, $pController, $pArguments) + public function fireMethod($pMethod, $pController, $pArguments): string { $callable = false; @@ -898,27 +983,29 @@ public function fireMethod($pMethod, $pController, $pArguments) if ($callable) { try { return $this->send(call_user_func_array($callable, $pArguments)); - } catch (\Exception $e) { + } catch (TypeError | InvalidArgumentException | Exception $e) { return $this->sendException($e); } } + return ""; } /** * Describe a route or the whole controller with all routes. * - * @param string $pUri - * @param boolean $pOnlyRoutes + * @param string $pUri + * @param boolean $pOnlyRoutes * @return array + * @throws ReflectionException */ - public function describe($pUri = null, $pOnlyRoutes = false) + public function describe($pUri = null, $pOnlyRoutes = false): array { $definition = array(); if (!$pOnlyRoutes) { $definition['parameters'] = array( '_method' => array('description' => 'Can be used as HTTP METHOD if the client does not support HTTP methods.', 'type' => 'string', - 'values' => 'GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH'), + 'values' => 'GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH'), '_suppress_status_code' => array('description' => 'Suppress the HTTP status code.', 'type' => 'boolean', 'values' => '1, 0'), '_format' => array('description' => 'Format of generated data. Can be added as suffix .json .xml', 'type' => 'string', 'values' => 'json, xml'), ); @@ -931,21 +1018,21 @@ public function describe($pUri = null, $pOnlyRoutes = false) foreach ($this->routes as $routeUri => $routeMethods) { $matches = array(); - if (!$pUri || ($pUri && preg_match('|^'.$routeUri.'$|', $pUri, $matches))) { + if (!$pUri || (preg_match('|^' . $routeUri . '$|', $pUri, $matches))) { if ($matches) { array_shift($matches); } $def = array(); - $def['uri'] = $this->getTriggerUrl().'/'.$routeUri; + $def['uri'] = $this->getTriggerUrl() . '/' . $routeUri; foreach ($routeMethods as $method => $phpMethod) { if (is_string($phpMethod)) { - $ref = new \ReflectionClass($this->controller); + $ref = new ReflectionClass($this->controller); $refMethod = $ref->getMethod($phpMethod); } else { - $refMethod = new \ReflectionFunction($phpMethod); + $refMethod = new ReflectionFunction($phpMethod); } $def['methods'][strtoupper($method)] = $this->getMethodMetaData($refMethod, $matches); @@ -965,13 +1052,14 @@ public function describe($pUri = null, $pOnlyRoutes = false) } /** - * Fetches all meta data informations as params, return type etc. + * Fetches all meta data information as params, return type etc. * - * @param \ReflectionMethod $pMethod - * @param array $pRegMatches - * @return array + * @param ReflectionFunctionAbstract $pMethod + * @param null $pRegMatches + * @return bool|array + * @throws ReflectionException */ - public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMatches = null) + public function getMethodMetaData(ReflectionFunctionAbstract $pMethod, $pRegMatches = null): bool|array { $file = $pMethod->getFileName(); $startLine = $pMethod->getStartLine(); @@ -993,7 +1081,7 @@ public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMat while ($line = array_pop($lines)) { if ($blockStarted) { - $phpDoc = $line.$phpDoc; + $phpDoc = $line . $phpDoc; //if start comment block: /* if (preg_match('/\s*\t*\/\*/', $line)) { @@ -1002,7 +1090,7 @@ public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMat continue; } else { //we are not in a comment block. - //if class def, array def or close bracked from fn comes above + //if class def, array def or close broken from fn comes above //then we dont have phpdoc if (preg_match('/^\s*\t*[a-zA-Z_&\s]*(\$|{|})/', $line)) { break; @@ -1014,7 +1102,7 @@ public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMat //if end comment block: */ if (preg_match('/\*\//', $line)) { - $phpDoc = $line.$phpDoc; + $phpDoc = $line . $phpDoc; $blockStarted = true; //one line php doc? if (preg_match('/\s*\t*\/\*/', $line)) { @@ -1035,7 +1123,7 @@ public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMat if ($fillPhpDocParam) { $phpDoc['param'][] = array( 'name' => $param->getName(), - 'type' => $param->isArray()?'array':'mixed' + 'type' => $param->isArray() ? 'array' : 'mixed' ); } } @@ -1056,7 +1144,7 @@ public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMat ); if ($pRegMatches && is_array($pRegMatches) && $pRegMatches[$c]) { - $parameter['fromRegex'] = '$'.($c+1); + $parameter['fromRegex'] = '$' . ($c + 1); } $parameter['required'] = !$param->isOptional(); @@ -1089,10 +1177,10 @@ public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMat /** * Parse phpDoc string and returns an array. * - * @param string $pString + * @param string $pString * @return array */ - public function parsePhpDoc($pString) + public function parsePhpDoc(string $pString): array { preg_match('#^/\*\*(.*)\*/#s', trim($pString), $comment); @@ -1122,7 +1210,7 @@ public function parsePhpDoc($pString) $currentTag = $match[1]; } - $currentData = trim($currentData.' '.$line); + $currentData = trim($currentData . ' ' . $line); } if ($currentTag) @@ -1143,9 +1231,9 @@ public function parsePhpDoc($pString) preg_match($regex[$tag][0], $item, $match); $item = array(); $c = count($match); - for ($i =1; $i < $c; $i++) { - if (isset($regex[$tag][1][$i-1])) { - $item[$regex[$tag][1][$i-1]] = $match[$i]; + for ($i = 1; $i < $c; $i++) { + if (isset($regex[$tag][1][$i - 1])) { + $item[$regex[$tag][1][$i - 1]] = $match[$i]; } } } @@ -1158,27 +1246,28 @@ public function parsePhpDoc($pString) } /** - * If the name is a camelcased one whereas the first char is lowercased, + * If the name is a camel-cased one whereas the first char is lower-cased, * then we remove the first char and set first char to lower case. * - * @param string $pName + * @param string $pName * @return string */ - public function argumentName($pName) + public function argumentName(string $pName): string { if (ctype_lower(substr($pName, 0, 1)) && ctype_upper(substr($pName, 1, 1))) { - return strtolower(substr($pName, 1, 1)).substr($pName, 2); - } return $pName; + return strtolower(substr($pName, 1, 1)) . substr($pName, 2); + } + return $pName; } /** * Find and return the route for $pUri. * - * @param string $pUri - * @param string $pMethod limit to method. + * @param string $pUri + * @param string $pMethod limit to method. * @return array|boolean */ - public function findRoute($pUri, $pMethod = '_all_') + public function findRoute(string $pUri, $pMethod = '_all_'): bool|array { if (isset($this->routes[$pUri][$pMethod]) && $method = $this->routes[$pUri][$pMethod]) { return array($method, array(), $pMethod, $pUri); @@ -1188,7 +1277,7 @@ public function findRoute($pUri, $pMethod = '_all_') //maybe we have a regex uri foreach ($this->routes as $routeUri => $routeMethods) { - if (preg_match('|^'.$routeUri.'$|', $pUri, $matches)) { + if (preg_match('|^' . $routeUri . '$|', $pUri, $matches)) { if (!isset($routeMethods[$pMethod])) { if (isset($routeMethods['_all_'])) @@ -1197,6 +1286,7 @@ public function findRoute($pUri, $pMethod = '_all_') continue; } + $arguments = []; array_shift($matches); foreach ($matches as $match) { $arguments[] = $match; From 0f309bb054e2163660e20403e6dd453f0c1acd25 Mon Sep 17 00:00:00 2001 From: SuperSonic Date: Sat, 17 Jul 2021 11:29:26 +0800 Subject: [PATCH 3/5] Update --- .github/workflows/integration.yml | 18 ++++ .gitignore | 1 + .travis.yml | 19 ---- README.md | 28 ++--- composer.json | 2 +- phpunit.xml | 2 +- src/Client.php | 6 +- src/Server.php | 68 +++++++----- tests/Basic/BasicTest.php | 23 ---- tests/Synthetic/CollectTest.php | 82 --------------- tests/Synthetic/CustomRoutesTest.php | 123 ---------------------- tests/Synthetic/RouteTest.php | 56 ---------- tests/Test/Basic/BasicTest.php | 19 ++++ tests/{ => Test}/Controller/MyRoutes.php | 17 ++- tests/Test/Synthetic/CollectTest.php | 77 ++++++++++++++ tests/Test/Synthetic/CustomRoutesTest.php | 109 +++++++++++++++++++ tests/Test/Synthetic/RouteTest.php | 51 +++++++++ tests/bootstrap.php | 4 +- 18 files changed, 346 insertions(+), 359 deletions(-) create mode 100644 .github/workflows/integration.yml delete mode 100644 .travis.yml delete mode 100644 tests/Basic/BasicTest.php delete mode 100644 tests/Synthetic/CollectTest.php delete mode 100644 tests/Synthetic/CustomRoutesTest.php delete mode 100644 tests/Synthetic/RouteTest.php create mode 100644 tests/Test/Basic/BasicTest.php rename tests/{ => Test}/Controller/MyRoutes.php (64%) create mode 100644 tests/Test/Synthetic/CollectTest.php create mode 100644 tests/Test/Synthetic/CustomRoutesTest.php create mode 100644 tests/Test/Synthetic/RouteTest.php diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..6ebbb6a --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,18 @@ +name: Integration + +on: [ push ] + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: PHP Composer + - uses: php-actions/composer@v5 + + - name: PHPUnit tests + uses: php-actions/phpunit@v3 + with: + php_version: 7.4 diff --git a/.gitignore b/.gitignore index f7015c3..dd90702 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ .idea/ composer.lock +*.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ffd7974..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: php - -php: - - 8.0 - - 7.4 - -matrix: - include: - - php: 7.4 - dist: precise - -before_script: - - apt update - - apt install -y git wget unzip - - wget http://getcomposer.org/composer.phar - - php composer.phar install - -script: - - php ./vendor/bin/phpunit diff --git a/README.md b/README.md index 4553113..2a32aaf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Features + Regular Expression support + Error handling through PHP Exceptions + Parameter validation through PHP function signature -+ Can return a summary of all routes or one route through `OPTIONS` method based on PHPDoc (if `OPTIONS` is not overridden) ++ Can return a summary of all routes or one route through `OPTIONS` method based on PHPDoc (if `OPTIONS` is not + overridden) + Support of `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD` and `OPTIONS` + Suppress the HTTP status code with ?_suppress_status_code=1 (for clients that have troubles with that) + Supports ?_method=`httpMethod` as addition to the actual HTTP method. @@ -19,8 +20,8 @@ Features Installation ------------ - - https://packagist.org/packages/marcj/php-rest-service. - - More information available under https://packagist.org/. +- https://packagist.org/packages/marcj/php-rest-service. +- More information available under https://packagist.org/. Create a `composer.json`: @@ -40,6 +41,7 @@ $ php composer.phar install ``` After the installation, you need to include the `vendor/autoload.php` to make the class in your script available. + ```php include 'vendor/autoload.php'; ``` @@ -47,13 +49,14 @@ include 'vendor/autoload.php'; Requirements ============ - - PHP 5.3 and above. - - PHPUnit to execute the test suite. - - Setup PATH_INFO in mod_rewrite (.htaccess) or other webserver configuration - +- PHP 5.3 and above. +- PHPUnit to execute the test suite. +- Setup PATH_INFO in mod_rewrite (.htaccess) or other webserver configuration + Example config: apache webserver ---------------- + ``` #.htaccess RewriteEngine On @@ -61,8 +64,10 @@ RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule (.+) index.php/$1 [L,QSA] ``` + nginx webserver --------------- + ``` // edit virtualhost /etc/nginx/conf.d/name_virtualhost_file server { @@ -84,7 +89,6 @@ Usage Demo ### Way 1. The dirty & fast - ```php use RestService\Server; @@ -159,6 +163,7 @@ class Admin { ``` Generates following entry points: + ``` + GET /admin/logged-in + POST /admin/login?username=&password= @@ -166,7 +171,6 @@ Generates following entry points: + GET /admin/stats ``` - ### Way 3. Custom rules with controller `index.php`: @@ -244,11 +248,10 @@ class Tools { } ``` - ## Responses -The response body is always a array (JSON per default) containing a status code and the actual data. -If a exception has been thrown, it contains the status 500, the exception class name as error and the message as message. +The response body is always a array (JSON per default) containing a status code and the actual data. If a exception has +been thrown, it contains the status 500, the exception class name as error and the message as message. Some examples: @@ -303,5 +306,4 @@ Licensed under the MIT License. See the LICENSE file for more details. Take a look into the code, to get more information about the possibilities. It's well documented. - [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/marcj/php-rest-service/trend.png)](https://bitdeli.com/free "Bitdeli Badge") diff --git a/composer.json b/composer.json index 82fb26c..bcab2c8 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^4" + "phpunit/phpunit": "^9" } } diff --git a/phpunit.xml b/phpunit.xml index 91ce382..ec98dda 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,4 +1,4 @@ -outputFormats as $formatCode => $formatMethod) { - if (str_contains($_SERVER['HTTP_ACCEPT'], $formatCode)) { + if (strpos($_SERVER['HTTP_ACCEPT'], $formatCode) !== false) { $this->outputFormat = $formatCode; break; } diff --git a/src/Server.php b/src/Server.php index cd95703..344d442 100644 --- a/src/Server.php +++ b/src/Server.php @@ -2,6 +2,15 @@ namespace Abigail; +use Exception; +use InvalidArgumentException; +use ReflectionClass; +use ReflectionException; +use ReflectionFunction; +use ReflectionFunctionAbstract; +use ReflectionMethod; +use TypeError; + /** * \RestService\Server - A REST server class for RESTful APIs. */ @@ -59,7 +68,7 @@ class Server * * @var Server|null */ - protected Server|null $parentController = null; + protected ?Server $parentController = null; /** * The client @@ -73,7 +82,7 @@ class Server * * @var array|string array('methodOne', 'methodTwo') or * for all methods */ - protected array|string $collectRoutesExclude = array('__construct'); + protected $collectRoutesExclude = array('__construct'); /** * List of possible methods. @@ -189,7 +198,7 @@ public function __construct(string $pTriggerUrl, $pControllerClass = null, $pPar * Factory. * * @param string $pTriggerUrl - * @param string $pControllerClass + * @param mixed $pControllerClass * * @return Server $this */ @@ -215,7 +224,7 @@ public function setControllerFactory(callable $controllerFactory): Server /** * @return callable|null */ - public function getControllerFactory(): callable|null + public function getControllerFactory(): ?callable { return $this->controllerFactory; } @@ -261,7 +270,7 @@ public function setCheckAccess(callable $pFn): Server * Getter for checkAccess * @return callable|null */ - public function getCheckAccess(): callable|null + public function getCheckAccess(): ?callable { return $this->checkAccessFn; } @@ -284,7 +293,7 @@ public function setSuccessResponseWrapper(callable $pFn): Server * Getter for successResponseWrapper * @return callable|null */ - public function getSuccessResponseWrapper(): callable|null + public function getSuccessResponseWrapper(): ?callable { return $this->successResponseWrapper; } @@ -355,7 +364,7 @@ public function setExceptionHandler(callable $pFn): Server * Getter for checkAccess * @return callable|null */ - public function getExceptionHandler(): callable|null + public function getExceptionHandler(): ?callable { return $this->sendExceptionFn; } @@ -397,7 +406,7 @@ public function done(): Server * * @return Server|null $this */ - public function getParentController(): Server|null + public function getParentController(): ?Server { return $this->parentController; } @@ -436,11 +445,16 @@ public function readDataFromBody() $raw_content_type_array = explode(";", $raw_content_type); if (isset($raw_content_type_array[0])) { $raw_content = file_get_contents("php://input"); - $this->body_data = match ($raw_content_type_array[0]) { - "application/json" => json_decode($raw_content, true) ?? [], - "application/x-www-form-urlencoded" => self::form_decode($raw_content) ?? [], - default => [] - }; + switch ($raw_content_type_array[0]) { + case "application/json" : + $this->body_data = json_decode($raw_content, true) ?? []; + break; + case "application/x-www-form-urlencoded" : + $this->body_data = self::form_decode($raw_content) ?? []; + break; + default: + $this->body_data = []; + } } else { throw new Exception("No the header content-type configured."); } @@ -462,7 +476,7 @@ private static function form_decode($raw_content) * @param Client|string $pClient * @return Server $this */ - public function setClient(string|Client $pClient): Server + public function setClient($pClient): Server { if (is_string($pClient)) { $pClient = new $pClient($this); @@ -550,7 +564,7 @@ public function sendException($pException) * @param string $pHttpMethod If you want to limit to a HTTP method. * @return Server */ - public function addRoute(string $pUri, callable|string $pCb, $pHttpMethod = '_all_'): Server + public function addRoute(string $pUri, $pCb, string $pHttpMethod = '_all_'): Server { $this->routes[$pUri][$pHttpMethod] = $pCb; @@ -564,7 +578,7 @@ public function addRoute(string $pUri, callable|string $pCb, $pHttpMethod = '_al * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addGetRoute(string $pUri, callable|string $pCb): Server + public function addGetRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'get'); @@ -578,7 +592,7 @@ public function addGetRoute(string $pUri, callable|string $pCb): Server * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addPostRoute(string $pUri, callable|string $pCb): Server + public function addPostRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'post'); @@ -592,7 +606,7 @@ public function addPostRoute(string $pUri, callable|string $pCb): Server * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addPutRoute(string $pUri, callable|string $pCb): Server + public function addPutRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'put'); @@ -606,7 +620,7 @@ public function addPutRoute(string $pUri, callable|string $pCb): Server * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addPatchRoute(string $pUri, callable|string $pCb): Server + public function addPatchRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'patch'); @@ -620,7 +634,7 @@ public function addPatchRoute(string $pUri, callable|string $pCb): Server * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addHeadRoute(string $pUri, callable|string $pCb): Server + public function addHeadRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'head'); @@ -634,7 +648,7 @@ public function addHeadRoute(string $pUri, callable|string $pCb): Server * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addOptionsRoute(string $pUri, callable|string $pCb): Server + public function addOptionsRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'options'); @@ -648,7 +662,7 @@ public function addOptionsRoute(string $pUri, callable|string $pCb): Server * @param callable|string $pCb The method name of the passed controller or a php callable. * @return Server */ - public function addDeleteRoute(string $pUri, callable|string $pCb): Server + public function addDeleteRoute(string $pUri, $pCb): Server { $this->addRoute($pUri, $pCb, 'delete'); @@ -674,7 +688,7 @@ public function removeRoute(string $pUri): Server * @param string|object $pClass * @throws Exception */ - public function setClass(object|string $pClass) + public function setClass($pClass) { if (is_string($pClass)) { $this->createControllerClass($pClass); @@ -824,7 +838,7 @@ public function collectRoutes(): Server * @return bool|string * @throws ReflectionException */ - public function simulateCall(string $pUri, $pMethod = 'get'): bool|string + public function simulateCall(string $pUri, string $pMethod = 'get') { if (($idx = strpos($pUri, '?')) !== false) { parse_str(substr($pUri, $idx + 1), $_GET); @@ -845,7 +859,7 @@ public function simulateCall(string $pUri, $pMethod = 'get'): bool|string * @throws ReflectionException * @throws Exception */ - public function run(): bool|string + public function run() { //check sub controller foreach ($this->controllers as $controller) { @@ -1059,7 +1073,7 @@ public function describe($pUri = null, $pOnlyRoutes = false): array * @return bool|array * @throws ReflectionException */ - public function getMethodMetaData(ReflectionFunctionAbstract $pMethod, $pRegMatches = null): bool|array + public function getMethodMetaData(ReflectionFunctionAbstract $pMethod, $pRegMatches = null) { $file = $pMethod->getFileName(); $startLine = $pMethod->getStartLine(); @@ -1267,7 +1281,7 @@ public function argumentName(string $pName): string * @param string $pMethod limit to method. * @return array|boolean */ - public function findRoute(string $pUri, $pMethod = '_all_'): bool|array + public function findRoute(string $pUri, string $pMethod = '_all_') { if (isset($this->routes[$pUri][$pMethod]) && $method = $this->routes[$pUri][$pMethod]) { return array($method, array(), $pMethod, $pUri); diff --git a/tests/Basic/BasicTest.php b/tests/Basic/BasicTest.php deleted file mode 100644 index ea89f8e..0000000 --- a/tests/Basic/BasicTest.php +++ /dev/null @@ -1,23 +0,0 @@ -setClient('RestService\\InternalClient') - ->collectRoutes(); - - $response = $restService->simulateCall('/test/test', 'get'); - $this->assertEquals('{ - "status": 200, - "data": "test" -}', $response); - - } -} diff --git a/tests/Synthetic/CollectTest.php b/tests/Synthetic/CollectTest.php deleted file mode 100644 index f0a0693..0000000 --- a/tests/Synthetic/CollectTest.php +++ /dev/null @@ -1,82 +0,0 @@ -restService = Server::create('/', new MyRoutes) - ->setClient('RestService\\InternalClient') - ->collectRoutes(); - } - public function testNonPhpDocMethod() - { - $response = $this->restService->simulateCall('/method-without-php-doc', 'get'); - $this->assertEquals('{ - "status": 200, - "data": "hi" -}', $response); - } - - public function testUrlAnnotation() - { - $response = $this->restService->simulateCall('/stats', 'get'); - $this->assertEquals('{ - "status": 200, - "data": "Stats for 1" -}', $response); - - $response = $this->restService->simulateCall('/stats/23', 'get'); - $this->assertEquals('{ - "status": 200, - "data": "Stats for 23" -}', $response); - - } - - public function testOwnController() - { - - $response = $this->restService->simulateCall('/login', 'post'); - - $this->assertEquals('{ - "status": 400, - "error": "MissingRequiredArgumentException", - "message": "Argument \'username\' is missing." -}', $response); - - $response = $this->restService->simulateCall('/login?username=bla', 'post'); - - $this->assertEquals('{ - "status": 400, - "error": "MissingRequiredArgumentException", - "message": "Argument \'password\' is missing." -}', $response); - - $response = $this->restService->simulateCall('/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - $response = $this->restService->simulateCall('/login?username=peter&password=pwd', 'get'); - - $this->assertEquals('{ - "status": 400, - "error": "RouteNotFoundException", - "message": "There is no route for \'login\'." -}', $response); - - } - -} diff --git a/tests/Synthetic/CustomRoutesTest.php b/tests/Synthetic/CustomRoutesTest.php deleted file mode 100644 index cde7213..0000000 --- a/tests/Synthetic/CustomRoutesTest.php +++ /dev/null @@ -1,123 +0,0 @@ -setClient('RestService\\InternalClient') - ->addPostRoute('login', 'postLogin'); - - $response = $restService->simulateCall('/login?', 'post'); - - $this->assertEquals('{ - "status": 400, - "error": "MissingRequiredArgumentException", - "message": "Argument \'username\' is missing." -}', $response); - - $response = $restService->simulateCall('/login?username=bla', 'post'); - - $this->assertEquals('{ - "status": 400, - "error": "MissingRequiredArgumentException", - "message": "Argument \'password\' is missing." -}', $response); - - $response = $restService->simulateCall('/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - $response = $restService->simulateCall('/login?username=peter&password=pwd', 'get'); - - $this->assertEquals('{ - "status": 400, - "error": "RouteNotFoundException", - "message": "There is no route for \'login\'." -}', $response); - - } - - public function testOwnControllerWithDifferentPrefix() - { - $restService = Server::create('/v1', new MyRoutes) - ->setClient('RestService\\InternalClient') - ->addPostRoute('login', 'postLogin'); - - $response = $restService->simulateCall('/v1/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - $restService = Server::create('/v1/', new MyRoutes) - ->setClient('RestService\\InternalClient') - ->addPostRoute('login', 'postLogin'); - - $response = $restService->simulateCall('/v1/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - $restService = Server::create('v1', new MyRoutes) - ->setClient('RestService\\InternalClient') - ->addPostRoute('login', 'postLogin'); - - $response = $restService->simulateCall('/v1/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - } - - public function testSubController() - { - $restService = Server::create('v1', new MyRoutes) - ->setClient('RestService\\InternalClient') - ->addPostRoute('login', 'postLogin') - ->addSubController('sub', new MyRoutes()) - ->addPostRoute('login', 'postLogin') - ->done() - ; - - $response = $restService->simulateCall('/v1/sub/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - } - - public function testSubControllerWithSlashRootParent() - { - $restService = Server::create('/', new MyRoutes) - ->setClient('RestService\\InternalClient') - ->addSubController('sub', new MyRoutes()) - ->addPostRoute('login', 'postLogin') - ->done() - ; - - $response = $restService->simulateCall('/sub/login?username=peter&password=pwd', 'post'); - - $this->assertEquals('{ - "status": 200, - "data": true -}', $response); - - } -} diff --git a/tests/Synthetic/RouteTest.php b/tests/Synthetic/RouteTest.php deleted file mode 100644 index 22a1ad6..0000000 --- a/tests/Synthetic/RouteTest.php +++ /dev/null @@ -1,56 +0,0 @@ -setClient('RestService\\InternalClient') - ->addGetRoute('test', function(){ - return 'getTest'; - }) - ->addPostRoute('test', function(){ - return 'postTest'; - }) - ->addPatchRoute('test', function(){ - return 'patchTest'; - }) - ->addPutRoute('test', function(){ - return 'putTest'; - }) - ->addOptionsRoute('test', function(){ - return 'optionsTest'; - }) - ->addDeleteRoute('test', function(){ - return 'deleteTest'; - }) - ->addHeadRoute('test', function(){ - return 'headTest'; - }) - ->addRoute('all-test', function(){ - return 'allTest'; - }); - - foreach ($restService->getClient()->methods as $method) { - - $this->assertEquals('{ - "status": 200, - "data": "'.$method.'Test" -}', $restService->simulateCall('/test', $method)); - - $this->assertEquals('{ - "status": 200, - "data": "allTest" -}', $restService->simulateCall('/all-test', $method)); - - } - - } - -} diff --git a/tests/Test/Basic/BasicTest.php b/tests/Test/Basic/BasicTest.php new file mode 100644 index 0000000..92164b2 --- /dev/null +++ b/tests/Test/Basic/BasicTest.php @@ -0,0 +1,19 @@ +setClient('Abigail\\InternalClient') + ->collectRoutes(); + $response = $abigail->simulateCall('/test/test', 'get'); + $this->assertEquals(["status" => 200, "data" => "test"], json_decode($response, true)); + } +} diff --git a/tests/Controller/MyRoutes.php b/tests/Test/Controller/MyRoutes.php similarity index 64% rename from tests/Controller/MyRoutes.php rename to tests/Test/Controller/MyRoutes.php index ed3e3ec..93a440e 100644 --- a/tests/Controller/MyRoutes.php +++ b/tests/Test/Controller/MyRoutes.php @@ -7,7 +7,7 @@ class MyRoutes /** * @return string */ - public function get() + public function get(): string { return "root GET"; } @@ -15,17 +15,17 @@ public function get() /** * @return string */ - public function post() + public function post(): string { return "root POST"; } /** - * @param string $username - * @param string $password + * @param string $username + * @param string $password * @return bool */ - public function postLogin($username, $password) + public function postLogin(string $username, string $password): bool { return $username == 'peter' && $password == 'pwd'; } @@ -36,13 +36,13 @@ public function postLogin($username, $password) * @url stats * @return string */ - public function getStats($server = '1') + public function getStats($server = '1'): string { return sprintf('Stats for %s', $server); } - public function getMethodWithoutPhpDoc() + public function getMethodWithoutPhpDoc(): string { return 'hi'; } @@ -52,9 +52,8 @@ public function getMethodWithoutPhpDoc() * * @return string */ - public function getTest() + public function getTest(): string { return 'test'; } - } diff --git a/tests/Test/Synthetic/CollectTest.php b/tests/Test/Synthetic/CollectTest.php new file mode 100644 index 0000000..fa655c3 --- /dev/null +++ b/tests/Test/Synthetic/CollectTest.php @@ -0,0 +1,77 @@ +abigail = Server::create('/', new MyRoutes) + ->setClient('Abigail\\InternalClient') + ->collectRoutes(); + } + + public function testNonPhpDocMethod() + { + $response = $this->abigail->simulateCall('/method-without-php-doc', 'get'); + $this->assertEquals(["status" => 200, "data" => "hi"], json_decode($response, true)); + } + + public function testUrlAnnotation() + { + $response = $this->abigail->simulateCall('/stats', 'get'); + $this->assertEquals(["status" => 200, "data" => "Stats for 1"], json_decode($response, true)); + + $response = $this->abigail->simulateCall('/stats/23', 'get'); + $this->assertEquals(["status" => 200, "data" => "Stats for 23"], json_decode($response, true)); + } + + public function testOwnController() + { + $response = $this->abigail->simulateCall('/login', 'post'); + + $this->assertEquals( + [ + "status" => 400, + "error" => "MissingRequiredArgumentException", + "message" => "Argument 'username' is missing." + ], + json_decode($response, true) + ); + + $response = $this->abigail->simulateCall('/login?username=bla', 'post'); + + $this->assertEquals( + [ + "status" => 400, + "error" => "MissingRequiredArgumentException", + "message" => "Argument 'password' is missing." + ], + json_decode($response, true) + ); + + $response = $this->abigail->simulateCall('/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + + $response = $this->abigail->simulateCall('/login?username=peter&password=pwd', 'get'); + + $this->assertEquals( + [ + "status" => 400, + "error" => "RouteNotFoundException", + "message" => "There is no route for 'login'." + ], + json_decode($response, true) + ); + } +} diff --git a/tests/Test/Synthetic/CustomRoutesTest.php b/tests/Test/Synthetic/CustomRoutesTest.php new file mode 100644 index 0000000..37263ab --- /dev/null +++ b/tests/Test/Synthetic/CustomRoutesTest.php @@ -0,0 +1,109 @@ +setClient('Abigail\\InternalClient') + ->addPostRoute('login', 'postLogin'); + + $response = $abigail->simulateCall('/login?', 'post'); + + $this->assertEquals( + [ + "status" => 400, + "error" => "MissingRequiredArgumentException", + "message" => "Argument 'username' is missing." + ], + json_decode($response, true) + ); + + $response = $abigail->simulateCall('/login?username=bla', 'post'); + + $this->assertEquals( + [ + "status" => 400, + "error" => "MissingRequiredArgumentException", + "message" => "Argument 'password' is missing." + ], + json_decode($response, true) + ); + + $response = $abigail->simulateCall('/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + + $response = $abigail->simulateCall('/login?username=peter&password=pwd', 'get'); + + $this->assertEquals( + [ + "status" => 400, + "error" => "RouteNotFoundException", + "message" => "There is no route for 'login'." + ], + json_decode($response, true) + ); + } + + public function testOwnControllerWithDifferentPrefix() + { + $abigail = Server::create('/v1', new MyRoutes) + ->setClient('Abigail\\InternalClient') + ->addPostRoute('login', 'postLogin'); + + $response = $abigail->simulateCall('/v1/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + + $abigail = Server::create('/v1/', new MyRoutes) + ->setClient('Abigail\\InternalClient') + ->addPostRoute('login', 'postLogin'); + + $response = $abigail->simulateCall('/v1/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + + $abigail = Server::create('v1', new MyRoutes) + ->setClient('Abigail\\InternalClient') + ->addPostRoute('login', 'postLogin'); + + $response = $abigail->simulateCall('/v1/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + } + + public function testSubController() + { + $abigail = Server::create('v1', new MyRoutes) + ->setClient('Abigail\\InternalClient') + ->addPostRoute('login', 'postLogin') + ->addSubController('sub', new MyRoutes()) + ->addPostRoute('login', 'postLogin') + ->done(); + + $response = $abigail->simulateCall('/v1/sub/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + } + + public function testSubControllerWithSlashRootParent() + { + $abigail = Server::create('/', new MyRoutes) + ->setClient('Abigail\\InternalClient') + ->addSubController('sub', new MyRoutes()) + ->addPostRoute('login', 'postLogin') + ->done(); + + $response = $abigail->simulateCall('/sub/login?username=peter&password=pwd', 'post'); + + $this->assertEquals(["status" => 200, "data" => true], json_decode($response, true)); + } +} diff --git a/tests/Test/Synthetic/RouteTest.php b/tests/Test/Synthetic/RouteTest.php new file mode 100644 index 0000000..da9173d --- /dev/null +++ b/tests/Test/Synthetic/RouteTest.php @@ -0,0 +1,51 @@ +setClient('Abigail\\InternalClient') + ->addGetRoute('test', function () { + return 'getTest'; + }) + ->addPostRoute('test', function () { + return 'postTest'; + }) + ->addPatchRoute('test', function () { + return 'patchTest'; + }) + ->addPutRoute('test', function () { + return 'putTest'; + }) + ->addOptionsRoute('test', function () { + return 'optionsTest'; + }) + ->addDeleteRoute('test', function () { + return 'deleteTest'; + }) + ->addHeadRoute('test', function () { + return 'headTest'; + }) + ->addRoute('all-test', function () { + return 'allTest'; + }); + + foreach ($abigail->getClient()->methods as $method) { + $response = $abigail->simulateCall('/test', $method); + $this->assertEquals(["status" => 200, "data" => "{$method}Test"], json_decode($response, true)); + + $response = $abigail->simulateCall('/all-test', $method); + $this->assertEquals(["status" => 200, "data" => "allTest"], json_decode($response, true)); + } + + } + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 61410e9..1f06e07 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,4 @@ add('Test\\', __DIR__.'/../'); +$loader->setPsr4('Test\\', __DIR__ . '/Test'); From b7c2415bddf21d64cbe4e96ee43469ac538bda42 Mon Sep 17 00:00:00 2001 From: SuperSonic Date: Sat, 17 Jul 2021 11:31:09 +0800 Subject: [PATCH 4/5] Fix typo for integration.yml --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 6ebbb6a..1b75862 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - name: PHP Composer - - uses: php-actions/composer@v5 + uses: php-actions/composer@v5 - name: PHPUnit tests uses: php-actions/phpunit@v3 From 74d368df82bb5f53a0f63b6fcf37a41a6a3a8ef1 Mon Sep 17 00:00:00 2001 From: SuperSonic Date: Sat, 17 Jul 2021 11:38:45 +0800 Subject: [PATCH 5/5] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a32aaf..0d06700 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ Abigail ============== +[![Integration](https://github.com/star-inc/Abigail/actions/workflows/integration.yml/badge.svg)](https://github.com/star-inc/Abigail/actions/workflows/integration.yml) -Abigail is the fork from marcj/php-rest-service, which is a simple and fast PHP class for server side RESTful APIs. +Abigail is the fork from `marcj/php-rest-service`, which is a simple and fast PHP class for server side RESTful APIs. Features -------- @@ -49,7 +50,7 @@ include 'vendor/autoload.php'; Requirements ============ -- PHP 5.3 and above. +- PHP 7.4 and above. - PHPUnit to execute the test suite. - Setup PATH_INFO in mod_rewrite (.htaccess) or other webserver configuration @@ -305,5 +306,3 @@ License Licensed under the MIT License. See the LICENSE file for more details. Take a look into the code, to get more information about the possibilities. It's well documented. - -[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/marcj/php-rest-service/trend.png)](https://bitdeli.com/free "Bitdeli Badge")