* feat: user settings page (#182) - Settings link in the backoffice nav next to Logout - Language selector (Dutch only, noop save) - Change password form with current-password check - Change email form that re-triggers email confirmation - Resend confirmation email button for unconfirmed addresses - Disabled Download data button with Soon(tm) popover - Delete account with password confirmation modal, removes seasons the user is the sole owner of - Well-known URLs: change-password redirect and security.txt * feat: base security.txt Expires on the container build time The BUILD_TIME build arg is baked into the prod image as an env var and set by CI at image build. security.txt expires one year after the build, so the file goes stale when deployments stop. Dev and test fall back to one year from the request time. * refactor: extract shared controller functionality - AbstractController: authenticatedUser property hook and assertSameSeason() (moved from QuestionBankController) - EmailVerifier::sendDefaultConfirmation() replaces the duplicated confirmation email block in RegistrationController and SettingsController - QuizController: deduplicate candidate-data preparation into buildCandidateData() - Drop manual 422 status handling in QuizQuestionController, QuestionBankController and SettingsController: render() already returns 422 for submitted invalid forms passed as parameters * fix: address PR review comments - Catch UniqueConstraintViolationException when changing email to handle the race between the uniqueness check and the flush - Avoid else-only block in UserRepository::deleteUser - Align duplicate-email translation with the validators domain * fix: apply code-review findings for PR #191 - Invalidate outstanding ResetPasswordRequests after password or email change to close the account-takeover window (tokens otherwise remain valid) - Exclude the current user from email uniqueness check so submitting your own address no longer returns an error - Surface transport failures from sendDefaultConfirmation via a warning flash instead of silently showing success - Move Dockerfile ARG BUILD_TIME/ENV to after all build steps so changing the build timestamp no longer busts the composer/asset cache - Throw in prod when BUILD_TIME is missing (WellKnownController) so the security.txt Expires goes stale as intended when deployments stop; fall back to 'now' only in dev/test
Tijd voor de test
PHP/Symfony application for WIDM-style quiz management. Built with FrankenPHP, PostgreSQL, and Docker.
Disclaimer: This is an unofficial, non-commercial, open-source fan project. It is not affiliated with, endorsed by, or associated with Wie is de Mol? (produced by IDTV, broadcast by AVROTROS/NPO) or De Mol (produced by Woestijnvis, broadcast by Play/De Vijver Media). Wie is de Mol? and De Mol are trademarks of their respective rights holders. No copyright infringement is intended.
Requirements
- Docker
- Just (
brew install just)
Local development
just up # Start PHP + PostgreSQL containers
just migrate # Run pending database migrations
just fixtures # Load dev fixtures (truncates first)
The app is available at https://localhost (self-signed cert — run
just trust-cert on macOS to trust it).
Useful commands
just shell # Shell inside the running PHP container
just shell-run # Shell in a fresh one-off container
just stop # Stop containers (keep volumes)
just down # Stop and remove containers
just clean # Nuclear: remove containers + volumes + generated files
just exec <cmd> # Run any command inside the PHP container
Environment
Copy .env and override locally via .env.local (not committed):
| Variable | Description |
|---|---|
APP_SECRET |
Symfony app secret |
DATABASE_URL |
PostgreSQL DSN (auto-set in Docker) |
SENTRY_DSN |
Sentry error tracking |
DEFAULT_URI |
Base URL for CLI-generated links |
Testing
just test # Full PHPUnit suite
just test tests/Path/To/TestFile.php # Single file
just test --coverage-html var/coverage # HTML coverage report
just reload-tests # Drop/recreate test DB + migrate + test fixtures
Tests use a separate database configured via .env.test. The DAMA
Doctrine bundle wraps each test in a transaction that is rolled back after.
just reload-tests loads the --group=test fixtures; just fixtures
loads the dev group and is unrelated to the test database.
Code quality
All checks run in CI and must pass before merging.
just fix-cs # Auto-fix PHP-CS-Fixer + Twig-CS-Fixer
just phpstan # PHPStan static analysis (level 8)
just rector # Apply Rector modernizations
just rector --dry-run # Preview Rector changes without applying
Database
just migrate # Run pending migrations
just fixtures # Load dev fixtures
bin/console make:migration # Generate a new migration (inside container)
Migrations live in migrations/ (namespace DoctrineMigrations). Test
fixtures are in src/DataFixtures/ loaded with --group=test.
Translations
just translations # Extract/update nl translation strings into translations/
Contributing
- Create a branch from
main— use a prefix likefeat/,fix/, ordocs/. - Open a pull request; CI must pass before merging.
- Install the pre-commit hook (see below) to catch issues before pushing.
Pre-commit hook
A pre-commit hook lives in .githooks/pre-commit. Install it once after cloning:
just install-hooks
On every commit it runs automatically, only on staged files:
| Staged file type | Tools run |
|---|---|
.php |
Rector → PHP-CS-Fixer (auto-fix + re-stage), then PHPStan (blocks on errors) |
.twig |
Twig-CS-Fixer (auto-fix + re-stage) |
| Other (docs, config, …) | Nothing — commit proceeds immediately |
If the PHP container is not running, the hook falls back to
docker compose run --rm so checks still execute. PHPUnit is not
run in the hook; CI covers that.
Deployment
Docker images are published to ghcr.io/marijndoeve/tijdvoordetest
for each tagged release.
First-time setup
- Copy
compose.yamlandcompose.prod.yamlto your server. - Create a
.env.prod.localfile with the required variables (see below). - Start the stack — migrations run automatically on container start:
IMAGE_TAG=latest docker compose -f compose.yaml -f compose.prod.yaml up -d
Updating to a new version
IMAGE_TAG=<tag> docker compose -f compose.yaml -f compose.prod.yaml pull
IMAGE_TAG=<tag> docker compose -f compose.yaml -f compose.prod.yaml up -d
Required environment variables
| Variable | Description |
|---|---|
IMAGE_TAG |
Image tag to run (e.g. 1.2.3 or latest) |
APP_SECRET |
Random secret string for Symfony |
CADDY_MERCURE_JWT_SECRET |
JWT secret for the Mercure hub |
POSTGRES_PASSWORD |
PostgreSQL password |
MAILER_DSN |
Mailer transport DSN |
MAILER_SENDER |
From address for emails |
SENTRY_DSN |
Sentry project DSN (optional) |
The compose.prod.yaml configures Traefik labels for TLS termination at
tijdvoordetest.nl. Adjust the traefik labels in that file if you're
hosting on a different domain or using a different reverse proxy.