Extract ThesisEditController from admin/edit.php and actions/edit.php

src/ThesisEditController.php (285 lines) centralises all data-fetching and
mutation logic for the thesis-edit workflow:

  load(int $thesisId): array
    Fetches the thesis row, current language/format/jury selections, and all
    lookup tables (orientations, AP programmes, finality types, languages,
    formats, licences, access types) in one call.  Returns a flat view-variable
    array that the dispatcher extracts directly.

  save(int $thesisId, array $post, array $files): void
    Runs the full edit inside a transaction: thesis metadata, authors, jury,
    languages, formats, tags.  Banner upload/removal is handled outside the
    transaction (filesystem op).  Rolls back and re-throws on any failure.

  static autofocusFieldForError(string $msg): ?string
    Centralises the WCAG 3.3.1 exception-message → field-name mapping that
    was previously duplicated inline in actions/edit.php.

Dispatcher changes:
  admin/edit.php      191 → 162 lines  (pure view + ThesisEditController::create() + load())
  actions/edit.php    153 →  53 lines  (CSRF guard + ThesisEditController::save() call)

Follows the same pattern as SearchController and SystemController.
This commit is contained in:
Pontoporeia
2026-04-05 19:17:27 +02:00
parent 40cb119448
commit 41629398d3
5 changed files with 301 additions and 143 deletions

View File

@@ -24,126 +24,26 @@ if ($thesisId <= 0) {
die("ID de TFE invalide.");
}
require_once __DIR__ . '/../../../src/Database.php';
require_once APP_ROOT . '/src/ThesisEditController.php';
try {
$db = new Database();
$ctrl = ThesisEditController::create();
$ctrl->save($thesisId, $_POST, $_FILES);
$db->beginTransaction();
// Thesis metadata
$db->updateThesis($thesisId, [
'title' => trim($_POST['titre']),
'subtitle' => trim($_POST['subtitle'] ?? ''),
'year' => intval($_POST['année']),
'orientation_id' => intval($_POST['orientation']),
'ap_program_id' => intval($_POST['ap']),
'finality_id' => intval($_POST['finality']),
'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']),
]);
// Authors
$authorsRaw = trim($_POST['auteurice'] ?? '');
$authorEntries = [];
if (!empty($authorsRaw)) {
$names = array_map('trim', explode(',', $authorsRaw));
foreach ($names as $index => $name) {
if ($name !== '') {
$authorEntries[] = [
'name' => $name,
'email' => $index === 0 ? ($_POST['mail'] ?? null) : null,
];
}
}
}
$db->setThesisAuthors($thesisId, $authorEntries);
// Jury
$juryMembers = [];
if (!empty(trim($_POST['jury_president'] ?? ''))) {
$juryMembers[] = ['name' => trim($_POST['jury_president']), 'role' => 'president', 'is_external' => 0];
}
if (!empty(trim($_POST['jury_promoteur'] ?? ''))) {
$juryMembers[] = [
'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 !== '') {
$juryMembers[] = [
'name' => $name,
'role' => 'lecteur',
'is_external' => isset($_POST['jury_lecteurs_ext'][$i]) ? 1 : 0,
];
}
}
$db->setThesisJury($thesisId, $juryMembers);
// Languages
$db->setThesisLanguages(
$thesisId,
isset($_POST['languages']) && is_array($_POST['languages']) ? $_POST['languages'] : []
);
// Formats
$db->setThesisFormats(
$thesisId,
isset($_POST['formats']) && is_array($_POST['formats']) ? $_POST['formats'] : []
);
// Tags
$keywordsRaw = trim($_POST['tag'] ?? '');
$editKeywords = !empty($keywordsRaw) ? array_map('trim', explode(',', $keywordsRaw)) : [];
$db->setThesisTags($thesisId, $editKeywords);
$db->commit();
// Banner upload/removal (after commit, outside transaction)
if (isset($_POST['remove_banner'])) {
$currentBannerPath = $db->getThesisBannerPath($thesisId);
if ($currentBannerPath && defined('STORAGE_ROOT')) {
$absPath = STORAGE_ROOT . '/' . $currentBannerPath;
if (file_exists($absPath)) {
unlink($absPath);
}
}
$db->setBannerPath($thesisId, null);
} else {
$db->handleBannerUpload($thesisId, $_FILES['banner'] ?? null);
}
// Regenerate CSRF token
// Regenerate CSRF token after successful save
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// Flash success and redirect back to edit form
App::flash('success', "TFE mis à jour avec succès!");
header('Location: ../edit.php?id=' . $thesisId);
exit();
} catch (Exception $e) {
if (isset($db)) {
$db->rollback();
}
error_log("Edit action error: " . $e->getMessage());
App::flash('error', $e->getMessage());
// WCAG 3.3.1 — map error to the field that caused it so the form can autofocus it.
$msg = $e->getMessage();
$autofocusField = null;
if (str_contains($msg, 'titre') || str_contains($msg, 'Titre')) $autofocusField = 'titre';
elseif (str_contains($msg, 'année') || str_contains($msg, 'année')) $autofocusField = 'année';
elseif (str_contains($msg, 'synopsis') || str_contains($msg, 'Synopsis')) $autofocusField = 'synopsis';
elseif (str_contains($msg, 'auteur') || str_contains($msg, 'Auteur')) $autofocusField = 'auteurice';
// WCAG 3.3.1 — map error message to field name for autofocus on re-render.
$autofocusField = ThesisEditController::autofocusFieldForError($e->getMessage());
if ($autofocusField !== null) {
App::flashAutofocus($autofocusField);
}

View File

@@ -11,7 +11,7 @@ if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../src/Database.php';
require_once APP_ROOT . '/src/ThesisEditController.php';
$thesisId = isset($_GET['id']) ? intval($_GET['id']) : 0;
@@ -19,43 +19,14 @@ if ($thesisId <= 0) {
die("ID invalide");
}
// Flash messages are consumed by the flash-messages partial below.
// WCAG 3.3.1 — consume the autofocus hint stored by the edit action on
// validation failure.
$autofocusField = App::consumeAutofocus();
try {
$db = new Database();
// Load thesis data
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
die("TFE non trouvé");
}
// Load current relationships via dedicated DB methods (no raw PDO)
$currentLanguages = $db->getThesisLanguageIds($thesisId);
$currentFormats = $db->getThesisFormatIds($thesisId);
$jury = $db->getThesisJury($thesisId);
// Reference / lookup data
$orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
$licenseTypes = $db->getAllLicenseTypes();
$accessTypes = $db->getAccessTypes();
// Fetch raw FK IDs (view only exposes name strings)
$rawRow = $db->getThesisRawFields($thesisId);
$currentLicenseId = $rawRow['license_id'] ?? null;
$currentAccessTypeId = $rawRow['access_type_id'] ?? null;
$currentContextNote = $rawRow['context_note'] ?? '';
// Set page title for header
$pageTitle = "Éditer TFE - " . htmlspecialchars($thesis['title']);
// WCAG 3.3.1 — consume the autofocus hint stored by the edit action on validation failure.
$autofocusField = App::consumeAutofocus();
$ctrl = ThesisEditController::create();
$view = $ctrl->load($thesisId);
extract($view); // thesis, currentLanguages, currentFormats, jury, lookup tables, pageTitle …
} catch (Exception $e) {
error_log("Error loading edit page: " . $e->getMessage());
die("Erreur lors du chargement: " . $e->getMessage());