diff --git a/src/APIlite.php b/src/APIlite.php new file mode 100644 index 0000000..e2a7149 --- /dev/null +++ b/src/APIlite.php @@ -0,0 +1,258 @@ + + License: GNU GPL-3.0 or later + + Description: A set of tools to simplify the work of creating backend APIs for your frontend projects. + Version: 1.0.0 + + Milestones: + 2025-05-28 07:42 Igor - Created + +*/ + +namespace TPsoft\APIlite; + +class APIlite +{ + + private string $apiName = ''; + private $methods = array(); + + public function __construct() + { + register_shutdown_function(array($this, '_shutdownHandler')); + $this->analyzeClass(); + if (isset($_REQUEST['action'])) { + $this->doAction($_REQUEST['action']); + } else { + if (isset($_REQUEST['format']) && $_REQUEST['format'] == 'html') { + $this->printHelpHTML(); + } else { + $this->printHelpJSON(); + } + } + } + + private function _shutdownHandler() + { + $error = error_get_last(); + if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) { + $this->responseERROR($error['message']); + } + } + + private function analyzeClass(): bool + { + $refClass = new \ReflectionClass($this); + $this->apiName = $refClass->getName(); + $this->methods = array(); + foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $ref_method) { + $method_name = $ref_method->getName(); + if (substr($method_name, 0, 1) == '_') continue; + $method = array( + 'name' => $method_name, + 'doc' => null, + 'description' => null, + 'params' => array(), + 'return' => null + ); + $docComment = $ref_method->getDocComment(); + if ($docComment) { + $method['doc'] = trim($docComment); + $method['description'] = $this->parseDescription($method['doc']); + } + foreach ($ref_method->getParameters() as $ref_param) { + $param = array( + 'name' => $ref_param->getName(), + 'type' => null, + 'optional' => $ref_param->isOptional(), + 'default' => null, + 'doc' => null, + ); + if ($ref_param->hasType()) { + $param['type'] = $ref_param->getType()->getName(); + } + if ($ref_param->isOptional()) { + $param['default'] = $ref_param->getDefaultValue(); + } + $param['doc'] = $this->parseParamDoc($method['doc'], $param['name']); + $method['params'][] = $param; + } + if ($ref_method->hasReturnType()) { + $method['return'] = $ref_method->getReturnType()->getName(); + } + $this->methods[] = $method; + } + return true; + } + + private function parseDescription(string $doc): string + { + $lines = explode("\n", $doc); + $desc = array(); + foreach ($lines as $line) { + $line = trim($line); + $line = trim(trim($line, '/*')); + if (substr($line, 0, 1) == '@') { + break; + } + if (strlen($line) <= 0) { + continue; + } + $desc[] = $line; + } + return implode("\n", $desc); + } + + private function parseParamDoc(string $doc, string $param_name): string|null + { + $lines = explode("\n", $doc); + if (substr($param_name, 0, 1) != '$') { + $param_name = '$' . $param_name; + } + foreach ($lines as $line) { + if (strpos($line, $param_name) !== false) { + return trim(substr($line, strpos($line, $param_name) + strlen($param_name))); + } + } + return null; + } + + private function response(array $arr): void + { + ob_clean(); + header('Content-Type: application/json'); + $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'; + header('Access-Control-Allow-Origin: ' . $origin); + header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); + header('Access-Control-Allow-Headers: Origin, Content-Type, Accept'); + echo json_encode($arr); + exit; + } + + private function responseOK(array $data): void + { + $this->response(array('status' => 'OK', 'data' => $data)); + } + + private function responseERROR(string $error): void + { + $this->response(array('status' => 'ERROR', 'msg' => $error)); + } + + private function printHelpJSON(): void + { + $this->response(array( + 'name' => $this->apiName, + 'html_version' => $this->getCurrentUrl().'?format=html', + 'actions' => $this->methods + )); + } + + private function printHelpHTML(): void + { + include __DIR__ . '/help.tpl.php'; + } + + private function getCurrentUrl(): string { + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || + $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://"; + $host = $_SERVER['HTTP_HOST']; + $uri = $_SERVER['REQUEST_URI']; + $path = parse_url($uri, PHP_URL_PATH); + return $protocol . $host . $path; + } + + private function getMethod(string $action): ?array + { + foreach ($this->methods as $method) { + if ($method['name'] == $action) { + return $method; + } + } + return null; + } + + private function doAction(string $action): void + { + $method = $this->getMethod($action); + if (is_null($method)) { + $this->responseERROR('Action "' . $action . '" not found'); + return; + } + $params = array(); + foreach ($method['params'] as $param) { + if (isset($_REQUEST[$param['name']])) { + $param_value = $_REQUEST[$param['name']]; + switch ($param['type']) { + case 'int': + $param_value = (int) $param_value; + break; + case 'float': + $param_value = (float) $this->fixDecimalPoint($param_value); + break; + case 'string': + $param_value = (string) $param_value; + break; + case 'bool': + $param_value = (bool) $param_value; + break; + case 'array': + $param_value = json_decode($param_value, true); + break; + case 'object': + $param_value = json_decode($param_value); + break; + default: + // do nothing + break; + } + $params[$param['name']] = $param_value; + } else { + if (!$param['optional']) { + $this->responseERROR('Parameter "' . $param['name'] . '" is required'); + return; + } + } + } + try { + $result = call_user_func_array(array($this, $method['name']), $params); + } catch (\Exception $e) { + $this->responseERROR($e->getMessage()); + return; + } + $this->responseOK(array('status' => 'OK', 'data' => $result)); + } + + private function fixDecimalPoint(mixed $number): float|array + { + if (is_array($number)) { + foreach ($number as $index => $val) { + $number[$index] = $this->fixDecimalPoint($val); + } + return $number; + } + $position_comma = strpos($number, ','); + $position_dot = strpos($number, '.'); + if ( + $position_comma !== false + && $position_dot !== false + ) { + if ($position_comma < $position_dot) { // e.g. 2,845,478.55 + $_number = str_replace(',', '', $number); + } else { // e.g. 2.845.478,55 + $_number = str_replace('.', '', $number); + } + $number = $_number; + } + $number = str_replace(' ', '', $number); + $number = str_replace(',', '.', $number); + $pos = strpos($number, '.'); + if ($pos !== false) $number = rtrim($number, '0'); + if (substr($number, -1) == '.') $number .= '0'; + return $number; + } +} diff --git a/src/help.tpl.php b/src/help.tpl.php new file mode 100644 index 0000000..d2bba1a --- /dev/null +++ b/src/help.tpl.php @@ -0,0 +1,37 @@ + + + + + + <?php echo $this->apiName; ?> + + + +

apiName; ?>

+ +

API documentation in JSON

+

API endpoint URL getCurrentUrl(); ?>

+ +methods)) foreach ($this->methods as $index => $method) { ?> + +

+ +

Endpoint URL: getCurrentUrl(); ?>?action=

+ + + +

+ : + — + +

+ + + + + + + \ No newline at end of file diff --git a/test/APIcalculator.php b/test/APIcalculator.php new file mode 100644 index 0000000..2ddc444 --- /dev/null +++ b/test/APIcalculator.php @@ -0,0 +1,126 @@ +