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 string $endpoint = ''; private $methods = array(); public function __construct(?string $format = null, ?string $endpoint = null) { register_shutdown_function(array($this, '_shutdownHandler')); $this->endpoint = $endpoint ?? $this->getCurrentUrl(); $this->analyzeClass(); if (isset($_REQUEST['action'])) { $this->doAction($_REQUEST['action']); } else { global $argv; if (isset($_REQUEST['format'])) { $format = $_REQUEST['format']; } if (isset($argv)) { $switches = array_map('strtolower', is_array($argv) ? $argv : []); if (in_array('--html', $switches)) { $format = 'html'; } if (in_array('--typescript', $switches)) { $format = 'typescript'; } } switch ($format) { case 'html': $this->printHelpHTML(); break; case 'typescript': $this->printHelpTypescript(); break; default: $this->printHelpJSON(); break; } } } 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->getShortName(); $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(); } if (!is_null($method['doc'])) { $param['doc'] = $this->parseParamDoc($method['doc'], $param['name']); } $method['params'][] = $param; } if ($ref_method->hasReturnType()) { $ref_type = $ref_method->getReturnType(); if ($ref_type instanceof \ReflectionNamedType) { $method['return'] = $ref_type->getName(); } if ($ref_type instanceof \ReflectionUnionType || $ref_type instanceof \ReflectionIntersectionType ) { $types = $ref_type->getTypes(); $method['return'] = []; foreach ($types as $type) { if ($type instanceof \ReflectionNamedType) { $method['return'][] = $type->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', 'typescript_version' => $this->getCurrentUrl().'?format=typescript', 'actions' => $this->methods )); } private function printHelpHTML(): void { include __DIR__ . '/help.tpl.php'; } private function printHelpTypescript(): void { ob_clean(); header('Content-Type: application/javascript'); header('Content-Disposition: attachment; filename="' . $this->apiName . '.js"'); include __DIR__ . '/typescript.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 { if ($action == '__HELP__') { $this->printHelpJSON(); return; } $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; } }