Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88bf5a84d8 | |||
| 808fd747fe | |||
| 70701bb8f1 | |||
| 8f9c744134 | |||
| c758dc9317 |
@ -4,8 +4,8 @@
|
||||
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`
|
||||
@ -22,6 +22,8 @@ The HTML help page is also interactive:
|
||||
- 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.
|
||||
@ -42,3 +44,5 @@ The HTML help page is also interactive:
|
||||
|
||||
## 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.
|
||||
|
||||
24
README.md
24
README.md
@ -238,14 +238,31 @@ 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):
|
||||
@ -253,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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -827,6 +827,7 @@ $renderParamControl = function (string $methodName, array $param) use ($formatDe
|
||||
<h3 style="margin-bottom: 1rem; color: #2c3e50;">Return:</h3>
|
||||
<?php if (is_string($method['return'])) { ?>
|
||||
<code class="parameter-type"><?php echo htmlspecialchars($method['return']); ?></code>
|
||||
<?php echo htmlspecialchars((string) $method['return_doc']); ?>
|
||||
<?php } ?>
|
||||
<?php if (is_array($method['return'])) foreach ($method['return'] as $return) { ?>
|
||||
<code class="parameter-type"><?php echo htmlspecialchars((string) $return); ?></code>
|
||||
|
||||
@ -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