4 Commits

Author SHA1 Message Date
88bf5a84d8 added into generated client response handlers 2026-05-26 13:45:15 +02:00
808fd747fe fixed empty docblock 2026-04-14 12:33:33 +02:00
70701bb8f1 allowed header Authorization 2026-04-02 08:23:39 +02:00
8f9c744134 added documentation for return 2026-04-01 07:26:06 +02:00
6 changed files with 154 additions and 9 deletions

View File

@ -23,6 +23,7 @@ The HTML help page is also interactive:
- JavaScript stays plain JS. - JavaScript stays plain JS.
- TypeScript stays typed (interfaces + typed method signatures). - 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). - 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. - 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 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. - If you change request metadata or tester behavior, update `README.md` and keep `src/help.tpl.php` aligned with the actual runtime request model.
@ -44,3 +45,4 @@ The HTML help page is also interactive:
## Documentation rule ## Documentation rule
When output format names/flags/URLs change, update `README.md` in the same change set. 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 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.

View File

@ -240,16 +240,29 @@ You can also download generated frontend clients:
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 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: JavaScript usage example:
```js ```js
import backend from './backend.js'; import backend from './backend.js';
backend.bearerSet('your-access-token'); backend.bearerSet('your-access-token');
const removeErrorHandler = backend.addErrorHandler((response, context) => {
console.error(context.method, response.msg);
});
backend.add(1, 2).then((response) => { backend.add(1, 2).then((response) => {
console.log(response.data); console.log(response.data);
}); });
// removeErrorHandler();
``` ```
TypeScript usage example (Vue + TS): TypeScript usage example (Vue + TS):
@ -258,10 +271,15 @@ TypeScript usage example (Vue + TS):
import backend from './backend'; import backend from './backend';
backend.bearerSet('your-access-token'); backend.bearerSet('your-access-token');
const removeResponseHandler = backend.addResponseHandler((response, context) => {
console.log(context.httpStatus, response.status);
});
backend.add(1, 2).then((response) => { backend.add(1, 2).then((response) => {
console.log(response.data); // typed value based on PHP return type 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. 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.

View File

@ -118,6 +118,7 @@ class APIlite
$ref_type = $ref_method->getReturnType(); $ref_type = $ref_method->getReturnType();
$method['return'] = $this->reflectionTypeToNames($ref_type, $ref_method->getDeclaringClass()); $method['return'] = $this->reflectionTypeToNames($ref_type, $ref_method->getDeclaringClass());
$method['return_structure'] = $this->reflectionTypeToStructure($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; $this->methods[] = $method;
} }
@ -271,6 +272,22 @@ class APIlite
return null; 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 private function response(array $arr): void
{ {
ob_clean(); ob_clean();
@ -279,7 +296,7 @@ class APIlite
header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); 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); echo json_encode($arr);
exit; exit;
} }

View File

@ -827,6 +827,7 @@ $renderParamControl = function (string $methodName, array $param) use ($formatDe
<h3 style="margin-bottom: 1rem; color: #2c3e50;">Return:</h3> <h3 style="margin-bottom: 1rem; color: #2c3e50;">Return:</h3>
<?php if (is_string($method['return'])) { ?> <?php if (is_string($method['return'])) { ?>
<code class="parameter-type"><?php echo htmlspecialchars($method['return']); ?></code> <code class="parameter-type"><?php echo htmlspecialchars($method['return']); ?></code>
<?php echo htmlspecialchars((string) $method['return_doc']); ?>
<?php } ?> <?php } ?>
<?php if (is_array($method['return'])) foreach ($method['return'] as $return) { ?> <?php if (is_array($method['return'])) foreach ($method['return'] as $return) { ?>
<code class="parameter-type"><?php echo htmlspecialchars((string) $return); ?></code> <code class="parameter-type"><?php echo htmlspecialchars((string) $return); ?></code>

View File

@ -8,6 +8,8 @@
class <?php echo $this->apiName; ?> { class <?php echo $this->apiName; ?> {
endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
bearerStorageKey = 'apilite_bearer_token'; bearerStorageKey = 'apilite_bearer_token';
responseHandlers = [];
errorHandlers = [];
normalizeBearerToken(token) { normalizeBearerToken(token) {
if (typeof token !== 'string') return null; if (typeof token !== 'string') return null;
@ -58,20 +60,64 @@ class <?php echo $this->apiName; ?> {
storage.setItem(this.bearerStorageKey, token); 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 * General API call
*/ */
call(method, data, callback) { call(method, data, callback) {
var xhttp = new XMLHttpRequest(); var xhttp = new XMLHttpRequest();
var headers = this.getRequestHeaders(); var headers = this.getRequestHeaders();
var api = this;
xhttp.withCredentials = true; xhttp.withCredentials = true;
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function() {
if (this.readyState === 4) { if (this.readyState === 4) {
var response;
if (this.status === 200) { if (this.status === 200) {
if (callback != null) callback(JSON.parse(this.responseText)); response = JSON.parse(this.responseText);
} else { } 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(); var form_data = new FormData();
@ -118,4 +164,4 @@ class <?php echo $this->apiName; ?> {
}; };
export default new <?php echo $this->apiName; ?>(); export default new <?php echo $this->apiName; ?>();

View File

@ -152,11 +152,23 @@ export interface APIliteHelpResponse {
msg: string; 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 APIliteRequestHeaders = Partial<Record<string, string>>;
type APIliteResponseHandler = (response: APIliteResponse, context: APIliteResponseContext) => void;
type APIliteErrorHandler = (response: APIliteErrorResponse, context: APIliteResponseContext) => void;
class <?php echo $this->apiName; ?> { class <?php echo $this->apiName; ?> {
endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
private readonly bearerStorageKey: string = 'apilite_bearer_token'; private readonly bearerStorageKey: string = 'apilite_bearer_token';
private responseHandlers: APIliteResponseHandler[] = [];
private errorHandlers: APIliteErrorHandler[] = [];
private normalizeBearerToken(token: string | null | undefined): string | null { private normalizeBearerToken(token: string | null | undefined): string | null {
if (typeof token !== 'string') { if (typeof token !== 'string') {
@ -219,22 +231,71 @@ class <?php echo $this->apiName; ?> {
storage.setItem(this.bearerStorageKey, normalizedToken); 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( private call(
method: string, method: string,
data: Record<string, unknown>, data: Record<string, unknown>,
callback: (response: APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse) => void callback: (response: APIliteResponse) => void
): void { ): void {
const xhttp = new XMLHttpRequest(); const xhttp = new XMLHttpRequest();
const headers = this.getRequestHeaders(); const headers = this.getRequestHeaders();
const api = this;
xhttp.withCredentials = true; xhttp.withCredentials = true;
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function() {
if (this.readyState === 4) { if (this.readyState === 4) {
let response: APIliteResponse;
if (this.status === 200) { if (this.status === 200) {
const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse; response = JSON.parse(this.responseText) as APIliteResponse;
callback(response);
} else { } 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);
} }
}; };
@ -297,4 +358,4 @@ class <?php echo $this->apiName; ?> {
<?php } ?> <?php } ?>
} }
export default new <?php echo $this->apiName; ?>(); export default new <?php echo $this->apiName; ?>();