Files
mystuff_backend/api/services/DocumentationService.php
Lars Behrends e38a6e1f7b Add strict types and type hints across API
Apply strict_types and extensive type declarations throughout the API and models, improving type safety and error handling. Key changes: add declare(strict_types=1) to many files; convert properties, method parameters and return values to typed signatures (PDO, arrays, ints, strings, bools, nullables); switch exception handling to Throwable in index and Router; improve Router, controllers and model method signatures and nullability handling; refine file/image serving security checks and headers in ImageController; strengthen Database typing and initialization methods; return explicit types from BaseModel CRUD helpers and counting; update Media/Cast/Adult/Game/Console/Settings controllers and models to use typed methods, better validation, and clearer update/create return types. Also add AGENTS.md (agent skills index), update README with Swagger/OpenAPI usage instructions, and add /.windsurf to .gitignore. These changes aim to harden runtime correctness, make intended contracts explicit, and prepare the codebase for easier maintenance and static analysis.
2026-04-16 16:40:31 +02:00

207 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
class DocumentationService {
private string $controllersPath;
private string $modelsPath;
public function __construct(string $controllersPath = __DIR__ . '/../controllers/', string $modelsPath = __DIR__ . '/../models/') {
$this->controllersPath = $controllersPath;
$this->modelsPath = $modelsPath;
}
public function generateDocumentation(): array {
$docs = [
'title' => 'Media API Documentation',
'version' => '1.0.0',
'baseUrl' => '/api',
'endpoints' => [],
'models' => []
];
// Controller scannen
$docs['endpoints'] = $this->scanControllers();
// Modelle scannen
$docs['models'] = $this->scanModels();
return $docs;
}
private function scanControllers(): array {
$endpoints = [];
$controllerFiles = glob($this->controllersPath . '*Controller.php');
foreach ($controllerFiles as $file) {
$className = basename($file, '.php');
require_once $file;
$reflection = new ReflectionClass($className);
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if ($method->getName() === '__construct') {
continue;
}
$docComment = $method->getDocComment();
$endpointInfo = $this->parseMethodDoc($docComment, $method->getName(), $className);
if ($endpointInfo) {
$endpoints[] = $endpointInfo;
}
}
}
return $endpoints;
}
private function parseMethodDoc(?string $docComment, string $methodName, string $className): ?array {
if (!$docComment) {
return null;
}
$info = [
'controller' => $className,
'method' => $methodName,
'description' => '',
'parameters' => [],
'response' => [],
'example' => null
];
// Beschreibung extrahieren
if (preg_match('/\*\s*(.+?)\n/', $docComment, $matches)) {
$info['description'] = trim($matches[1]);
}
// @param extrahieren
if (preg_match_all('/@param\s+(\w+)\s+\$(\w+)\s+(.+)/', $docComment, $matches)) {
foreach ($matches[1] as $i => $type) {
$info['parameters'][] = [
'name' => $matches[2][$i],
'type' => $type,
'description' => trim($matches[3][$i])
];
}
}
// @return extrahieren
if (preg_match('/@return\s+(\w+)\s+(.+)/', $docComment, $matches)) {
$info['response'] = [
'type' => $matches[1],
'description' => trim($matches[2])
];
}
// @example extrahieren
if (preg_match('/@example\s+(.+)/', $docComment, $matches)) {
$info['example'] = trim($matches[1]);
}
// HTTP-Methoden und Pfade aus Methodennamen ableiten
$info['httpMethods'] = $this->inferHttpMethods($methodName);
$info['path'] = $this->inferPath($className, $methodName);
return $info;
}
private function inferHttpMethods(string $methodName): array {
$methods = [];
if (strpos($methodName, 'get') === 0) {
$methods[] = 'GET';
}
if (strpos($methodName, 'create') !== false || strpos($methodName, 'add') !== false || strpos($methodName, 'post') !== false) {
$methods[] = 'POST';
}
if (strpos($methodName, 'update') !== false) {
$methods[] = 'PUT';
}
if (strpos($methodName, 'delete') !== false) {
$methods[] = 'DELETE';
}
if (empty($methods)) {
$methods[] = 'GET'; // Default
}
return $methods;
}
private function inferPath(string $className, string $methodName): string {
$resource = strtolower(str_replace('Controller', '', $className));
$path = "/{$resource}";
if (strpos($methodName, 'getOne') !== false || strpos($methodName, 'update') !== false || strpos($methodName, 'delete') !== false) {
$path .= '/:id';
}
if (strpos($methodName, 'getMedia') !== false) {
$path .= '/:id/media';
}
if (strpos($methodName, 'handleEpisodes') !== false) {
$path .= '/:id/episodes';
}
if (strpos($methodName, 'handleTracks') !== false) {
$path .= '/:id/tracks';
}
if (strpos($methodName, 'handleAdult') !== false) {
$path .= '/adult';
}
return $path;
}
private function scanModels(): array {
$models = [];
$modelFiles = glob($this->modelsPath . '*.php');
foreach ($modelFiles as $file) {
$className = basename($file, '.php');
if ($className === 'BaseModel' || $className === 'MediaType') {
continue;
}
require_once $file;
$reflection = new ReflectionClass($className);
$docComment = $reflection->getDocComment();
$modelInfo = [
'name' => $className,
'description' => '',
'fields' => []
];
if ($docComment) {
if (preg_match('/\*\s*(.+?)\n/', $docComment, $matches)) {
$modelInfo['description'] = trim($matches[1]);
}
}
// Properties aus Klasse ableiten
$properties = $reflection->getProperties();
foreach ($properties as $property) {
if ($property->isPublic() || $property->isProtected()) {
$modelInfo['fields'][] = [
'name' => $property->getName(),
'type' => 'mixed',
'description' => ''
];
}
}
$models[] = $modelInfo;
}
return $models;
}
}