feat: extract MediaController, wire into Dispatcher, delete media.php

This commit is contained in:
Pontoporeia
2026-04-17 11:44:08 +02:00
parent b03be51b92
commit 75f808bee4
157 changed files with 1713 additions and 452 deletions

View File

@@ -0,0 +1,293 @@
<?php
/**
* ThesisEditController
*
* Centralises all data-fetching and mutation logic for the admin thesis-edit
* workflow (admin/edit.php + admin/actions/edit.php).
*
* Responsibilities:
* - Loading thesis data and lookup tables for the edit form view
* - Validating and persisting POST submissions (thesis metadata, authors,
* jury, languages, formats, tags, banner)
* - WCAG 3.3.1: mapping validation exceptions to autofocus field hints
*
* The class has NO output side-effects; all redirects, flash writes, and
* template rendering stay in the thin dispatcher files so the view layer
* remains easy to inspect and modify.
*/
class ThesisEditController
{
private Database $db;
public function __construct(Database $db)
{
$this->db = $db;
}
// ── Factory ───────────────────────────────────────────────────────────────
/**
* Convenience factory — instantiates Database and returns a ready
* controller. Accepts an optional existing Database instance so callers
* that already hold one (e.g. during testing) can avoid a second
* connection.
*/
public static function create(?Database $db = null): self
{
require_once APP_ROOT . '/src/Database.php';
return new self($db ?? Database::getInstance());
}
// ── Read / view data ─────────────────────────────────────────────────────
/**
* Load all data required to render the edit form.
*
* Returns a flat array of view variables:
* - 'thesis' thesis row (from getThesis)
* - 'currentLanguages' int[]
* - 'currentFormats' int[]
* - 'jury' jury rows
* - 'orientations' lookup rows
* - 'apPrograms' lookup rows
* - 'finalityTypes' lookup rows
* - 'languages' lookup rows
* - 'formatTypes' lookup rows
* - 'licenseTypes' lookup rows
* - 'accessTypes' lookup rows
* - 'currentLicenseId' int|null
* - 'currentAccessTypeId' int|null
* - 'currentContextNote' string
* - 'pageTitle' string
*
* @throws Exception if the thesis is not found or a DB error occurs.
*/
public function load(int $thesisId): array
{
if ($thesisId <= 0) {
throw new InvalidArgumentException("ID invalide");
}
$thesis = $this->db->getThesis($thesisId);
if (!$thesis) {
throw new RuntimeException("TFE non trouvé");
}
$currentLanguages = $this->db->getThesisLanguageIds($thesisId);
$currentFormats = $this->db->getThesisFormatIds($thesisId);
$jury = $this->db->getThesisJury($thesisId);
$orientations = $this->db->getAllOrientations();
$apPrograms = $this->db->getAllAPPrograms();
$finalityTypes = $this->db->getAllFinalityTypes();
$languages = $this->db->getAllLanguages();
$formatTypes = $this->db->getAllFormatTypes();
$licenseTypes = $this->db->getAllLicenseTypes();
$accessTypes = $this->db->getAccessTypes();
$rawRow = $this->db->getThesisRawFields($thesisId);
$currentLicenseId = $rawRow['license_id'] ?? null;
$currentAccessTypeId = $rawRow['access_type_id'] ?? null;
$currentContextNote = $rawRow['context_note'] ?? '';
// Author contact info (from view)
$currentAuthorEmail = $thesis['author_email'] ?? '';
$currentAuthorShowContact = (bool)($thesis['author_show_contact'] ?? false);
return [
'thesis' => $thesis,
'currentLanguages' => $currentLanguages,
'currentFormats' => $currentFormats,
'jury' => $jury,
'orientations' => $orientations,
'apPrograms' => $apPrograms,
'finalityTypes' => $finalityTypes,
'languages' => $languages,
'formatTypes' => $formatTypes,
'licenseTypes' => $licenseTypes,
'accessTypes' => $accessTypes,
'currentLicenseId' => $currentLicenseId,
'currentAccessTypeId' => $currentAccessTypeId,
'currentContextNote' => $currentContextNote,
'currentAuthorEmail' => $currentAuthorEmail,
'currentAuthorShowContact' => $currentAuthorShowContact,
'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']),
];
}
// ── Write / action ────────────────────────────────────────────────────────
/**
* Validate and persist a thesis-edit POST submission.
*
* Runs the full update inside a transaction:
* 1. Thesis metadata (title, subtitle, year, orientation, ap, finality,
* synopsis, context_note, file_size_info, baiu_link, license_id,
* access_type_id, is_published)
* 2. Authors (setThesisAuthors)
* 3. Jury (setThesisJury)
* 4. Languages (setThesisLanguages)
* 5. Formats (setThesisFormats)
* 6. Tags (setThesisTags)
* Then handles banner upload/removal outside the transaction.
*
* @param int $thesisId Validated thesis ID (> 0).
* @param array $post Sanitised $_POST array.
* @param array $files $_FILES array (expects 'banner' key).
*
* @throws Exception on validation or DB error (caller must rollback if
* the transaction is still open, but this method rolls
* back internally before re-throwing).
*/
public function save(int $thesisId, array $post, array $files): void
{
if ($thesisId <= 0) {
throw new InvalidArgumentException("ID de TFE invalide.");
}
$this->db->beginTransaction();
try {
// ── 1. Thesis metadata ────────────────────────────────────────────
$this->db->updateThesis($thesisId, [
'title' => trim($post['titre'] ?? ''),
'subtitle' => trim($post['subtitle'] ?? ''),
'year' => intval($post['année'] ?? 0),
'orientation_id' => intval($post['orientation'] ?? 0),
'ap_program_id' => intval($post['ap'] ?? 0),
'finality_id' => intval($post['finality'] ?? 0),
'synopsis' => trim($post['synopsis'] ?? ''),
'context_note' => trim($post['context_note'] ?? ''),
'file_size_info' => trim($post['duration_info'] ?? ''),
'baiu_link' => trim($post['lien'] ?? ''),
'license_id' => filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
'access_type_id' => filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
'is_published' => isset($post['is_published']),
]);
// ── 2. Authors ────────────────────────────────────────────────────
$authorsRaw = trim($post['auteurice'] ?? '');
$showContact = !empty($post['contact_public']);
$authorEntries = [];
if ($authorsRaw !== '') {
foreach (array_map('trim', explode(',', $authorsRaw)) as $i => $name) {
if ($name !== '') {
$authorEntries[] = [
'name' => $name,
'email' => $i === 0 ? ($post['mail'] ?? null) : null,
'show_contact' => $i === 0 ? $showContact : false,
];
}
}
}
$this->db->setThesisAuthors($thesisId, $authorEntries);
// ── 3. Jury ───────────────────────────────────────────────────────
$juryMembers = $this->collectJuryMembers($post);
$this->db->setThesisJury($thesisId, $juryMembers);
// ── 4. Languages ──────────────────────────────────────────────────
$this->db->setThesisLanguages(
$thesisId,
isset($post['languages']) && is_array($post['languages'])
? $post['languages']
: []
);
// ── 5. Formats ────────────────────────────────────────────────────
$this->db->setThesisFormats(
$thesisId,
isset($post['formats']) && is_array($post['formats'])
? $post['formats']
: []
);
// ── 6. Tags ───────────────────────────────────────────────────────
$keywordsRaw = trim($post['tag'] ?? '');
$keywords = $keywordsRaw !== ''
? array_map('trim', explode(',', $keywordsRaw))
: [];
$this->db->setThesisTags($thesisId, $keywords);
$this->db->commit();
} catch (Exception $e) {
$this->db->rollback();
throw $e;
}
// ── Banner (outside transaction — filesystem op) ──────────────────────
if (isset($post['remove_banner'])) {
$currentBannerPath = $this->db->getThesisBannerPath($thesisId);
if ($currentBannerPath && defined('STORAGE_ROOT')) {
$absPath = STORAGE_ROOT . '/' . $currentBannerPath;
if (file_exists($absPath)) {
unlink($absPath);
}
}
$this->db->setBannerPath($thesisId, null);
} else {
$this->db->handleBannerUpload($thesisId, $files['banner'] ?? null);
}
}
// ── WCAG 3.3.1 helper ─────────────────────────────────────────────────────
/**
* Map a validation exception message to the name of the field that should
* receive autofocus when the form is re-rendered.
*
* Returns null when no field mapping is found.
*/
public static function autofocusFieldForError(string $message): ?string
{
if (str_contains($message, 'titre') || str_contains($message, 'Titre')) return 'titre';
if (str_contains($message, 'année') || str_contains($message, 'Année')) return 'année';
if (str_contains($message, 'synopsis') || str_contains($message, 'Synopsis')) return 'synopsis';
if (str_contains($message, 'auteur') || str_contains($message, 'Auteur')) return 'auteurice';
return null;
}
// ── Private helpers ───────────────────────────────────────────────────────
/**
* Build the jury-members array from POST data.
*
* @param array $post Raw $_POST.
* @return array<int, array{name: string, role: string, is_external: int}>
*/
private function collectJuryMembers(array $post): array
{
$members = [];
if (!empty(trim($post['jury_president'] ?? ''))) {
$members[] = [
'name' => trim($post['jury_president']),
'role' => 'president',
'is_external' => 0,
];
}
if (!empty(trim($post['jury_promoteur'] ?? ''))) {
$members[] = [
'name' => trim($post['jury_promoteur']),
'role' => 'promoteur',
'is_external' => isset($post['jury_promoteur_ext']) ? 1 : 0,
];
}
foreach ($post['jury_lecteurs'] ?? [] as $i => $name) {
$name = trim($name);
if ($name !== '') {
$members[] = [
'name' => $name,
'role' => 'lecteur',
'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
];
}
}
return $members;
}
}