Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f32935a29 | |||
| 951fe36da3 |
@ -256,6 +256,8 @@ 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:
|
These outputs can also be generated in command line:
|
||||||
|
|
||||||
* HTML: `$> php APIcalculator.php --html`
|
* HTML: `$> php APIcalculator.php --html`
|
||||||
|
|||||||
135
src/APIlite.php
135
src/APIlite.php
@ -87,7 +87,8 @@ class APIlite
|
|||||||
'doc' => null,
|
'doc' => null,
|
||||||
'description' => null,
|
'description' => null,
|
||||||
'params' => array(),
|
'params' => array(),
|
||||||
'return' => null
|
'return' => null,
|
||||||
|
'return_structure' => null
|
||||||
);
|
);
|
||||||
$docComment = $ref_method->getDocComment();
|
$docComment = $ref_method->getDocComment();
|
||||||
if ($docComment) {
|
if ($docComment) {
|
||||||
@ -115,27 +116,129 @@ class APIlite
|
|||||||
}
|
}
|
||||||
if ($ref_method->hasReturnType()) {
|
if ($ref_method->hasReturnType()) {
|
||||||
$ref_type = $ref_method->getReturnType();
|
$ref_type = $ref_method->getReturnType();
|
||||||
if ($ref_type instanceof \ReflectionNamedType) {
|
$method['return'] = $this->reflectionTypeToNames($ref_type, $ref_method->getDeclaringClass());
|
||||||
$method['return'] = $ref_type->getName();
|
$method['return_structure'] = $this->reflectionTypeToStructure($ref_type, $ref_method->getDeclaringClass());
|
||||||
}
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
return true;
|
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
|
private function parseDescription(string $doc): string
|
||||||
{
|
{
|
||||||
$lines = explode("\n", $doc);
|
$lines = explode("\n", $doc);
|
||||||
|
|||||||
@ -1,3 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
$formatTypeLabel = function (mixed $type): string {
|
||||||
|
if (is_array($type)) {
|
||||||
|
return implode(' | ', array_map(function ($item) {
|
||||||
|
return is_string($item) ? $item : 'mixed';
|
||||||
|
}, $type));
|
||||||
|
}
|
||||||
|
return is_string($type) ? $type : 'mixed';
|
||||||
|
};
|
||||||
|
|
||||||
|
$renderTypeStructure = function (mixed $structure) use (&$renderTypeStructure, $formatTypeLabel): string {
|
||||||
|
if (!is_array($structure)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$structures = (array_key_exists('type', $structure) && array_key_exists('properties', $structure))
|
||||||
|
? array($structure)
|
||||||
|
: $structure;
|
||||||
|
$html = '';
|
||||||
|
foreach ($structures as $item) {
|
||||||
|
if (!is_array($item) || !isset($item['type'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$html .= '<div style="margin-top: 1rem; padding: 1rem; background: #f8fafc; border: 1px solid #e1e5e9; border-radius: 8px;">';
|
||||||
|
$html .= '<div style="font-weight: 600; margin-bottom: 0.75rem;">' . htmlspecialchars($item['type']) . '</div>';
|
||||||
|
if (!empty($item['recursive'])) {
|
||||||
|
$html .= '<div style="color: #64748b;">Recursive reference</div>';
|
||||||
|
} else if (!empty($item['properties']) && is_array($item['properties'])) {
|
||||||
|
$html .= '<ul style="margin: 0; padding-left: 1.25rem;">';
|
||||||
|
foreach ($item['properties'] as $property) {
|
||||||
|
if (!is_array($property) || !isset($property['name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$html .= '<li style="margin-bottom: 0.5rem;">';
|
||||||
|
$html .= htmlspecialchars((string) $property['name']);
|
||||||
|
$html .= '<code class="parameter-type">' . htmlspecialchars($formatTypeLabel($property['type'] ?? null)) . '</code>';
|
||||||
|
$html .= $renderTypeStructure($property['type_structure'] ?? null);
|
||||||
|
$html .= '</li>';
|
||||||
|
}
|
||||||
|
$html .= '</ul>';
|
||||||
|
} else {
|
||||||
|
$html .= '<div style="color: #64748b;">No public properties</div>';
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
};
|
||||||
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
@ -461,6 +508,7 @@
|
|||||||
<?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 $return; ?></code>
|
<code class="parameter-type"><?php echo $return; ?></code>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
<?php echo $renderTypeStructure($method['return_structure'] ?? null); ?>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,6 +33,65 @@ $mapUnionType = function (mixed $type) use ($mapType): string {
|
|||||||
|
|
||||||
return $mapType(is_string($type) ? $type : null);
|
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
|
* Generated by APIlite
|
||||||
@ -44,6 +103,7 @@ $mapUnionType = function (mixed $type) use ($mapType): string {
|
|||||||
export interface APIliteActionResponse<T> {
|
export interface APIliteActionResponse<T> {
|
||||||
status: 'OK';
|
status: 'OK';
|
||||||
data: T;
|
data: T;
|
||||||
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIliteErrorResponse {
|
export interface APIliteErrorResponse {
|
||||||
@ -65,6 +125,20 @@ export interface APIliteMethodDoc {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
params: APIliteMethodParam[];
|
params: APIliteMethodParam[];
|
||||||
return: string | string[] | null;
|
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 {
|
export interface APIliteHelpResponse {
|
||||||
@ -73,6 +147,9 @@ export interface APIliteHelpResponse {
|
|||||||
javascript_version: string;
|
javascript_version: string;
|
||||||
typescript_version: string;
|
typescript_version: string;
|
||||||
actions: APIliteMethodDoc[];
|
actions: APIliteMethodDoc[];
|
||||||
|
status: string;
|
||||||
|
data: string;
|
||||||
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class <?php echo $this->apiName; ?> {
|
class <?php echo $this->apiName; ?> {
|
||||||
@ -145,7 +222,7 @@ class <?php echo $this->apiName; ?> {
|
|||||||
$paramsSignature[] = $param['name'] . ($param['optional'] ? '?' : '') . ': ' . $paramType;
|
$paramsSignature[] = $param['name'] . ($param['optional'] ? '?' : '') . ': ' . $paramType;
|
||||||
$paramsPayload[] = $param['name'];
|
$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; ?>>> {
|
<?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); ?> });
|
return this.callPromise<APIliteActionResponse<<?php echo $returnType; ?>>>('<?php echo $method['name']; ?>', { <?php echo implode(', ', $paramsPayload); ?> });
|
||||||
|
|||||||
Reference in New Issue
Block a user