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.
214 lines
7.8 KiB
PHP
214 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/BaseModel.php';
|
|
require_once __DIR__ . '/../services/ImageHandler.php';
|
|
|
|
class Cast extends BaseModel {
|
|
protected string $table = 'cast_staff';
|
|
private ImageHandler $imageHandler;
|
|
protected bool $isUpdate = false;
|
|
protected ?int $castId = null;
|
|
|
|
public function __construct(PDO $pdo) {
|
|
parent::__construct($pdo);
|
|
$this->imageHandler = new ImageHandler();
|
|
}
|
|
|
|
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(?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
|
|
INNER JOIN media_cast mc ON m.id = mc.media_id
|
|
WHERE mc.cast_id = ?
|
|
ORDER BY m.year DESC
|
|
");
|
|
$stmt->execute([$castId]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function search(array $filters = [], int $page = 1, int $limit = 20): array {
|
|
$conditions = [];
|
|
|
|
if (isset($filters['search'])) {
|
|
$conditions['name'] = ["%" . $filters['search'] . "%"];
|
|
}
|
|
|
|
$offset = ($page - 1) * $limit;
|
|
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
|
$total = $this->count($conditions);
|
|
|
|
// Load filmography for all cast members in a single query
|
|
if (!empty($items)) {
|
|
$castIds = array_column($items, 'id');
|
|
$placeholders = str_repeat('?,', count($castIds) - 1) . '?';
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT mc.cast_id, m.id, m.title, m.year, m.poster, m.category, m.type, mc.role, mc.characterName
|
|
FROM media m
|
|
INNER JOIN media_cast mc ON m.id = mc.media_id
|
|
WHERE mc.cast_id IN ($placeholders)
|
|
ORDER BY m.year DESC
|
|
");
|
|
$stmt->execute($castIds);
|
|
$allFilmography = $stmt->fetchAll();
|
|
|
|
// Group filmography by cast_id
|
|
$filmographyByCast = [];
|
|
foreach ($allFilmography as $film) {
|
|
$castId = $film['cast_id'];
|
|
unset($film['cast_id']);
|
|
$filmographyByCast[$castId][] = $film;
|
|
}
|
|
|
|
// Attach filmography to each cast member
|
|
foreach ($items as &$item) {
|
|
$item['filmography'] = $filmographyByCast[$item['id']] ?? [];
|
|
$mediaTypes = array_unique(array_column($item['filmography'], 'category'));
|
|
$item['media_types'] = array_values($mediaTypes);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'items' => $items,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'limit' => $limit
|
|
];
|
|
}
|
|
|
|
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'])) {
|
|
$currentCast = $this->findById($this->castId);
|
|
if ($currentCast && !empty($currentCast['photo'])) {
|
|
$oldPhoto = $currentCast['photo'];
|
|
if (strpos($oldPhoto, '/images/') === 0) {
|
|
error_log("Cast::processPhotoField - Deleting old photo: " . $oldPhoto);
|
|
$this->imageHandler->deleteImage($oldPhoto);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($data['photo']) && !empty($data['photo'])) {
|
|
error_log("Cast::processPhotoField - Photo found, length: " . strlen($data['photo']));
|
|
|
|
if (strpos($data['photo'], '/images/') === 0 || filter_var($data['photo'], FILTER_VALIDATE_URL)) {
|
|
error_log("Cast::processPhotoField - Photo is already a path or URL, skipping processing");
|
|
return $data;
|
|
}
|
|
|
|
$photoPath = $this->imageHandler->saveBase64Image($data['photo'], 'cast/photo');
|
|
error_log("Cast::processPhotoField - ImageHandler returned: " . ($photoPath ?: 'null'));
|
|
if ($photoPath) {
|
|
$data['photo'] = $photoPath;
|
|
error_log("Cast::processPhotoField - Photo path set to: " . $photoPath);
|
|
} else {
|
|
error_log("Cast::processPhotoField - Failed to process photo, keeping original data");
|
|
}
|
|
} else {
|
|
error_log("Cast::processPhotoField - No photo field found or empty");
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
public function createWithOccupations(array $data): int {
|
|
$name = $data['name'] ?? null;
|
|
if (!$name) {
|
|
throw new Exception('Name is required');
|
|
}
|
|
|
|
// cleanname generieren
|
|
$cleanname = generateCleanName($name);
|
|
|
|
$data = $this->processPhotoField($data);
|
|
|
|
$castData = [
|
|
'name' => $name,
|
|
'cleanname' => $cleanname,
|
|
'photo' => $data['photo'] ?? null,
|
|
'bio' => $data['bio'] ?? null,
|
|
'birthDate' => $data['birthDate'] ?? null,
|
|
'birthPlace' => $data['birthPlace'] ?? null
|
|
];
|
|
|
|
$castId = $this->create($castData);
|
|
|
|
if (isset($data['occupations']) && is_array($data['occupations'])) {
|
|
$this->saveRelatedItems('occupations', $castId, $data['occupations'], 'cast_id');
|
|
}
|
|
|
|
return $castId;
|
|
}
|
|
|
|
public function updateWithOccupations(int $id, array $data): bool {
|
|
$this->isUpdate = true;
|
|
$this->castId = $id;
|
|
|
|
$data = $this->processPhotoField($data);
|
|
|
|
$castData = [];
|
|
|
|
foreach (['name', 'photo', 'bio', 'birthDate', 'birthPlace'] as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
$castData[$field] = $data[$field];
|
|
}
|
|
}
|
|
|
|
// Wenn name geändert wurde, cleanname aktualisieren
|
|
if (isset($data['name'])) {
|
|
$castData['cleanname'] = generateCleanName($data['name']);
|
|
}
|
|
|
|
if (!empty($castData)) {
|
|
$this->update($id, $castData);
|
|
}
|
|
|
|
if (isset($data['occupations']) && is_array($data['occupations'])) {
|
|
$this->pdo->prepare("DELETE FROM occupations WHERE cast_id = ?")->execute([$id]);
|
|
$this->saveRelatedItems('occupations', $id, $data['occupations'], 'cast_id');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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(string $table, int $id, string $fkColumn = 'media_id'): array {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM $table WHERE $fkColumn = ?");
|
|
$stmt->execute([$id]);
|
|
$items = $stmt->fetchAll();
|
|
$result = [];
|
|
foreach ($items as $item) {
|
|
$result[] = $fkColumn === 'cast_id' ? $item['occupation'] : $item['genre'];
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
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) {
|
|
$stmt->execute([$id, $item]);
|
|
}
|
|
}
|
|
}
|