Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 394a85ef45 | |||
| 1f32935a29 |
@ -7,6 +7,9 @@ The project also generates frontend clients:
|
||||
- JavaScript client: `format=javascript` (`.js`)
|
||||
- TypeScript client: `format=typescript` (`.ts`, real typed output)
|
||||
|
||||
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
|
||||
- `src/help.tpl.php`: HTML documentation template
|
||||
@ -19,7 +22,9 @@ The project also generates frontend clients:
|
||||
- Maintain both generated clients:
|
||||
- JavaScript stays plain JS.
|
||||
- TypeScript stays typed (interfaces + typed method signatures).
|
||||
- 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,6 +34,8 @@ 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`
|
||||
|
||||
15
README.md
15
README.md
@ -231,6 +231,8 @@ 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`
|
||||
@ -256,8 +258,21 @@ backend.add(1, 2).then((response) => {
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
135
src/APIlite.php
135
src/APIlite.php
@ -87,7 +87,8 @@ class APIlite
|
||||
'doc' => null,
|
||||
'description' => null,
|
||||
'params' => array(),
|
||||
'return' => null
|
||||
'return' => null,
|
||||
'return_structure' => null
|
||||
);
|
||||
$docComment = $ref_method->getDocComment();
|
||||
if ($docComment) {
|
||||
@ -115,27 +116,129 @@ class APIlite
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
$method['return'] = $this->reflectionTypeToNames($ref_type, $ref_method->getDeclaringClass());
|
||||
$method['return_structure'] = $this->reflectionTypeToStructure($ref_type, $ref_method->getDeclaringClass());
|
||||
}
|
||||
$this->methods[] = $method;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function reflectionTypeToNames(?\ReflectionType $ref_type, ?\ReflectionClass $context_class = null): string|array|null
|
||||
{
|
||||
if ($ref_type instanceof \ReflectionNamedType) {
|
||||
$type_name = $this->resolveTypeName($ref_type->getName(), $context_class);
|
||||
if ($ref_type->allowsNull() && strtolower($type_name) !== 'null' && strtolower($type_name) !== 'mixed') {
|
||||
return array($type_name, 'null');
|
||||
}
|
||||
return $type_name;
|
||||
}
|
||||
if ($ref_type instanceof \ReflectionUnionType || $ref_type instanceof \ReflectionIntersectionType) {
|
||||
$types = array();
|
||||
foreach ($ref_type->getTypes() as $type) {
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
continue;
|
||||
}
|
||||
$one_type = $this->reflectionTypeToNames($type, $context_class);
|
||||
if (is_array($one_type)) {
|
||||
$types = array_merge($types, $one_type);
|
||||
} else if (is_string($one_type)) {
|
||||
$types[] = $one_type;
|
||||
}
|
||||
}
|
||||
$types = array_values(array_unique($types));
|
||||
return empty($types) ? null : $types;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function reflectionTypeToStructure(?\ReflectionType $ref_type, ?\ReflectionClass $context_class = null, array $visited = array()): array|null
|
||||
{
|
||||
if ($ref_type instanceof \ReflectionNamedType) {
|
||||
return $this->classTypeStructure(
|
||||
$this->resolveTypeName($ref_type->getName(), $context_class),
|
||||
$visited
|
||||
);
|
||||
}
|
||||
if ($ref_type instanceof \ReflectionUnionType || $ref_type instanceof \ReflectionIntersectionType) {
|
||||
$structures = array();
|
||||
foreach ($ref_type->getTypes() as $type) {
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
continue;
|
||||
}
|
||||
$structure = $this->reflectionTypeToStructure($type, $context_class, $visited);
|
||||
if (!is_null($structure)) {
|
||||
$structures[] = $structure;
|
||||
}
|
||||
}
|
||||
return count($structures) > 0 ? $structures : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function classTypeStructure(string $type_name, array $visited = array()): array|null
|
||||
{
|
||||
$type_name = ltrim($type_name, '\\');
|
||||
if ($this->isBuiltinTypeName($type_name) || !class_exists($type_name)) {
|
||||
return null;
|
||||
}
|
||||
if (in_array($type_name, $visited, true)) {
|
||||
return array(
|
||||
'type' => $type_name,
|
||||
'recursive' => true,
|
||||
'properties' => array()
|
||||
);
|
||||
}
|
||||
|
||||
$visited[] = $type_name;
|
||||
$refClass = new \ReflectionClass($type_name);
|
||||
$properties = array();
|
||||
foreach ($refClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $ref_property) {
|
||||
if ($ref_property->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
$property_type = $ref_property->getType();
|
||||
$properties[] = array(
|
||||
'name' => $ref_property->getName(),
|
||||
'type' => $this->reflectionTypeToNames($property_type, $refClass),
|
||||
'nullable' => is_null($property_type) ? true : $property_type->allowsNull(),
|
||||
'type_structure' => $this->reflectionTypeToStructure($property_type, $refClass, $visited)
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => $type_name,
|
||||
'properties' => $properties
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveTypeName(string $type_name, ?\ReflectionClass $context_class = null): string
|
||||
{
|
||||
$type_name = ltrim($type_name, '\\');
|
||||
if (is_null($context_class)) {
|
||||
return $type_name;
|
||||
}
|
||||
switch (strtolower($type_name)) {
|
||||
case 'self':
|
||||
case 'static':
|
||||
return $context_class->getName();
|
||||
case 'parent':
|
||||
$parent = $context_class->getParentClass();
|
||||
return $parent ? $parent->getName() : $type_name;
|
||||
default:
|
||||
return $type_name;
|
||||
}
|
||||
}
|
||||
|
||||
private function isBuiltinTypeName(string $type_name): bool
|
||||
{
|
||||
return in_array(strtolower(ltrim($type_name, '\\')), array(
|
||||
'array', 'bool', 'boolean', 'callable', 'closure', 'false', 'float', 'int', 'integer',
|
||||
'iterable', 'mixed', 'never', 'null', 'object', 'resource', 'scalar', 'string',
|
||||
'true', 'void'
|
||||
), true);
|
||||
}
|
||||
|
||||
private function parseDescription(string $doc): string
|
||||
{
|
||||
$lines = explode("\n", $doc);
|
||||
|
||||
1156
src/help.tpl.php
1156
src/help.tpl.php
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,65 @@ $mapUnionType = function (mixed $type) use ($mapType): string {
|
||||
|
||||
return $mapType(is_string($type) ? $type : null);
|
||||
};
|
||||
|
||||
$normalizeTypeStructures = function (mixed $structure): array {
|
||||
if (!is_array($structure)) {
|
||||
return array();
|
||||
}
|
||||
if (array_key_exists('type', $structure) && array_key_exists('properties', $structure)) {
|
||||
return array($structure);
|
||||
}
|
||||
return array_values(array_filter($structure, 'is_array'));
|
||||
};
|
||||
|
||||
$findTypeStructure = function (?string $type, mixed $structure) use ($normalizeTypeStructures): array|null {
|
||||
if (!is_string($type) || $type === '') {
|
||||
return null;
|
||||
}
|
||||
$type = ltrim($type, '\\');
|
||||
foreach ($normalizeTypeStructures($structure) as $one_structure) {
|
||||
if (($one_structure['type'] ?? null) === $type) {
|
||||
return $one_structure;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$mapTypeWithStructure = null;
|
||||
$mapUnionTypeWithStructure = null;
|
||||
|
||||
$mapTypeWithStructure = function (?string $type, mixed $structure) use ($mapType, &$mapUnionTypeWithStructure): string {
|
||||
if (is_array($structure) && array_key_exists('type', $structure) && array_key_exists('properties', $structure)) {
|
||||
$properties = array();
|
||||
foreach (($structure['properties'] ?? array()) as $property) {
|
||||
if (!is_array($property) || !isset($property['name'])) {
|
||||
continue;
|
||||
}
|
||||
$propertyType = $mapUnionTypeWithStructure($property['type'] ?? null, $property['type_structure'] ?? null);
|
||||
if (!empty($property['nullable']) && strpos($propertyType, 'null') === false) {
|
||||
$propertyType .= ' | null';
|
||||
}
|
||||
$properties[] = $property['name'] . ': ' . $propertyType;
|
||||
}
|
||||
return empty($properties) ? 'Record<string, unknown>' : '{ ' . implode('; ', $properties) . ' }';
|
||||
}
|
||||
|
||||
return $mapType($type);
|
||||
};
|
||||
|
||||
$mapUnionTypeWithStructure = function (mixed $type, mixed $structure) use ($mapType, $findTypeStructure, &$mapTypeWithStructure): string {
|
||||
if (is_array($type)) {
|
||||
$parts = array();
|
||||
foreach ($type as $singleType) {
|
||||
$typeName = is_string($singleType) ? $singleType : null;
|
||||
$parts[] = $mapTypeWithStructure($typeName, $findTypeStructure($typeName, $structure));
|
||||
}
|
||||
$parts = array_values(array_unique($parts));
|
||||
return empty($parts) ? 'unknown' : implode(' | ', $parts);
|
||||
}
|
||||
|
||||
return $mapTypeWithStructure(is_string($type) ? $type : null, $structure);
|
||||
};
|
||||
?>
|
||||
/**
|
||||
* Generated by APIlite
|
||||
@ -66,6 +125,20 @@ export interface APIliteMethodDoc {
|
||||
description: string | null;
|
||||
params: APIliteMethodParam[];
|
||||
return: string | string[] | null;
|
||||
return_structure: APIliteTypeStructure | APIliteTypeStructure[] | null;
|
||||
}
|
||||
|
||||
export interface APIliteTypeStructureProperty {
|
||||
name: string;
|
||||
type: string | string[] | null;
|
||||
nullable: boolean;
|
||||
type_structure: APIliteTypeStructure | APIliteTypeStructure[] | null;
|
||||
}
|
||||
|
||||
export interface APIliteTypeStructure {
|
||||
type: string;
|
||||
recursive?: boolean;
|
||||
properties: APIliteTypeStructureProperty[];
|
||||
}
|
||||
|
||||
export interface APIliteHelpResponse {
|
||||
@ -149,7 +222,7 @@ class <?php echo $this->apiName; ?> {
|
||||
$paramsSignature[] = $param['name'] . ($param['optional'] ? '?' : '') . ': ' . $paramType;
|
||||
$paramsPayload[] = $param['name'];
|
||||
}
|
||||
$returnType = $mapUnionType($method['return']);
|
||||
$returnType = $mapUnionTypeWithStructure($method['return'], $method['return_structure'] ?? null);
|
||||
?>
|
||||
<?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); ?> });
|
||||
|
||||
Reference in New Issue
Block a user