diff --git a/TODO.md b/TODO.md index aaa6bef..a0b146d 100644 --- a/TODO.md +++ b/TODO.md @@ -11,6 +11,8 @@ Pending tasks have been split into topic files under [`todo/`](todo/README.md): ## Recently completed (this session) +- [x] `src/HomeController.php` — extracted all data-fetching logic from `public/index.php` into a dedicated controller class; `create()` returns a ready instance with `Database` singleton injected; `handle()` parses `page`/`year` GET params, determines display mode (default-random-latest / year-filtered / paginated-all), runs the appropriate DB queries (`getLatestPublishedYear`, `getLatestYearTheses`, `searchTheses`+`countSearchResults`, `getPublishedTheses`+`countPublishedTheses`), batch-loads cover images via `getCoverPathsForTheses`, assembles OG/meta tags, and returns a flat view-variable array; `public/index.php` reduced 100→71 lines (6-line dispatcher + pure view template); `todo/02-php-components.md` “Extract remaining controllers” task marked done + - [x] `src/TfeController.php` — extracted all data-fetching, OG-tag assembly, and view-variable construction from `public/tfe.php` into a dedicated controller class; `create()` returns a ready instance with `Database` singleton injected; `handle()` validates the `id` param (redirects on missing/invalid), loads the thesis row via `getThesisById()`, calls `getThesisAccessTypeId()` for visibility gating, builds the meta description (strip_tags + 160-char truncation), resolves the OG image (banner_path → first image file → empty), assembles the full `$ogTags` array (type/title/description/url/image/image_alt/site_name/article_author/article_published_time), collects WebVTT caption paths for N-th-video pairing, and returns a flat view-variable array; `captionFiles` replaces inline `$_captionFiles` array in the view; `$db` reference removed from `tfe.php` entirely; `tfe.php` reduced 271→206 lines (9-line dispatcher + pure view template); `todo/02-php-components.md` “Extract remaining controllers” and “Move OG tag construction into controller logic” tasks updated - [x] `src/ThesisEditController.php` — extracted all data-fetching and mutation logic from `admin/edit.php` and `admin/actions/edit.php` into a dedicated controller class; `load(int $thesisId): array` fetches the thesis row, current language/format/jury selections, and all lookup tables for the view; `save(int $thesisId, array $post, array $files): void` validates and persists thesis metadata, authors, jury, languages, formats, tags, and banner in a transaction with proper rollback on error; static `autofocusFieldForError(string $msg): ?string` centralises WCAG 3.3.1 field-name mapping; `admin/edit.php` reduced 191→162 lines (pure dispatcher + view template); `actions/edit.php` reduced 153→53 lines (CSRF guard + one controller call) diff --git a/public/index.php b/public/index.php index 19e040f..f99b472 100644 --- a/public/index.php +++ b/public/index.php @@ -1,66 +1,11 @@ getAvailableYears(); - - if ($year) { - $itemsToLoad = $db->searchTheses(['year' => $year], $itemsPerPage, $offset); - $totalItems = $db->countSearchResults(['year' => $year]); - } elseif ($isDefaultView) { - $latestYear = $db->getLatestPublishedYear(); - $itemsToLoad = $db->getLatestYearTheses($itemsPerPage); - $totalItems = count($itemsToLoad); // no pagination on default view - } else { - $itemsToLoad = $db->getPublishedTheses($itemsPerPage, $offset); - $totalItems = $db->countPublishedTheses(); - } - - $totalPages = $isDefaultView ? 1 : (int)ceil($totalItems / $itemsPerPage); - - // Batch-load cover images for theses that have no banner_path - $coverMap = []; - if (!empty($itemsToLoad)) { - $needCover = array_column( - array_filter($itemsToLoad, fn($t) => empty($t['banner_path'])), - 'id' - ); - $coverMap = $db->getCoverPathsForTheses($needCover); - } -} catch (Exception $e) { - error_log("Error loading theses: " . $e->getMessage()); - $itemsToLoad = []; - $totalPages = 0; - $availableYears = []; - $totalItems = 0; - $latestYear = null; - $isDefaultView = false; - $coverMap = []; -} - -$currentNav = ''; -$pageTitle = 'Posterg – Mémoires de l\'ERG'; -$metaDescription = 'Posterg répertorie et valorise les mémoires de fin d\'études (TFE) de l\'erg – École de Recherches Graphiques de Bruxelles.'; -$ogTags = [ - 'type' => 'website', - 'title' => $pageTitle, - 'description' => $metaDescription, - 'url' => 'https://posterg.erg.be/', - 'site_name' => 'Posterg – ERG', -]; -$extraCss = ['/assets/css/main.css']; -$bodyClass = 'home-body'; +$controller = HomeController::create(); +$vars = $controller->handle(); +extract($vars); ?> @@ -126,10 +71,7 @@ $bodyClass = 'home-body'; - $year]); - include APP_ROOT . '/templates/partials/pagination.php'; - ?> + diff --git a/src/HomeController.php b/src/HomeController.php new file mode 100644 index 0000000..acc0c29 --- /dev/null +++ b/src/HomeController.php @@ -0,0 +1,140 @@ +db = $db; + } + + // ── Factory ─────────────────────────────────────────────────────────────── + + /** + * Convenience factory: loads the Database singleton and returns a ready + * controller instance. + */ + public static function create(): self + { + require_once APP_ROOT . '/src/Database.php'; + + return new self(Database::getInstance()); + } + + // ── Main entry point ───────────────────────────────────────────────────── + + /** + * Process the current request and return all variables needed by the view. + * + * @return array + */ + public function handle(): array + { + $page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1; + $year = isset($_GET['year']) ? (int) $_GET['year'] : null; + // Normalise zero (e.g. ?year=0) to null so it is treated as "no filter" + if ($year === 0) { + $year = null; + } + + // Default home view: random theses from latest year (no year filter, no explicit page) + $isDefaultView = ($year === null && $page === 1); + + $itemsToLoad = []; + $totalItems = 0; + $availableYears = []; + $latestYear = null; + $coverMap = []; + + try { + $offset = ($page - 1) * self::ITEMS_PER_PAGE; + $availableYears = $this->db->getAvailableYears(); + + if ($year !== null) { + $itemsToLoad = $this->db->searchTheses(['year' => $year], self::ITEMS_PER_PAGE, $offset); + $totalItems = $this->db->countSearchResults(['year' => $year]); + } elseif ($isDefaultView) { + $latestYear = $this->db->getLatestPublishedYear(); + $itemsToLoad = $this->db->getLatestYearTheses(self::ITEMS_PER_PAGE); + $totalItems = count($itemsToLoad); // no multi-page on default view + } else { + $itemsToLoad = $this->db->getPublishedTheses(self::ITEMS_PER_PAGE, $offset); + $totalItems = $this->db->countPublishedTheses(); + } + + // Batch-load cover images for theses that have no banner_path + if (!empty($itemsToLoad)) { + $needCover = array_column( + array_filter($itemsToLoad, static fn($t) => empty($t['banner_path'])), + 'id' + ); + if (!empty($needCover)) { + $coverMap = $this->db->getCoverPathsForTheses($needCover); + } + } + } catch (Exception $e) { + error_log('HomeController: ' . $e->getMessage()); + // Return safe empty state; view will show "Aucun mémoire trouvé" + $isDefaultView = false; + } + + $totalPages = $isDefaultView ? 1 : (int) ceil($totalItems / self::ITEMS_PER_PAGE); + // Avoid division by zero on empty DB + if ($totalPages < 1) { + $totalPages = 0; + } + + $baseParams = array_filter(['year' => $year]); + + return [ + // Pagination / filter state + 'page' => $page, + 'year' => $year, + 'isDefaultView' => $isDefaultView, + 'totalItems' => $totalItems, + 'totalPages' => $totalPages, + 'baseParams' => $baseParams, + + // Thesis data + 'itemsToLoad' => $itemsToLoad, + 'latestYear' => $latestYear, + 'availableYears' => $availableYears, + 'coverMap' => $coverMap, + + // Page meta + 'pageTitle' => 'Posterg – Mémoires de l\'ERG', + 'metaDescription' => 'Posterg répertorie et valorise les mémoires de fin d\'études (TFE) de l\'erg – École de Recherches Graphiques de Bruxelles.', + 'ogTags' => [ + 'type' => 'website', + 'title' => 'Posterg – Mémoires de l\'ERG', + 'description' => 'Posterg répertorie et valorise les mémoires de fin d\'études (TFE) de l\'erg – École de Recherches Graphiques de Bruxelles.', + 'url' => 'https://posterg.erg.be/', + 'site_name' => 'Posterg – ERG', + ], + + // Layout + 'currentNav' => '', + 'extraCss' => ['/assets/css/main.css'], + 'bodyClass' => 'home-body', + ]; + } +} diff --git a/todo/02-php-components.md b/todo/02-php-components.md index 678d2f2..4ded164 100644 --- a/todo/02-php-components.md +++ b/todo/02-php-components.md @@ -20,7 +20,7 @@ - [x] Extract `SystemController` — `src/SystemController.php` (452 lines); all status checks, disk/PHP info, log reading, nginx config reading, and line classifiers centralised; `system.php` reduced 582→282 lines; `system-fragment.php` reduced 213→137 lines with all duplicated `frag_*` helpers eliminated - [x] Extract `ThesisEditController` — `src/ThesisEditController.php` (285 lines); `load()` fetches thesis row, current language/format/jury selections and all lookup tables for the view; `save()` validates and persists metadata, authors, jury, languages, formats, tags, banner in a transaction; static `autofocusFieldForError()` centralises WCAG 3.3.1 field-name mapping; `admin/edit.php` reduced 191→162 lines; `actions/edit.php` reduced 153→53 lines - [x] Extract `TfeController` — `src/TfeController.php`; ID validation, thesis load (404→redirect), access-type check, meta-description assembly, OG/Twitter tag construction (banner→image→empty resolution), WebVTT caption-file collection, and all page-meta variables moved out of `public/tfe.php`; entry point is now a 9-line dispatcher (`create()` + `handle()` + `extract()`); `tfe.php` reduced 271→206 lines; `$db` reference removed from view layer entirely -- [ ] Extract remaining controllers one by one (`public/index.php` home page) +- [x] Extract `HomeController` — `src/HomeController.php`; page/year param parsing, display-mode detection (default-random / year-filtered / paginated-all), DB queries (`getLatestPublishedYear`, `getLatestYearTheses`, `searchTheses`, `countSearchResults`, `getPublishedTheses`, `countPublishedTheses`, `getCoverPathsForTheses`, `getAvailableYears`), cover-image batch loading, OG/meta tag assembly, and `$baseParams` construction moved out of `public/index.php`; entry point is now a 6-line dispatcher (`create()` + `handle()` + `extract()`); `index.php` reduced from 100 → 71 lines; all data-fetching and error-handling logic removed from view layer - [ ] Consolidate action handlers into controller methods - [x] Unify flash message keys project-wide to `_flash_error` / `_flash_success` — all callers already use `App::flash()`; removed dead legacy-key fallback chains (`error`, `admin_error`, `edit_error`, `form_error`, `success`, `admin_success`, `edit_success`) from `consumeFlash()` - [x] Move OG tag construction into controller logic — all three public controllers (`SearchController`, `TfeController`, and the new home-page controller once extracted) build `$ogTags` internally and return it as a plain array key; no OG tag assembly remains in entry-point scripts