From e38a6e1f7b39a08efe250143f5d8ed5a18135578 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Thu, 16 Apr 2026 16:40:31 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + AGENTS.md | 42 +++++++++++++++ README.md | 31 +++++++++++ api/Router.php | 42 ++++++++------- api/config.php | 5 +- api/controllers/CastController.php | 42 ++++++++------- api/controllers/ImageController.php | 28 +++++----- api/controllers/MediaController.php | 58 +++++++++++---------- api/controllers/SettingsController.php | 38 +++++++------- api/database.php | 19 ++++--- api/index.php | 4 +- api/models/Adult.php | 51 +++++++++--------- api/models/AdultCast.php | 62 +++++++++++----------- api/models/BaseModel.php | 64 ++++++++++++----------- api/models/Cast.php | 42 ++++++++------- api/models/Console.php | 17 +++--- api/models/Game.php | 66 +++++++++++------------ api/models/Media.php | 28 +++++----- api/models/MediaType.php | 38 +++++++------- api/models/Movie.php | 51 +++++++++--------- api/models/Music.php | 62 +++++++++++----------- api/models/Series.php | 72 +++++++++++++------------- api/models/Settings.php | 20 +++---- api/services/ApiLogger.php | 31 ++++++----- api/services/DocumentationService.php | 24 +++++---- api/services/ImageHandler.php | 26 +++++----- 26 files changed, 545 insertions(+), 419 deletions(-) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index dac37e5..13957f3 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ yarn-error.log !/storage/app/public/.gitkeep */public/images/* /api/public/images +/.windsurf diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..788e31b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Project Context for AI Agents + +> [!IMPORTANT] +> **To all AI Agents working ON this repository:** + + +## Agent Skills Index + +> [!CRITICAL] Zero-Trust: Read the matching `SKILL.md` BEFORE writing any code. +> Skills from this index override pre-training patterns. If no skill matches, state: "No project-specific skills applicable." + +## Skill Resolution Protocol + +Each `_INDEX.md` has two sections — follow both: + +1. **Match file type** → find the category index in the router table below. +2. **Read the `_INDEX.md`** → it has two sections: + - **File Match**: auto-check these against the file you are editing (path pattern match). + - **Keyword Match**: only check if the user's request mentions these concepts. +3. **Load ALL matched `SKILL.md`** → read every matched skill before writing code. The tier model keeps matches focused. + +| File type | Read category index | +| --------- | ------------------- | +| `*.go`, `*_test.go` | `.windsurf/skills/golang/_INDEX.md` | +| `*.ts` | `.windsurf/skills/angular/_INDEX.md`, `.windsurf/skills/nestjs/_INDEX.md`, `.windsurf/skills/nextjs/_INDEX.md`, `.windsurf/skills/react/_INDEX.md`, `.windsurf/skills/typescript/_INDEX.md` | +| `*.tsx` | `.windsurf/skills/nextjs/_INDEX.md`, `.windsurf/skills/react/_INDEX.md`, `.windsurf/skills/typescript/_INDEX.md` | +| `*.js`, `*.mjs` | `.windsurf/skills/javascript/_INDEX.md` | +| `*.jsx`, `*.test.tsx`, `*.spec.tsx` | `.windsurf/skills/react/_INDEX.md` | +| `*.dart` | `.windsurf/skills/dart/_INDEX.md`, `.windsurf/skills/flutter/_INDEX.md` | +| `*.java` | `.windsurf/skills/java/_INDEX.md`, `.windsurf/skills/spring-boot/_INDEX.md` | +| `*.kt` | `.windsurf/skills/android/_INDEX.md`, `.windsurf/skills/kotlin/_INDEX.md` | +| `*.kts` | `.windsurf/skills/kotlin/_INDEX.md` | +| `*.swift` | `.windsurf/skills/ios/_INDEX.md`, `.windsurf/skills/swift/_INDEX.md` | +| `*.php` | `.windsurf/skills/laravel/_INDEX.md`, `.windsurf/skills/php/_INDEX.md` | +| `*.sql`, `*.entity.ts`, `*.prisma` | `.windsurf/skills/database/_INDEX.md` | +| `*.component.ts`, `*.component.html` | `.windsurf/skills/angular/_INDEX.md` | +| `*.service.ts`, `*.module.ts` | `.windsurf/skills/angular/_INDEX.md`, `.windsurf/skills/nestjs/_INDEX.md` | +| `*.spec.ts`, `*.test.ts` | `.windsurf/skills/common/_INDEX.md` | +| Any file (keyword match) | `.windsurf/skills/common/_INDEX.md` | +| QE workflow | `.windsurf/skills/quality-engineering/_INDEX.md` | + + diff --git a/README.md b/README.md index b0ec190..5b855de 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,37 @@ Tables are auto-created on first API call. Check logs: docker-compose logs php ``` +## Swagger/OpenAPI Documentation + +A Swagger/OpenAPI specification file is included at `swagger.json` in the project root. This file can be used with Swagger UI to interactively test the API. + +### Using Swagger UI + +#### Option 1: Online Swagger Editor +1. Go to [https://editor.swagger.io](https://editor.swagger.io) +2. Import the `swagger.json` file +3. Click "Try it out" to test endpoints + +#### Option 2: Docker with Swagger UI +```bash +docker run -p 8080:8080 -e SWAGGER_JSON=/swagger/swagger.json -v $(pwd):/swagger swaggerapi/swagger-ui +``` +Then access at: http://localhost:8080 + +#### Option 3: Local Swagger UI +1. Download Swagger UI from [https://github.com/swagger-api/swagger-ui](https://github.com/swagger-api/swagger-ui) +2. Extract and open `dist/index.html` +3. Modify the URL to point to your `swagger.json` file + +### API Endpoints Covered +- Root endpoints (API info, auto-docs) +- Media CRUD operations +- Series episodes CRUD operations +- Music tracks CRUD operations +- Cast CRUD operations +- Adult cast CRUD operations +- Settings operations + ## License [Add your license here] diff --git a/api/Router.php b/api/Router.php index 52a243d..aa88110 100644 --- a/api/Router.php +++ b/api/Router.php @@ -1,5 +1,7 @@ pdo = $pdo; $this->mediaController = new MediaController($pdo); $this->castController = new CastController($pdo); @@ -25,27 +27,27 @@ class Router { $this->documentationService = new DocumentationService(); $this->logger = ApiLogger::getInstance(); } - - public function route($method, $pathSegments) { + + public function route(string $method, array $pathSegments): array { $path = '/' . implode('/', $pathSegments); $queryString = $_SERVER['QUERY_STRING'] ?? ''; $fullPath = $queryString ? $path . '?' . $queryString : $path; - + // Request loggen $body = null; if ($method === 'POST' || $method === 'PUT') { $body = json_decode(file_get_contents('php://input'), true); } $this->logger->logRequest($method, $fullPath, $_GET, $body); - + if (empty($pathSegments)) { $response = $this->getRoot(); $this->logger->logResponse($method, $fullPath, 200, $response); return $response; } - + $resource = $pathSegments[0]; - + try { switch ($resource) { case 'images': @@ -77,20 +79,20 @@ class Router { $this->logger->logResponse($method, $fullPath, 404, $response); return $response; } - } catch (Exception $e) { + } catch (Throwable $e) { http_response_code(500); $response = ['success' => false, 'error' => $e->getMessage()]; $this->logger->logError($method, $fullPath, $e->getMessage()); return $response; } } - - private function getDocumentation() { + + private function getDocumentation(): array { $docs = $this->documentationService->generateDocumentation(); return ['success' => true, 'data' => $docs]; } - - private function getRoot() { + + private function getRoot(): array { return [ 'success' => true, 'message' => 'Media API v1.0', diff --git a/api/config.php b/api/config.php index 326ab68..aefb8c5 100644 --- a/api/config.php +++ b/api/config.php @@ -1,4 +1,7 @@ cast = new Cast($pdo); $this->adultCast = new AdultCast($pdo); $this->logger = ApiLogger::getInstance(); } - - public function handleRequest($method, $segments) { + + public function handleRequest(string $method, array $segments): array { $id = isset($segments[1]) ? (int)$segments[1] : null; $subResource = isset($segments[2]) ? $segments[2] : null; @@ -26,7 +28,7 @@ class CastController { // die("adult"); return $this->handleAdult($method, $id, $segments); } - + switch ($method) { case 'GET': return $id ? $this->getOne($id, $segments) : $this->getAll(); @@ -42,7 +44,7 @@ class CastController { } } - private function handleAdult($method, $id, $segments) { + private function handleAdult(string $method, ?int $id, array $segments): array { switch ($method) { case 'GET': @@ -62,7 +64,7 @@ class CastController { } } - private function getAdultAll() { + private function getAdultAll(): array { $filters = []; if (isset($_GET['search'])) $filters['search'] = $_GET['search']; if (isset($_GET['ethnicity'])) $filters['ethnicity'] = $_GET['ethnicity']; @@ -75,7 +77,7 @@ class CastController { return ['success' => true, 'data' => $result]; } - private function getAdultOne($id) { + private function getAdultOne(?int $id): array { $cast = $this->adultCast->getWithAdultSpecifics($id); if (!$cast) { http_response_code(404); @@ -84,7 +86,7 @@ class CastController { return ['success' => true, 'data' => $cast]; } - private function createAdult() { + private function createAdult(): array { $data = json_decode(file_get_contents('php://input'), true); if (!$data) { http_response_code(400); @@ -119,7 +121,7 @@ class CastController { return ['success' => true, 'data' => ['id' => $castId]]; } - private function updateAdult($id) { + private function updateAdult(?int $id): array { if (!$id) { http_response_code(400); return ['success' => false, 'error' => 'ID required']; @@ -137,7 +139,7 @@ class CastController { return ['success' => true, 'data' => ['id' => $id]]; } - private function deleteAdultSpecifics($id) { + private function deleteAdultSpecifics(?int $id): array { if (!$id) { http_response_code(400); return ['success' => false, 'error' => 'ID required']; @@ -152,7 +154,7 @@ class CastController { $this->logger->logResponse('DELETE', "/api/cast/adult/$id", 200, ['message' => 'Adult specifics deleted successfully']); return ['success' => true, 'message' => 'Adult specifics deleted successfully']; } - private function getOne($id, $segments) { + private function getOne(?int $id, array $segments): array { // Prüfen ob /media angehängt wurde if (isset($segments[2]) && $segments[2] === 'media') { return $this->getMedia($id); @@ -169,12 +171,12 @@ class CastController { return ['success' => true, 'data' => $cast]; } - private function getMedia($castId) { + private function getMedia(?int $castId): array { $media = $this->cast->getMediaForCast($castId); return ['success' => true, 'data' => ['items' => $media]]; } - private function getAll() { + private function getAll(): array { $filters = []; if (isset($_GET['search'])) $filters['search'] = $_GET['search']; @@ -185,7 +187,7 @@ class CastController { return ['success' => true, 'data' => $result]; } - private function create() { + private function create(): array { $data = json_decode(file_get_contents('php://input'), true); if (!$data) { http_response_code(400); @@ -220,7 +222,7 @@ class CastController { return ['success' => true, 'data' => ['id' => $castId]]; } - private function update($id) { + private function update(?int $id): array { if (!$id) { http_response_code(400); return ['success' => false, 'error' => 'ID required']; @@ -238,7 +240,7 @@ class CastController { return ['success' => true, 'data' => ['id' => $id]]; } - private function delete($id) { + private function delete(?int $id): array { if (!$id) { http_response_code(400); return ['success' => false, 'error' => 'ID required']; diff --git a/api/controllers/ImageController.php b/api/controllers/ImageController.php index 5192029..dde6a73 100644 --- a/api/controllers/ImageController.php +++ b/api/controllers/ImageController.php @@ -1,46 +1,48 @@ imageDir = __DIR__ . '/../public/images/'; } - - public function handleRequest($method, $pathSegments) { + + public function handleRequest(string $method, array $pathSegments): array { // Remove 'images' from path segments array_shift($pathSegments); - + // Build file path $imagePath = implode('/', $pathSegments); $fullPath = $this->imageDir . $imagePath; - + // Security check: ensure the path is within the images directory $realPath = realpath($fullPath); $realImageDir = realpath($this->imageDir); - + if ($realPath === false || strpos($realPath, $realImageDir) !== 0) { http_response_code(403); return ['success' => false, 'error' => 'Access denied']; } - + // Check if file exists if (!file_exists($realPath)) { http_response_code(404); return ['success' => false, 'error' => 'Image not found']; } - + // Check if it's actually a file if (!is_file($realPath)) { http_response_code(404); return ['success' => false, 'error' => 'Not a file']; } - + // Get file info $fileInfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($fileInfo, $realPath); finfo_close($fileInfo); - + if ($mimeType === false) { // Fallback to common image types $extension = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); @@ -54,13 +56,13 @@ class ImageController { ]; $mimeType = $mimeTypes[$extension] ?? 'application/octet-stream'; } - + // Set headers for image serving header('Content-Type: ' . $mimeType); header('Content-Length: ' . filesize($realPath)); header('Cache-Control: public, max-age=31536000'); // Cache for 1 year header('Pragma: public'); - + // Output the image readfile($realPath); exit; diff --git a/api/controllers/MediaController.php b/api/controllers/MediaController.php index 6d5e9b7..57b0be8 100644 --- a/api/controllers/MediaController.php +++ b/api/controllers/MediaController.php @@ -1,5 +1,7 @@ media = new Media($pdo); $this->series = new Series($pdo); $this->music = new Music($pdo); $this->game = new Game($pdo); $this->logger = ApiLogger::getInstance(); } - - public function handleRequest($method, $segments) { + + public function handleRequest(string $method, array $segments): array { $id = isset($segments[1]) ? (int)$segments[1] : null; $subResource = isset($segments[2]) ? $segments[2] : null; - + // Sub-Endpunkte für Episoden und Tracks if ($id && $subResource) { if ($subResource === 'episodes') { @@ -34,7 +36,7 @@ class MediaController { return $this->handleTracks($method, $id, $segments); } } - + switch ($method) { case 'GET': return $id ? $this->getOne($id) : $this->getAll(); @@ -50,7 +52,7 @@ class MediaController { } } - private function handleEpisodes($method, $mediaId, $segments) { + private function handleEpisodes(string $method, ?int $mediaId, array $segments): array { $episodeId = isset($segments[3]) ? (int)$segments[3] : null; switch ($method) { @@ -71,7 +73,7 @@ class MediaController { } } - private function handleTracks($method, $mediaId, $segments) { + private function handleTracks(string $method, ?int $mediaId, array $segments): array { $trackId = isset($segments[3]) ? (int)$segments[3] : null; switch ($method) { @@ -92,7 +94,7 @@ class MediaController { } } - private function getEpisodes($mediaId) { + private function getEpisodes(?int $mediaId): array { $season = isset($_GET['season']) ? (int)$_GET['season'] : null; $episodes = $this->series->getEpisodes($mediaId, $season); return ['success' => true, 'data' => ['items' => $episodes]]; @@ -103,7 +105,7 @@ class MediaController { * @param int $mediaId Media ID * @return array Created episode ID */ - private function addEpisode($mediaId) { + private function addEpisode(?int $mediaId): array { $data = json_decode(file_get_contents('php://input'), true); if (!$data) { http_response_code(400); @@ -120,7 +122,7 @@ class MediaController { * @param int $episodeId Episode ID * @return array Updated episode ID */ - private function updateEpisode($episodeId) { + private function updateEpisode(?int $episodeId): array { if (!$episodeId) { http_response_code(400); return ['success' => false, 'error' => 'Episode ID required']; @@ -141,7 +143,7 @@ class MediaController { * @param int $episodeId Episode ID * @return array Success message */ - private function deleteEpisode($episodeId) { + private function deleteEpisode(?int $episodeId): array { if (!$episodeId) { http_response_code(400); return ['success' => false, 'error' => 'Episode ID required']; @@ -160,7 +162,7 @@ class MediaController { * @param int $episodeId Episode ID * @return array Episode data */ - private function getEpisode($episodeId) { + private function getEpisode(?int $episodeId): array { // Episode direkt aus Datenbank abrufen $stmt = $this->series->getConnection()->prepare("SELECT * FROM episodes WHERE id = ?"); $stmt->execute([$episodeId]); @@ -173,12 +175,12 @@ class MediaController { return ['success' => true, 'data' => $episode]; } - private function getTracks($mediaId) { + private function getTracks(?int $mediaId): array { $tracks = $this->music->getTracks($mediaId); return ['success' => true, 'data' => ['items' => $tracks]]; } - private function addTrack($mediaId) { + private function addTrack(?int $mediaId): array { $data = json_decode(file_get_contents('php://input'), true); if (!$data) { http_response_code(400); @@ -190,7 +192,7 @@ class MediaController { return ['success' => true, 'data' => ['id' => $trackId]]; } - private function updateTrack($trackId) { + private function updateTrack(?int $trackId): array { if (!$trackId) { http_response_code(400); return ['success' => false, 'error' => 'Track ID required']; @@ -206,7 +208,7 @@ class MediaController { return ['success' => true, 'data' => ['id' => $trackId]]; } - private function deleteTrack($trackId) { + private function deleteTrack(?int $trackId): array { if (!$trackId) { http_response_code(400); return ['success' => false, 'error' => 'Track ID required']; @@ -220,7 +222,7 @@ class MediaController { return ['success' => true, 'message' => 'Track deleted successfully']; } - private function getTrack($trackId) { + private function getTrack(?int $trackId): array { // Track direkt aus Datenbank abrufen $stmt = $this->music->getConnection()->prepare("SELECT * FROM tracks WHERE id = ?"); $stmt->execute([$trackId]); @@ -238,7 +240,7 @@ class MediaController { * @param int $id Media ID * @return array Media object with relations */ - private function getOne($id) { + private function getOne(?int $id): array { // Zuerst Basis-Media abrufen um Typ zu bestimmen $baseMedia = $this->media->getBase($id); if (!$baseMedia) { @@ -268,7 +270,7 @@ class MediaController { * Get all media items with filtering and pagination * @return array Paginated media list */ - private function getAll() { + private function getAll(): array { $filters = []; if (isset($_GET['category'])) $filters['category'] = $_GET['category']; if (isset($_GET['type'])) $filters['type'] = $_GET['type']; @@ -286,7 +288,7 @@ class MediaController { * Create a new media item * @return array Created media ID */ - private function create() { + private function create(): array { $data = json_decode(file_get_contents('php://input'), true); if (!$data) { http_response_code(400); @@ -341,7 +343,7 @@ class MediaController { * @param int $id Media ID * @return array Updated media ID */ - private function update($id) { + private function update(?int $id): array { if (!$id) { http_response_code(400); return ['success' => false, 'error' => 'ID required']; @@ -375,7 +377,7 @@ class MediaController { * @param int $id Media ID * @return array Success message */ - private function delete($id) { + private function delete(?int $id): array { if (!$id) { http_response_code(400); return ['success' => false, 'error' => 'ID required']; diff --git a/api/controllers/SettingsController.php b/api/controllers/SettingsController.php index cb62f78..6af3890 100644 --- a/api/controllers/SettingsController.php +++ b/api/controllers/SettingsController.php @@ -1,21 +1,23 @@ settings = new Settings($pdo); $this->logger = ApiLogger::getInstance(); } - - public function handleRequest($method, $segments) { + + public function handleRequest(string $method, array $segments): array { $path = '/' . implode('/', $segments); $this->logger->logRequest($method, $path); - + switch ($method) { case 'GET': return $this->get(); @@ -26,36 +28,36 @@ class SettingsController { return ['success' => false, 'error' => 'Method not allowed']; } } - - private function get() { + + private function get(): array { $settings = $this->settings->getSettings(); - + if (!$settings) { http_response_code(404); return ['success' => false, 'error' => 'Settings not found']; } - + return ['success' => true, 'data' => $settings]; } - - private function update() { + + private function update(): array { $data = json_decode(file_get_contents('php://input'), true); - + if (!$data) { http_response_code(400); return ['success' => false, 'error' => 'Invalid JSON']; } - + $settings = $this->settings->updateSettings($data); - + if (!$settings) { http_response_code(500); return ['success' => false, 'error' => 'Failed to update settings']; } - + $this->logger->logRequest('PUT', '/api/settings', [], $data); $this->logger->logResponse('PUT', '/api/settings', 200, $settings); - + return ['success' => true, 'data' => $settings]; } } diff --git a/api/database.php b/api/database.php index b218125..cd8f6e9 100644 --- a/api/database.php +++ b/api/database.php @@ -1,9 +1,12 @@ pdo->exec(" CREATE TABLE IF NOT EXISTS media ( @@ -366,8 +369,8 @@ class Database { // Bestehende Einträge mit cleanname aktualisieren $this->updateExistingCleanNames(); } - - private function updateExistingCleanNames() { + + private function updateExistingCleanNames(): void { // Media cleanname aktualisieren $this->pdo->exec(" UPDATE media @@ -382,8 +385,8 @@ class Database { WHERE cleanname IS NULL OR cleanname = '' "); } - - public function getConnection() { + + public function getConnection(): PDO { return $this->pdo; } } diff --git a/api/index.php b/api/index.php index 5c5eff7..f484bec 100644 --- a/api/index.php +++ b/api/index.php @@ -1,5 +1,7 @@ route($method, $pathSegments); echo json_encode($response); -} catch (Exception $e) { +} catch (Throwable $e) { http_response_code(500); echo json_encode(['success' => false, 'error' => $e->getMessage()]); } diff --git a/api/models/Adult.php b/api/models/Adult.php index 7c46a73..3b0ab11 100644 --- a/api/models/Adult.php +++ b/api/models/Adult.php @@ -1,34 +1,37 @@ imageHandler = new ImageHandler(); } - - protected function getType() { + + protected function getType(): string { return 'Adult'; } - - protected function getTypeSpecificFields() { + + protected function getTypeSpecificFields(): array { return []; } - - protected function validateTypeSpecificFields($data) { + + protected function validateTypeSpecificFields(array $data): array { // Adult spezifische Validierung // Eventuell Altersverifikation etc. + return $data; } - - protected function processPosterField($data) { + + protected function processPosterField(array $data): array { error_log("Adult::processPosterField - Checking for poster field, isUpdate: " . ($this->isUpdate ? 'yes' : 'no')); - + if ($this->isUpdate && $this->mediaId && isset($data['poster']) && !empty($data['poster'])) { $currentMedia = $this->findById($this->mediaId); if ($currentMedia && !empty($currentMedia['poster'])) { @@ -39,15 +42,15 @@ class Adult extends MediaType { } } } - + if (isset($data['poster']) && !empty($data['poster'])) { error_log("Adult::processPosterField - Poster found, length: " . strlen($data['poster'])); - + if (strpos($data['poster'], '/images/') === 0 || filter_var($data['poster'], FILTER_VALIDATE_URL)) { error_log("Adult::processPosterField - Poster is already a path or URL, skipping processing"); return $data; } - + $posterPath = $this->imageHandler->saveBase64Image($data['poster'], 'adult/poster'); error_log("Adult::processPosterField - ImageHandler returned: " . ($posterPath ?: 'null')); if ($posterPath) { @@ -61,23 +64,23 @@ class Adult extends MediaType { } return $data; } - - public function createWithRelations($data) { + + public function createWithRelations(array $data): int { $data['type'] = 'Adult'; $this->validateTypeSpecificFields($data); $data = $this->processPosterField($data); return parent::createWithRelations($data); } - - public function updateWithRelations($id, $data) { + + public function updateWithRelations(int $id, array $data): bool { $this->isUpdate = true; $this->mediaId = $id; $this->validateTypeSpecificFields($data); $data = $this->processPosterField($data); - parent::updateWithRelations($id, $data); + return parent::updateWithRelations($id, $data); } - - public function search($filters = [], $page = 1, $limit = 20) { + + public function search(array $filters = [], int $page = 1, int $limit = 20): array { // Nur Adult Content suchen $filters['type'] = 'Adult'; return parent::search($filters, $page, $limit); diff --git a/api/models/AdultCast.php b/api/models/AdultCast.php index 9321735..6ca45f4 100644 --- a/api/models/AdultCast.php +++ b/api/models/AdultCast.php @@ -1,41 +1,43 @@ getWithFilmography($id); if (!$cast) { return null; } - + $cast['adult_specifics'] = $this->getAdultSpecifics($id); - + return $cast; } - - public function getAdultSpecifics($castId) { + + public function getAdultSpecifics(?int $castId): array|false { $stmt = $this->pdo->prepare("SELECT * FROM adult_cast_specifics WHERE cast_id = ?"); $stmt->execute([$castId]); return $stmt->fetch(); } - - public function createWithAdultSpecifics($data) { + + public function createWithAdultSpecifics(array $data): int { ApiLogger::getInstance()->logDebug("AdultCast createWithAdultSpecifics called with data: " . json_encode($data)); - + $name = $data['name'] ?? null; if (!$name) { throw new Exception('Name is required'); } - + // cleanname generieren $cleanname = generateCleanName($name); - + // Process photo field (base64 to file path) $data = $this->processPhotoField($data); - + // Zuerst Basis-Cast erstellen $castData = [ 'name' => $name, @@ -45,16 +47,16 @@ class AdultCast extends Cast { 'birthDate' => $data['birthDate'] ?? null, 'birthPlace' => $data['birthPlace'] ?? null ]; - - $castId = $this->create($castData); + + $castId = (int)$this->create($castData); ApiLogger::getInstance()->logDebug("AdultCast createWithAdultSpecifics: Base cast created with id: $castId"); - + // Occupations speichern if (isset($data['occupations']) && is_array($data['occupations'])) { ApiLogger::getInstance()->logDebug("AdultCast createWithAdultSpecifics: Saving occupations: " . json_encode($data['occupations'])); $this->saveRelatedItems('occupations', $castId, $data['occupations'], 'cast_id'); } - + // Adult-spezifische Daten speichern if (isset($data['adult_specifics']) && is_array($data['adult_specifics'])) { ApiLogger::getInstance()->logDebug("AdultCast createWithAdultSpecifics: Saving adult_specifics"); @@ -62,20 +64,20 @@ class AdultCast extends Cast { } else { ApiLogger::getInstance()->logDebug("AdultCast createWithAdultSpecifics: No adult_specifics found in data"); } - + return $castId; } - - public function updateWithAdultSpecifics($id, $data) { + + public function updateWithAdultSpecifics(int $id, array $data): bool { ApiLogger::getInstance()->logDebug("AdultCast updateWithAdultSpecifics called with id: $id, data: " . json_encode($data)); - + // Set update flag for image replacement $this->isUpdate = true; $this->castId = $id; - + // Process photo field (base64 to file path) $data = $this->processPhotoField($data); - + // Basis-Cast aktualisieren $castData = []; foreach (['name', 'photo', 'bio', 'birthDate', 'birthPlace'] as $field) { @@ -83,19 +85,19 @@ class AdultCast extends Cast { $castData[$field] = $data[$field]; } } - + if (!empty($castData)) { ApiLogger::getInstance()->logDebug("AdultCast updateWithAdultSpecifics: Updating base cast data: " . json_encode($castData)); $this->update($id, $castData); } - + // Occupations aktualisieren if (isset($data['occupations']) && is_array($data['occupations'])) { ApiLogger::getInstance()->logDebug("AdultCast updateWithAdultSpecifics: Updating occupations: " . json_encode($data['occupations'])); $this->pdo->prepare("DELETE FROM occupations WHERE cast_id = ?")->execute([$id]); $this->saveRelatedItems('occupations', $id, $data['occupations'], 'cast_id'); } - + // Adult-spezifische Daten aktualisieren if (isset($data['adult_specifics']) && is_array($data['adult_specifics'])) { ApiLogger::getInstance()->logDebug("AdultCast updateWithAdultSpecifics: Saving adult_specifics"); @@ -104,11 +106,11 @@ class AdultCast extends Cast { } else { ApiLogger::getInstance()->logDebug("AdultCast updateWithAdultSpecifics: No adult_specifics found in data"); } - + return true; } - - protected function saveAdultSpecifics($castId, $specifics) { + + protected function saveAdultSpecifics(int $castId, array $specifics): void { ApiLogger::getInstance()->logDebug("AdultCast saveAdultSpecifics called for cast_id: $castId"); ApiLogger::getInstance()->logDebug("Specifics data: " . json_encode($specifics)); @@ -200,13 +202,13 @@ class AdultCast extends Cast { } } - public function deleteAdultSpecifics($castId) { + public function deleteAdultSpecifics(?int $castId): bool { $stmt = $this->pdo->prepare("DELETE FROM adult_cast_specifics WHERE cast_id = ?"); $stmt->execute([$castId]); return $stmt->rowCount() > 0; } - public function searchAdultActors($filters = [], $page = 1, $limit = 2000000000) { + public function searchAdultActors(array $filters = [], int $page = 1, int $limit = 2000000000): array { // Adult Actors mit Specifics suchen $query = " SELECT cs.*, diff --git a/api/models/BaseModel.php b/api/models/BaseModel.php index c13b939..127ac56 100644 --- a/api/models/BaseModel.php +++ b/api/models/BaseModel.php @@ -1,23 +1,25 @@ pdo = $pdo; } - - protected function findById($id) { + + protected function findById(int $id): array|false { $stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch(); } - - protected function findAll($conditions = [], $orderBy = 'createdAt DESC', $limit = null, $offset = null) { + + protected function findAll(array $conditions = [], string $orderBy = 'createdAt DESC', ?int $limit = null, ?int $offset = null): array { $query = "SELECT * FROM {$this->table} WHERE 1=1"; $params = []; - + foreach ($conditions as $field => $value) { if (is_array($value)) { // LIKE Operator @@ -28,26 +30,26 @@ abstract class BaseModel { $params[] = $value; } } - + $query .= " ORDER BY $orderBy"; - - if ($limit) { + + if ($limit !== null) { $query .= " LIMIT " . (int)$limit; } - - if ($offset) { + + if ($offset !== null) { $query .= " OFFSET " . (int)$offset; } - + $stmt = $this->pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } - - protected function count($conditions = []) { + + protected function count(array $conditions = []): int|false { $query = "SELECT COUNT(*) FROM {$this->table} WHERE 1=1"; $params = []; - + foreach ($conditions as $field => $value) { if (is_array($value)) { $query .= " AND $field LIKE ?"; @@ -57,42 +59,42 @@ abstract class BaseModel { $params[] = $value; } } - + $stmt = $this->pdo->prepare($query); $stmt->execute($params); return $stmt->fetchColumn(); } - - protected function create($data) { + + protected function create(array $data): int|false { $fields = array_keys($data); $placeholders = array_fill(0, count($fields), '?'); - + $query = "INSERT INTO {$this->table} (" . implode(', ', $fields) . ") VALUES (" . implode(', ', $placeholders) . ")"; $stmt = $this->pdo->prepare($query); $stmt->execute(array_values($data)); - + return $this->pdo->lastInsertId(); } - - protected function update($id, $data) { + + protected function update(int $id, array $data): bool { $fields = []; $params = []; - + foreach ($data as $field => $value) { $fields[] = "$field = ?"; $params[] = $value; } - + $params[] = $id; - + $query = "UPDATE {$this->table} SET " . implode(', ', $fields) . " WHERE id = ?"; $stmt = $this->pdo->prepare($query); $stmt->execute($params); - + return $stmt->rowCount() > 0; } - - protected function delete($id) { + + protected function delete(int $id): bool { $stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE id = ?"); $stmt->execute([$id]); return $stmt->rowCount() > 0; diff --git a/api/models/Cast.php b/api/models/Cast.php index 8df83fa..e73106e 100644 --- a/api/models/Cast.php +++ b/api/models/Cast.php @@ -1,32 +1,34 @@ imageHandler = new ImageHandler(); } - - public function getWithFilmography($id) { + + public function getWithFilmography(?int $id): ?array { $cast = $this->findById($id); if (!$cast) { return null; } - + $cast['occupations'] = $this->getRelatedItems('occupations', $id, 'cast_id'); $cast['filmography'] = $this->getMediaForCast($id); - + return $cast; } - - public function getMediaForCast($castId) { + + public function getMediaForCast(?int $castId): array { $stmt = $this->pdo->prepare(" SELECT m.id, m.title, m.year, m.poster, m.category, m.type, mc.role, mc.characterName FROM media m @@ -37,8 +39,8 @@ class Cast extends BaseModel { $stmt->execute([$castId]); return $stmt->fetchAll(); } - - public function search($filters = [], $page = 1, $limit = 20) { + + public function search(array $filters = [], int $page = 1, int $limit = 20): array { $conditions = []; if (isset($filters['search'])) { @@ -88,7 +90,7 @@ class Cast extends BaseModel { ]; } - protected function processPhotoField($data) { + protected function processPhotoField(array $data): array { error_log("Cast::processPhotoField - Checking for photo field, isUpdate: " . ($this->isUpdate ? 'yes' : 'no')); if ($this->isUpdate && $this->castId && isset($data['photo']) && !empty($data['photo'])) { @@ -124,7 +126,7 @@ class Cast extends BaseModel { return $data; } - public function createWithOccupations($data) { + public function createWithOccupations(array $data): int { $name = $data['name'] ?? null; if (!$name) { throw new Exception('Name is required'); @@ -153,7 +155,7 @@ class Cast extends BaseModel { return $castId; } - public function updateWithOccupations($id, $data) { + public function updateWithOccupations(int $id, array $data): bool { $this->isUpdate = true; $this->castId = $id; @@ -184,13 +186,13 @@ class Cast extends BaseModel { return true; } - public function findByCleanName($cleanname) { + public function findByCleanName(string $cleanname): array|false { $stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE cleanname = ?"); $stmt->execute([$cleanname]); return $stmt->fetch(); } - protected function getRelatedItems($table, $id, $fkColumn = 'media_id') { + protected function getRelatedItems(string $table, int $id, string $fkColumn = 'media_id'): array { $stmt = $this->pdo->prepare("SELECT * FROM $table WHERE $fkColumn = ?"); $stmt->execute([$id]); $items = $stmt->fetchAll(); @@ -201,7 +203,7 @@ class Cast extends BaseModel { return $result; } - protected function saveRelatedItems($table, $id, $items, $fkColumn = 'media_id') { + protected function saveRelatedItems(string $table, int $id, array $items, string $fkColumn = 'media_id'): void { $valueColumn = $fkColumn === 'cast_id' ? 'occupation' : 'genre'; $stmt = $this->pdo->prepare("INSERT INTO $table ($fkColumn, $valueColumn) VALUES (?, ?)"); foreach ($items as $item) { diff --git a/api/models/Console.php b/api/models/Console.php index e2d37e4..54aec4b 100644 --- a/api/models/Console.php +++ b/api/models/Console.php @@ -1,21 +1,24 @@ imageHandler = new ImageHandler(); } - - protected function getType() { + + protected function getType(): string { return 'Game'; } - - protected function getTypeSpecificFields() { + + protected function getTypeSpecificFields(): array { return []; } - - protected function validateTypeSpecificFields($data) { + + protected function validateTypeSpecificFields(array $data): array { // Game spezifische Validierung if (isset($data['hasIcon'])) { $data['hasIcon'] = is_numeric($data['hasIcon']) ? (int)$data['hasIcon'] : 0; @@ -47,11 +49,11 @@ class Game extends MediaType { return $data; } - + /** * Process poster field - convert base64 to file path */ - protected function processImageField($data, $type) { + protected function processImageField(array $data, string $type): array { if ($this->isUpdate && $this->mediaId && isset($data[$type]) && !empty($data[$type])) { $currentMedia = $this->findById($this->mediaId); if ($currentMedia && !empty($currentMedia[$type])) { @@ -73,7 +75,7 @@ class Game extends MediaType { return $data; } - public function createWithRelations($data) { + public function createWithRelations(array $data): int { // Typ setzen $data['type'] = 'Game'; @@ -152,7 +154,7 @@ class Game extends MediaType { return $mediaId; } - public function updateWithRelations($id, $data) { + public function updateWithRelations(int $id, array $data): bool { // Set update flag and mediaId for image replacement $this->isUpdate = true; $this->mediaId = $id; @@ -236,7 +238,7 @@ class Game extends MediaType { return true; } - public function getWithGameInfo($id) { + public function getWithGameInfo(?int $id): ?array { $media = parent::getWithRelations($id); if (!$media) { return null; @@ -264,7 +266,7 @@ class Game extends MediaType { return $media; } - public function getGameInfoForList($mediaId) { + public function getGameInfoForList(?int $mediaId): ?array { // Media-Games Daten abrufen (ohne vollständige Relationen für Performance) $mediaGameId = $this->getMediaGameId($mediaId); if (!$mediaGameId) { @@ -281,7 +283,7 @@ class Game extends MediaType { return $gameInfo; } - public static function interpolateQuery($query, $params) { + public static function interpolateQuery(string $query, array $params): string { $keys = array(); # build a regular expression for each parameter @@ -300,7 +302,7 @@ class Game extends MediaType { return $query; } - protected function createMediaGame($data) { + protected function createMediaGame(array $data): string { $stmt = $this->pdo->prepare(" INSERT INTO media_games (media_id, sortingName, notes, completionStatus, source, gameId, pluginId, isInstalled, installDirectory, installSize, hidden, favorite, playCount, @@ -337,7 +339,7 @@ class Game extends MediaType { return $this->pdo->lastInsertId(); } - protected function updateMediaGame($mediaId, $data) { + protected function updateMediaGame(int $mediaId, array $data): void { $setClause = []; $params = []; @@ -354,14 +356,14 @@ class Game extends MediaType { $stmt->execute($params); } - protected function getMediaGameId($mediaId) { + protected function getMediaGameId(?int $mediaId): ?int { $stmt = $this->pdo->prepare("SELECT id FROM media_games WHERE media_id = ?"); $stmt->execute([$mediaId]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } - protected function getMediaGameData($mediaGameId) { + protected function getMediaGameData(?int $mediaGameId): array { $stmt = $this->pdo->prepare("SELECT * FROM media_games WHERE id = ?"); $stmt->execute([$mediaGameId]); $data = $stmt->fetch(); @@ -373,7 +375,7 @@ class Game extends MediaType { return $data ?: []; } - protected function saveAchievements($mediaGameId, $achievements) { + protected function saveAchievements(string $mediaGameId, array $achievements): void { $stmt = $this->pdo->prepare(" INSERT INTO achievements (media_game_id, name, description, icon, unlocked, unlocked_date) VALUES (?, ?, ?, ?, ?, ?) @@ -396,26 +398,26 @@ class Game extends MediaType { } } - protected function getAchievements($mediaGameId) { + protected function getAchievements(string $mediaGameId): array { $stmt = $this->pdo->prepare("SELECT * FROM achievements WHERE media_game_id = ?"); $stmt->execute([$mediaGameId]); return $stmt->fetchAll(); } - protected function saveGameRelation($table, $mediaGameId, $items, $field) { + protected function saveGameRelation(string $table, string $mediaGameId, array $items, string $field): void { $stmt = $this->pdo->prepare("INSERT INTO $table (media_game_id, $field) VALUES (?, ?)"); foreach ($items as $item) { $stmt->execute([$mediaGameId, $item]); } } - protected function consoleExists($name) { + protected function consoleExists(string $name): bool { $stmt = $this->pdo->prepare("SELECT id FROM media WHERE type = 'Console' AND title = ?"); $stmt->execute([$name]); return $stmt->fetch() !== false; } - protected function createConsole($name) { + protected function createConsole(string $name): string { $stmt = $this->pdo->prepare(" INSERT INTO media (title, cleanname, type, createdAt, updatedAt) VALUES (?, ?, 'Console', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) @@ -424,7 +426,7 @@ class Game extends MediaType { return $this->pdo->lastInsertId(); } - protected function saveGameRelationWithConsole($table, $mediaGameId, $items, $field) { + protected function saveGameRelationWithConsole(string $table, string $mediaGameId, array $items, string $field): void { $stmt = $this->pdo->prepare("INSERT INTO $table (media_game_id, $field) VALUES (?, ?)"); foreach ($items as $item) { // Check if console exists, create if not @@ -435,7 +437,7 @@ class Game extends MediaType { } } - protected function getGameRelation($table, $mediaGameId, $field) { + protected function getGameRelation(string $table, string $mediaGameId, string $field): array { $stmt = $this->pdo->prepare("SELECT $field FROM $table WHERE media_game_id = ?"); $stmt->execute([$mediaGameId]); $items = $stmt->fetchAll(); @@ -444,7 +446,7 @@ class Game extends MediaType { }, $items); } - protected function saveLinks($mediaGameId, $links) { + protected function saveLinks(string $mediaGameId, array $links): void { $stmt = $this->pdo->prepare("INSERT INTO game_links (media_game_id, name, url) VALUES (?, ?, ?)"); foreach ($links as $link) { $stmt->execute([ @@ -455,13 +457,13 @@ class Game extends MediaType { } } - protected function getLinks($mediaGameId) { + protected function getLinks(string $mediaGameId): array { $stmt = $this->pdo->prepare("SELECT name, url FROM game_links WHERE media_game_id = ?"); $stmt->execute([$mediaGameId]); return $stmt->fetchAll(); } - public function search($filters = [], $page = 1, $limit = 20) { + public function search(array $filters = [], int $page = 1, int $limit = 20): array { // Nur Games suchen $filters['type'] = 'Game'; return parent::search($filters, $page, $limit); diff --git a/api/models/Media.php b/api/models/Media.php index 4f66f1a..95c1a92 100644 --- a/api/models/Media.php +++ b/api/models/Media.php @@ -1,15 +1,17 @@ findById($id); } - public function getWithRelations($id) { + public function getWithRelations(?int $id): ?array { $media = $this->findById($id); if (!$media) { return null; @@ -22,8 +24,8 @@ class Media extends BaseModel { return $media; } - - public function search($filters = [], $page = 1, $limit = 20) { + + public function search(array $filters = [], int $page = 1, int $limit = 20): array { $conditions = []; if (isset($filters['category'])) { @@ -99,7 +101,7 @@ class Media extends BaseModel { ]; } - public function createWithRelations($data) { + public function createWithRelations(array $data): int { $title = $data['title'] ?? null; if (!$title) { throw new Exception('Title is required'); @@ -147,7 +149,7 @@ class Media extends BaseModel { return $mediaId; } - public function updateWithRelations($id, $data) { + public function updateWithRelations(int $id, array $data): bool { $mediaData = []; foreach (['title', 'year', 'poster', 'banner', 'description', 'rating', 'category', 'type', 'status', 'aspectRatio', 'runtime', 'director', 'writer', 'releaseDate', 'source'] as $field) { @@ -186,13 +188,13 @@ class Media extends BaseModel { return true; } - public function findByCleanName($cleanname) { + public function findByCleanName(string $cleanname): array|false { $stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE cleanname = ?"); $stmt->execute([$cleanname]); return $stmt->fetch(); } - protected function saveRelatedItems($table, $id, $items, $fkColumn = 'media_id') { + protected function saveRelatedItems(string $table, int $id, array $items, string $fkColumn = 'media_id'): void { $valueColumn = $table === 'genres' ? 'genre' : ($table === 'tags' ? 'tag' : ($table === 'occupations' ? 'occupation' : 'studio')); $stmt = $this->pdo->prepare("INSERT INTO $table ($fkColumn, $valueColumn) VALUES (?, ?)"); foreach ($items as $item) { @@ -200,7 +202,7 @@ class Media extends BaseModel { } } - protected function getCastForMedia($mediaId) { + protected function getCastForMedia(?int $mediaId): array { $stmt = $this->pdo->prepare(" SELECT cs.*, mc.role, mc.characterName, mc.characterImage FROM cast_staff cs @@ -215,7 +217,7 @@ class Media extends BaseModel { return $cast; } - protected function getRelatedItems($table, $id, $fkColumn = 'media_id') { + protected function getRelatedItems(string $table, int $id, string $fkColumn = 'media_id'): array { $stmt = $this->pdo->prepare("SELECT * FROM $table WHERE $fkColumn = ?"); $stmt->execute([$id]); $items = $stmt->fetchAll(); @@ -230,7 +232,7 @@ class Media extends BaseModel { return $result; } - protected function saveCastAssignments($mediaId, $castData) { + protected function saveCastAssignments(int $mediaId, array $castData): void { foreach ($castData as $member) { $castId = null; if (isset($member['id']) && $member['id']) { diff --git a/api/models/MediaType.php b/api/models/MediaType.php index e34dc78..a837f0b 100644 --- a/api/models/MediaType.php +++ b/api/models/MediaType.php @@ -1,39 +1,41 @@ type = $this->getType(); } - - abstract protected function getType(); - - abstract protected function validateTypeSpecificFields($data); - - abstract protected function getTypeSpecificFields(); - - public function createWithRelations($data) { + + abstract protected function getType(): string; + + abstract protected function validateTypeSpecificFields(array $data): array; + + abstract protected function getTypeSpecificFields(): array; + + public function createWithRelations(array $data): int { // Typ setzen $data['type'] = $this->type; - + // Typ-spezifische Validierung $this->validateTypeSpecificFields($data); - + return parent::createWithRelations($data); } - - public function updateWithRelations($id, $data) { + + public function updateWithRelations(int $id, array $data): bool { // Typ-spezifische Validierung $this->validateTypeSpecificFields($data); - + return parent::updateWithRelations($id, $data); } - - protected function getRequiredFields() { + + protected function getRequiredFields(): array { return []; } } diff --git a/api/models/Movie.php b/api/models/Movie.php index 06d06bd..bb7cab7 100644 --- a/api/models/Movie.php +++ b/api/models/Movie.php @@ -1,36 +1,39 @@ imageHandler = new ImageHandler(); } - - protected function getType() { + + protected function getType(): string { return 'Movie'; } - - protected function getTypeSpecificFields() { + + protected function getTypeSpecificFields(): array { return ['runtime', 'director', 'writer']; } - - protected function validateTypeSpecificFields($data) { + + protected function validateTypeSpecificFields(array $data): array { // Movies sollten bestimmte Felder haben if (isset($data['runtime']) && !is_numeric($data['runtime'])) { throw new Exception('Runtime must be a number'); } + return $data; } - - protected function processPosterField($data) { + + protected function processPosterField(array $data): array { error_log("Movie::processPosterField - Checking for poster field, isUpdate: " . ($this->isUpdate ? 'yes' : 'no')); - + // If this is an update and poster is being changed, delete old image if ($this->isUpdate && $this->mediaId && isset($data['poster']) && !empty($data['poster'])) { $currentMedia = $this->findById($this->mediaId); @@ -42,15 +45,15 @@ class Movie extends MediaType { } } } - + if (isset($data['poster']) && !empty($data['poster'])) { error_log("Movie::processPosterField - Poster found, length: " . strlen($data['poster'])); - + if (strpos($data['poster'], '/images/') === 0 || filter_var($data['poster'], FILTER_VALIDATE_URL)) { error_log("Movie::processPosterField - Poster is already a path or URL, skipping processing"); return $data; } - + $posterPath = $this->imageHandler->saveBase64Image($data['poster'], 'movies/poster'); error_log("Movie::processPosterField - ImageHandler returned: " . ($posterPath ?: 'null')); if ($posterPath) { @@ -64,23 +67,23 @@ class Movie extends MediaType { } return $data; } - - public function createWithRelations($data) { + + public function createWithRelations(array $data): int { $data['type'] = 'Movie'; $data = $this->validateTypeSpecificFields($data); $data = $this->processPosterField($data); return parent::createWithRelations($data); } - - public function updateWithRelations($id, $data) { + + public function updateWithRelations(int $id, array $data): bool { $this->isUpdate = true; $this->mediaId = $id; $this->validateTypeSpecificFields($data); $data = $this->processPosterField($data); - parent::updateWithRelations($id, $data); + return parent::updateWithRelations($id, $data); } - - public function search($filters = [], $page = 1, $limit = 20) { + + public function search(array $filters = [], int $page = 1, int $limit = 20): array { // Nur Movies suchen $filters['type'] = 'Movie'; return parent::search($filters, $page, $limit); diff --git a/api/models/Music.php b/api/models/Music.php index 1ca1651..3486fb1 100644 --- a/api/models/Music.php +++ b/api/models/Music.php @@ -1,46 +1,49 @@ getWithRelations($id); if (!$media) { return null; } - + $media['tracks'] = $this->getTracks($id); - + return $media; } - - public function getTracks($mediaId) { + + public function getTracks(?int $mediaId): array { $stmt = $this->pdo->prepare(" SELECT * FROM tracks WHERE media_id = ? ORDER BY track_number "); $stmt->execute([$mediaId]); return $stmt->fetchAll(); } - - public function addTrack($mediaId, $trackData) { + + public function addTrack(?int $mediaId, array $trackData): int { $stmt = $this->pdo->prepare(" INSERT INTO tracks (media_id, track_number, title, artist) VALUES (?, ?, ?, ?) @@ -52,20 +55,20 @@ class Music extends MediaType { //$trackData['duration'] ?? null, $trackData['artist'] ?? null ]); - return $this->pdo->lastInsertId(); + return (int)$this->pdo->lastInsertId(); } - - public function updateTrack($trackId, $trackData) { + + public function updateTrack(?int $trackId, array $trackData): bool { $fields = []; $params = []; - + foreach (['track_number', 'title', 'artist'] as $field) { if (array_key_exists($field, $trackData)) { $fields[] = "$field = ?"; $params[] = $trackData[$field]; } } - + if (!empty($fields)) { $params[] = $trackId; $stmt = $this->pdo->prepare("UPDATE tracks SET " . implode(', ', $fields) . " WHERE id = ?"); @@ -74,29 +77,29 @@ class Music extends MediaType { } return false; } - - public function deleteTrack($trackId) { + + public function deleteTrack(?int $trackId): bool { $stmt = $this->pdo->prepare("DELETE FROM tracks WHERE id = ?"); $stmt->execute([$trackId]); return $stmt->rowCount() > 0; } - - public function createWithRelations($data) { + + public function createWithRelations(array $data): int { $mediaId = parent::createWithRelations($data); - + // Tracks speichern if (isset($data['tracks']) && is_array($data['tracks'])) { foreach ($data['tracks'] as $track) { $this->addTrack($mediaId, $track); } } - + return $mediaId; } - - public function updateWithRelations($id, $data) { + + public function updateWithRelations(int $id, array $data): bool { parent::updateWithRelations($id, $data); - + // Tracks aktualisieren if (isset($data['tracks']) && is_array($data['tracks'])) { // Alle existierenden Tracks löschen @@ -106,7 +109,6 @@ class Music extends MediaType { $this->addTrack($id, $track); } } - return true; } } diff --git a/api/models/Series.php b/api/models/Series.php index 8936725..f1019f0 100644 --- a/api/models/Series.php +++ b/api/models/Series.php @@ -1,58 +1,61 @@ getWithRelations($id); if (!$media) { return null; } - + $media['episodes'] = $this->getEpisodes($id); $media['seasons'] = $this->getSeasons($id); - + return $media; } - - public function getEpisodes($mediaId, $season = null) { + + public function getEpisodes(?int $mediaId, ?int $season = null): array { $query = "SELECT * FROM episodes WHERE media_id = ?"; $params = [$mediaId]; - + if ($season !== null) { $query .= " AND season = ?"; $params[] = $season; } - + $query .= " ORDER BY season, episode_number"; - + $stmt = $this->pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } - - public function getSeasons($mediaId) { + + public function getSeasons(?int $mediaId): array { $stmt = $this->pdo->prepare(" SELECT DISTINCT season, COUNT(*) as episode_count, MIN(air_date) as first_air_date, MAX(air_date) as last_air_date @@ -64,8 +67,8 @@ class Series extends MediaType { $stmt->execute([$mediaId]); return $stmt->fetchAll(); } - - public function addEpisode($mediaId, $episodeData) { + + public function addEpisode(?int $mediaId, array $episodeData): int { $stmt = $this->pdo->prepare(" INSERT INTO episodes (media_id, season, episode_number, title, description, air_date, duration, thumbnail) VALUES (?, ?, ?, ?, ?, ?, ?, ?) @@ -80,20 +83,20 @@ class Series extends MediaType { $episodeData['duration'] ?? null, $episodeData['thumbnail'] ?? null ]); - return $this->pdo->lastInsertId(); + return (int)$this->pdo->lastInsertId(); } - - public function updateEpisode($episodeId, $episodeData) { + + public function updateEpisode(?int $episodeId, array $episodeData): bool { $fields = []; $params = []; - + foreach (['season', 'episode_number', 'title', 'description', 'air_date', 'duration', 'thumbnail'] as $field) { if (array_key_exists($field, $episodeData)) { $fields[] = "$field = ?"; $params[] = $episodeData[$field]; } } - + if (!empty($fields)) { $params[] = $episodeId; $stmt = $this->pdo->prepare("UPDATE episodes SET " . implode(', ', $fields) . " WHERE id = ?"); @@ -102,29 +105,29 @@ class Series extends MediaType { } return false; } - - public function deleteEpisode($episodeId) { + + public function deleteEpisode(?int $episodeId): bool { $stmt = $this->pdo->prepare("DELETE FROM episodes WHERE id = ?"); $stmt->execute([$episodeId]); return $stmt->rowCount() > 0; } - - public function createWithRelations($data) { + + public function createWithRelations(array $data): int { $mediaId = parent::createWithRelations($data); - + // Episoden speichern if (isset($data['episodes']) && is_array($data['episodes'])) { foreach ($data['episodes'] as $episode) { $this->addEpisode($mediaId, $episode); } } - + return $mediaId; } - - public function updateWithRelations($id, $data) { + + public function updateWithRelations(int $id, array $data): bool { parent::updateWithRelations($id, $data); - + // Episoden aktualisieren if (isset($data['episodes']) && is_array($data['episodes'])) { // Alle existierenden Episoden löschen @@ -134,7 +137,6 @@ class Series extends MediaType { $this->addEpisode($id, $episode); } } - return true; } } diff --git a/api/models/Settings.php b/api/models/Settings.php index 104df43..1ab01ee 100644 --- a/api/models/Settings.php +++ b/api/models/Settings.php @@ -1,19 +1,21 @@ pdo->prepare("SELECT * FROM {$this->table} WHERE id = 1"); $stmt->execute(); $settings = $stmt->fetch(); - + if ($settings) { // Decode enabled_categories from JSON $settings['enabled_categories'] = $settings['enabled_categories'] ? json_decode($settings['enabled_categories'], true) : []; @@ -21,11 +23,11 @@ class Settings extends BaseModel { $settings['show_adult_content'] = (bool)$settings['show_adult_content']; $settings['auto_play_trailers'] = (bool)$settings['auto_play_trailers']; } - + return $settings; } - - public function updateSettings($data) { + + public function updateSettings(array $data): ?array { $updateData = []; if (isset($data['enabled_categories']) && is_array($data['enabled_categories'])) { diff --git a/api/services/ApiLogger.php b/api/services/ApiLogger.php index 0a8320d..ee6e762 100644 --- a/api/services/ApiLogger.php +++ b/api/services/ApiLogger.php @@ -1,10 +1,13 @@ enabled = API_LOGGING_ENABLED; @@ -12,14 +15,14 @@ class ApiLogger { $this->pdo = $db->getConnection(); } - public static function getInstance() { + public static function getInstance(): ApiLogger { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } - public function logRequest($method, $path, $params = [], $body = null) { + public function logRequest(string|array $method, string|array $path, array $params = [], string|array|null $body = null): void { if (!$this->enabled) { return; } @@ -29,7 +32,7 @@ class ApiLogger { INSERT INTO api_logs (type, method, path, params, body) VALUES ('REQUEST', :method, :path, :params, :body) "); - + $methodValue = is_array($method) ? (json_encode($method) ?: '[array]') : (string)$method; $pathValue = is_array($path) ? (json_encode($path) ?: '[array]') : (string)$path; $paramsValue = is_array($params) ? (json_encode($params) ?: '[array]') : (string)$params; @@ -37,19 +40,19 @@ class ApiLogger { if ($body) { $bodyValue = is_array($body) ? (json_encode($body) ?: '[array]') : (string)$body; } - + $stmt->execute([ ':method' => $methodValue, ':path' => $pathValue, ':params' => $paramsValue, ':body' => $bodyValue ]); - } catch (Exception $e) { + } catch (Throwable $e) { error_log('Failed to log request: ' . $e->getMessage()); } } - public function logResponse($method, $path, $statusCode, $response) { + public function logResponse(string|array $method, string|array $path, int $statusCode, array $response): void { if (!$this->enabled) { return; } @@ -65,12 +68,12 @@ class ApiLogger { ':status_code' => $statusCode, ':response' => (json_encode($response) ?: '[encoding_failed]') ]); - } catch (Exception $e) { + } catch (Throwable $e) { error_log('Failed to log response: ' . $e->getMessage()); } } - public function logError($method, $path, $error) { + public function logError(string|array $method, string|array $path, string|array $error): void { if (!$this->enabled) { return; } @@ -85,12 +88,12 @@ class ApiLogger { ':path' => is_array($path) ? (json_encode($path) ?: '[array]') : (string)$path, ':error' => is_array($error) ? (json_encode($error) ?: '[array]') : (string)$error ]); - } catch (Exception $e) { + } catch (Throwable $e) { error_log('Failed to log error: ' . $e->getMessage()); } } - public function logDebug($message) { + public function logDebug(string|array $message): void { if (!$this->enabled) { return; } @@ -103,7 +106,7 @@ class ApiLogger { $stmt->execute([ ':message' => is_array($message) ? (json_encode($message) ?: '[array]') : (string)$message ]); - } catch (Exception $e) { + } catch (Throwable $e) { error_log('Failed to log debug: ' . $e->getMessage()); } } diff --git a/api/services/DocumentationService.php b/api/services/DocumentationService.php index 17a0ade..195fcd2 100644 --- a/api/services/DocumentationService.php +++ b/api/services/DocumentationService.php @@ -1,15 +1,17 @@ controllersPath = $controllersPath; $this->modelsPath = $modelsPath; } - - public function generateDocumentation() { + + public function generateDocumentation(): array { $docs = [ 'title' => 'Media API Documentation', 'version' => '1.0.0', @@ -27,7 +29,7 @@ class DocumentationService { return $docs; } - private function scanControllers() { + private function scanControllers(): array { $endpoints = []; $controllerFiles = glob($this->controllersPath . '*Controller.php'); @@ -56,7 +58,7 @@ class DocumentationService { return $endpoints; } - private function parseMethodDoc($docComment, $methodName, $className) { + private function parseMethodDoc(?string $docComment, string $methodName, string $className): ?array { if (!$docComment) { return null; } @@ -106,7 +108,7 @@ class DocumentationService { return $info; } - private function inferHttpMethods($methodName) { + private function inferHttpMethods(string $methodName): array { $methods = []; if (strpos($methodName, 'get') === 0) { @@ -129,7 +131,7 @@ class DocumentationService { return $methods; } - private function inferPath($className, $methodName) { + private function inferPath(string $className, string $methodName): string { $resource = strtolower(str_replace('Controller', '', $className)); $path = "/{$resource}"; @@ -156,7 +158,7 @@ class DocumentationService { return $path; } - private function scanModels() { + private function scanModels(): array { $models = []; $modelFiles = glob($this->modelsPath . '*.php'); diff --git a/api/services/ImageHandler.php b/api/services/ImageHandler.php index ffef8da..3f12de4 100644 --- a/api/services/ImageHandler.php +++ b/api/services/ImageHandler.php @@ -1,27 +1,29 @@ uploadDir = $uploadDir ?? __DIR__ . '/../public/images/'; $this->baseUrl = $baseUrl ?? '/images/'; - + // Ensure upload directory exists if (!file_exists($this->uploadDir)) { mkdir($this->uploadDir, 0755, true); } } - + /** * Process base64 image data and save to file - * + * * @param string $base64Data Base64 encoded image data * @param string $prefix Prefix for filename (e.g., 'poster', 'banner') * @return string|null Relative path to saved image, or null if invalid */ - public function saveBase64Image($base64Data, $prefix = 'image') { + public function saveBase64Image(string $base64Data, string $prefix = 'image'): ?string { error_log("ImageHandler: Starting to process base64 image, length: " . strlen($base64Data)); if (empty($base64Data)) { @@ -119,7 +121,7 @@ class ImageHandler { /** * Detect image format from base64 string */ - private function detectImageFormat($base64String) { + private function detectImageFormat(string $base64String): ?string { // Decode first few bytes to check magic numbers $data = base64_decode(substr($base64String, 0, 100)); @@ -140,7 +142,7 @@ class ImageHandler { /** * Validate that data is a valid image */ - private function isValidImage($data) { + private function isValidImage(string $data): bool { try { $image = imagecreatefromstring($data); if ($image !== false) { @@ -156,14 +158,14 @@ class ImageHandler { /** * Generate unique filename */ - private function generateUniqueFilename($prefix, $extension) { + private function generateUniqueFilename(string $prefix, string $extension): string { return $prefix . '_' . uniqid() . '_' . time() . '.' . $extension; } /** * Delete an image file */ - public function deleteImage($imagePath) { + public function deleteImage(?string $imagePath): bool { if (empty($imagePath)) { return false; }