APIlite/src/APIlite.php
igor a59044102c added format and endpoint into constructor,
fixed processing of parameters in methods,
added return for HTML manual,
chaged TypeScript code to class object export as default
2025-06-01 19:19:48 +02:00

310 lines
8.0 KiB
PHP

<?php
/*
Copyright (c) TPsoft.org 2000-2025
Author: Ing. Igor Mino <mino@tpsoft.org>
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;
}
}