Add PHP Media API scaffold and Docker configs

Initial project scaffold for a PHP Media API including routing, controllers, models and services under api/ (Router, Media/Cast/Image/Settings controllers, models, database/bootstrap files and automatic docs service). Adds Docker support (Dockerfile, docker-compose.yml, DOCKER_README.md, php-custom.ini), .htaccess for pretty URLs, API documentation and example payloads (API_EXAMPLES.md, api/README.md, api_examples/*.json), image handling service and logging, plus a comprehensive .gitignore. This commit provides a runnable development environment and example requests to get the API up and tested quickly.
This commit is contained in:
Lars Behrends
2026-04-12 00:46:30 +02:00
commit 66f69bc90d
54 changed files with 6035 additions and 0 deletions

291
api/models/Media.php Normal file
View File

@@ -0,0 +1,291 @@
<?php
require_once __DIR__ . '/BaseModel.php';
class Media extends BaseModel {
protected $table = 'media';
public function getBase($id) {
return $this->findById($id);
}
public function getWithRelations($id) {
$media = $this->findById($id);
if (!$media) {
return null;
}
$media['genres'] = $this->getRelatedItems('genres', $id);
$media['tags'] = $this->getRelatedItems('tags', $id);
$media['studios'] = $this->getRelatedItems('studios', $id);
$media['staff'] = $this->getCastForMedia($id);
return $media;
}
public function search($filters = [], $page = 1, $limit = 20) {
$conditions = [];
if (isset($filters['category'])) {
$conditions['category'] = $filters['category'];
}
if (isset($filters['type'])) {
$conditions['type'] = $filters['type'];
}
if (isset($filters['search'])) {
$searchTerm = "%" . $filters['search'] . "%";
$conditions['title'] = [$searchTerm];
// OR Bedingung für description wird separat behandelt
}
$offset = ($page - 1) * $limit;
if (isset($filters['search'])) {
// Komplexe Suche mit OR
$query = "SELECT * FROM {$this->table} WHERE 1=1";
$params = [];
if (isset($filters['category'])) {
$query .= " AND category = ?";
$params[] = $filters['category'];
}
if (isset($filters['type'])) {
$query .= " AND type = ?";
$params[] = $filters['type'];
}
$query .= " AND (title LIKE ? OR description LIKE ?)";
$searchTerm = "%" . $filters['search'] . "%";
$params[] = $searchTerm;
$params[] = $searchTerm;
$query .= " ORDER BY createdAt DESC LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
$items = $stmt->fetchAll();
// Cast-Mitglieder für jedes Medium laden
foreach ($items as &$item) {
$item['staff'] = $this->getCastForMedia($item['id']);
}
// Count Query
$countQuery = "SELECT COUNT(*) FROM {$this->table} WHERE 1=1";
$countParams = [];
if (isset($filters['category'])) {
$countQuery .= " AND category = ?";
$countParams[] = $filters['category'];
}
if (isset($filters['type'])) {
$countQuery .= " AND type = ?";
$countParams[] = $filters['type'];
}
$countQuery .= " AND (title LIKE ? OR description LIKE ?)";
$countParams[] = $searchTerm;
$countParams[] = $searchTerm;
$countStmt = $this->pdo->prepare($countQuery);
$countStmt->execute($countParams);
$total = $countStmt->fetchColumn();
} else {
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
$total = $this->count($conditions);
// Cast-Mitglieder für jedes Medium laden
foreach ($items as &$item) {
$item['staff'] = $this->getCastForMedia($item['id']);
}
}
return [
'items' => $items,
'total' => $total,
'page' => $page,
'limit' => $limit,
'totalPages' => ceil($total / $limit)
];
}
public function createWithRelations($data) {
$title = $data['title'] ?? null;
if (!$title) {
throw new Exception('Title is required');
}
// cleanname generieren
$cleanname = generateCleanName($title);
// Basis-Media-Daten extrahieren
$mediaData = [
'title' => $title,
'cleanname' => $cleanname,
'year' => $data['year'] ?? null,
'poster' => $data['poster'] ?? null,
'banner' => $data['banner'] ?? null,
'description' => $data['description'] ?? null,
'rating' => $data['rating'] ?? null,
'category' => $data['category'] ?? null,
'type' => $data['type'] ?? null,
'status' => $data['status'] ?? null,
'aspectRatio' => $data['aspectRatio'] ?? null,
'runtime' => $data['runtime'] ?? null,
'director' => $data['director'] ?? null,
'writer' => $data['writer'] ?? null,
'source' => $data['source'] ?? null,
'releaseDate' => $data['releaseDate'] ?? null
];
$mediaId = $this->create($mediaData);
// Relationen speichern
if (isset($data['genres']) && is_array($data['genres'])) {
$this->saveRelatedItems('genres', $mediaId, $data['genres']);
}
if (isset($data['tags']) && is_array($data['tags'])) {
$this->saveRelatedItems('tags', $mediaId, $data['tags']);
}
if (isset($data['studios']) && is_array($data['studios'])) {
$this->saveRelatedItems('studios', $mediaId, $data['studios']);
}
if (isset($data['staff']) && is_array($data['staff'])) {
$this->saveCastAssignments($mediaId, $data['staff']);
}
return $mediaId;
}
public function updateWithRelations($id, $data) {
$mediaData = [];
foreach (['title', 'year', 'poster', 'banner', 'description', 'rating', 'category', 'type', 'status', 'aspectRatio', 'runtime', 'director', 'writer', 'releaseDate', 'source'] as $field) {
if (array_key_exists($field, $data)) {
$mediaData[$field] = $data[$field];
}
}
// Wenn title geändert wurde, cleanname aktualisieren
if (isset($data['title'])) {
$mediaData['cleanname'] = generateCleanName($data['title']);
}
if (!empty($mediaData)) {
$this->update($id, $mediaData);
}
// Relationen aktualisieren
if (isset($data['genres']) && is_array($data['genres'])) {
$this->pdo->prepare("DELETE FROM genres WHERE media_id = ?")->execute([$id]);
$this->saveRelatedItems('genres', $id, $data['genres']);
}
if (isset($data['tags']) && is_array($data['tags'])) {
$this->pdo->prepare("DELETE FROM tags WHERE media_id = ?")->execute([$id]);
$this->saveRelatedItems('tags', $id, $data['tags']);
}
if (isset($data['studios']) && is_array($data['studios'])) {
$this->pdo->prepare("DELETE FROM studios WHERE media_id = ?")->execute([$id]);
$this->saveRelatedItems('studios', $id, $data['studios']);
}
if (isset($data['staff']) && is_array($data['staff'])) {
$this->pdo->prepare("DELETE FROM media_cast WHERE media_id = ?")->execute([$id]);
$this->saveCastAssignments($id, $data['staff']);
}
return true;
}
public function findByCleanName($cleanname) {
$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') {
$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) {
$stmt->execute([$id, $item]);
}
}
protected function getCastForMedia($mediaId) {
$stmt = $this->pdo->prepare("
SELECT cs.*, mc.role, mc.characterName, mc.characterImage
FROM cast_staff cs
INNER JOIN media_cast mc ON cs.id = mc.cast_id
WHERE mc.media_id = ?
");
$stmt->execute([$mediaId]);
$cast = $stmt->fetchAll();
foreach ($cast as &$member) {
$member['occupations'] = $this->getRelatedItems('occupations', $member['id'], 'cast_id');
}
return $cast;
}
protected function getRelatedItems($table, $id, $fkColumn = 'media_id') {
$stmt = $this->pdo->prepare("SELECT * FROM $table WHERE $fkColumn = ?");
$stmt->execute([$id]);
$items = $stmt->fetchAll();
$result = [];
foreach ($items as $item) {
if ($fkColumn === 'cast_id') {
$result[] = $item['occupation'];
} else {
$result[] = $table === 'genres' ? $item['genre'] : ($table === 'tags' ? $item['tag'] : $item['studio']);
}
}
return $result;
}
protected function saveCastAssignments($mediaId, $castData) {
foreach ($castData as $member) {
$castId = null;
if (isset($member['id']) && $member['id']) {
$castId = $member['id'];
} elseif (isset($member['name'])) {
$stmt = $this->pdo->prepare("SELECT id FROM cast_staff WHERE name = ? LIMIT 1");
$stmt->execute([$member['name']]);
$existing = $stmt->fetch();
if ($existing) {
$castId = $existing['id'];
} else {
$cleanname = generateCleanName($member['name']);
$stmt = $this->pdo->prepare("
INSERT INTO cast_staff (name, cleanname, photo, bio, birthDate, birthPlace)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$member['name'] ?? null,
$cleanname,
$member['photo'] ?? null,
$member['bio'] ?? null,
$member['birthDate'] ?? null,
$member['birthPlace'] ?? null
]);
$castId = $this->pdo->lastInsertId();
if (isset($member['occupations']) && is_array($member['occupations'])) {
$this->saveRelatedItems('occupations', $castId, $member['occupations'], 'cast_id');
}
}
}
if ($castId) {
$stmt = $this->pdo->prepare("
INSERT INTO media_cast (media_id, cast_id, role, characterName, characterImage)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([
$mediaId,
$castId,
$member['role'] ?? null,
$member['characterName'] ?? null,
$member['characterImage'] ?? null
]);
}
}
}
}