Compare commits
13 Commits
b8ef58e132
...
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 951fe36da3 | |||
| d258bcc919 | |||
| fab8efd780 | |||
| 746b06e317 | |||
| 3fa131e1b2 | |||
| c0fd7b3fe5 | |||
| dab07e55ec | |||
| a10e864ba3 | |||
| 9d45bb5ceb | |||
| f1e6e92f46 | |||
| 2d3f8bfdd4 | |||
| 8502508a11 | |||
| a59044102c |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/composer.lock
|
||||||
|
/vendor
|
||||||
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
APIlite is a lightweight PHP library. A class extends `TPsoft\APIlite\APIlite`, public methods become API actions, requests are processed automatically, and responses are returned as JSON.
|
||||||
|
|
||||||
|
The project also generates frontend clients:
|
||||||
|
- JavaScript client: `format=javascript` (`.js`)
|
||||||
|
- TypeScript client: `format=typescript` (`.ts`, real typed output)
|
||||||
|
|
||||||
|
## Core files
|
||||||
|
- `src/APIlite.php`: runtime, routing, docs JSON/HTML/client generation
|
||||||
|
- `src/help.tpl.php`: HTML documentation template
|
||||||
|
- `src/javascript.tpl.php`: JavaScript client template
|
||||||
|
- `src/typescript.tpl.php`: TypeScript client template
|
||||||
|
- `bin/apilite-files`: helper script for bootstrapping files in consumer projects
|
||||||
|
|
||||||
|
## Working rules
|
||||||
|
- Keep API behavior backward compatible unless explicitly requested.
|
||||||
|
- Maintain both generated clients:
|
||||||
|
- JavaScript stays plain JS.
|
||||||
|
- TypeScript stays typed (interfaces + typed method signatures).
|
||||||
|
- If you change output field names in JSON help, update README and templates consistently.
|
||||||
|
- Prefer small, focused changes over broad rewrites.
|
||||||
|
|
||||||
|
## Verification checklist
|
||||||
|
- Run syntax checks on edited PHP files:
|
||||||
|
- `php -l src/APIlite.php`
|
||||||
|
- `php -l src/help.tpl.php`
|
||||||
|
- `php -l src/javascript.tpl.php`
|
||||||
|
- `php -l src/typescript.tpl.php`
|
||||||
|
- `php -l bin/apilite-files`
|
||||||
|
- If client generation changes, test with:
|
||||||
|
- `php test/APIcalculator.php --javascript`
|
||||||
|
- `php test/APIcalculator.php --typescript`
|
||||||
|
|
||||||
|
## Documentation rule
|
||||||
|
When output format names/flags/URLs change, update `README.md` in the same change set.
|
||||||
136
README.md
136
README.md
@ -2,6 +2,44 @@
|
|||||||
|
|
||||||
A set of tools to simplify the work of creating backend APIs for your frontend projects. Includes tutorials, patterns and practical examples for creating projects based on REST APIs.
|
A set of tools to simplify the work of creating backend APIs for your frontend projects. Includes tutorials, patterns and practical examples for creating projects based on REST APIs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download source code and add to your project
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/APIlite/src/APIlite.php';
|
||||||
|
```
|
||||||
|
|
||||||
|
or use composer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require tpsoft/apilite
|
||||||
|
```
|
||||||
|
|
||||||
|
and add to your project
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
class YourAPI extends \TPsoft\APIlite\APIlite {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* My first method for greetings.
|
||||||
|
*
|
||||||
|
* @param string $name Your name
|
||||||
|
* @param int $age Your age
|
||||||
|
* @return string Greetings
|
||||||
|
*/
|
||||||
|
public function myFirstMethod(string $name, int $age): string
|
||||||
|
{
|
||||||
|
return 'Hi, I`m '.$name.' and I`m '.$age.' years old.';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
For example, we create an API for calculator. So we create class `APIcalculator` and store in file `test/APIcalculator.php` where we defined each actions for API as public method.
|
For example, we create an API for calculator. So we create class `APIcalculator` and store in file `test/APIcalculator.php` where we defined each actions for API as public method.
|
||||||
@ -94,6 +132,7 @@ When you run this subfile through the webserver, you will see the JSON documenta
|
|||||||
{
|
{
|
||||||
"name": "APIcalculator",
|
"name": "APIcalculator",
|
||||||
"html_version": "http://localhost/APIlite/test/APIcalculator.php?format=html",
|
"html_version": "http://localhost/APIlite/test/APIcalculator.php?format=html",
|
||||||
|
"javascript_version": "http://localhost/APIlite/test/APIcalculator.php?format=javascript",
|
||||||
"typescript_version": "http://localhost/APIlite/test/APIcalculator.php?format=typescript",
|
"typescript_version": "http://localhost/APIlite/test/APIcalculator.php?format=typescript",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
@ -192,84 +231,33 @@ and there is also an HTML version available
|
|||||||
|
|
||||||
<img src="test/Example.APIcalculator.png" />
|
<img src="test/Example.APIcalculator.png" />
|
||||||
|
|
||||||
To connect to the API from TypeScript (e.g. Vue application) it is possible to download the backend script
|
You can also download generated frontend clients:
|
||||||
|
|
||||||
```ts
|
* JavaScript: `?format=javascript`
|
||||||
/**
|
* TypeScript (typed for Vue/TS projects): `?format=typescript`
|
||||||
* Generated by APIlite
|
|
||||||
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
|
||||||
*
|
|
||||||
* 2025-05-28 15:44:07 */
|
|
||||||
|
|
||||||
export const backend = {
|
JavaScript usage example:
|
||||||
endpont: window.location.origin + "APIcalculator.php",
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
```js
|
||||||
* General API call
|
import backend from './backend.js';
|
||||||
*/
|
|
||||||
call(method, data, callback) {
|
backend.add(1, 2).then((response) => {
|
||||||
var xhttp = new XMLHttpRequest();
|
console.log(response.data);
|
||||||
xhttp.withCredentials = true;
|
|
||||||
xhttp.onreadystatechange = function() {
|
|
||||||
if (this.readyState == 4) {
|
|
||||||
if (this.status == 200) {
|
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
|
||||||
} else {
|
|
||||||
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var form_data = new FormData();
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
let val = data[key];
|
|
||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
|
||||||
form_data.append(key, val);
|
|
||||||
});
|
});
|
||||||
xhttp.open('POST', this.endpont + '?action=' + method);
|
|
||||||
xhttp.send(form_data);
|
|
||||||
},
|
|
||||||
|
|
||||||
callPromise(method, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.call(method, data, function(response) {
|
|
||||||
if (response.status == 'OK') {
|
|
||||||
resolve(response.data);
|
|
||||||
} else {
|
|
||||||
reject(response.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
|
||||||
* API actions
|
|
||||||
*/
|
|
||||||
help() {
|
|
||||||
return this.callPromise('__HELP__', {});
|
|
||||||
},
|
|
||||||
|
|
||||||
add(a, b) {
|
|
||||||
return this.callPromise('add', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
subtract(a, b) {
|
|
||||||
return this.callPromise('subtract', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
multiply(a, b) {
|
|
||||||
return this.callPromise('multiply', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
divide(a, b) {
|
|
||||||
return this.callPromise('divide', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
These outputs can also be generated in the command line as follows
|
TypeScript usage example (Vue + TS):
|
||||||
|
|
||||||
* for HTML`$> php APIcalculator.php --html`
|
```ts
|
||||||
* for TypeScript`$> php APIcalculator.php --typescript`
|
import backend from './backend';
|
||||||
|
|
||||||
|
backend.add(1, 2).then((response) => {
|
||||||
|
console.log(response.data); // typed value based on PHP return type
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
These outputs can also be generated in command line:
|
||||||
|
|
||||||
|
* HTML: `$> php APIcalculator.php --html`
|
||||||
|
* JavaScript: `$> php APIcalculator.php --javascript > backend.js` (`--js` alias is available)
|
||||||
|
* TypeScript: `$> php APIcalculator.php --typescript > backend.ts`
|
||||||
|
|||||||
129
bin/apilite-files
Normal file
129
bin/apilite-files
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
echo "ℹ️ APIlite files\n\n";
|
||||||
|
|
||||||
|
$project_root = findProjectRoot(__DIR__);
|
||||||
|
|
||||||
|
if (!$project_root) {
|
||||||
|
echo "⛔ Project root not found\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$composer_arr = json_decode(file_get_contents($project_root . '/composer.json'), true);
|
||||||
|
|
||||||
|
echo "Project root: {$project_root}\n";
|
||||||
|
$source_dirs = array_values($composer_arr['autoload']['psr-4']);
|
||||||
|
foreach ($source_dirs as $index => $source_dir) {
|
||||||
|
echo "➡️ [$index]: $project_root/$source_dir\n";
|
||||||
|
}
|
||||||
|
$selected = readline("❔ Select source dir [default: 0]: ");
|
||||||
|
$selected = empty($selected) ? 0 : $selected;
|
||||||
|
if (!isset($source_dirs[$selected])) {
|
||||||
|
echo "⛔ Invalid selection\n";
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
$source_dir = $project_root . '/' . $source_dirs[$selected];
|
||||||
|
$source_dir = rtrim($source_dir, '/') . '/';
|
||||||
|
$namespaces = array_keys($composer_arr['autoload']['psr-4']);
|
||||||
|
$namespace = $namespaces[$selected];
|
||||||
|
echo "✔️ Source dir: {$source_dir} with namespace: {$namespace}\n";
|
||||||
|
|
||||||
|
// Copy files
|
||||||
|
echo "ℹ️ Copy files\n";
|
||||||
|
|
||||||
|
copyFile($source_dir . 'API.php', '<' . '?php
|
||||||
|
|
||||||
|
namespace ' . rtrim($namespace, '\\') . ';
|
||||||
|
|
||||||
|
use TPsoft\APIlite\APIlite;
|
||||||
|
|
||||||
|
class API extends APIlite {
|
||||||
|
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
copyFile($project_root . '/scripts/buildTypeScript.php', '<' . '?php
|
||||||
|
|
||||||
|
require __DIR__ . \'/../vendor/autoload.php\';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$backend_api = new TPsoft\BugreportBackend\API(\'typescript\', \'import.meta.env.VITE_BACKENDAPI_URL\', \'backend\');
|
||||||
|
|
||||||
|
$output = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
$ts_path = realpath(__DIR__ . \'/../../frontend/src\').\'/backend.ts\';
|
||||||
|
$suc = file_put_contents($ts_path, $output);
|
||||||
|
if ($suc === false) {
|
||||||
|
echo "✗ TypeScript store into file failed\n";
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TypeScript backend script created\n";
|
||||||
|
');
|
||||||
|
|
||||||
|
copyFile($project_root . '/public/API.php', '<' . '?php
|
||||||
|
|
||||||
|
require_once __DIR__.\'/../src/API.php\';
|
||||||
|
new \\' . $namespace . 'API();
|
||||||
|
');
|
||||||
|
|
||||||
|
// Change composer.json
|
||||||
|
if (!isset($composer_arr['scripts']['build'])) {
|
||||||
|
$composer_arr['scripts']['build'] = 'php scripts/buildTypeScript.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$composer_json = json_encode($composer_arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$composer_json = preg_replace_callback(
|
||||||
|
'/^( +)/m',
|
||||||
|
fn($m) => str_repeat("\t", intdiv(strlen($m[1]), 4)),
|
||||||
|
$composer_json
|
||||||
|
);
|
||||||
|
$suc = file_put_contents($project_root . '/composer.json', $composer_json);
|
||||||
|
if ($suc) {
|
||||||
|
echo "✅ File changed: $project_root/composer.json\n";
|
||||||
|
} else {
|
||||||
|
echo "🔴 Failed to change file: $project_root/composer.json\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions
|
||||||
|
*/
|
||||||
|
function findProjectRoot(string $startDir): ?string
|
||||||
|
{
|
||||||
|
$dir = realpath($startDir);
|
||||||
|
if (!$dir) return null;
|
||||||
|
while ($dir && $dir !== dirname($dir)) {
|
||||||
|
if (is_file($dir . '/vendor/autoload.php')) {
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
$dir = dirname($dir);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFile(string $destination, string $content): bool
|
||||||
|
{
|
||||||
|
$dir = dirname($destination);
|
||||||
|
if (!file_exists($dir)) {
|
||||||
|
mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
if (file_exists($destination)) {
|
||||||
|
echo "⚠️ File already exists: $destination\n";
|
||||||
|
$confirm = readline("❔ Overwrite? (y - yes, other - no): ");
|
||||||
|
if ($confirm != 'y') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$suc = file_put_contents($destination, $content) !== false;
|
||||||
|
if ($suc) {
|
||||||
|
echo "✅ File created: $destination\n";
|
||||||
|
} else {
|
||||||
|
echo "🔴 Failed to create file: $destination\n";
|
||||||
|
}
|
||||||
|
return $suc;
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@
|
|||||||
"rest",
|
"rest",
|
||||||
"json",
|
"json",
|
||||||
"php",
|
"php",
|
||||||
|
"javascript",
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
@ -32,5 +33,8 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"TPsoft\\APIlite\\": "src/"
|
"TPsoft\\APIlite\\": "src/"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/apilite-files"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,23 +18,48 @@ class APIlite
|
|||||||
{
|
{
|
||||||
|
|
||||||
private string $apiName = '';
|
private string $apiName = '';
|
||||||
|
private string $endpoint = '';
|
||||||
private $methods = array();
|
private $methods = array();
|
||||||
|
|
||||||
public function __construct()
|
public function __construct(?string $format = null, ?string $endpoint = null, ?string $apiName = null)
|
||||||
{
|
{
|
||||||
register_shutdown_function(array($this, '_shutdownHandler'));
|
register_shutdown_function(array($this, '_shutdownHandler'));
|
||||||
|
$this->endpoint = $endpoint ?? $this->getCurrentUrl();
|
||||||
|
$this->apiName = $apiName ?? '';
|
||||||
$this->analyzeClass();
|
$this->analyzeClass();
|
||||||
if (isset($_REQUEST['action'])) {
|
if (isset($_REQUEST['action'])) {
|
||||||
$this->doAction($_REQUEST['action']);
|
$this->doAction($_REQUEST['action']);
|
||||||
} else {
|
} else {
|
||||||
global $argv;
|
global $argv;
|
||||||
|
if (isset($_REQUEST['format'])) {
|
||||||
|
$format = $_REQUEST['format'];
|
||||||
|
}
|
||||||
|
if (isset($argv)) {
|
||||||
$switches = array_map('strtolower', is_array($argv) ? $argv : []);
|
$switches = array_map('strtolower', is_array($argv) ? $argv : []);
|
||||||
if ((isset($_REQUEST['format']) && $_REQUEST['format'] == 'html') || in_array('--html', $switches)) {
|
if (in_array('--html', $switches)) {
|
||||||
|
$format = 'html';
|
||||||
|
}
|
||||||
|
if (in_array('--javascript', $switches) || in_array('--js', $switches)) {
|
||||||
|
$format = 'javascript';
|
||||||
|
}
|
||||||
|
if (in_array('--typescript', $switches)) {
|
||||||
|
$format = 'typescript';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch ($format) {
|
||||||
|
case 'html':
|
||||||
$this->printHelpHTML();
|
$this->printHelpHTML();
|
||||||
} elseif ((isset($_REQUEST['format']) && $_REQUEST['format'] == 'typescript') || in_array('--typescript', $switches)) {
|
break;
|
||||||
|
case 'javascript':
|
||||||
|
case 'js':
|
||||||
|
$this->printHelpJavascript();
|
||||||
|
break;
|
||||||
|
case 'typescript':
|
||||||
$this->printHelpTypescript();
|
$this->printHelpTypescript();
|
||||||
} else {
|
break;
|
||||||
|
default:
|
||||||
$this->printHelpJSON();
|
$this->printHelpJSON();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +75,9 @@ class APIlite
|
|||||||
private function analyzeClass(): bool
|
private function analyzeClass(): bool
|
||||||
{
|
{
|
||||||
$refClass = new \ReflectionClass($this);
|
$refClass = new \ReflectionClass($this);
|
||||||
$this->apiName = $refClass->getName();
|
if (strlen($this->apiName) <= 0) {
|
||||||
|
$this->apiName = $refClass->getShortName();
|
||||||
|
}
|
||||||
$this->methods = array();
|
$this->methods = array();
|
||||||
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $ref_method) {
|
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $ref_method) {
|
||||||
$method_name = $ref_method->getName();
|
$method_name = $ref_method->getName();
|
||||||
@ -81,11 +108,28 @@ class APIlite
|
|||||||
if ($ref_param->isOptional()) {
|
if ($ref_param->isOptional()) {
|
||||||
$param['default'] = $ref_param->getDefaultValue();
|
$param['default'] = $ref_param->getDefaultValue();
|
||||||
}
|
}
|
||||||
|
if (!is_null($method['doc'])) {
|
||||||
$param['doc'] = $this->parseParamDoc($method['doc'], $param['name']);
|
$param['doc'] = $this->parseParamDoc($method['doc'], $param['name']);
|
||||||
|
}
|
||||||
$method['params'][] = $param;
|
$method['params'][] = $param;
|
||||||
}
|
}
|
||||||
if ($ref_method->hasReturnType()) {
|
if ($ref_method->hasReturnType()) {
|
||||||
$method['return'] = $ref_method->getReturnType()->getName();
|
$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;
|
$this->methods[] = $method;
|
||||||
}
|
}
|
||||||
@ -152,6 +196,7 @@ class APIlite
|
|||||||
$this->response(array(
|
$this->response(array(
|
||||||
'name' => $this->apiName,
|
'name' => $this->apiName,
|
||||||
'html_version' => $this->getCurrentUrl().'?format=html',
|
'html_version' => $this->getCurrentUrl().'?format=html',
|
||||||
|
'javascript_version' => $this->getCurrentUrl().'?format=javascript',
|
||||||
'typescript_version' => $this->getCurrentUrl().'?format=typescript',
|
'typescript_version' => $this->getCurrentUrl().'?format=typescript',
|
||||||
'actions' => $this->methods
|
'actions' => $this->methods
|
||||||
));
|
));
|
||||||
@ -162,20 +207,36 @@ class APIlite
|
|||||||
include __DIR__ . '/help.tpl.php';
|
include __DIR__ . '/help.tpl.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function printHelpTypescript(): void
|
private function printHelpJavascript(): void
|
||||||
{
|
{
|
||||||
ob_clean();
|
ob_clean();
|
||||||
header('Content-Type: application/javascript');
|
header('Content-Type: application/javascript');
|
||||||
header('Content-Disposition: attachment; filename="' . $this->apiName . '.js"');
|
header('Content-Disposition: attachment; filename="' . $this->apiName . '.js"');
|
||||||
|
include __DIR__ . '/javascript.tpl.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function printHelpTypescript(): void
|
||||||
|
{
|
||||||
|
ob_clean();
|
||||||
|
header('Content-Type: application/typescript');
|
||||||
|
header('Content-Disposition: attachment; filename="' . $this->apiName . '.ts"');
|
||||||
include __DIR__ . '/typescript.tpl.php';
|
include __DIR__ . '/typescript.tpl.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCurrentUrl(): string {
|
private function getCurrentUrl(): string {
|
||||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ||
|
$https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||||
$_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
$serverPort = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null;
|
||||||
$host = $_SERVER['HTTP_HOST'];
|
$protocol = ($https || $serverPort === 443) ? 'https://' : 'http://';
|
||||||
$uri = $_SERVER['REQUEST_URI'];
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? ($_SERVER['SCRIPT_NAME'] ?? '/');
|
||||||
$path = parse_url($uri, PHP_URL_PATH);
|
$path = parse_url($uri, PHP_URL_PATH);
|
||||||
|
if (!is_string($path) || $path === '') {
|
||||||
|
$path = '/';
|
||||||
|
}
|
||||||
|
$path = str_replace('\\', '/', $path);
|
||||||
|
if (substr($path, 0, 1) !== '/') {
|
||||||
|
$path = '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
return $protocol . $host . $path;
|
return $protocol . $host . $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
159
src/help.tpl.php
159
src/help.tpl.php
@ -236,6 +236,140 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Methods grid */
|
||||||
|
.methods-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.method-card:nth-child(odd) {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card:nth-child(even) {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card:nth-child(3n) {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card:nth-child(4n) {
|
||||||
|
background: linear-gradient(135deg,rgb(86, 209, 127) 0%,rgb(52, 199, 172) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card:nth-child(5n) {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
.method-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.methods-section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methods-section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 80px;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for methods grid */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.methods-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card {
|
||||||
|
padding: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card a {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methods-section-title {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.methods-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-card a {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methods-section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Utility classes */
|
/* Utility classes */
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -262,10 +396,23 @@
|
|||||||
<nav class="nav-menu">
|
<nav class="nav-menu">
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="<?php echo $this->getCurrentUrl(); ?>?format=json">🔗 JSON Documentation</a>
|
<a href="<?php echo $this->getCurrentUrl(); ?>?format=json">🔗 JSON Documentation</a>
|
||||||
|
<a href="<?php echo $this->getCurrentUrl(); ?>?format=javascript">📄 JavaScript backend script</a>
|
||||||
<a href="<?php echo $this->getCurrentUrl(); ?>?format=typescript">📄 TypeScript backend script</a>
|
<a href="<?php echo $this->getCurrentUrl(); ?>?format=typescript">📄 TypeScript backend script</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- List of Methods -->
|
||||||
|
<section class="method-section">
|
||||||
|
<h2 class="methods-section-title">List of Methods</h2>
|
||||||
|
<div class="methods-grid">
|
||||||
|
<?php if (is_array($this->methods)) foreach ($this->methods as $index => $method) { ?>
|
||||||
|
<div class="method-card">
|
||||||
|
<a href="#<?php echo $method['name']; ?>"><?php echo $method['name']; ?></a>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="endpoint-url mb-3">
|
<div class="endpoint-url mb-3">
|
||||||
@ -274,7 +421,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (is_array($this->methods)) foreach ($this->methods as $index => $method) { ?>
|
<?php if (is_array($this->methods)) foreach ($this->methods as $index => $method) { ?>
|
||||||
<section class="method-section">
|
<section class="method-section" id="<?php echo $method['name']; ?>">
|
||||||
<h2 class="method-title"><?php echo $method['name']; ?></h2>
|
<h2 class="method-title"><?php echo $method['name']; ?></h2>
|
||||||
|
|
||||||
<?php if (!empty($method['description'])) { ?>
|
<?php if (!empty($method['description'])) { ?>
|
||||||
@ -306,6 +453,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
|
<h3 style="margin-bottom: 1rem; color: #2c3e50; display: inline-block;">Return:</h3>
|
||||||
|
<?php if (is_string($method['return'])) { ?>
|
||||||
|
<code class="parameter-type"><?php echo $method['return']; ?></code>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if (is_array($method['return'])) foreach ($method['return'] as $return) { ?>
|
||||||
|
<code class="parameter-type"><?php echo $return; ?></code>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
69
src/javascript.tpl.php
Normal file
69
src/javascript.tpl.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Generated by APIlite
|
||||||
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
|
*
|
||||||
|
* <?php echo date('Y-m-d H:i:s'); ?>
|
||||||
|
*/
|
||||||
|
|
||||||
|
class <?php echo $this->apiName; ?> {
|
||||||
|
endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||||
|
|
||||||
|
/* ----------------------------------------------------
|
||||||
|
* General API call
|
||||||
|
*/
|
||||||
|
call(method, data, callback) {
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.withCredentials = true;
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState === 4) {
|
||||||
|
if (this.status === 200) {
|
||||||
|
if (callback != null) callback(JSON.parse(this.responseText));
|
||||||
|
} else {
|
||||||
|
if (callback != null) callback({'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var form_data = new FormData();
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
let val = data[key];
|
||||||
|
if (typeof val === 'undefined') return;
|
||||||
|
if (typeof val == 'object') val = JSON.stringify(val);
|
||||||
|
form_data.append(key, val);
|
||||||
|
});
|
||||||
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
|
xhttp.send(form_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
callPromise(method, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.call(method, data, function(response) {
|
||||||
|
if (method === '__HELP__') {
|
||||||
|
resolve(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
resolve(response.data);
|
||||||
|
} else {
|
||||||
|
reject(response.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------
|
||||||
|
* API actions
|
||||||
|
*/
|
||||||
|
help() {
|
||||||
|
return this.callPromise('__HELP__', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
<?php if (is_array($this->methods)) foreach ($this->methods as $method) {
|
||||||
|
echo "\t".$method['name'].'('.implode(', ', array_map(function($param) { return $param['name']; }, $method['params'])).') {';
|
||||||
|
echo "\n\t\treturn this.callPromise('".$method['name']."', {".implode(', ', array_map(function($param) { return $param['name'].': '.$param['name']; }, $method['params']))."});";
|
||||||
|
echo "\n\t}\n\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new <?php echo $this->apiName; ?>();
|
||||||
@ -1,3 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
$mapType = function (?string $type): string {
|
||||||
|
if (!is_string($type) || $type === '') {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = strtolower(ltrim($type, '\\'));
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
'int', 'integer', 'float', 'double' => 'number',
|
||||||
|
'string' => 'string',
|
||||||
|
'bool', 'boolean' => 'boolean',
|
||||||
|
'array', 'iterable' => 'unknown[]',
|
||||||
|
'object', 'stdclass' => 'Record<string, unknown>',
|
||||||
|
'mixed', 'resource' => 'unknown',
|
||||||
|
'null', 'void' => 'null',
|
||||||
|
'scalar' => 'string | number | boolean',
|
||||||
|
'callable', 'closure' => '(...args: unknown[]) => unknown',
|
||||||
|
'never' => 'never',
|
||||||
|
default => 'unknown',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$mapUnionType = function (mixed $type) use ($mapType): string {
|
||||||
|
if (is_array($type)) {
|
||||||
|
$parts = array();
|
||||||
|
foreach ($type as $singleType) {
|
||||||
|
$parts[] = $mapType(is_string($singleType) ? $singleType : null);
|
||||||
|
}
|
||||||
|
$parts = array_values(array_unique($parts));
|
||||||
|
return empty($parts) ? 'unknown' : implode(' | ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapType(is_string($type) ? $type : null);
|
||||||
|
};
|
||||||
|
?>
|
||||||
/**
|
/**
|
||||||
* Generated by APIlite
|
* Generated by APIlite
|
||||||
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
@ -5,58 +41,121 @@
|
|||||||
* <?php echo date('Y-m-d H:i:s'); ?>
|
* <?php echo date('Y-m-d H:i:s'); ?>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const backend = {
|
export interface APIliteActionResponse<T> {
|
||||||
endpont: window.location.origin + "<?php echo $this->apiName; ?>.php",
|
status: 'OK';
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
export interface APIliteErrorResponse {
|
||||||
* General API call
|
status: 'ERROR';
|
||||||
*/
|
msg: string;
|
||||||
call(method, data, callback) {
|
}
|
||||||
var xhttp = new XMLHttpRequest();
|
|
||||||
|
export interface APIliteMethodParam {
|
||||||
|
name: string;
|
||||||
|
type: string | null;
|
||||||
|
optional: boolean;
|
||||||
|
default: unknown;
|
||||||
|
doc: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteMethodDoc {
|
||||||
|
name: string;
|
||||||
|
doc: string | null;
|
||||||
|
description: string | null;
|
||||||
|
params: APIliteMethodParam[];
|
||||||
|
return: string | string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteHelpResponse {
|
||||||
|
name: string;
|
||||||
|
html_version: string;
|
||||||
|
javascript_version: string;
|
||||||
|
typescript_version: string;
|
||||||
|
actions: APIliteMethodDoc[];
|
||||||
|
status: string;
|
||||||
|
data: string;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class <?php echo $this->apiName; ?> {
|
||||||
|
endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||||
|
|
||||||
|
private call(
|
||||||
|
method: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
callback: (response: APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse) => void
|
||||||
|
): void {
|
||||||
|
const xhttp = new XMLHttpRequest();
|
||||||
xhttp.withCredentials = true;
|
xhttp.withCredentials = true;
|
||||||
xhttp.onreadystatechange = function() {
|
xhttp.onreadystatechange = function() {
|
||||||
if (this.readyState == 4) {
|
if (this.readyState === 4) {
|
||||||
if (this.status == 200) {
|
if (this.status === 200) {
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
||||||
|
callback(response);
|
||||||
} else {
|
} else {
|
||||||
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
callback({ status: 'ERROR', msg: 'HTTP STATUS ' + this.status });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
var form_data = new FormData();
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
let val = data[key];
|
|
||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
|
||||||
form_data.append(key, val);
|
|
||||||
});
|
|
||||||
xhttp.open('POST', this.endpont + '?action=' + method);
|
|
||||||
xhttp.send(form_data);
|
|
||||||
},
|
|
||||||
|
|
||||||
callPromise(method, data) {
|
const formData = new FormData();
|
||||||
return new Promise((resolve, reject) => {
|
Object.keys(data).forEach((key) => {
|
||||||
this.call(method, data, function(response) {
|
const rawValue = data[key];
|
||||||
if (response.status == 'OK') {
|
if (typeof rawValue === 'undefined') {
|
||||||
resolve(response.data);
|
return;
|
||||||
|
}
|
||||||
|
let value: string | Blob;
|
||||||
|
if (rawValue instanceof Blob) {
|
||||||
|
value = rawValue;
|
||||||
|
} else if (typeof rawValue === 'object' && rawValue !== null) {
|
||||||
|
value = JSON.stringify(rawValue);
|
||||||
} else {
|
} else {
|
||||||
|
value = String(rawValue);
|
||||||
|
}
|
||||||
|
formData.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
|
xhttp.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private callPromise<T>(method: string, data: Record<string, unknown>): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
this.call(method, data, (response) => {
|
||||||
|
if (method === '__HELP__') {
|
||||||
|
resolve(response as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
resolve(response.data as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
reject(response.msg);
|
reject(response.msg);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
help(): Promise<APIliteHelpResponse> {
|
||||||
* API actions
|
return this.callPromise<APIliteHelpResponse>('__HELP__', {});
|
||||||
*/
|
}
|
||||||
help() {
|
|
||||||
return this.callPromise('__HELP__', {});
|
|
||||||
},
|
|
||||||
|
|
||||||
<?php if (is_array($this->methods)) foreach ($this->methods as $method) {
|
<?php if (is_array($this->methods)) foreach ($this->methods as $method) {
|
||||||
echo "\t".$method['name'].'('.implode(', ', array_map(function($param) { return $param['name']; }, $method['params'])).') {';
|
$paramsSignature = array();
|
||||||
echo "\n\t\treturn this.callPromise('".$method['name']."', {".implode(', ', array_map(function($param) { return $param['name'].': '.$param['name']; }, $method['params']))."});";
|
$paramsPayload = array();
|
||||||
echo "\n\t},\n\n";
|
foreach ($method['params'] as $param) {
|
||||||
|
$paramType = $mapType($param['type']);
|
||||||
|
$paramsSignature[] = $param['name'] . ($param['optional'] ? '?' : '') . ': ' . $paramType;
|
||||||
|
$paramsPayload[] = $param['name'];
|
||||||
}
|
}
|
||||||
|
$returnType = $mapUnionType($method['return']);
|
||||||
?>
|
?>
|
||||||
|
<?php echo $method['name']; ?>(<?php echo implode(', ', $paramsSignature); ?>): Promise<APIliteActionResponse<<?php echo $returnType; ?>>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<<?php echo $returnType; ?>>>('<?php echo $method['name']; ?>', { <?php echo implode(', ', $paramsPayload); ?> });
|
||||||
|
}
|
||||||
|
|
||||||
};
|
<?php } ?>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new <?php echo $this->apiName; ?>();
|
||||||
|
|||||||
Reference in New Issue
Block a user