Extract TfeController from public/tfe.php

src/TfeController.php (new, 195 lines):
- Dedicated controller for the public TFE detail page
- create(): Database singleton injection, ready-to-use factory
- handle(): validates id param (redirect to index.php on missing/invalid/404),
  loads thesis via getThesisById(), fetches access type via getThesisAccessTypeId()
- buildMetaDescription(): strip_tags + 160-char mb_substr truncation
- resolveOgImage(): banner_path → first image file → empty string resolution
- buildOgTags(): full og:type/title/description/url/image/image_alt/site_name +
  article:author / article:published_time assembly
- collectCaptionPaths(): ordered list of VTT paths for N-th-video pairing
- returns flat array of all view variables including ogTags, captionFiles,
  pageTitle, metaDescription, isInterdit, bodyClass, extraCss, currentNav

public/tfe.php (271 → 206 lines):
- Reduced to 9-line dispatcher: require TfeController, create(), handle(), extract()
- $db reference removed from view layer entirely
- Inline OG tag block (~20 lines) removed
- Inline meta-description block (~5 lines) removed
- Inline caption-collection loop (~10 lines) removed
- $captionFiles replaces $_captionFiles in the video pairing section

todo/02-php-components.md:
- TfeController extraction marked done
- 'Move OG tag construction into controller logic' marked done
- Remaining item narrowed to public/index.php home-page controller
This commit is contained in:
Pontoporeia
2026-04-06 14:25:54 +02:00
parent 41629398d3
commit 89067a521f
4 changed files with 217 additions and 73 deletions

205
src/TfeController.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
/**
* TfeController
*
* Handles all data-fetching and view-variable assembly for the public TFE
* detail page (public/tfe.php).
*
* Responsibilities:
* - Validate the `id` GET parameter and load the thesis record
* - Enforce publication visibility (redirect to index on 404)
* - Resolve the OG image (banner → first image file)
* - Build the complete OG / Twitter Card tag array
* - Assemble the meta description from the synopsis
* - Collect WebVTT caption file paths for video pairing
* - Return a flat array of view variables ready for template extraction
*
* The class has NO output side-effects; all template rendering stays in
* public/tfe.php so the view layer remains easy to inspect and modify.
*/
class TfeController
{
private const BASE_URL = 'https://posterg.erg.be';
private const META_MAX_LEN = 160;
private Database $db;
public function __construct(Database $db)
{
$this->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.
*
* On success returns an array of view variables.
* On failure (missing id, thesis not found) sends a redirect and exits.
*
* @return array<string, mixed>
*/
public function handle(): array
{
$thesisId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if ($thesisId <= 0) {
$this->redirectHome();
}
try {
$data = $this->db->getThesisById($thesisId);
} catch (Exception $e) {
error_log('TfeController: ' . $e->getMessage());
$this->redirectHome();
}
if (empty($data)) {
$this->redirectHome();
}
// Access type (1 = open, 2 = restricted, 3 = forbidden)
$accessTypeId = $this->db->getThesisAccessTypeId($thesisId) ?? 1;
$isInterdit = ($accessTypeId === 3);
// Caption (WebVTT) files — N-th VTT is paired with the N-th <video>
$captionFiles = $this->collectCaptionPaths($data['files'] ?? []);
// Page meta
$metaDescription = $this->buildMetaDescription($data['synopsis'] ?? '');
$ogTags = $this->buildOgTags($data, $thesisId, $metaDescription);
$pageTitle = $data['title']
. (!empty($data['authors']) ? ' ' . $data['authors'] : '')
. ' Posterg';
return [
// Core data
'thesisId' => $thesisId,
'data' => $data,
'accessTypeId' => $accessTypeId,
'isInterdit' => $isInterdit,
'captionFiles' => $captionFiles,
// Page meta
'pageTitle' => $pageTitle,
'metaDescription' => $metaDescription,
'ogTags' => $ogTags,
// Layout
'currentNav' => '',
'extraCss' => ['/assets/css/tfe.css'],
'bodyClass' => 'tfe-body',
];
}
// ── Private helpers ───────────────────────────────────────────────────────
/**
* Build a ~160-character meta description from the thesis synopsis.
*/
private function buildMetaDescription(string $synopsis): string
{
$plain = strip_tags($synopsis);
if (empty($plain)) {
return 'Mémoire de fin d\'études Posterg, répertoire des TFE de l\'erg.';
}
return mb_strlen($plain) > self::META_MAX_LEN
? mb_substr($plain, 0, self::META_MAX_LEN - 3) . '…'
: $plain;
}
/**
* Resolve the OG image URL: banner_path → first image file → empty string.
*
* @param array<int, array<string, mixed>> $files
*/
private function resolveOgImage(array $files, ?string $bannerPath): string
{
if (!empty($bannerPath)) {
return self::BASE_URL . '/media.php?path=' . rawurlencode($bannerPath);
}
foreach ($files as $file) {
$ext = strtolower(pathinfo($file['file_path'] ?? '', PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
return self::BASE_URL . '/media.php?path=' . rawurlencode($file['file_path']);
}
}
return '';
}
/**
* Build the complete $ogTags array consumed by templates/head.php.
*
* @param array<string, mixed> $data
* @return array<string, string>
*/
private function buildOgTags(array $data, int $thesisId, string $metaDescription): array
{
$ogImage = $this->resolveOgImage($data['files'] ?? [], $data['banner_path'] ?? null);
$title = $data['title'] . (!empty($data['authors']) ? ' ' . $data['authors'] : '');
$imageAlt = $data['title'] . (!empty($data['authors']) ? ' par ' . $data['authors'] : '');
return [
'type' => 'article',
'title' => $title,
'description' => $metaDescription,
'url' => self::BASE_URL . '/tfe.php?id=' . $thesisId,
'image' => $ogImage,
'image_alt' => $imageAlt,
'site_name' => 'Posterg ERG',
'article_author' => $data['authors'] ?? '',
'article_published_time' => !empty($data['year']) ? $data['year'] . '-01-01' : '',
];
}
/**
* Return an ordered list of VTT caption file paths from the files array.
* The N-th entry corresponds to the N-th <video> element in document order.
*
* @param array<int, array<string, mixed>> $files
* @return list<string>
*/
private function collectCaptionPaths(array $files): array
{
$captions = [];
foreach ($files as $file) {
$mime = $file['mime_type'] ?? '';
$ext = strtolower(pathinfo($file['file_path'] ?? '', PATHINFO_EXTENSION));
if ($mime === 'text/vtt' || $ext === 'vtt') {
$captions[] = $file['file_path'];
}
}
return $captions;
}
// ── Response helpers ──────────────────────────────────────────────────────
/**
* Redirect to the home page and terminate. Never returns.
*/
private function redirectHome(): never
{
header('Location: index.php');
exit;
}
}