Files
xamxam/src/ThesisEditController.php

294 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}