| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- <?php
- declare(strict_types=1);
- namespace App\Http;
- use Twig\Environment;
- use Twig\Loader\FilesystemLoader;
- use Twig\TwigFunction;
- /**
- * Twig 3 wrapper that keeps the historical View::render($name, $data, $layout)
- * signature so controllers don't change. Templates live under views/ as
- * *.twig and use {% extends "layout.twig" %} for inheritance — the $layout
- * parameter is now vestigial (only `null` is honoured, used by the bare
- * /sprints/{id}/present route to skip layout inheritance — but Twig's
- * extends handles that just by picking the right base template).
- */
- final class View
- {
- private readonly Environment $twig;
- public function __construct(string $viewsDir, ?string $cacheDir = null)
- {
- $loader = new FilesystemLoader($viewsDir);
- $this->twig = new Environment($loader, [
- 'cache' => $cacheDir ?? false,
- 'auto_reload' => (getenv('APP_ENV') ?: 'production') !== 'production',
- 'strict_variables' => false,
- 'autoescape' => 'html',
- ]);
- // fmt_days(x) — same shape used by the previous PHP templates and the
- // CapacityCalculator: 0 → "0", whole numbers → integer, halves → x.5.
- $this->twig->addFunction(new TwigFunction(
- 'fmt_days',
- static function (mixed $x): string {
- $n = (float) $x;
- if (abs($n - round($n)) < 1e-9) {
- return (string) (int) round($n);
- }
- return number_format($n, 1);
- }
- ));
- // fmt_rtb(x) — two decimals, e.g. 0.05.
- $this->twig->addFunction(new TwigFunction(
- 'fmt_rtb',
- static fn(mixed $x): string => number_format((float) $x, 2, '.', '')
- ));
- // pretty_json(raw) — used by audit/index.twig for the diff <pre> blocks.
- $this->twig->addFunction(new TwigFunction(
- 'pretty_json',
- static function (?string $raw): string {
- if ($raw === null || $raw === '') {
- return '';
- }
- try {
- $v = json_decode($raw, true, 64, JSON_THROW_ON_ERROR);
- } catch (\JsonException) {
- return $raw;
- }
- return (string) (json_encode(
- $v,
- JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
- ) ?: $raw);
- }
- ));
- // query_string(filters, drop, extra) — rebuilds a query string while
- // dropping one key and merging extras. Used by /audit pagination.
- $this->twig->addFunction(new TwigFunction(
- 'query_string',
- /**
- * @param array<string,scalar|null> $filters
- * @param array<string,scalar|null> $extra
- */
- static function (array $filters, string $drop, array $extra = []): string {
- $params = array_filter(
- array_merge($filters, $extra),
- static fn($v) => $v !== '' && $v !== null
- );
- unset($params[$drop]);
- return $params === [] ? '' : '?' . http_build_query($params);
- }
- ));
- }
- /**
- * Render a view and return the HTML.
- *
- * @param string $name view name; "home" → views/home.twig
- * @param array<string,mixed> $data
- * @param string|null $layout retained for back-compat; ignored
- * by Twig (templates use {% extends %})
- */
- public function render(string $name, array $data = [], ?string $layout = 'layout'): string
- {
- unset($layout);
- return $this->twig->render($name . '.twig', $data);
- }
- /** @param array<string,mixed> $data */
- public function renderRaw(string $name, array $data): string
- {
- return $this->twig->render($name . '.twig', $data);
- }
- /** Direct access to the Twig env (rarely needed; mostly for tests). */
- public function twig(): Environment
- {
- return $this->twig;
- }
- }
- /** Escape for HTML. Kept so legacy callers still resolve; Twig auto-escapes. */
- function e(mixed $v): string
- {
- if ($v === null) {
- return '';
- }
- return htmlspecialchars((string) $v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
- }
|