* 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>
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 multipleAnswers. - 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.yamlfor automatic discovery - Main entry point:
config/routes.yaml
Service Container & Dependency Injection
- Services in
src/are automatically registered via PSR-4 namespaceTvdt\ - 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, namespaceDoctrineMigrations(intentionally not autoloaded); generate withbin/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.phploads env vars and autoloader;tests/symfony-container.phpboots 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/safewrappers for standard PHP functions that returnfalseon 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 prodfor compiled configuration - Key variables:
APP_ENV- Environment (dev/test/prod)DATABASE_URL- PostgreSQL connection stringMAILER_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 viabin/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):
- Linting: Dockerfile (hadolint), Twig templates
- Code Quality:
- PHP-CS-Fixer style check
- Twig-CS-Fixer style check
- PHPStan static analysis
- Rector dry-run
- 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
- 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
FlashTypeenum 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 clearassets:install- Copy public assetsimportmap: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