mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Docs\Annotation;
|
||||
|
||||
interface AnnotationInterface
|
||||
{
|
||||
/**
|
||||
* Get annotation name.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Get annotation parameters.
|
||||
*/
|
||||
public function getParameters(): array;
|
||||
|
||||
/**
|
||||
* Get annotation value.
|
||||
*/
|
||||
public function getValue(): mixed;
|
||||
|
||||
/**
|
||||
* Get annotation description.
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* Convert annotation to array.
|
||||
*/
|
||||
public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Convert annotation to JSON.
|
||||
*/
|
||||
public function toJson(): string;
|
||||
|
||||
/**
|
||||
* Validate annotation.
|
||||
*/
|
||||
public function validate(): bool;
|
||||
|
||||
/**
|
||||
* Get annotation priority.
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Check if annotation is deprecated.
|
||||
*/
|
||||
public function isDeprecated(): bool;
|
||||
|
||||
/**
|
||||
* Get annotation version.
|
||||
*/
|
||||
public function getVersion(): string;
|
||||
|
||||
/**
|
||||
* Get annotation examples.
|
||||
*/
|
||||
public function getExamples(): array;
|
||||
|
||||
/**
|
||||
* Merge with another annotation.
|
||||
*/
|
||||
public function merge(AnnotationInterface $other): AnnotationInterface;
|
||||
|
||||
/**
|
||||
* Clone annotation.
|
||||
*/
|
||||
public function __clone();
|
||||
}
|
||||
544
fendx-framework/fendx-docs/src/Annotation/AnnotationParser.php
Normal file
544
fendx-framework/fendx-docs/src/Annotation/AnnotationParser.php
Normal file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Docs\Annotation;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
use ReflectionParameter;
|
||||
use Fendx\Docs\Annotation\AnnotationInterface;
|
||||
use Fendx\Docs\Annotation\Route\RouteAnnotation;
|
||||
use Fendx\Docs\Annotation\Param\ParamAnnotation;
|
||||
use Fendx\Docs\Annotation\Response\ResponseAnnotation;
|
||||
use Fendx\Docs\Annotation\Example\ExampleAnnotation;
|
||||
|
||||
class AnnotationParser
|
||||
{
|
||||
protected array $annotations = [];
|
||||
protected array $cache = [];
|
||||
|
||||
public function __construct(array $annotations = [])
|
||||
{
|
||||
$this->registerDefaultAnnotations();
|
||||
|
||||
foreach ($annotations as $annotation) {
|
||||
$this->registerAnnotation($annotation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse annotations from a class.
|
||||
*/
|
||||
public function parseClass(string $className): array
|
||||
{
|
||||
if (isset($this->cache[$className])) {
|
||||
return $this->cache[$className];
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($className);
|
||||
$result = [
|
||||
'class' => $this->parseClassAnnotations($reflection),
|
||||
'methods' => $this->parseMethodsAnnotations($reflection),
|
||||
'properties' => $this->parsePropertiesAnnotations($reflection)
|
||||
];
|
||||
|
||||
$this->cache[$className] = $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse class-level annotations.
|
||||
*/
|
||||
protected function parseClassAnnotations(ReflectionClass $reflection): array
|
||||
{
|
||||
return $this->parseDocComment($reflection->getDocComment());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse method-level annotations.
|
||||
*/
|
||||
protected function parseMethodsAnnotations(ReflectionClass $reflection): array
|
||||
{
|
||||
$methods = [];
|
||||
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
if ($method->isPublic() && !$method->isStatic()) {
|
||||
$methods[$method->getName()] = [
|
||||
'annotations' => $this->parseDocComment($method->getDocComment()),
|
||||
'parameters' => $this->parseMethodParameters($method),
|
||||
'return_type' => $this->getReturnType($method),
|
||||
'description' => $this->extractDescription($method->getDocComment())
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse property-level annotations.
|
||||
*/
|
||||
protected function parsePropertiesAnnotations(ReflectionClass $reflection): array
|
||||
{
|
||||
$properties = [];
|
||||
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
if ($property->isPublic()) {
|
||||
$properties[$property->getName()] = [
|
||||
'annotations' => $this->parseDocComment($property->getDocComment()),
|
||||
'type' => $this->getPropertyType($property),
|
||||
'description' => $this->extractDescription($property->getDocComment())
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse doc comment for annotations.
|
||||
*/
|
||||
protected function parseDocComment(?string $docComment): array
|
||||
{
|
||||
if (empty($docComment)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$annotations = [];
|
||||
$lines = explode("\n", $docComment);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t/*");
|
||||
|
||||
if (str_starts_with($line, '@')) {
|
||||
$annotation = $this->parseAnnotationLine($line);
|
||||
if ($annotation) {
|
||||
$annotations[] = $annotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single annotation line.
|
||||
*/
|
||||
protected function parseAnnotationLine(string $line): ?AnnotationInterface
|
||||
{
|
||||
// Remove @ prefix
|
||||
$line = substr($line, 1);
|
||||
|
||||
// Extract annotation name and parameters
|
||||
$parts = preg_split('/\s+/', $line, 2);
|
||||
$name = $parts[0] ?? '';
|
||||
$params = $parts[1] ?? '';
|
||||
|
||||
if (!isset($this->annotations[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$annotationClass = $this->annotations[$name];
|
||||
|
||||
if ($params) {
|
||||
$parsedParams = $this->parseAnnotationParameters($params);
|
||||
return new $annotationClass($parsedParams);
|
||||
}
|
||||
|
||||
return new $annotationClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse annotation parameters.
|
||||
*/
|
||||
protected function parseAnnotationParameters(string $params): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// Handle named parameters: name=value, name2=value2
|
||||
if (preg_match_all('/(\w+)=([^\s]+)/', $params, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$result[$match[1]] = $this->parseValue($match[2]);
|
||||
}
|
||||
} else {
|
||||
// Handle single value parameter
|
||||
$result['value'] = $this->parseValue($params);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a parameter value.
|
||||
*/
|
||||
protected function parseValue(string $value): mixed
|
||||
{
|
||||
$value = trim($value, '"\'');
|
||||
|
||||
// Handle boolean values
|
||||
if (strtolower($value) === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (strtolower($value) === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle null values
|
||||
if (strtolower($value) === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle numeric values
|
||||
if (is_numeric($value)) {
|
||||
return strpos($value, '.') !== false ? (float) $value : (int) $value;
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (str_starts_with($value, '[') && str_ends_with($value, ']')) {
|
||||
$content = substr($value, 1, -1);
|
||||
if (empty($content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = explode(',', $content);
|
||||
return array_map([$this, 'parseValue'], array_map('trim', $items));
|
||||
}
|
||||
|
||||
// Handle JSON
|
||||
if ((str_starts_with($value, '{') && str_ends_with($value, '}')) ||
|
||||
(str_starts_with($value, '[') && str_ends_with($value, ']'))) {
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract description from doc comment.
|
||||
*/
|
||||
protected function extractDescription(?string $docComment): string
|
||||
{
|
||||
if (empty($docComment)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = explode("\n", $docComment);
|
||||
$description = [];
|
||||
$inDescription = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t/*");
|
||||
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($line, '@')) {
|
||||
if ($inDescription) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$inDescription = true;
|
||||
$description[] = $line;
|
||||
}
|
||||
|
||||
return implode(' ', $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse method parameters.
|
||||
*/
|
||||
protected function parseMethodParameters(ReflectionMethod $method): array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
foreach ($method->getParameters() as $param) {
|
||||
$parameters[$param->getName()] = [
|
||||
'type' => $this->getParameterType($param),
|
||||
'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
|
||||
'optional' => $param->isOptional(),
|
||||
'description' => $this->extractParameterDescription($method, $param->getName())
|
||||
];
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter type.
|
||||
*/
|
||||
protected function getParameterType(ReflectionParameter $parameter): ?string
|
||||
{
|
||||
$type = $parameter->getType();
|
||||
|
||||
if ($type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
return $type->getName();
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
$types = [];
|
||||
foreach ($type->getTypes() as $unionType) {
|
||||
$types[] = $unionType->getName();
|
||||
}
|
||||
return implode('|', $types);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get property type.
|
||||
*/
|
||||
protected function getPropertyType(ReflectionProperty $property): ?string
|
||||
{
|
||||
$type = $property->getType();
|
||||
|
||||
if ($type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
return $type->getName();
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
$types = [];
|
||||
foreach ($type->getTypes() as $unionType) {
|
||||
$types[] = $unionType->getName();
|
||||
}
|
||||
return implode('|', $types);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get return type.
|
||||
*/
|
||||
protected function getReturnType(ReflectionMethod $method): ?string
|
||||
{
|
||||
$type = $method->getReturnType();
|
||||
|
||||
if ($type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
return $type->getName();
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
$types = [];
|
||||
foreach ($type->getTypes() as $unionType) {
|
||||
$types[] = $unionType->getName();
|
||||
}
|
||||
return implode('|', $types);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameter description from doc comment.
|
||||
*/
|
||||
protected function extractParameterDescription(ReflectionMethod $method, string $paramName): string
|
||||
{
|
||||
$docComment = $method->getDocComment();
|
||||
if (empty($docComment)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = explode("\n", $docComment);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t/*");
|
||||
|
||||
if (preg_match('/@param\s+\S+\s+\$' . preg_quote($paramName) . '\s+(.+)/', $line, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an annotation.
|
||||
*/
|
||||
public function registerAnnotation(string $name, string $class = null): void
|
||||
{
|
||||
if ($class === null) {
|
||||
$class = $name;
|
||||
}
|
||||
|
||||
$this->annotations[$name] = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register multiple annotations.
|
||||
*/
|
||||
public function registerAnnotations(array $annotations): void
|
||||
{
|
||||
foreach ($annotations as $name => $class) {
|
||||
$this->registerAnnotation($name, $class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered annotations.
|
||||
*/
|
||||
public function getRegisteredAnnotations(): array
|
||||
{
|
||||
return $this->annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache.
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default annotations.
|
||||
*/
|
||||
protected function registerDefaultAnnotations(): void
|
||||
{
|
||||
$this->registerAnnotations([
|
||||
'Route' => RouteAnnotation::class,
|
||||
'GetRoute' => RouteAnnotation::class,
|
||||
'PostRoute' => RouteAnnotation::class,
|
||||
'PutRoute' => RouteAnnotation::class,
|
||||
'DeleteRoute' => RouteAnnotation::class,
|
||||
'PatchRoute' => RouteAnnotation::class,
|
||||
'Param' => ParamAnnotation::class,
|
||||
'QueryParam' => ParamAnnotation::class,
|
||||
'PathParam' => ParamAnnotation::class,
|
||||
'BodyParam' => ParamAnnotation::class,
|
||||
'Response' => ResponseAnnotation::class,
|
||||
'SuccessResponse' => ResponseAnnotation::class,
|
||||
'ErrorResponse' => ResponseAnnotation::class,
|
||||
'Example' => ExampleAnnotation::class,
|
||||
'ApiDoc' => ApiDocAnnotation::class,
|
||||
'Deprecated' => DeprecatedAnnotation::class,
|
||||
'Security' => SecurityAnnotation::class,
|
||||
'Tag' => TagAnnotation::class,
|
||||
'Summary' => SummaryAnnotation::class,
|
||||
'Description' => DescriptionAnnotation::class
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse multiple classes.
|
||||
*/
|
||||
public function parseClasses(array $classNames): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($classNames as $className) {
|
||||
try {
|
||||
$result[$className] = $this->parseClass($className);
|
||||
} catch (\Exception $e) {
|
||||
$result[$className] = ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse directory for classes.
|
||||
*/
|
||||
public function parseDirectory(string $directory, string $namespace = ''): array
|
||||
{
|
||||
$result = [];
|
||||
$files = $this->findPhpFiles($directory);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$className = $this->getClassNameFromFile($file, $namespace);
|
||||
|
||||
if ($className && class_exists($className)) {
|
||||
try {
|
||||
$result[$className] = $this->parseClass($className);
|
||||
} catch (\Exception $e) {
|
||||
$result[$className] = ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find PHP files in directory.
|
||||
*/
|
||||
protected function findPhpFiles(string $directory): array
|
||||
{
|
||||
$files = [];
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($directory)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class name from file.
|
||||
*/
|
||||
protected function getClassNameFromFile(string $file, string $namespace = ''): ?string
|
||||
{
|
||||
$content = file_get_contents($file);
|
||||
|
||||
// Extract namespace
|
||||
if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) {
|
||||
$namespace = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Extract class name
|
||||
if (preg_match('/(?:class|interface|trait)\s+(\w+)/', $content, $matches)) {
|
||||
$className = $matches[1];
|
||||
return $namespace ? $namespace . '\\' . $className : $className;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate annotation.
|
||||
*/
|
||||
public function validateAnnotation(AnnotationInterface $annotation): bool
|
||||
{
|
||||
// Basic validation - can be extended
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation statistics.
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
$stats = [
|
||||
'registered_annotations' => count($this->annotations),
|
||||
'cached_classes' => count($this->cache),
|
||||
'annotation_types' => array_keys($this->annotations)
|
||||
];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
473
fendx-framework/fendx-docs/src/Annotation/BaseAnnotation.php
Normal file
473
fendx-framework/fendx-docs/src/Annotation/BaseAnnotation.php
Normal file
@@ -0,0 +1,473 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Docs\Annotation;
|
||||
|
||||
abstract class BaseAnnotation implements AnnotationInterface
|
||||
{
|
||||
protected array $parameters = [];
|
||||
protected string $name;
|
||||
protected mixed $value = null;
|
||||
protected string $description = '';
|
||||
protected int $priority = 0;
|
||||
protected bool $deprecated = false;
|
||||
protected string $version = '1.0.0';
|
||||
protected array $examples = [];
|
||||
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
$this->parameters = $parameters;
|
||||
$this->value = $parameters['value'] ?? null;
|
||||
$this->name = $this->getDefaultName();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize annotation with parameters.
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default annotation name.
|
||||
*/
|
||||
protected function getDefaultName(): string
|
||||
{
|
||||
$className = static::class;
|
||||
return substr($className, strrpos($className, '\\') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation name.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation name.
|
||||
*/
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation parameters.
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation parameters.
|
||||
*/
|
||||
public function setParameters(array $parameters): self
|
||||
{
|
||||
$this->parameters = $parameters;
|
||||
$this->value = $parameters['value'] ?? null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation value.
|
||||
*/
|
||||
public function getValue(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation value.
|
||||
*/
|
||||
public function setValue(mixed $value): self
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->parameters['value'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation description.
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation description.
|
||||
*/
|
||||
public function setDescription(string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation priority.
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation priority.
|
||||
*/
|
||||
public function setPriority(int $priority): self
|
||||
{
|
||||
$this->priority = $priority;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation is deprecated.
|
||||
*/
|
||||
public function isDeprecated(): bool
|
||||
{
|
||||
return $this->deprecated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set deprecated status.
|
||||
*/
|
||||
public function setDeprecated(bool $deprecated): self
|
||||
{
|
||||
$this->deprecated = $deprecated;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation version.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation version.
|
||||
*/
|
||||
public function setVersion(string $version): self
|
||||
{
|
||||
$this->version = $version;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation examples.
|
||||
*/
|
||||
public function getExamples(): array
|
||||
{
|
||||
return $this->examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set annotation examples.
|
||||
*/
|
||||
public function setExamples(array $examples): self
|
||||
{
|
||||
$this->examples = $examples;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an example.
|
||||
*/
|
||||
public function addExample(string $example): self
|
||||
{
|
||||
$this->examples[] = $example;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a parameter value.
|
||||
*/
|
||||
public function getParameter(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->parameters[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a parameter value.
|
||||
*/
|
||||
public function setParameter(string $name, mixed $value): self
|
||||
{
|
||||
$this->parameters[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parameter exists.
|
||||
*/
|
||||
public function hasParameter(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a parameter.
|
||||
*/
|
||||
public function removeParameter(string $name): self
|
||||
{
|
||||
unset($this->parameters[$name]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert annotation to array.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'value' => $this->value,
|
||||
'parameters' => $this->parameters,
|
||||
'description' => $this->description,
|
||||
'priority' => $this->priority,
|
||||
'deprecated' => $this->deprecated,
|
||||
'version' => $this->version,
|
||||
'examples' => $this->examples
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert annotation to JSON.
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate annotation.
|
||||
*/
|
||||
public function validate(): bool
|
||||
{
|
||||
// Basic validation - can be overridden in subclasses
|
||||
return !empty($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge with another annotation.
|
||||
*/
|
||||
public function merge(AnnotationInterface $other): AnnotationInterface
|
||||
{
|
||||
if (get_class($other) !== static::class) {
|
||||
throw new \InvalidArgumentException('Cannot merge annotations of different types');
|
||||
}
|
||||
|
||||
$merged = clone $this;
|
||||
$merged->parameters = array_merge($this->parameters, $other->getParameters());
|
||||
$merged->value = $other->getValue() ?? $this->value;
|
||||
$merged->description = $other->getDescription() ?: $this->description;
|
||||
$merged->examples = array_merge($this->examples, $other->getExamples());
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone annotation.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
// Deep copy parameters if needed
|
||||
$this->parameters = array_map(function ($value) {
|
||||
return is_object($value) ? clone $value : $value;
|
||||
}, $this->parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert annotation to string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation summary.
|
||||
*/
|
||||
public function getSummary(): string
|
||||
{
|
||||
$summary = $this->name;
|
||||
|
||||
if ($this->value !== null) {
|
||||
$summary .= ': ' . $this->formatValue($this->value);
|
||||
}
|
||||
|
||||
if (!empty($this->description)) {
|
||||
$summary .= ' - ' . $this->description;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for display.
|
||||
*/
|
||||
protected function formatValue(mixed $value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return '[' . implode(', ', array_map([$this, 'formatValue'], $value)) . ']';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_null($value)) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return '"' . $value . '"';
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation type.
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation matches criteria.
|
||||
*/
|
||||
public function matches(array $criteria): bool
|
||||
{
|
||||
foreach ($criteria as $key => $value) {
|
||||
$method = 'get' . ucfirst($key);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$actualValue = $this->$method();
|
||||
if ($actualValue !== $value) {
|
||||
return false;
|
||||
}
|
||||
} elseif (isset($this->parameters[$key])) {
|
||||
if ($this->parameters[$key] !== $value) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation metadata.
|
||||
*/
|
||||
public function getMetadata(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'name' => $this->name,
|
||||
'priority' => $this->priority,
|
||||
'deprecated' => $this->deprecated,
|
||||
'version' => $this->version,
|
||||
'parameter_count' => count($this->parameters),
|
||||
'has_examples' => !empty($this->examples),
|
||||
'has_description' => !empty($this->description)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required parameters.
|
||||
*/
|
||||
protected function validateRequiredParameters(array $required): bool
|
||||
{
|
||||
foreach ($required as $param) {
|
||||
if (!$this->hasParameter($param)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing required parameters.
|
||||
*/
|
||||
protected function getMissingParameters(array $required): array
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
foreach ($required as $param) {
|
||||
if (!$this->hasParameter($param)) {
|
||||
$missing[] = $param;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple parameters at once.
|
||||
*/
|
||||
public function setParametersBatch(array $parameters): self
|
||||
{
|
||||
foreach ($parameters as $key => $value) {
|
||||
$this->setParameter($key, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parameter names.
|
||||
*/
|
||||
public function getParameterNames(): array
|
||||
{
|
||||
return array_keys($this->parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation has any parameters.
|
||||
*/
|
||||
public function hasParameters(): bool
|
||||
{
|
||||
return !empty($this->parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter count.
|
||||
*/
|
||||
public function getParameterCount(): int
|
||||
{
|
||||
return count($this->parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all parameters.
|
||||
*/
|
||||
public function clearParameters(): self
|
||||
{
|
||||
$this->parameters = [];
|
||||
$this->value = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset annotation to default state.
|
||||
*/
|
||||
public function reset(): self
|
||||
{
|
||||
$this->parameters = [];
|
||||
$this->value = null;
|
||||
$this->description = '';
|
||||
$this->examples = [];
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Docs\Annotation\Param;
|
||||
|
||||
use Fendx\Docs\Annotation\BaseAnnotation;
|
||||
|
||||
class ParamAnnotation extends BaseAnnotation
|
||||
{
|
||||
protected string $type = 'string';
|
||||
protected string $name = '';
|
||||
protected string $location = 'query'; // query, path, body, header, cookie
|
||||
protected bool $required = false;
|
||||
protected mixed $default = null;
|
||||
protected string $description = '';
|
||||
protected array $enum = [];
|
||||
protected ?float $minimum = null;
|
||||
protected ?float $maximum = null;
|
||||
protected ?int $minLength = null;
|
||||
protected ?int $maxLength = null;
|
||||
protected string $format = '';
|
||||
protected string $pattern = '';
|
||||
protected bool $deprecated = false;
|
||||
protected bool $nullable = false;
|
||||
protected mixed $example = null;
|
||||
protected array $examples = [];
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->name = $this->getParameter('name', $this->value ?? '');
|
||||
$this->type = $this->getParameter('type', 'string');
|
||||
$this->location = $this->getParameter('location', 'query');
|
||||
$this->required = $this->getParameter('required', false);
|
||||
$this->default = $this->getParameter('default', null);
|
||||
$this->description = $this->getParameter('description', '');
|
||||
$this->enum = $this->getParameter('enum', []);
|
||||
$this->minimum = $this->getParameter('minimum', null);
|
||||
$this->maximum = $this->getParameter('maximum', null);
|
||||
$this->minLength = $this->getParameter('minLength', null);
|
||||
$this->maxLength = $this->getParameter('maxLength', null);
|
||||
$this->format = $this->getParameter('format', '');
|
||||
$this->pattern = $this->getParameter('pattern', '');
|
||||
$this->deprecated = $this->getParameter('deprecated', false);
|
||||
$this->nullable = $this->getParameter('nullable', false);
|
||||
$this->example = $this->getParameter('example', null);
|
||||
$this->examples = $this->getParameter('examples', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter name.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parameter name.
|
||||
*/
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->setParameter('name', $name);
|
||||
$this->setValue($name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter type.
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parameter type.
|
||||
*/
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->setParameter('type', $type);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter location.
|
||||
*/
|
||||
public function getLocation(): string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parameter location.
|
||||
*/
|
||||
public function setLocation(string $location): self
|
||||
{
|
||||
$this->location = $location;
|
||||
$this->setParameter('location', $location);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parameter is required.
|
||||
*/
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set required status.
|
||||
*/
|
||||
public function setRequired(bool $required): self
|
||||
{
|
||||
$this->required = $required;
|
||||
$this->setParameter('required', $required);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default value.
|
||||
*/
|
||||
public function getDefault(): mixed
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default value.
|
||||
*/
|
||||
public function setDefault(mixed $default): self
|
||||
{
|
||||
$this->default = $default;
|
||||
$this->setParameter('default', $default);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description.
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description.
|
||||
*/
|
||||
public function setDescription(string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
$this->setParameter('description', $description);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enum values.
|
||||
*/
|
||||
public function getEnum(): array
|
||||
{
|
||||
return $this->enum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set enum values.
|
||||
*/
|
||||
public function setEnum(array $enum): self
|
||||
{
|
||||
$this->enum = $enum;
|
||||
$this->setParameter('enum', $enum);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum value.
|
||||
*/
|
||||
public function getMinimum(): ?float
|
||||
{
|
||||
return $this->minimum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum value.
|
||||
*/
|
||||
public function setMinimum(?float $minimum): self
|
||||
{
|
||||
$this->minimum = $minimum;
|
||||
$this->setParameter('minimum', $minimum);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum value.
|
||||
*/
|
||||
public function getMaximum(): ?float
|
||||
{
|
||||
return $this->maximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum value.
|
||||
*/
|
||||
public function setMaximum(?float $maximum): self
|
||||
{
|
||||
$this->maximum = $maximum;
|
||||
$this->setParameter('maximum', $maximum);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum length.
|
||||
*/
|
||||
public function getMinLength(): ?int
|
||||
{
|
||||
return $this->minLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum length.
|
||||
*/
|
||||
public function setMinLength(?int $minLength): self
|
||||
{
|
||||
$this->minLength = $minLength;
|
||||
$this->setParameter('minLength', $minLength);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum length.
|
||||
*/
|
||||
public function getMaxLength(): ?int
|
||||
{
|
||||
return $this->maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum length.
|
||||
*/
|
||||
public function setMaxLength(?int $maxLength): self
|
||||
{
|
||||
$this->maxLength = $maxLength;
|
||||
$this->setParameter('maxLength', $maxLength);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get format.
|
||||
*/
|
||||
public function getFormat(): string
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set format.
|
||||
*/
|
||||
public function setFormat(string $format): self
|
||||
{
|
||||
$this->format = $format;
|
||||
$this->setParameter('format', $format);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pattern.
|
||||
*/
|
||||
public function getPattern(): string
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set pattern.
|
||||
*/
|
||||
public function setPattern(string $pattern): self
|
||||
{
|
||||
$this->pattern = $pattern;
|
||||
$this->setParameter('pattern', $pattern);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parameter is deprecated.
|
||||
*/
|
||||
public function isDeprecated(): bool
|
||||
{
|
||||
return $this->deprecated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set deprecated status.
|
||||
*/
|
||||
public function setDeprecated(bool $deprecated): self
|
||||
{
|
||||
$this->deprecated = $deprecated;
|
||||
$this->setParameter('deprecated', $deprecated);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parameter is nullable.
|
||||
*/
|
||||
public function isNullable(): bool
|
||||
{
|
||||
return $this->nullable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nullable status.
|
||||
*/
|
||||
public function setNullable(bool $nullable): self
|
||||
{
|
||||
$this->nullable = $nullable;
|
||||
$this->setParameter('nullable', $nullable);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get example.
|
||||
*/
|
||||
public function getExample(): mixed
|
||||
{
|
||||
return $this->example;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set example.
|
||||
*/
|
||||
public function setExample(mixed $example): self
|
||||
{
|
||||
$this->example = $example;
|
||||
$this->setParameter('example', $example);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get examples.
|
||||
*/
|
||||
public function getExamples(): array
|
||||
{
|
||||
return $this->examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set examples.
|
||||
*/
|
||||
public function setExamples(array $examples): self
|
||||
{
|
||||
$this->examples = $examples;
|
||||
$this->setParameter('examples', $examples);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add example.
|
||||
*/
|
||||
public function addExample(string $name, mixed $value): self
|
||||
{
|
||||
$this->examples[$name] = $value;
|
||||
$this->setParameter('examples', $this->examples);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameter annotation.
|
||||
*/
|
||||
public function validate(): bool
|
||||
{
|
||||
if (!parent::validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($this->location, ['query', 'path', 'body', 'header', 'cookie'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($this->type, ['string', 'integer', 'number', 'boolean', 'array', 'object', 'file'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate value against constraints.
|
||||
*/
|
||||
public function validateValue(mixed $value): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Check if null is allowed
|
||||
if ($value === null) {
|
||||
if (!$this->nullable && !$this->required) {
|
||||
$errors[] = 'Value cannot be null';
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// Type validation
|
||||
switch ($this->type) {
|
||||
case 'string':
|
||||
if (!is_string($value)) {
|
||||
$errors[] = 'Value must be a string';
|
||||
} else {
|
||||
if ($this->minLength !== null && strlen($value) < $this->minLength) {
|
||||
$errors[] = "String length must be at least {$this->minLength}";
|
||||
}
|
||||
if ($this->maxLength !== null && strlen($value) > $this->maxLength) {
|
||||
$errors[] = "String length must not exceed {$this->maxLength}";
|
||||
}
|
||||
if ($this->pattern && !preg_match('/' . $this->pattern . '/', $value)) {
|
||||
$errors[] = 'String does not match required pattern';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'integer':
|
||||
if (!is_int($value)) {
|
||||
$errors[] = 'Value must be an integer';
|
||||
} else {
|
||||
if ($this->minimum !== null && $value < $this->minimum) {
|
||||
$errors[] = "Value must be at least {$this->minimum}";
|
||||
}
|
||||
if ($this->maximum !== null && $value > $this->maximum) {
|
||||
$errors[] = "Value must not exceed {$this->maximum}";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
if (!is_numeric($value)) {
|
||||
$errors[] = 'Value must be a number';
|
||||
} else {
|
||||
if ($this->minimum !== null && $value < $this->minimum) {
|
||||
$errors[] = "Value must be at least {$this->minimum}";
|
||||
}
|
||||
if ($this->maximum !== null && $value > $this->maximum) {
|
||||
$errors[] = "Value must not exceed {$this->maximum}";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
if (!is_bool($value)) {
|
||||
$errors[] = 'Value must be a boolean';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'array':
|
||||
if (!is_array($value)) {
|
||||
$errors[] = 'Value must be an array';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Enum validation
|
||||
if (!empty($this->enum) && !in_array($value, $this->enum)) {
|
||||
$errors[] = 'Value must be one of: ' . implode(', ', $this->enum);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_merge(parent::toArray(), [
|
||||
'type' => $this->type,
|
||||
'name' => $this->name,
|
||||
'location' => $this->location,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'description' => $this->description,
|
||||
'enum' => $this->enum,
|
||||
'minimum' => $this->minimum,
|
||||
'maximum' => $this->maximum,
|
||||
'minLength' => $this->minLength,
|
||||
'maxLength' => $this->maxLength,
|
||||
'format' => $this->format,
|
||||
'pattern' => $this->pattern,
|
||||
'deprecated' => $this->deprecated,
|
||||
'nullable' => $this->nullable,
|
||||
'example' => $this->example,
|
||||
'examples' => $this->examples
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter summary.
|
||||
*/
|
||||
public function getSummary(): string
|
||||
{
|
||||
$summary = sprintf('%s (%s)', $this->name, $this->type);
|
||||
|
||||
if ($this->required) {
|
||||
$summary .= ' required';
|
||||
}
|
||||
|
||||
if ($this->deprecated) {
|
||||
$summary .= ' deprecated';
|
||||
}
|
||||
|
||||
if (!empty($this->description)) {
|
||||
$summary .= ' - ' . $this->description;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from array.
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$annotation = new self();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$method = 'set' . ucfirst($key);
|
||||
if (method_exists($annotation, $method)) {
|
||||
$annotation->$method($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenAPI schema.
|
||||
*/
|
||||
public function getOpenAPISchema(): array
|
||||
{
|
||||
$schema = [
|
||||
'type' => $this->type,
|
||||
'description' => $this->description
|
||||
];
|
||||
|
||||
if (!empty($this->enum)) {
|
||||
$schema['enum'] = $this->enum;
|
||||
}
|
||||
|
||||
if ($this->minimum !== null) {
|
||||
$schema['minimum'] = $this->minimum;
|
||||
}
|
||||
|
||||
if ($this->maximum !== null) {
|
||||
$schema['maximum'] = $this->maximum;
|
||||
}
|
||||
|
||||
if ($this->minLength !== null) {
|
||||
$schema['minLength'] = $this->minLength;
|
||||
}
|
||||
|
||||
if ($this->maxLength !== null) {
|
||||
$schema['maxLength'] = $this->maxLength;
|
||||
}
|
||||
|
||||
if (!empty($this->format)) {
|
||||
$schema['format'] = $this->format;
|
||||
}
|
||||
|
||||
if (!empty($this->pattern)) {
|
||||
$schema['pattern'] = $this->pattern;
|
||||
}
|
||||
|
||||
if ($this->nullable) {
|
||||
$schema['nullable'] = true;
|
||||
}
|
||||
|
||||
if ($this->deprecated) {
|
||||
$schema['deprecated'] = true;
|
||||
}
|
||||
|
||||
if ($this->example !== null) {
|
||||
$schema['example'] = $this->example;
|
||||
}
|
||||
|
||||
if (!empty($this->examples)) {
|
||||
$schema['examples'] = $this->examples;
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter identifier.
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return sprintf('%s:%s', $this->location, $this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parameter has constraints.
|
||||
*/
|
||||
public function hasConstraints(): bool
|
||||
{
|
||||
return !empty($this->enum) ||
|
||||
$this->minimum !== null ||
|
||||
$this->maximum !== null ||
|
||||
$this->minLength !== null ||
|
||||
$this->maxLength !== null ||
|
||||
!empty($this->pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get constraint summary.
|
||||
*/
|
||||
public function getConstraintSummary(): string
|
||||
{
|
||||
$constraints = [];
|
||||
|
||||
if (!empty($this->enum)) {
|
||||
$constraints[] = 'enum: ' . implode(', ', $this->enum);
|
||||
}
|
||||
|
||||
if ($this->minimum !== null) {
|
||||
$constraints[] = "min: {$this->minimum}";
|
||||
}
|
||||
|
||||
if ($this->maximum !== null) {
|
||||
$constraints[] = "max: {$this->maximum}";
|
||||
}
|
||||
|
||||
if ($this->minLength !== null) {
|
||||
$constraints[] = "minLength: {$this->minLength}";
|
||||
}
|
||||
|
||||
if ($this->maxLength !== null) {
|
||||
$constraints[] = "maxLength: {$this->maxLength}";
|
||||
}
|
||||
|
||||
if (!empty($this->pattern)) {
|
||||
$constraints[] = "pattern: {$this->pattern}";
|
||||
}
|
||||
|
||||
return implode(', ', $constraints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Docs\Annotation\Route;
|
||||
|
||||
use Fendx\Docs\Annotation\BaseAnnotation;
|
||||
|
||||
class RouteAnnotation extends BaseAnnotation
|
||||
{
|
||||
protected string $method = 'GET';
|
||||
protected string $path = '';
|
||||
protected string $name = '';
|
||||
protected array $middleware = [];
|
||||
protected array $where = [];
|
||||
protected array $defaults = [];
|
||||
protected string $domain = '';
|
||||
protected array $schemes = [];
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->method = $this->getParameter('method', 'GET');
|
||||
$this->path = $this->getParameter('path', $this->value ?? '');
|
||||
$this->name = $this->getParameter('name', '');
|
||||
$this->middleware = $this->getParameter('middleware', []);
|
||||
$this->where = $this->getParameter('where', []);
|
||||
$this->defaults = $this->getParameter('defaults', []);
|
||||
$this->domain = $this->getParameter('domain', '');
|
||||
$this->schemes = $this->getParameter('schemes', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP method.
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set HTTP method.
|
||||
*/
|
||||
public function setMethod(string $method): self
|
||||
{
|
||||
$this->method = strtoupper($method);
|
||||
$this->setParameter('method', $this->method);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route path.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set route path.
|
||||
*/
|
||||
public function setPath(string $path): self
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->setParameter('path', $path);
|
||||
$this->setValue($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route name.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set route name.
|
||||
*/
|
||||
public function setRouteName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->setParameter('name', $name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get middleware.
|
||||
*/
|
||||
public function getMiddleware(): array
|
||||
{
|
||||
return $this->middleware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set middleware.
|
||||
*/
|
||||
public function setMiddleware(array $middleware): self
|
||||
{
|
||||
$this->middleware = $middleware;
|
||||
$this->setParameter('middleware', $middleware);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add middleware.
|
||||
*/
|
||||
public function addMiddleware(string $middleware): self
|
||||
{
|
||||
$this->middleware[] = $middleware;
|
||||
$this->setParameter('middleware', $this->middleware);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get where constraints.
|
||||
*/
|
||||
public function getWhere(): array
|
||||
{
|
||||
return $this->where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set where constraints.
|
||||
*/
|
||||
public function setWhere(array $where): self
|
||||
{
|
||||
$this->where = $where;
|
||||
$this->setParameter('where', $where);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add where constraint.
|
||||
*/
|
||||
public function addWhere(string $parameter, string $constraint): self
|
||||
{
|
||||
$this->where[$parameter] = $constraint;
|
||||
$this->setParameter('where', $this->where);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default values.
|
||||
*/
|
||||
public function getDefaults(): array
|
||||
{
|
||||
return $this->defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values.
|
||||
*/
|
||||
public function setDefaults(array $defaults): self
|
||||
{
|
||||
$this->defaults = $defaults;
|
||||
$this->setParameter('defaults', $defaults);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default value.
|
||||
*/
|
||||
public function addDefault(string $parameter, mixed $value): self
|
||||
{
|
||||
$this->defaults[$parameter] = $value;
|
||||
$this->setParameter('defaults', $this->defaults);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain.
|
||||
*/
|
||||
public function getDomain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set domain.
|
||||
*/
|
||||
public function setDomain(string $domain): self
|
||||
{
|
||||
$this->domain = $domain;
|
||||
$this->setParameter('domain', $domain);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schemes.
|
||||
*/
|
||||
public function getSchemes(): array
|
||||
{
|
||||
return $this->schemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set schemes.
|
||||
*/
|
||||
public function setSchemes(array $schemes): self
|
||||
{
|
||||
$this->schemes = array_map('strtoupper', $schemes);
|
||||
$this->setParameter('schemes', $this->schemes);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add scheme.
|
||||
*/
|
||||
public function addScheme(string $scheme): self
|
||||
{
|
||||
$scheme = strtoupper($scheme);
|
||||
if (!in_array($scheme, $this->schemes)) {
|
||||
$this->schemes[] = $scheme;
|
||||
$this->setParameter('schemes', $this->schemes);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if route has parameters.
|
||||
*/
|
||||
public function hasParameters(): bool
|
||||
{
|
||||
return preg_match('/\{[^}]+\}/', $this->path) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route parameters from path.
|
||||
*/
|
||||
public function getPathParameters(): array
|
||||
{
|
||||
preg_match_all('/\{([^}]+)\}/', $this->path, $matches);
|
||||
return $matches[1] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full URL.
|
||||
*/
|
||||
public function getFullUrl(string $baseUrl = ''): string
|
||||
{
|
||||
$url = $baseUrl;
|
||||
|
||||
if (!empty($this->domain)) {
|
||||
$url = rtrim($this->domain, '/') . '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
$url .= '/' . ltrim($this->path, '/');
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate route annotation.
|
||||
*/
|
||||
public function validate(): bool
|
||||
{
|
||||
if (!parent::validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($this->method, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_merge(parent::toArray(), [
|
||||
'method' => $this->method,
|
||||
'path' => $this->path,
|
||||
'route_name' => $this->name,
|
||||
'middleware' => $this->middleware,
|
||||
'where' => $this->where,
|
||||
'defaults' => $this->defaults,
|
||||
'domain' => $this->domain,
|
||||
'schemes' => $this->schemes,
|
||||
'has_parameters' => $this->hasParameters(),
|
||||
'path_parameters' => $this->getPathParameters()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route summary.
|
||||
*/
|
||||
public function getSummary(): string
|
||||
{
|
||||
return sprintf('%s %s', $this->method, $this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from array.
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$annotation = new self();
|
||||
|
||||
if (isset($data['method'])) {
|
||||
$annotation->setMethod($data['method']);
|
||||
}
|
||||
|
||||
if (isset($data['path'])) {
|
||||
$annotation->setPath($data['path']);
|
||||
}
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$annotation->setRouteName($data['name']);
|
||||
}
|
||||
|
||||
if (isset($data['middleware'])) {
|
||||
$annotation->setMiddleware($data['middleware']);
|
||||
}
|
||||
|
||||
if (isset($data['where'])) {
|
||||
$annotation->setWhere($data['where']);
|
||||
}
|
||||
|
||||
if (isset($data['defaults'])) {
|
||||
$annotation->setDefaults($data['defaults']);
|
||||
}
|
||||
|
||||
if (isset($data['domain'])) {
|
||||
$annotation->setDomain($data['domain']);
|
||||
}
|
||||
|
||||
if (isset($data['schemes'])) {
|
||||
$annotation->setSchemes($data['schemes']);
|
||||
}
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$annotation->setDescription($data['description']);
|
||||
}
|
||||
|
||||
return $annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if route matches method.
|
||||
*/
|
||||
public function matchesMethod(string $method): bool
|
||||
{
|
||||
return strtoupper($this->method) === strtoupper($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if route matches path pattern.
|
||||
*/
|
||||
public function matchesPath(string $path): bool
|
||||
{
|
||||
// Simple pattern matching - can be enhanced
|
||||
$pattern = preg_replace('/\{[^}]+\}/', '([^/]+)', $this->path);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
return preg_match($pattern, $path) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameters from path.
|
||||
*/
|
||||
public function extractParameters(string $path): array
|
||||
{
|
||||
$parameters = [];
|
||||
$pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $this->path);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
if (preg_match($pattern, $path, $matches)) {
|
||||
$paramNames = $this->getPathParameters();
|
||||
foreach ($paramNames as $index => $name) {
|
||||
if (isset($matches[$index + 1])) {
|
||||
$parameters[$name] = $matches[$index + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route identifier.
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return sprintf('%s:%s', $this->method, $this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if route is secure (HTTPS).
|
||||
*/
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return in_array('HTTPS', $this->schemes) ||
|
||||
(empty($this->schemes) && str_starts_with($this->domain ?? '', 'https://'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key.
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'route_' . md5($this->getIdentifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get middleware groups.
|
||||
*/
|
||||
public function getMiddlewareGroups(): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($this->middleware as $middleware) {
|
||||
if (str_contains($middleware, ':')) {
|
||||
[$group] = explode(':', $middleware, 2);
|
||||
$groups[] = $group;
|
||||
} else {
|
||||
$groups[] = $middleware;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($groups);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Docs\Generator;
|
||||
|
||||
use Fendx\Docs\Annotation\AnnotationParser;
|
||||
use Fendx\Docs\Template\TemplateEngine;
|
||||
use Fendx\Docs\Writer\DocumentationWriter;
|
||||
|
||||
class DocumentationGenerator
|
||||
{
|
||||
protected AnnotationParser $parser;
|
||||
protected TemplateEngine $templateEngine;
|
||||
protected DocumentationWriter $writer;
|
||||
protected array $config = [];
|
||||
protected array $processedClasses = [];
|
||||
|
||||
public function __construct(
|
||||
AnnotationParser $parser,
|
||||
TemplateEngine $templateEngine,
|
||||
DocumentationWriter $writer,
|
||||
array $config = []
|
||||
) {
|
||||
$this->parser = $parser;
|
||||
$this->templateEngine = $templateEngine;
|
||||
$this->writer = $writer;
|
||||
$this->config = array_merge($this->getDefaultConfig(), $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documentation from classes.
|
||||
*/
|
||||
public function generate(array $classNames): array
|
||||
{
|
||||
$this->processedClasses = [];
|
||||
$documentation = [];
|
||||
|
||||
foreach ($classNames as $className) {
|
||||
$classDoc = $this->generateClassDocumentation($className);
|
||||
if ($classDoc) {
|
||||
$documentation[$className] = $classDoc;
|
||||
$this->processedClasses[] = $className;
|
||||
}
|
||||
}
|
||||
|
||||
return $documentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documentation from directory.
|
||||
*/
|
||||
public function generateFromDirectory(string $directory, string $namespace = ''): array
|
||||
{
|
||||
$this->processedClasses = [];
|
||||
$documentation = [];
|
||||
|
||||
$parsedData = $this->parser->parseDirectory($directory, $namespace);
|
||||
|
||||
foreach ($parsedData as $className => $data) {
|
||||
if (isset($data['error'])) {
|
||||
$this->logError("Error parsing {$className}: {$data['error']}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$classDoc = $this->processParsedData($className, $data);
|
||||
if ($classDoc) {
|
||||
$documentation[$className] = $classDoc;
|
||||
$this->processedClasses[] = $className;
|
||||
}
|
||||
}
|
||||
|
||||
return $documentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documentation for a single class.
|
||||
*/
|
||||
protected function generateClassDocumentation(string $className): ?array
|
||||
{
|
||||
try {
|
||||
$parsedData = $this->parser->parseClass($className);
|
||||
return $this->processParsedData($className, $parsedData);
|
||||
} catch (\Exception $e) {
|
||||
$this->logError("Error generating documentation for {$className}: {$e->getMessage()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process parsed data into documentation.
|
||||
*/
|
||||
protected function processParsedData(string $className, array $data): ?array
|
||||
{
|
||||
if (isset($data['error'])) {
|
||||
$this->logError("Error in {$className}: {$data['error']}");
|
||||
return null;
|
||||
}
|
||||
|
||||
$classDoc = [
|
||||
'class_name' => $className,
|
||||
'short_name' => substr($className, strrpos($className, '\\') + 1),
|
||||
'namespace' => substr($className, 0, strrpos($className, '\\')),
|
||||
'annotations' => $data['class'],
|
||||
'methods' => [],
|
||||
'properties' => [],
|
||||
'routes' => [],
|
||||
'summary' => $this->generateClassSummary($data),
|
||||
'metadata' => $this->generateClassMetadata($className, $data)
|
||||
];
|
||||
|
||||
// Process methods
|
||||
foreach ($data['methods'] as $methodName => $methodData) {
|
||||
$methodDoc = $this->processMethodData($methodName, $methodData);
|
||||
if ($methodDoc) {
|
||||
$classDoc['methods'][$methodName] = $methodDoc;
|
||||
|
||||
// Extract routes from method annotations
|
||||
foreach ($methodData['annotations'] as $annotation) {
|
||||
if ($this->isRouteAnnotation($annotation)) {
|
||||
$classDoc['routes'][] = $this->processRouteAnnotation($methodName, $annotation, $methodDoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process properties
|
||||
foreach ($data['properties'] as $propertyName => $propertyData) {
|
||||
$propertyDoc = $this->processPropertyData($propertyName, $propertyData);
|
||||
if ($propertyDoc) {
|
||||
$classDoc['properties'][$propertyName] = $propertyDoc;
|
||||
}
|
||||
}
|
||||
|
||||
return $classDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process method data.
|
||||
*/
|
||||
protected function processMethodData(string $methodName, array $methodData): ?array
|
||||
{
|
||||
$methodDoc = [
|
||||
'name' => $methodName,
|
||||
'annotations' => $methodData['annotations'],
|
||||
'parameters' => $methodData['parameters'],
|
||||
'return_type' => $methodData['return_type'],
|
||||
'description' => $methodData['description'],
|
||||
'params' => [],
|
||||
'responses' => [],
|
||||
'examples' => [],
|
||||
'summary' => $this->generateMethodSummary($methodData)
|
||||
];
|
||||
|
||||
// Process parameter annotations
|
||||
foreach ($methodData['annotations'] as $annotation) {
|
||||
if ($this->isParamAnnotation($annotation)) {
|
||||
$methodDoc['params'][] = $this->processParamAnnotation($annotation);
|
||||
} elseif ($this->isResponseAnnotation($annotation)) {
|
||||
$methodDoc['responses'][] = $this->processResponseAnnotation($annotation);
|
||||
} elseif ($this->isExampleAnnotation($annotation)) {
|
||||
$methodDoc['examples'][] = $this->processExampleAnnotation($annotation);
|
||||
}
|
||||
}
|
||||
|
||||
return $methodDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process property data.
|
||||
*/
|
||||
protected function processPropertyData(string $propertyName, array $propertyData): ?array
|
||||
{
|
||||
return [
|
||||
'name' => $propertyName,
|
||||
'annotations' => $propertyData['annotations'],
|
||||
'type' => $propertyData['type'],
|
||||
'description' => $propertyData['description']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process route annotation.
|
||||
*/
|
||||
protected function processRouteAnnotation(string $methodName, $annotation, array $methodDoc): array
|
||||
{
|
||||
return [
|
||||
'method' => $annotation->getMethod(),
|
||||
'path' => $annotation->getPath(),
|
||||
'name' => $annotation->getName(),
|
||||
'method_name' => $methodName,
|
||||
'summary' => $methodDoc['description'],
|
||||
'parameters' => $this->extractRouteParameters($methodDoc),
|
||||
'responses' => $methodDoc['responses'],
|
||||
'examples' => $methodDoc['examples'],
|
||||
'middleware' => $annotation->getMiddleware()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process parameter annotation.
|
||||
*/
|
||||
protected function processParamAnnotation($annotation): array
|
||||
{
|
||||
return [
|
||||
'name' => $annotation->getName(),
|
||||
'type' => $annotation->getType(),
|
||||
'location' => $annotation->getLocation(),
|
||||
'required' => $annotation->isRequired(),
|
||||
'default' => $annotation->getDefault(),
|
||||
'description' => $annotation->getDescription(),
|
||||
'enum' => $annotation->getEnum(),
|
||||
'minimum' => $annotation->getMinimum(),
|
||||
'maximum' => $annotation->getMaximum(),
|
||||
'minLength' => $annotation->getMinLength(),
|
||||
'maxLength' => $annotation->getMaxLength(),
|
||||
'format' => $annotation->getFormat(),
|
||||
'pattern' => $annotation->getPattern(),
|
||||
'deprecated' => $annotation->isDeprecated(),
|
||||
'nullable' => $annotation->isNullable(),
|
||||
'example' => $annotation->getExample(),
|
||||
'examples' => $annotation->getExamples()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process response annotation.
|
||||
*/
|
||||
protected function processResponseAnnotation($annotation): array
|
||||
{
|
||||
return [
|
||||
'code' => $annotation->getCode(),
|
||||
'description' => $annotation->getDescription(),
|
||||
'type' => $annotation->getType(),
|
||||
'example' => $annotation->getExample(),
|
||||
'headers' => $annotation->getHeaders()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process example annotation.
|
||||
*/
|
||||
protected function processExampleAnnotation($annotation): array
|
||||
{
|
||||
return [
|
||||
'title' => $annotation->getTitle(),
|
||||
'description' => $annotation->getDescription(),
|
||||
'request' => $annotation->getRequest(),
|
||||
'response' => $annotation->getResponse()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract route parameters from method documentation.
|
||||
*/
|
||||
protected function extractRouteParameters(array $methodDoc): array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
foreach ($methodDoc['params'] as $param) {
|
||||
if ($param['location'] === 'path' || $param['location'] === 'query') {
|
||||
$parameters[] = $param;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate class summary.
|
||||
*/
|
||||
protected function generateClassSummary(array $data): string
|
||||
{
|
||||
$annotations = $data['class'] ?? [];
|
||||
|
||||
foreach ($annotations as $annotation) {
|
||||
if ($this->isApiDocAnnotation($annotation)) {
|
||||
return $annotation->getDescription() ?: '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate method summary.
|
||||
*/
|
||||
protected function generateMethodSummary(array $methodData): string
|
||||
{
|
||||
return $methodData['description'] ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate class metadata.
|
||||
*/
|
||||
protected function generateClassMetadata(string $className, array $data): array
|
||||
{
|
||||
return [
|
||||
'total_methods' => count($data['methods']),
|
||||
'total_properties' => count($data['properties']),
|
||||
'total_routes' => count($this->extractRoutesFromData($data)),
|
||||
'has_routes' => !empty($this->extractRoutesFromData($data)),
|
||||
'file_path' => $this->getClassFilePath($className),
|
||||
'last_modified' => $this->getClassLastModified($className)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract routes from parsed data.
|
||||
*/
|
||||
protected function extractRoutesFromData(array $data): array
|
||||
{
|
||||
$routes = [];
|
||||
|
||||
foreach ($data['methods'] as $methodData) {
|
||||
foreach ($methodData['annotations'] as $annotation) {
|
||||
if ($this->isRouteAnnotation($annotation)) {
|
||||
$routes[] = $annotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class file path.
|
||||
*/
|
||||
protected function getClassFilePath(string $className): ?string
|
||||
{
|
||||
$reflection = new \ReflectionClass($className);
|
||||
return $reflection->getFileName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class last modified time.
|
||||
*/
|
||||
protected function getClassLastModified(string $className): ?string
|
||||
{
|
||||
$filePath = $this->getClassFilePath($className);
|
||||
return $filePath ? date('Y-m-d H:i:s', filemtime($filePath)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation is a route annotation.
|
||||
*/
|
||||
protected function isRouteAnnotation($annotation): bool
|
||||
{
|
||||
return $annotation instanceof \Fendx\Docs\Annotation\Route\RouteAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation is a parameter annotation.
|
||||
*/
|
||||
protected function isParamAnnotation($annotation): bool
|
||||
{
|
||||
return $annotation instanceof \Fendx\Docs\Annotation\Param\ParamAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation is a response annotation.
|
||||
*/
|
||||
protected function isResponseAnnotation($annotation): bool
|
||||
{
|
||||
return $annotation instanceof \Fendx\Docs\Annotation\Response\ResponseAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation is an example annotation.
|
||||
*/
|
||||
protected function isExampleAnnotation($annotation): bool
|
||||
{
|
||||
return $annotation instanceof \Fendx\Docs\Annotation\Example\ExampleAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if annotation is an API doc annotation.
|
||||
*/
|
||||
protected function isApiDocAnnotation($annotation): bool
|
||||
{
|
||||
return $annotation instanceof \Fendx\Docs\Annotation\ApiDocAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML documentation.
|
||||
*/
|
||||
public function generateHtml(array $documentation, string $outputPath): bool
|
||||
{
|
||||
try {
|
||||
$html = $this->templateEngine->render('documentation', [
|
||||
'documentation' => $documentation,
|
||||
'config' => $this->config,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'statistics' => $this->generateStatistics($documentation)
|
||||
]);
|
||||
|
||||
return $this->writer->write($outputPath, $html);
|
||||
} catch (\Exception $e) {
|
||||
$this->logError("Error generating HTML: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Markdown documentation.
|
||||
*/
|
||||
public function generateMarkdown(array $documentation, string $outputPath): bool
|
||||
{
|
||||
try {
|
||||
$markdown = $this->templateEngine->render('documentation.md', [
|
||||
'documentation' => $documentation,
|
||||
'config' => $this->config,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'statistics' => $this->generateStatistics($documentation)
|
||||
]);
|
||||
|
||||
return $this->writer->write($outputPath, $markdown);
|
||||
} catch (\Exception $e) {
|
||||
$this->logError("Error generating Markdown: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OpenAPI specification.
|
||||
*/
|
||||
public function generateOpenAPI(array $documentation, string $outputPath): bool
|
||||
{
|
||||
try {
|
||||
$openApi = $this->generateOpenAPISpec($documentation);
|
||||
$json = json_encode($openApi, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return $this->writer->write($outputPath, $json);
|
||||
} catch (\Exception $e) {
|
||||
$this->logError("Error generating OpenAPI: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OpenAPI specification.
|
||||
*/
|
||||
protected function generateOpenAPISpec(array $documentation): array
|
||||
{
|
||||
$spec = [
|
||||
'openapi' => '3.0.0',
|
||||
'info' => [
|
||||
'title' => $this->config['title'] ?? 'API Documentation',
|
||||
'version' => $this->config['version'] ?? '1.0.0',
|
||||
'description' => $this->config['description'] ?? 'Generated API documentation'
|
||||
],
|
||||
'paths' => [],
|
||||
'components' => [
|
||||
'schemas' => []
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($documentation as $classData) {
|
||||
foreach ($classData['routes'] as $route) {
|
||||
$path = $route['path'];
|
||||
$method = strtolower($route['method']);
|
||||
|
||||
if (!isset($spec['paths'][$path])) {
|
||||
$spec['paths'][$path] = [];
|
||||
}
|
||||
|
||||
$spec['paths'][$path][$method] = [
|
||||
'summary' => $route['summary'],
|
||||
'description' => $route['summary'],
|
||||
'parameters' => $this->convertParametersToOpenAPI($route['parameters']),
|
||||
'responses' => $this->convertResponsesToOpenAPI($route['responses']),
|
||||
'tags' => [$classData['short_name']]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert parameters to OpenAPI format.
|
||||
*/
|
||||
protected function convertParametersToOpenAPI(array $parameters): array
|
||||
{
|
||||
$openApiParams = [];
|
||||
|
||||
foreach ($parameters as $param) {
|
||||
$openApiParam = [
|
||||
'name' => $param['name'],
|
||||
'in' => $param['location'],
|
||||
'required' => $param['required'],
|
||||
'schema' => [
|
||||
'type' => $param['type']
|
||||
],
|
||||
'description' => $param['description']
|
||||
];
|
||||
|
||||
if ($param['enum']) {
|
||||
$openApiParam['schema']['enum'] = $param['enum'];
|
||||
}
|
||||
|
||||
if ($param['minimum'] !== null) {
|
||||
$openApiParam['schema']['minimum'] = $param['minimum'];
|
||||
}
|
||||
|
||||
if ($param['maximum'] !== null) {
|
||||
$openApiParam['schema']['maximum'] = $param['maximum'];
|
||||
}
|
||||
|
||||
if ($param['minLength'] !== null) {
|
||||
$openApiParam['schema']['minLength'] = $param['minLength'];
|
||||
}
|
||||
|
||||
if ($param['maxLength'] !== null) {
|
||||
$openApiParam['schema']['maxLength'] = $param['maxLength'];
|
||||
}
|
||||
|
||||
if ($param['format']) {
|
||||
$openApiParam['schema']['format'] = $param['format'];
|
||||
}
|
||||
|
||||
if ($param['pattern']) {
|
||||
$openApiParam['schema']['pattern'] = $param['pattern'];
|
||||
}
|
||||
|
||||
$openApiParams[] = $openApiParam;
|
||||
}
|
||||
|
||||
return $openApiParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert responses to OpenAPI format.
|
||||
*/
|
||||
protected function convertResponsesToOpenAPI(array $responses): array
|
||||
{
|
||||
$openApiResponses = [];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
$openApiResponses[$response['code']] = [
|
||||
'description' => $response['description'],
|
||||
'content' => [
|
||||
'application/json' => [
|
||||
'schema' => [
|
||||
'type' => $response['type']
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $openApiResponses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate statistics.
|
||||
*/
|
||||
protected function generateStatistics(array $documentation): array
|
||||
{
|
||||
$stats = [
|
||||
'total_classes' => count($documentation),
|
||||
'total_methods' => 0,
|
||||
'total_properties' => 0,
|
||||
'total_routes' => 0,
|
||||
'routes_by_method' => [],
|
||||
'classes_with_routes' => 0
|
||||
];
|
||||
|
||||
foreach ($documentation as $classData) {
|
||||
$stats['total_methods'] += count($classData['methods']);
|
||||
$stats['total_properties'] += count($classData['properties']);
|
||||
$stats['total_routes'] += count($classData['routes']);
|
||||
|
||||
if (!empty($classData['routes'])) {
|
||||
$stats['classes_with_routes']++;
|
||||
}
|
||||
|
||||
foreach ($classData['routes'] as $route) {
|
||||
$method = $route['method'];
|
||||
if (!isset($stats['routes_by_method'][$method])) {
|
||||
$stats['routes_by_method'][$method] = 0;
|
||||
}
|
||||
$stats['routes_by_method'][$method]++;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default configuration.
|
||||
*/
|
||||
protected function getDefaultConfig(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'API Documentation',
|
||||
'version' => '1.0.0',
|
||||
'description' => 'Generated API documentation',
|
||||
'theme' => 'default',
|
||||
'include_private' => false,
|
||||
'include_protected' => false,
|
||||
'sort_methods' => true,
|
||||
'group_by_namespace' => true
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error.
|
||||
*/
|
||||
protected function logError(string $message): void
|
||||
{
|
||||
error_log("[DocumentationGenerator] {$message}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processed classes.
|
||||
*/
|
||||
public function getProcessedClasses(): array
|
||||
{
|
||||
return $this->processedClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration.
|
||||
*/
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear processed classes.
|
||||
*/
|
||||
public function clearProcessedClasses(): void
|
||||
{
|
||||
$this->processedClasses = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user