Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88bf5a84d8 | |||
| 808fd747fe | |||
| 70701bb8f1 | |||
| 8f9c744134 | |||
| c758dc9317 | |||
| 394a85ef45 |
15
AGENTS.md
15
AGENTS.md
@ -4,8 +4,11 @@
|
||||
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)
|
||||
- JavaScript client: `format=javascript` (`.js`, includes `bearerSet()` helper backed by `localStorage`)
|
||||
- TypeScript client: `format=typescript` (`.ts`, real typed output, includes `bearerSet()` helper backed by `localStorage`)
|
||||
|
||||
The HTML help page is also interactive:
|
||||
- HTML help: `format=html` includes endpoint docs, a built-in request tester, and optional Bearer token support stored in browser `localStorage`
|
||||
|
||||
## Core files
|
||||
- `src/APIlite.php`: runtime, routing, docs JSON/HTML/client generation
|
||||
@ -19,7 +22,11 @@ The project also generates frontend clients:
|
||||
- Maintain both generated clients:
|
||||
- JavaScript stays plain JS.
|
||||
- TypeScript stays typed (interfaces + typed method signatures).
|
||||
- Bearer token behavior stays aligned between JavaScript and TypeScript clients (`bearerSet()`, `apilite_bearer_token`, automatic `Authorization: Bearer ...` header).
|
||||
- Response/error handler behavior stays aligned between JavaScript and TypeScript clients (`addResponseHandler()`, `addErrorHandler()`, unsubscribe return value, `(response, context)` arguments, ignored handler exceptions).
|
||||
- Keep the HTML help page usable both as documentation and as a lightweight in-browser tester.
|
||||
- If you change output field names in JSON help, update README and templates consistently.
|
||||
- If you change request metadata or tester behavior, update `README.md` and keep `src/help.tpl.php` aligned with the actual runtime request model.
|
||||
- Prefer small, focused changes over broad rewrites.
|
||||
|
||||
## Verification checklist
|
||||
@ -29,9 +36,13 @@ The project also generates frontend clients:
|
||||
- `php -l src/javascript.tpl.php`
|
||||
- `php -l src/typescript.tpl.php`
|
||||
- `php -l bin/apilite-files`
|
||||
- If HTML help changes, also render it once:
|
||||
- `php test/APIcalculator.php --html`
|
||||
- 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.
|
||||
When Bearer token helper/storage behavior changes, update `README.md` and keep JS/TS client docs aligned with the actual generated templates.
|
||||
When generated client handler behavior changes, update `README.md` and keep JS/TS client docs aligned with the actual generated templates.
|
||||
|
||||
37
README.md
37
README.md
@ -231,19 +231,38 @@ and there is also an HTML version available
|
||||
|
||||
<img src="test/Example.APIcalculator.png" />
|
||||
|
||||
The HTML documentation page also includes a built-in interactive tester. Each endpoint can be expanded with `Try it`, filled directly in the browser, and executed against the current API endpoint. If needed, you can set a shared Bearer token at the top of the page. The token is stored in `localStorage` and is sent as `Authorization: Bearer ...` for test requests made from that page.
|
||||
|
||||
You can also download generated frontend clients:
|
||||
|
||||
* JavaScript: `?format=javascript`
|
||||
* TypeScript (typed for Vue/TS projects): `?format=typescript`
|
||||
|
||||
Both generated clients support a shared Bearer token helper. Call `bearerSet(token)` once, the token is stored in `localStorage` under `apilite_bearer_token`, and subsequent requests automatically send `Authorization: Bearer ...`.
|
||||
|
||||
Both generated clients also support response hooks:
|
||||
|
||||
* `addResponseHandler(handler)` registers a handler called for every API response.
|
||||
* `addErrorHandler(handler)` registers a handler called only when the API response has `status === 'ERROR'`.
|
||||
* Both methods return an unsubscribe function.
|
||||
* Handlers receive `(response, context)`, where `context` contains `method`, `data`, and `httpStatus`.
|
||||
* Handler exceptions are ignored, so they do not change the original API callback or promise result.
|
||||
|
||||
JavaScript usage example:
|
||||
|
||||
```js
|
||||
import backend from './backend.js';
|
||||
|
||||
backend.bearerSet('your-access-token');
|
||||
const removeErrorHandler = backend.addErrorHandler((response, context) => {
|
||||
console.error(context.method, response.msg);
|
||||
});
|
||||
|
||||
backend.add(1, 2).then((response) => {
|
||||
console.log(response.data);
|
||||
});
|
||||
|
||||
// removeErrorHandler();
|
||||
```
|
||||
|
||||
TypeScript usage example (Vue + TS):
|
||||
@ -251,9 +270,16 @@ TypeScript usage example (Vue + TS):
|
||||
```ts
|
||||
import backend from './backend';
|
||||
|
||||
backend.bearerSet('your-access-token');
|
||||
const removeResponseHandler = backend.addResponseHandler((response, context) => {
|
||||
console.log(context.httpStatus, response.status);
|
||||
});
|
||||
|
||||
backend.add(1, 2).then((response) => {
|
||||
console.log(response.data); // typed value based on PHP return type
|
||||
});
|
||||
|
||||
// removeResponseHandler();
|
||||
```
|
||||
|
||||
If a method return type is a PHP class with public properties, JSON help now also includes `return_structure`, and the generated TypeScript client maps that class to an object shape based on those public properties.
|
||||
@ -263,3 +289,14 @@ 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`
|
||||
|
||||
## HTML help tester notes
|
||||
|
||||
The built-in HTML tester currently follows the default APIlite request model:
|
||||
|
||||
* request method is `POST`
|
||||
* endpoint action is sent as query parameter `action`
|
||||
* request fields are sent as top-level form fields
|
||||
* object and array field values are serialized as JSON strings
|
||||
|
||||
Because the current metadata does not explicitly distinguish path params, query params and request body schemas, the HTML tester renders the parts that are reliably available today and keeps the rest minimal.
|
||||
|
||||
@ -118,6 +118,7 @@ class APIlite
|
||||
$ref_type = $ref_method->getReturnType();
|
||||
$method['return'] = $this->reflectionTypeToNames($ref_type, $ref_method->getDeclaringClass());
|
||||
$method['return_structure'] = $this->reflectionTypeToStructure($ref_type, $ref_method->getDeclaringClass());
|
||||
$method['return_doc'] = $this->parseReturnDoc($method['doc']);
|
||||
}
|
||||
$this->methods[] = $method;
|
||||
}
|
||||
@ -271,6 +272,22 @@ class APIlite
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseReturnDoc(?string $doc): string|null
|
||||
{
|
||||
if (is_null($doc) || $doc === '') {
|
||||
return null;
|
||||
}
|
||||
$lines = explode("\n", $doc);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, '@return') !== false) {
|
||||
$ret = trim(substr($line, strpos($line, '@return') + strlen('@return')));
|
||||
if (strlen($ret) <= 0) return null;
|
||||
return trim(stristr($ret, ' '));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function response(array $arr): void
|
||||
{
|
||||
ob_clean();
|
||||
@ -279,7 +296,7 @@ class APIlite
|
||||
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');
|
||||
header('Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization');
|
||||
echo json_encode($arr);
|
||||
exit;
|
||||
}
|
||||
|
||||
1141
src/help.tpl.php
1141
src/help.tpl.php
File diff suppressed because it is too large
Load Diff
@ -7,20 +7,117 @@
|
||||
|
||||
class <?php echo $this->apiName; ?> {
|
||||
endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||
bearerStorageKey = 'apilite_bearer_token';
|
||||
responseHandlers = [];
|
||||
errorHandlers = [];
|
||||
|
||||
normalizeBearerToken(token) {
|
||||
if (typeof token !== 'string') return null;
|
||||
token = token.trim();
|
||||
return token === '' ? null : token;
|
||||
}
|
||||
|
||||
getStorage() {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.localStorage) return window.localStorage;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getBearerToken() {
|
||||
var storage = this.getStorage();
|
||||
if (storage == null) return null;
|
||||
return this.normalizeBearerToken(storage.getItem(this.bearerStorageKey));
|
||||
}
|
||||
|
||||
getRequestHeaders(headers = {}) {
|
||||
var requestHeaders = Object.assign({}, headers);
|
||||
if (typeof requestHeaders.Authorization === 'undefined') {
|
||||
var token = this.getBearerToken();
|
||||
if (token != null) requestHeaders.Authorization = 'Bearer ' + token;
|
||||
}
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
applyHeaders(xhttp, headers) {
|
||||
Object.keys(headers).forEach(key => {
|
||||
var value = headers[key];
|
||||
if (typeof value === 'undefined' || value === null) return;
|
||||
xhttp.setRequestHeader(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
bearerSet(token) {
|
||||
var storage = this.getStorage();
|
||||
if (storage == null) return;
|
||||
token = this.normalizeBearerToken(token);
|
||||
if (token == null) {
|
||||
storage.removeItem(this.bearerStorageKey);
|
||||
return;
|
||||
}
|
||||
storage.setItem(this.bearerStorageKey, token);
|
||||
}
|
||||
|
||||
addResponseHandler(handler) {
|
||||
this.responseHandlers.push(handler);
|
||||
return () => {
|
||||
this.responseHandlers = this.responseHandlers.filter(registeredHandler => registeredHandler !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
addErrorHandler(handler) {
|
||||
this.errorHandlers.push(handler);
|
||||
return () => {
|
||||
this.errorHandlers = this.errorHandlers.filter(registeredHandler => registeredHandler !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
emitResponseHandlers(response, context) {
|
||||
this.responseHandlers.forEach(handler => {
|
||||
try {
|
||||
handler(response, context);
|
||||
} catch (error) {
|
||||
// Response handlers must not change the API request result.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
emitErrorHandlers(response, context) {
|
||||
this.errorHandlers.forEach(handler => {
|
||||
try {
|
||||
handler(response, context);
|
||||
} catch (error) {
|
||||
// Error handlers must not change the API request result.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
emitHandlers(response, context) {
|
||||
this.emitResponseHandlers(response, context);
|
||||
if (response.status === 'ERROR') this.emitErrorHandlers(response, context);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
* General API call
|
||||
*/
|
||||
call(method, data, callback) {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var headers = this.getRequestHeaders();
|
||||
var api = this;
|
||||
xhttp.withCredentials = true;
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState === 4) {
|
||||
var response;
|
||||
if (this.status === 200) {
|
||||
if (callback != null) callback(JSON.parse(this.responseText));
|
||||
response = JSON.parse(this.responseText);
|
||||
} else {
|
||||
if (callback != null) callback({'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status});
|
||||
response = {'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status};
|
||||
}
|
||||
var context = {method: method, data: data, httpStatus: this.status};
|
||||
api.emitHandlers(response, context);
|
||||
if (callback != null) callback(response);
|
||||
}
|
||||
}
|
||||
var form_data = new FormData();
|
||||
@ -31,6 +128,7 @@ class <?php echo $this->apiName; ?> {
|
||||
form_data.append(key, val);
|
||||
});
|
||||
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||
this.applyHeaders(xhttp, headers);
|
||||
xhttp.send(form_data);
|
||||
}
|
||||
|
||||
|
||||
@ -152,24 +152,150 @@ export interface APIliteHelpResponse {
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export type APIliteResponse = APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
||||
|
||||
export interface APIliteResponseContext {
|
||||
method: string;
|
||||
data: Record<string, unknown>;
|
||||
httpStatus: number;
|
||||
}
|
||||
|
||||
type APIliteRequestHeaders = Partial<Record<string, string>>;
|
||||
type APIliteResponseHandler = (response: APIliteResponse, context: APIliteResponseContext) => void;
|
||||
type APIliteErrorHandler = (response: APIliteErrorResponse, context: APIliteResponseContext) => void;
|
||||
|
||||
class <?php echo $this->apiName; ?> {
|
||||
endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||
private readonly bearerStorageKey: string = 'apilite_bearer_token';
|
||||
private responseHandlers: APIliteResponseHandler[] = [];
|
||||
private errorHandlers: APIliteErrorHandler[] = [];
|
||||
|
||||
private normalizeBearerToken(token: string | null | undefined): string | null {
|
||||
if (typeof token !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const normalizedToken = token.trim();
|
||||
return normalizedToken === '' ? null : normalizedToken;
|
||||
}
|
||||
|
||||
private getStorage(): Storage | null {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
|
||||
return window.localStorage;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getBearerToken(): string | null {
|
||||
const storage = this.getStorage();
|
||||
if (storage === null) {
|
||||
return null;
|
||||
}
|
||||
return this.normalizeBearerToken(storage.getItem(this.bearerStorageKey));
|
||||
}
|
||||
|
||||
private getRequestHeaders(headers: APIliteRequestHeaders = {}): APIliteRequestHeaders {
|
||||
const requestHeaders: APIliteRequestHeaders = { ...headers };
|
||||
if (typeof requestHeaders.Authorization === 'undefined') {
|
||||
const token = this.getBearerToken();
|
||||
if (token !== null) {
|
||||
requestHeaders.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
private applyHeaders(xhttp: XMLHttpRequest, headers: APIliteRequestHeaders): void {
|
||||
Object.keys(headers).forEach((key) => {
|
||||
const value = headers[key];
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
return;
|
||||
}
|
||||
xhttp.setRequestHeader(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
bearerSet(token: string | null): void {
|
||||
const storage = this.getStorage();
|
||||
if (storage === null) {
|
||||
return;
|
||||
}
|
||||
const normalizedToken = this.normalizeBearerToken(token);
|
||||
if (normalizedToken === null) {
|
||||
storage.removeItem(this.bearerStorageKey);
|
||||
return;
|
||||
}
|
||||
storage.setItem(this.bearerStorageKey, normalizedToken);
|
||||
}
|
||||
|
||||
addResponseHandler(handler: APIliteResponseHandler): () => void {
|
||||
this.responseHandlers.push(handler);
|
||||
return () => {
|
||||
this.responseHandlers = this.responseHandlers.filter((registeredHandler) => registeredHandler !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
addErrorHandler(handler: APIliteErrorHandler): () => void {
|
||||
this.errorHandlers.push(handler);
|
||||
return () => {
|
||||
this.errorHandlers = this.errorHandlers.filter((registeredHandler) => registeredHandler !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
private emitResponseHandlers(response: APIliteResponse, context: APIliteResponseContext): void {
|
||||
this.responseHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(response, context);
|
||||
} catch {
|
||||
// Response handlers must not change the API request result.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private emitErrorHandlers(response: APIliteErrorResponse, context: APIliteResponseContext): void {
|
||||
this.errorHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(response, context);
|
||||
} catch {
|
||||
// Error handlers must not change the API request result.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isErrorResponse(response: APIliteResponse): response is APIliteErrorResponse {
|
||||
return response.status === 'ERROR';
|
||||
}
|
||||
|
||||
private emitHandlers(response: APIliteResponse, context: APIliteResponseContext): void {
|
||||
this.emitResponseHandlers(response, context);
|
||||
if (this.isErrorResponse(response)) {
|
||||
this.emitErrorHandlers(response, context);
|
||||
}
|
||||
}
|
||||
|
||||
private call(
|
||||
method: string,
|
||||
data: Record<string, unknown>,
|
||||
callback: (response: APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse) => void
|
||||
callback: (response: APIliteResponse) => void
|
||||
): void {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
const headers = this.getRequestHeaders();
|
||||
const api = this;
|
||||
xhttp.withCredentials = true;
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState === 4) {
|
||||
let response: APIliteResponse;
|
||||
if (this.status === 200) {
|
||||
const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
||||
callback(response);
|
||||
response = JSON.parse(this.responseText) as APIliteResponse;
|
||||
} else {
|
||||
callback({ status: 'ERROR', msg: 'HTTP STATUS ' + this.status });
|
||||
response = { status: 'ERROR', msg: 'HTTP STATUS ' + this.status };
|
||||
}
|
||||
const context: APIliteResponseContext = { method, data, httpStatus: this.status };
|
||||
api.emitHandlers(response, context);
|
||||
callback(response);
|
||||
}
|
||||
};
|
||||
|
||||
@ -191,6 +317,7 @@ class <?php echo $this->apiName; ?> {
|
||||
});
|
||||
|
||||
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||
this.applyHeaders(xhttp, headers);
|
||||
xhttp.send(formData);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user