feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -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();
}

View 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;
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 = [];
}
}