Files
Marijn 404c0dcc26 Summer cleanup: XLSX export, WIDM-style quiz UI, CSS fixes (#162)
* Add CLAUDE.md, replace Makefile with Justfile, remove .junie

- Add CLAUDE.md with project overview, commands, architecture, and domain entity docs
- Remove Makefile in favour of the existing Justfile
- Remove .junie/AGENTS.md (knowledge transferred to CLAUDE.md)
- Update .gitignore: drop .junie/ entries, add .claude/settings.local.json
- Minor doc fixes in config/reference.php (typo, type correction)

* Clean up templates and CSS

- season.html.twig: remove dead empty column, drop redundant flex-row
- tab_overview.html.twig: extract Twig macro for confirm modals, fix duplicate aria-labelledby IDs
- tab_result.html.twig: remove dead comment, replace inline widths with CSS classes, simplify nested row/col forms to d-flex gap-1
- backoffice.scss: add col-result-xs/sm/md column width classes
- quiz.scss: replace broken display:grid + justify-self:center with flexbox centering

* Implement quizToXlsx() export and add export button

- QuizSpreadsheetService: implement quizToXlsx() as the inverse of
  fillQuizFromArray() — writes quiz questions and answers to XLSX using
  the same column layout as the import template
- BackofficeController: add exportQuiz() action at GET /backoffice/quiz/{quiz}/export
- tab_overview.html.twig: add Export to XLSX button in Quick actions

* Add unit tests for QuizSpreadsheetService

7 tests covering generateTemplate(), quizToXlsx(), and xlsxToQuiz():
- valid XLSX output and MIME type
- template without example reimports as empty
- template example data survives a reimport
- round-trip (export → reimport) preserves questions, answers, and correct flags
- empty quiz exports and reimports cleanly
- invalid MIME type throws InvalidArgumentException
- question with no answers throws SpreadsheetDataException with error list

* Fix quiz page vertical centering regression

The CSS cleanup broke vertical centering: flex on body causes main to
stretch full-width; place-items:center on a grid body only centers
items within their auto-sized track (not the track within body).

Fix: move background/color to html (full-viewport grid that centers
body), give body height:100% + display:grid + align-content:center
(centers the content track within full-height body) + justify-self:center
(shrink-wraps body width). Matches production behavior exactly.

* Improve quiz page layouts: WIDM-style answers and responsive centering

- Add green square answer buttons styled after the TV show
- Two-column answer grid for 6+ answers, single column on mobile
- fit-content centering for question pages so block matches question width
- Narrow fixed-width centering for form pages (enter name, select season)

* Use HeaderUtils::makeDisposition() for safe Content-Disposition filename

* Fix quizToXlsx to support unlimited answers and add header count tests

- Replace hardcoded 6-column arrays with dynamic Coordinate arithmetic
- Write data rows first to determine max answer count, write headers last
- Replace try/catch ErrorException in fillQuizFromArray with array_key_exists
- Add data-provider test covering 2, 6, 7, and 10 answers
- Add cross-question max-header and 7-answer round-trip tests

* Fix Sass healthcheck

* Improve quiz layout: add fixed topbar, include navigation, and clean up unused elements

- Add `.quiz-topbar` with fixed positioning and spacing in `quiz.scss`
- Update `base.html.twig` to include `quiz/nav.html.twig` in a new `nav` block
- Remove unused "Manage Quiz" button from `select_season.html.twig`

* Refactor generateTemplate to reuse quizToXlsx and add second example question

- generateTemplate now builds an in-memory Quiz entity and delegates to
  quizToXlsx, eliminating duplicate spreadsheet-building logic
- Adds a second example question "Wie is de mol?" with 10 Dutch names
  (5 male, 5 female) to better illustrate the import format
- Updates tests to assert both example questions and adds a test for the
  blank-row halt behaviour in fillQuizFromArray (achieving 100% coverage)

* Move PHPUnit cache to /tmp to avoid writing into the mounted volume

* Update src/Service/QuizSpreadsheetService.php

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-02 20:25:02 +00:00

9.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Tijd voor de test is a PHP/Symfony 8.1 application for managing quizzes in the style of Wie is de Mol? (WIDM) — a Dutch TV show where contestants try to identify a saboteur ("de Mol") among them. At the end of each episode, participants take a quiz about the Mol's identity and actions; the candidate with the least correct answers is eliminated. This app replicates that quiz format with:

  • Test creation with variable question counts
  • Season management with active test controls
  • Candidate answer tracking with automatic timing
  • Elimination tracking with joker adjustments
  • Backoffice management for quiz administration and statistics

Tech Stack:

  • Framework: Symfony 8.1
  • PHP: 8.5+
  • Database: PostgreSQL 16
  • ORM: Doctrine
  • Server: FrankenPHP with Caddy
  • Container: Docker Compose
  • Frontend: Twig templates with SASS (via asset mapper)
  • Testing: PHPUnit 13 with DAMA Doctrine test bundle

Build & Development Commands

All commands assume Docker is running. The project uses a Justfile as the primary interface.

Essential Commands

just up           # Start Docker services (PHP, PostgreSQL)
just stop         # Stop services
just down         # Stop and remove containers/orphans
just shell        # Interactive shell inside the PHP container
just shell-run    # Shell in a fresh one-off container

Database

just migrate                  # Run Doctrine migrations (starts services first)
just fixtures                 # Load dev fixtures (truncates first)
just reload-tests             # Drop/recreate test DB, migrate, load test fixtures

Testing

just test                                  # Run full PHPUnit suite
just test tests/Path/To/TestFile.php       # Run a specific test file
just test --coverage-html var/coverage     # Generate HTML coverage report

Code Quality & Linting

just fix-cs                        # Auto-fix PHP-CS-Fixer + Twig-CS-Fixer
just phpstan                       # PHPStan static analysis (level 9)
just phpstan --no-progress         # Without progress output
just rector                        # Apply Rector modernizations
just rector --dry-run              # Preview Rector changes

Other

just translations   # Extract/update nl translation strings
just clean          # Nuke containers (volumes) + all generated files (prompts for confirmation)
just trust-cert     # Trust the local Caddy TLS certificate (macOS)
just exec <cmd>     # Run any command inside the PHP container

All code quality checks run in CI/CD (.github/workflows/ci.yml) and should pass before merging.

Project Structure

src/
  Controller/              # HTTP request handlers (attribute-routed)
    Backoffice/           # Admin panel controllers
  Entity/                 # Doctrine ORM entities
  Repository/             # Database queries
  Service/                # Business logic
  Command/                # CLI commands
  Form/                   # Symfony form types
  Dto/                    # Data transfer objects
  Enum/                   # Enumerations (FlashType, etc.)
  Exception/              # Custom exceptions
  Factory/                # Object factories
  Helpers/                # Utility functions
  Security/               # Auth and voter classes
    Voter/                # Authorization voters
  DataFixtures/           # Test data loaders

config/
  packages/               # Symfony bundle configurations
  routes/                 # Route definitions
  services.yaml           # Service container configuration
  routes.yaml             # Main route entry point

templates/
  backoffice/             # Admin UI templates
  quiz/                   # Public quiz UI templates
  base.html.twig          # Main layout

tests/
  Command/                # Command tests
  Controller/             # Controller/integration tests
  Repository/             # Repository tests
  Security/               # Auth tests
  Helpers/                # Utility tests
  bootstrap.php           # PHPUnit bootstrap with test container setup

Core Domain Entities

  • Season: Groups quizzes and candidates for a specific period, with a linked SeasonSettings.
  • Quiz: A test within a season containing multiple Questions, each with multiple Answers.
  • Candidate: A participant in the season.
  • QuizCandidate: Represents a candidate's attempt at a specific quiz (tracks start/end time).
  • GivenAnswer: The specific answer a candidate selected during a quiz.
  • Elimination: Records red/green screens and forced results with joker adjustments.
  • User: Administrative accounts for managing the system.

Architecture Notes

Routing

  • Routes are attribute-based (PHP 8 attributes in controller methods)
  • Configured in config/routes/attributes.yaml for automatic discovery
  • Main entry point: config/routes.yaml

Service Container & Dependency Injection

  • Services in src/ are automatically registered via PSR-4 namespace Tvdt\
  • Exclusions: Entity, DependencyInjection, Kernel classes
  • Autowiring and autoconfiguration enabled by default
  • Service definitions in config/services.yaml

Database & Migrations

  • PostgreSQL-based with Doctrine ORM
  • Migrations in migrations/ at project root, namespace DoctrineMigrations (intentionally not autoloaded); generate with bin/console make:migration
  • Test fixtures in src/DataFixtures/ (loaded with --group=test)
  • Test database configured separately via .env.test

Testing Infrastructure

  • PHPUnit 13 with DAMA Doctrine Test Bundle for transaction rollback
  • Bootstrap: tests/bootstrap.php loads env vars and autoloader; tests/symfony-container.php boots the test kernel/container (used by Rector)
  • Symfony test utilities (BrowserKit, CSS selectors) available
  • Coverage excluded from: src/DataFixtures/
  • Test environment: APP_ENV=test (set in phpunit.dist.xml)

Code Style & Standards

  • PHP-CS-Fixer: Symfony ruleset + risky rules enabled
    • Strict types declaration required
    • Trailing commas in multiline structures
    • No else-only blocks
  • Rector: Aggressive modernization with all attribute sets + prepared sets (dead code, code quality, Doctrine, Symfony, PHPUnit)
  • PHPStan: Level 8 with extensions for Doctrine and Symfony
  • Twig-CS-Fixer: Template style enforcement
  • Safe functions: Use thecodingmachine/safe wrappers for standard PHP functions that return false on failure — they throw exceptions instead

Environment Configuration

  • .env - Local development defaults (uncommitted in .env.local)
  • .env.dev - Development overrides
  • .env.test - Test environment configuration
  • Production uses composer dump-env prod for compiled configuration
  • Key variables:
    • APP_ENV - Environment (dev/test/prod)
    • DATABASE_URL - PostgreSQL connection string
    • MAILER_SENDER - From address for emails

Frontend Build

  • Asset mapper (no Node.js/Webpack) for JS/CSS bundling; JS modules declared in importmap.php
  • Stimulus controllers in assets/controllers/, Turbo for SPA-like navigation
  • Sass sources in assets/styles/, compiled via bin/console sass:build
  • Production: Assets precompiled during Docker build
  • Development: Watch mode enabled in FrankenPHP container

CI/CD Pipeline

GitHub Actions workflow (.github/workflows/ci.yml):

  1. Linting: Dockerfile (hadolint), Twig templates
  2. Code Quality:
    • PHP-CS-Fixer style check
    • Twig-CS-Fixer style check
    • PHPStan static analysis
    • Rector dry-run
  3. Integration Tests:
    • Docker image build and start services
    • Database creation and migration
    • Fixture loading
    • Full PHPUnit test suite with JUnit XML output
    • Doctrine schema validation
  4. Build & Deploy (on tags or main, disabled currently):
    • Docker image push to GitHub Container Registry
    • Sentry release creation
    • Portainer webhook trigger for production deployment

Runs on all pushes to main and pull requests. Concurrency cancels old runs on new commits.

Important Files & Conventions

  • Kernel: src/Kernel.php - Symfony kernel class
  • AbstractController: Base class for all controllers — defines route parameter regexes (SEASON_CODE_REGEX, CANDIDATE_HASH_REGEX) and flash helpers
  • Flash Messages: Use FlashType enum instead of string literals
  • QuizSpreadsheetService: Handles importing quizzes from XLSX files
  • Rector container: tests/symfony-container.php — boots a test kernel so Rector can resolve Symfony service types
  • .gitignore: Excludes var/, vendor/, .env.local, .phpunit.cache
  • Dockerfile: Multi-stage build with dev/prod separation, FrankenPHP-based
  • Docker Compose: PHP service with Caddy, PostgreSQL database, persistent volumes

Security & Authorization

  • Doctrine extensions enabled (timestamps, slugs, etc.)
  • Voter-based authorization in src/Security/Voter/
  • User entity with security encoding configured
  • CSRF protection enabled
  • Email verification available via SymfonyCasts bundle

Composer Scripts

Auto-executed scripts on install/update:

  • cache:clear - Symfony cache clear
  • assets:install - Copy public assets
  • importmap:install - JS import map setup

Notes for Future Work

  • The backoffice elimination logic is in Controller/Backoffice/PrepareEliminationController.php
  • Quiz timing logic starts on candidate start click and stops on final answer selection
  • Background music feature noted but not yet implemented (requirements only)
  • Statistics functionality is marked TBD in README