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.
472 lines
20 KiB
PHP
472 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/MediaType.php';
|
|
require_once __DIR__ . '/../services/ImageHandler.php';
|
|
|
|
class Game extends MediaType {
|
|
private ImageHandler $imageHandler;
|
|
private bool $isUpdate = false;
|
|
private ?int $mediaId = null;
|
|
|
|
public function __construct(PDO $pdo) {
|
|
parent::__construct($pdo);
|
|
$this->imageHandler = new ImageHandler();
|
|
}
|
|
|
|
protected function getType(): string {
|
|
return 'Game';
|
|
}
|
|
|
|
protected function getTypeSpecificFields(): array {
|
|
return [];
|
|
}
|
|
|
|
protected function validateTypeSpecificFields(array $data): array {
|
|
// Game spezifische Validierung
|
|
if (isset($data['hasIcon'])) {
|
|
$data['hasIcon'] = is_numeric($data['hasIcon']) ? (int)$data['hasIcon'] : 0;
|
|
}
|
|
if (isset($data['hasCover'])) {
|
|
$data['hasCover'] = is_numeric($data['hasCover']) ? (int)$data['hasCover'] : 0;
|
|
}
|
|
if (isset($data['hasBackground'])) {
|
|
$data['hasBackground'] = is_numeric($data['hasBackground']) ? (int)$data['hasBackground'] : 0;
|
|
}
|
|
if (isset($data['playtime']) && !is_numeric($data['playtime'])) {
|
|
$data['playtime'] = is_numeric($data['playtime']) ? (int)$data['playtime'] : 0;
|
|
}
|
|
if (isset($data['isInstalled'])) {
|
|
$data['isInstalled'] = is_numeric($data['isInstalled']) ? (int)$data['isInstalled'] : 0;
|
|
}
|
|
if (isset($data['hidden'])) {
|
|
$data['hidden'] = is_numeric($data['hidden']) ? (int)$data['hidden'] : 0;
|
|
}
|
|
if (isset($data['favorite'])) {
|
|
$data['favorite'] = is_numeric($data['favorite']) ? (int)$data['favorite'] : 0;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Process poster field - convert base64 to file path
|
|
*/
|
|
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])) {
|
|
$oldPoster = $currentMedia[$type];
|
|
if (strpos($oldPoster, '/images/') === 0) {
|
|
$this->imageHandler->deleteImage($oldPoster);
|
|
}
|
|
}
|
|
}
|
|
if (isset($data[$type]) && !empty($data[$type])) {
|
|
if (strpos($data[$type], '/images/') === 0 || filter_var($data[$type], FILTER_VALIDATE_URL)) {
|
|
return $data;
|
|
}
|
|
$posterPath = $this->imageHandler->saveBase64Image($data[$type], 'games/'.$type.'/');
|
|
if ($posterPath) {
|
|
$data[$type] = $posterPath;
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
public function createWithRelations(array $data): int {
|
|
// Typ setzen
|
|
$data['type'] = 'Game';
|
|
|
|
// Typ-spezifische Validierung
|
|
$data = $this->validateTypeSpecificFields($data);
|
|
|
|
// Poster verarbeiten (Base64 zu Dateipfad)
|
|
$data = $this->processImageField($data, 'poster');
|
|
$data = $this->processImageField($data, 'banner');
|
|
$data = $this->processImageField($data, 'icon');
|
|
|
|
// Basis-Media erstellen
|
|
$mediaId = parent::createWithRelations($data);
|
|
|
|
// Media-Games Eintrag erstellen
|
|
$gameData = [
|
|
'media_id' => $mediaId,
|
|
'sortingName' => $data['sortingName'] ?? null,
|
|
'notes' => $data['notes'] ?? null,
|
|
'completionStatus' => $data['completionStatus'] ?? null,
|
|
'source' => $data['source'] ?? null,
|
|
'gameId' => $data['gameId'] ?? null,
|
|
'pluginId' => $data['pluginId'] ?? null,
|
|
'isInstalled' => $data['isInstalled'] ?? false,
|
|
'installDirectory' => $data['installDirectory'] ?? null,
|
|
'installSize' => $data['installSize'] ?? 0,
|
|
'hidden' => $data['hidden'] ?? false,
|
|
'favorite' => $data['favorite'] ?? false,
|
|
'playCount' => $data['playCount'] ?? 0,
|
|
'lastActivity' => $data['lastActivity'] ?? null,
|
|
'added' => null,
|
|
'modified' => null,
|
|
'communityScore' => $data['communityScore'] ?? 0,
|
|
'criticScore' => $data['criticScore'] ?? 0,
|
|
'userScore' => $data['userScore'] ?? 0,
|
|
'hasIcon' => $data['hasIcon'] ?? 0,
|
|
'hasCover' => $data['hasCover'] ?? 0,
|
|
'hasBackground' => $data['hasBackground'] ?? 0,
|
|
'version' => $data['version'] ?? null,
|
|
'playtime' => $data['playtime'] ?? 0
|
|
];
|
|
|
|
$mediaGameId = $this->createMediaGame($gameData);
|
|
|
|
// Relationen speichern
|
|
if (isset($data['achievements']) && is_array($data['achievements'])) {
|
|
$this->saveAchievements($mediaGameId, $data['achievements']);
|
|
}
|
|
if (isset($data['categories']) && is_array($data['categories'])) {
|
|
$this->saveGameRelation('game_categories', $mediaGameId, $data['categories'], 'category');
|
|
}
|
|
if (isset($data['features']) && is_array($data['features'])) {
|
|
$this->saveGameRelation('game_features', $mediaGameId, $data['features'], 'feature');
|
|
}
|
|
if (isset($data['platforms']) && is_array($data['platforms'])) {
|
|
$this->saveGameRelationWithConsole('game_platforms', $mediaGameId, $data['platforms'], 'platform');
|
|
}
|
|
if (isset($data['developers']) && is_array($data['developers'])) {
|
|
$this->saveGameRelation('game_developers', $mediaGameId, $data['developers'], 'developer');
|
|
}
|
|
if (isset($data['publishers']) && is_array($data['publishers'])) {
|
|
$this->saveGameRelation('game_publishers', $mediaGameId, $data['publishers'], 'publisher');
|
|
}
|
|
if (isset($data['series']) && is_array($data['series'])) {
|
|
$this->saveGameRelation('game_series', $mediaGameId, $data['series'], 'series');
|
|
}
|
|
if (isset($data['ageRatings']) && is_array($data['ageRatings'])) {
|
|
$this->saveGameRelation('game_age_ratings', $mediaGameId, $data['ageRatings'], 'age_rating');
|
|
}
|
|
if (isset($data['regions']) && is_array($data['regions'])) {
|
|
$this->saveGameRelation('game_regions', $mediaGameId, $data['regions'], 'region');
|
|
}
|
|
if (isset($data['links']) && is_array($data['links'])) {
|
|
$this->saveLinks($mediaGameId, $data['links']);
|
|
}
|
|
return $mediaId;
|
|
}
|
|
|
|
public function updateWithRelations(int $id, array $data): bool {
|
|
// Set update flag and mediaId for image replacement
|
|
$this->isUpdate = true;
|
|
$this->mediaId = $id;
|
|
|
|
// Typ-spezifische Validierung
|
|
$this->validateTypeSpecificFields($data);
|
|
|
|
// Poster verarbeiten (Base64 zu Dateipfad)
|
|
$data = $this->processImageField($data, 'poster');
|
|
$data = $this->processImageField($data, 'banner');
|
|
$data = $this->processImageField($data, 'icon');
|
|
|
|
// Basis-Media aktualisieren
|
|
parent::updateWithRelations($id, $data);
|
|
|
|
// Media-Games Eintrag aktualisieren
|
|
$gameData = [];
|
|
$gameFields = ['sortingName', 'notes', 'completionStatus', 'source', 'gameId', 'pluginId',
|
|
'isInstalled', 'installDirectory', 'installSize', 'hidden', 'favorite',
|
|
'playCount', 'lastActivity', 'added', 'modified', 'communityScore',
|
|
'criticScore', 'userScore', 'hasIcon', 'hasCover', 'hasBackground',
|
|
'version', 'playtime'];
|
|
|
|
foreach ($gameFields as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
$gameData[$field] = $data[$field];
|
|
}
|
|
}
|
|
|
|
if (!empty($gameData)) {
|
|
$this->updateMediaGame($id, $gameData);
|
|
}
|
|
|
|
// Media-Games ID abrufen
|
|
$mediaGameId = $this->getMediaGameId($id);
|
|
|
|
if ($mediaGameId) {
|
|
// Relationen aktualisieren
|
|
if (isset($data['achievements']) && is_array($data['achievements'])) {
|
|
$this->pdo->prepare("DELETE FROM achievements WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveAchievements($mediaGameId, $data['achievements']);
|
|
}
|
|
if (isset($data['categories']) && is_array($data['categories'])) {
|
|
$this->pdo->prepare("DELETE FROM game_categories WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_categories', $mediaGameId, $data['categories'], 'category');
|
|
}
|
|
if (isset($data['features']) && is_array($data['features'])) {
|
|
$this->pdo->prepare("DELETE FROM game_features WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_features', $mediaGameId, $data['features'], 'feature');
|
|
}
|
|
if (isset($data['platforms']) && is_array($data['platforms'])) {
|
|
$this->pdo->prepare("DELETE FROM game_platforms WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelationWithConsole('game_platforms', $mediaGameId, $data['platforms'], 'platform');
|
|
}
|
|
if (isset($data['developers']) && is_array($data['developers'])) {
|
|
$this->pdo->prepare("DELETE FROM game_developers WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_developers', $mediaGameId, $data['developers'], 'developer');
|
|
}
|
|
if (isset($data['publishers']) && is_array($data['publishers'])) {
|
|
$this->pdo->prepare("DELETE FROM game_publishers WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_publishers', $mediaGameId, $data['publishers'], 'publisher');
|
|
}
|
|
if (isset($data['series']) && is_array($data['series'])) {
|
|
$this->pdo->prepare("DELETE FROM game_series WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_series', $mediaGameId, $data['series'], 'series');
|
|
}
|
|
if (isset($data['ageRatings']) && is_array($data['ageRatings'])) {
|
|
$this->pdo->prepare("DELETE FROM game_age_ratings WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_age_ratings', $mediaGameId, $data['ageRatings'], 'age_rating');
|
|
}
|
|
if (isset($data['regions']) && is_array($data['regions'])) {
|
|
$this->pdo->prepare("DELETE FROM game_regions WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveGameRelation('game_regions', $mediaGameId, $data['regions'], 'region');
|
|
}
|
|
if (isset($data['links']) && is_array($data['links'])) {
|
|
$this->pdo->prepare("DELETE FROM game_links WHERE media_game_id = ?")->execute([$mediaGameId]);
|
|
$this->saveLinks($mediaGameId, $data['links']);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function getWithGameInfo(?int $id): ?array {
|
|
$media = parent::getWithRelations($id);
|
|
if (!$media) {
|
|
return null;
|
|
}
|
|
|
|
// Media-Games Daten abrufen
|
|
$mediaGameId = $this->getMediaGameId($id);
|
|
if ($mediaGameId) {
|
|
$gameInfo = $this->getMediaGameData($mediaGameId);
|
|
$media = array_merge($media, $gameInfo);
|
|
|
|
// Relationen abrufen
|
|
$media['achievements'] = $this->getAchievements($mediaGameId);
|
|
$media['categories'] = $this->getGameRelation('game_categories', $mediaGameId, 'category');
|
|
$media['features'] = $this->getGameRelation('game_features', $mediaGameId, 'feature');
|
|
$media['platforms'] = $this->getGameRelation('game_platforms', $mediaGameId, 'platform');
|
|
$media['developers'] = $this->getGameRelation('game_developers', $mediaGameId, 'developer');
|
|
$media['publishers'] = $this->getGameRelation('game_publishers', $mediaGameId, 'publisher');
|
|
$media['series'] = $this->getGameRelation('game_series', $mediaGameId, 'series');
|
|
$media['ageRatings'] = $this->getGameRelation('game_age_ratings', $mediaGameId, 'age_rating');
|
|
$media['regions'] = $this->getGameRelation('game_regions', $mediaGameId, 'region');
|
|
$media['links'] = $this->getLinks($mediaGameId);
|
|
}
|
|
|
|
return $media;
|
|
}
|
|
|
|
public function getGameInfoForList(?int $mediaId): ?array {
|
|
// Media-Games Daten abrufen (ohne vollständige Relationen für Performance)
|
|
$mediaGameId = $this->getMediaGameId($mediaId);
|
|
if (!$mediaGameId) {
|
|
return null;
|
|
}
|
|
|
|
$gameInfo = $this->getMediaGameData($mediaGameId);
|
|
|
|
// Nur wichtige Relationen für Listenansicht laden
|
|
$gameInfo['categories'] = $this->getGameRelation('game_categories', $mediaGameId, 'category');
|
|
$gameInfo['platforms'] = $this->getGameRelation('game_platforms', $mediaGameId, 'platform');
|
|
$gameInfo['developers'] = $this->getGameRelation('game_developers', $mediaGameId, 'developer');
|
|
|
|
return $gameInfo;
|
|
}
|
|
|
|
public static function interpolateQuery(string $query, array $params): string {
|
|
$keys = array();
|
|
|
|
# build a regular expression for each parameter
|
|
foreach ($params as $key => $value) {
|
|
if (is_string($key)) {
|
|
$keys[] = '/:'.$key.'/';
|
|
} else {
|
|
$keys[] = '/[?]/';
|
|
}
|
|
}
|
|
|
|
$query = preg_replace($keys, $params, $query, 1, $count);
|
|
|
|
#trigger_error('replaced '.$count.' keys');
|
|
|
|
return $query;
|
|
}
|
|
|
|
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,
|
|
lastActivity, added, modified, communityScore, criticScore, userScore,
|
|
hasIcon, hasCover, hasBackground, version, playtime)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$data['media_id'],
|
|
$data['sortingName'],
|
|
$data['notes'],
|
|
$data['completionStatus'],
|
|
$data['source'],
|
|
$data['gameId'],
|
|
$data['pluginId'],
|
|
$data['isInstalled'],
|
|
$data['installDirectory'],
|
|
$data['installSize'],
|
|
$data['hidden'],
|
|
$data['favorite'],
|
|
$data['playCount'],
|
|
$data['lastActivity'],
|
|
$data['added'],
|
|
$data['modified'],
|
|
$data['communityScore'],
|
|
$data['criticScore'],
|
|
$data['userScore'],
|
|
$data['hasIcon'],
|
|
$data['hasCover'],
|
|
$data['hasBackground'],
|
|
$data['version'],
|
|
$data['playtime']
|
|
]);
|
|
return $this->pdo->lastInsertId();
|
|
}
|
|
|
|
protected function updateMediaGame(int $mediaId, array $data): void {
|
|
$setClause = [];
|
|
$params = [];
|
|
|
|
foreach ($data as $key => $value) {
|
|
$setClause[] = "$key = ?";
|
|
$params[] = $value;
|
|
}
|
|
|
|
$params[] = $mediaId;
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
UPDATE media_games SET " . implode(', ', $setClause) . " WHERE media_id = ?
|
|
");
|
|
$stmt->execute($params);
|
|
}
|
|
|
|
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(?int $mediaGameId): array {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM media_games WHERE id = ?");
|
|
$stmt->execute([$mediaGameId]);
|
|
$data = $stmt->fetch();
|
|
|
|
if ($data) {
|
|
unset($data['id'], $data['media_id']);
|
|
}
|
|
|
|
return $data ?: [];
|
|
}
|
|
|
|
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 (?, ?, ?, ?, ?, ?)
|
|
");
|
|
|
|
foreach ($achievements as $achievement) {
|
|
$unlockedDate = null;
|
|
if (isset($achievement['unlocked']) && $achievement['unlocked'] && isset($achievement['unlocked_date'])) {
|
|
$unlockedDate = $achievement['unlocked_date'];
|
|
}
|
|
|
|
$stmt->execute([
|
|
$mediaGameId,
|
|
$achievement['name'] ?? null,
|
|
$achievement['description'] ?? null,
|
|
$achievement['icon'] ?? null,
|
|
$achievement['unlocked'] ?? false,
|
|
$unlockedDate
|
|
]);
|
|
}
|
|
}
|
|
|
|
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(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(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(string $name): string {
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO media (title, cleanname, type, createdAt, updatedAt)
|
|
VALUES (?, ?, 'Console', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
");
|
|
$stmt->execute([$name, strtolower(str_replace(' ', '-', $name))]);
|
|
return $this->pdo->lastInsertId();
|
|
}
|
|
|
|
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
|
|
if ($field === 'platform' && !$this->consoleExists($item)) {
|
|
$this->createConsole($item);
|
|
}
|
|
$stmt->execute([$mediaGameId, $item]);
|
|
}
|
|
}
|
|
|
|
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();
|
|
return array_map(function($item) use ($field) {
|
|
return $item[$field];
|
|
}, $items);
|
|
}
|
|
|
|
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([
|
|
$mediaGameId,
|
|
$link['name'] ?? null,
|
|
$link['url'] ?? null
|
|
]);
|
|
}
|
|
}
|
|
|
|
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(array $filters = [], int $page = 1, int $limit = 20): array {
|
|
// Nur Games suchen
|
|
$filters['type'] = 'Game';
|
|
return parent::search($filters, $page, $limit);
|
|
}
|
|
}
|