7 Commits
python ... php

Author SHA1 Message Date
Marijn Doeve
3192fb6c83 Refactor Base64 encoding/decoding methods and enhance candidate not found flash message 2025-04-02 21:20:44 +02:00
29bc74fe4f Implement flash messages, refactor candidate retrieval, and enhance quiz functionality 2025-03-04 22:20:35 +01:00
26b99a8353 Add second quiz 2025-03-04 20:39:17 +01:00
Marijn Doeve
c155fd6754 Refactor Krtek 2025-03-04 11:09:35 +01:00
451d117d9e WIP 3-3 2025-03-04 08:15:50 +01:00
d337ce86fb Base Symfony Setup 2024-12-29 16:18:38 +01:00
628bfa22f0 Start of Symfony 2024-12-29 14:58:03 +01:00
171 changed files with 14294 additions and 5631 deletions

View File

@@ -1,4 +0,0 @@
/.idea/
/containers/
.gitignore
compose.*

34
.dockerignore Normal file
View File

@@ -0,0 +1,34 @@
**/*.log
**/*.md
**/*.php~
**/*.dist.php
**/*.dist
**/*.cache
**/._*
**/.dockerignore
**/.DS_Store
**/.git/
**/.gitattributes
**/.gitignore
**/.gitmodules
**/compose.*.yaml
**/compose.*.yml
**/compose.yaml
**/compose.yml
**/docker-compose.*.yaml
**/docker-compose.*.yml
**/docker-compose.yaml
**/docker-compose.yml
**/Dockerfile
**/Thumbs.db
.github/
docs/
public/bundles/
tests/
var/
vendor/
.editorconfig
.env.*.local
.env.local
.env.local.php
.env.test

57
.editorconfig Normal file
View File

@@ -0,0 +1,57 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,html,ts,tsx}]
indent_size = 2
[*.json]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.sh]
indent_style = tab
[*.xml.dist]
indent_style = space
indent_size = 4
[*.{yaml,yml}]
trim_trailing_whitespace = false
[.github/workflows/*.yml]
indent_size = 2
[.gitmodules]
indent_style = tab
[.php_cs.dist]
indent_style = space
indent_size = 4
[composer.json]
indent_size = 4
[{compose,docker-compose}.*.{yaml,yml}]
indent_size = 2
[*.*Dockerfile]
indent_style = tab
[*.*Caddyfile]
indent_style = tab

30
.env Normal file
View File

@@ -0,0 +1,30 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###

4
.env.dev Normal file
View File

@@ -0,0 +1,4 @@
#APP_DEBUG=1
###> symfony/framework-bundle ###
APP_SECRET=e26b9552d9e7f969b160373effaa7690
###< symfony/framework-bundle ###

17
.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
* text=auto eol=lf
*.conf text eol=lf
*.html text eol=lf
*.ini text eol=lf
*.js text eol=lf
*.json text eol=lf
*.md text eol=lf
*.php text eol=lf
*.sh text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
bin/console text eol=lf
composer.lock text eol=lf merge=ours
*.ico binary
*.png binary

76
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: CI
on:
push:
branches:
- main
pull_request: ~
workflow_dispatch: ~
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build Docker images
uses: docker/bake-action@v4
with:
pull: true
load: true
files: |
compose.yaml
compose.override.yaml
set: |
*.cache-from=type=gha,scope=${{github.ref}}
*.cache-from=type=gha,scope=refs/heads/main
*.cache-to=type=gha,scope=${{github.ref}},mode=max
-
name: Start services
run: docker compose up --wait --no-build
-
name: Check HTTP reachability
run: curl -v --fail-with-body http://localhost
-
name: Check HTTPS reachability
if: false # Remove this line when the homepage will be configured, or change the path to check
run: curl -vk --fail-with-body https://localhost
-
name: Check Mercure reachability
run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test
-
name: Create test database
if: false # Remove this line if Doctrine ORM is installed
run: docker compose exec -T php bin/console -e test doctrine:database:create
-
name: Run migrations
if: false # Remove this line if Doctrine Migrations is installed
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
-
name: Run PHPUnit
if: false # Remove this line if PHPUnit is installed
run: docker compose exec -T php bin/phpunit
-
name: Doctrine Schema Validator
if: false # Remove this line if Doctrine ORM is installed
run: docker compose exec -T php bin/console -e test doctrine:schema:validate
lint:
name: Docker Lint
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0

222
.gitignore vendored
View File

@@ -1,174 +1,60 @@
compose.override.yaml
/tvdt/staticfiles/
.idea/
### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Python.gitignore
### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Symfony.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Cache and logs (Symfony2)
/app/cache/*
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep
# C extensions
*.so
# Email spool folder
/app/spool/*
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Cache, session files and logs (Symfony3)
/var/cache/*
/var/logs/*
/var/sessions/*
!var/cache/.gitkeep
!var/logs/.gitkeep
!var/sessions/.gitkeep
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Logs (Symfony4)
/var/log/*
!var/log/.gitkeep
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Parameters
/app/config/parameters.yml
/app/config/parameters.ini
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Managed by Composer
/app/bootstrap.php.cache
/var/bootstrap.php.cache
/bin/*
!bin/console
!bin/symfony_requirements
/vendor/
# Translations
*.mo
*.pot
# Assets and user uploads
/web/bundles/
/web/uploads/
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# PHPUnit
/app/phpunit.xml
/phpunit.xml
# Flask stuff:
instance/
.webassets-cache
# Build data
/build/
# Scrapy stuff:
.scrapy
# Composer PHAR
/composer.phar
# Sphinx documentation
docs/_build/
# Backup entities generated with doctrine:generate:entities command
**/Entity/*~
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Embedded web-server pid file
/.web-server-pid
### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/76739a38b56907118c5a880d63250c99d5690a5a/Global/macOS.gitignore
### https://raw.github.com/github/gitignore/6eeebe6f49678aacd8311ce079842c971b3ebe96/Global/macOS.gitignore
# General
.DS_Store
@@ -197,3 +83,25 @@ Icon
Network Trash Folder
Temporary Items
.apdisk
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###

141
.idea/TijdVoorDeTest.iml generated Normal file
View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/migrations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/orm" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/persistence" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/sql-formatter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/evenement/evenement" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fidry/cpu-core-counter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/friendsofphp/php-cs-fixer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpstan" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/child-process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/dns" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/event-loop" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/promise" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/socket" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/stream" />
<excludeFolder url="file://$MODULE_DIR$/vendor/rector/rector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/runtime/frankenphp-symfony" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/staabm/side-effects-detector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dependency-injection" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/flex" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/framework-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/extension-installer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpstan-doctrine" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpstan-phpunit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpstan-symfony" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/form" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-icu" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-core" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-csrf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-http" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/type-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/uid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-profiler-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/safe" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
<excludeFolder url="file://$MODULE_DIR$/vendor/easycorp/easyadmin-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/intl" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/ux-twig-component" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/html-extra" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="bootstrap" level="application" />
</component>
</module>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="app@localhost" uuid="7c4a7798-5e31-4429-bc70-035404e2d83b">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/app</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpStanGlobal" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{bootstrap}" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/TijdVoorDeTest.iml" filepath="$PROJECT_DIR$/.idea/TijdVoorDeTest.iml" />
</modules>
</component>
</project>

14
.idea/php-test-framework.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpTestFrameworkVersionCache">
<tools_cache>
<tool tool_name="PHPUnit">
<cache>
<versions>
<info id="interpreter-c1266788-d465-407a-ac5d-1f67a9cf3e8a" version="11.5.2" />
</versions>
</cache>
</tool>
</tools_cache>
</component>
</project>

268
.idea/php.xml generated Normal file
View File

@@ -0,0 +1,268 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="codingStandard" value="Custom" />
<option name="rulesetPath" value="/app/.php-cs-fixer.dist.php" />
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpCSFixer">
<phpcsfixer_settings>
<phpcs_fixer_by_interpreter interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" tool_path="/app/vendor/bin/php-cs-fixer" timeout="30000" />
<phpcs_fixer_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" standards="DoctrineAnnotation;PER;PER-CS;PER-CS1.0;PER-CS2.0;PHP54Migration;PHP56Migration;PHP70Migration;PHP71Migration;PHP73Migration;PHP74Migration;PHP80Migration;PHP81Migration;PHP82Migration;PHP83Migration;PHP84Migration;PHPUnit100Migration;PHPUnit30Migration;PHPUnit32Migration;PHPUnit35Migration;PHPUnit43Migration;PHPUnit48Migration;PHPUnit50Migration;PHPUnit52Migration;PHPUnit54Migration;PHPUnit55Migration;PHPUnit56Migration;PHPUnit57Migration;PHPUnit60Migration;PHPUnit75Migration;PHPUnit84Migration;PHPUnit91Migration;PSR1;PSR12;PSR2;PhpCsFixer;Symfony" tool_path="/app/vendor/bin/php-cs-fixer" timeout="30000" />
<PhpCSFixerConfiguration deletedFromTheList="true" tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
</phpcsfixer_settings>
</component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpExternalFormatter">
<option name="externalFormatter" value="PHP_CS_FIXER" />
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/rector/rector" />
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
<path value="$PROJECT_DIR$/vendor/react/cache" />
<path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/react/child-process" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
<path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
<path value="$PROJECT_DIR$/vendor/symfony/form" />
<path value="$PROJECT_DIR$/vendor/twig/html-extra" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
</include_path>
</component>
<component name="PhpInterpreters">
<interpreters>
<interpreter id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" name="php" home="docker-compose://DATA" auto="false" debugger_id="php.debugger.XDebug">
<remote_data INTERPRETER_PATH="php" HELPERS_PATH="/opt/.phpstorm_helpers" VALID="true" RUN_AS_ROOT_VIA_SUDO="false" DOCKER_ACCOUNT_NAME="Colima" DOCKER_COMPOSE_SERVICE_NAME="php" DOCKER_REMOTE_PROJECT_PATH="/opt/project">
<type_data command="EXEC" />
<dockerComposeConfigurationPaths>
<item value="$PROJECT_DIR$/compose.yaml" />
<item value="$PROJECT_DIR$/compose.override.yaml" />
</dockerComposeConfigurationPaths>
<envs />
</remote_data>
</interpreter>
</interpreters>
</component>
<component name="PhpInterpretersPhpInfoCache">
<phpInfoCache>
<interpreter name="php">
<phpinfo binary_type="PHP" php_cgi="/usr/local/bin/php-cgi" php_cli="/usr/local/bin/php" path_separator=":" version="8.3.15">
<additional_php_ini>/usr/local/etc/php/conf.d/docker-php-ext-apcu.ini, /usr/local/etc/php/conf.d/docker-php-ext-intl.ini, /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini, /usr/local/etc/php/conf.d/docker-php-ext-pdo_pgsql.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, /usr/local/etc/php/conf.d/docker-php-ext-zip.ini, /usr/local/etc/php/app.conf.d/10-app.ini, /usr/local/etc/php/app.conf.d/20-app.dev.ini</additional_php_ini>
<configuration_file>/usr/local/etc/php/php.ini</configuration_file>
<configuration_options>
<configuration_option name="include_path" value=".:/usr/local/lib/php" />
</configuration_options>
<debuggers>
<debugger_info debugger="xdebug" debugger_version="3.4.0">
<debug_extensions />
</debugger_info>
</debuggers>
<loaded_extensions>
<extension name="Core" />
<extension name="PDO" />
<extension name="Phar" />
<extension name="Reflection" />
<extension name="SPL" />
<extension name="SimpleXML" />
<extension name="Zend OPcache" />
<extension name="apcu" />
<extension name="ctype" />
<extension name="curl" />
<extension name="date" />
<extension name="dom" />
<extension name="fileinfo" />
<extension name="filter" />
<extension name="hash" />
<extension name="iconv" />
<extension name="intl" />
<extension name="json" />
<extension name="libxml" />
<extension name="mbstring" />
<extension name="mysqlnd" />
<extension name="openssl" />
<extension name="pcre" />
<extension name="pdo_pgsql" />
<extension name="pdo_sqlite" />
<extension name="posix" />
<extension name="random" />
<extension name="readline" />
<extension name="session" />
<extension name="sodium" />
<extension name="sqlite3" />
<extension name="standard" />
<extension name="tokenizer" />
<extension name="xdebug" />
<extension name="xml" />
<extension name="xmlreader" />
<extension name="xmlwriter" />
<extension name="zip" />
<extension name="zlib" />
</loaded_extensions>
</phpinfo>
</interpreter>
</phpInfoCache>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.3" />
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" tool_path="/app/vendor/bin/phpstan" timeout="60000" />
<phpstan_by_interpreter asDefaultInterpreter="true" deletedFromTheList="true" interpreter_id="afbe50d9-a569-498c-8dcc-cf68a5d73813" timeout="60000" />
<PhpStanConfiguration deletedFromTheList="true" tool_path="$PROJECT_DIR$/vendor/bin/phpstan" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration">
<option name="config" value="$PROJECT_DIR$/phpstan.dist.neon" />
<option name="level" value="8" />
<option name="transferred" value="true" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<phpunit_by_interpreter interpreter_id="c1266788-d465-407a-ac5d-1f67a9cf3e8a" custom_loader_path="/app/vendor/autoload.php" phpunit_phar_path="" />
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
<component name="Psalm">
<Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="afbe50d9-a569-498c-8dcc-cf68a5d73813" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

6
.idea/symfony2.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Symfony2PluginSettings">
<option name="pluginEnabled" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

30
.php-cs-fixer.dist.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude('var')
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'declare_strict_types' => true,
'linebreak_after_opening_tag' => true,
'mb_str_functions' => true,
'no_php4_constructor' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'php_unit_strict' => true,
'phpdoc_order' => true,
'strict_comparison' => true,
'strict_param' => true,
'blank_line_between_import_groups' => false,
'phpdoc_line_span' => ['const' => 'single', 'method' => 'single', 'property' => 'single'],
])
->setRiskyAllowed(true)
->setFinder($finder)
;

97
Dockerfile Normal file
View File

@@ -0,0 +1,97 @@
#syntax=docker/dockerfile:1
# Versions
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
# The different stages of this Dockerfile are meant to be built into separate images
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
# https://docs.docker.com/compose/compose-file/#target
# Base FrankenPHP image
FROM frankenphp_upstream AS frankenphp_base
WORKDIR /app
VOLUME /app/var/
# persistent / runtime deps
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
acl \
file \
gettext \
git \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
install-php-extensions \
@composer \
apcu \
intl \
opcache \
zip \
;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
###> recipes ###
###> doctrine/doctrine-bundle ###
RUN install-php-extensions pdo_pgsql
###< doctrine/doctrine-bundle ###
###< recipes ###
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
COPY --link frankenphp/Caddyfile /etc/caddy/Caddyfile
ENTRYPOINT ["docker-entrypoint"]
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile" ]
# Dev FrankenPHP image
FROM frankenphp_base AS frankenphp_dev
ENV APP_ENV=dev XDEBUG_MODE=off
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
RUN set -eux; \
install-php-extensions \
xdebug \
;
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
# Prod FrankenPHP image
FROM frankenphp_base AS frankenphp_prod
ENV APP_ENV=prod
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
# prevent the reinstallation of vendors at every changes in the source code
COPY --link composer.* symfony.* ./
RUN set -eux; \
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
# copy sources
COPY --link . ./
RUN rm -Rf frankenphp/
RUN set -eux; \
mkdir -p var/cache var/log; \
composer dump-autoload --classmap-authoritative --no-dev; \
composer dump-env prod; \
composer run-script --no-dev post-install-cmd; \
chmod +x bin/console; sync;

View File

@@ -1,59 +1,16 @@
DOCKER_EXEC=docker compose exec app
.DEFAULT_GOAL := help
.PHONY: up
up: ## Start application
@docker compose up -d
stop: ## Stop application
@docker compose stop
.PHONY: shell
shell: ## Start a shell inside the container
@docker compose exec php bash
.PHONY: help
help:
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.PHONY: init
init: install up migrate fixtures ## setup the application from scratch
.PHONY: up
up: ## Starts the app
@docker compose up -d --force-recreate
.PHONY: down
down: ## Stops the app
@docker compose down --remove-orphans
.PHONY: build
build: ## (Re)build the containers
@echo ✨ Building container
@docker compose build
.PHONY: shell
shell: ## Opens a shell in the app container
@${DOCKER_EXEC} bash
.PHONY: migrate
migrate: ## Migrate the database to the latest version
@echo ✨ Appling migrations
@${DOCKER_EXEC} python manage.py migrate
.PHONY: compilemessages
messages: ## Compile translations
@echo ✨ Finding translations
@${DOCKER_EXEC} python manage.py makemessages -l nl
@echo ✨ Compiling translations
@${DOCKER_EXEC} python manage.py compilemessages --ignore .venv
.PHONY: install
install: ## Install dependencies in container
@echo ✨ Installing dependencies
@docker compose run --rm --entrypoint="" app poetry install --without prod --sync
.PHONY: fixtures
fixtures:
@echo ✨ Loading fixtures
@${DOCKER_EXEC} python manage.py loaddata krtek
.PHONY: _clean
_clean:
@echo ✨ Stopping containers
@docker compose down -v
@echo ✨ Removing compiled files
@rm -f tvdt/*/locale/*/LC_MESSAGES/django.mo tvdt/locale/*/LC_MESSAGES/django.mo
.PHONY: clean
clean: _clean init
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}'

21
bin/console Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

30
compose.override.yaml Normal file
View File

@@ -0,0 +1,30 @@
# Development environment override
services:
php:
build:
context: .
target: frankenphp_dev
volumes:
- ./:/app
- ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro
- ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
# If you develop on Mac or Windows you can remove the vendor/ directory
# from the bind-mount for better performance by enabling the next line:
#- /app/vendor
environment:
MERCURE_EXTRA_DIRECTIVES: demo
# See https://xdebug.org/docs/all_settings#mode
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
extra_hosts:
# Ensure that host.docker.internal is correctly defined on Linux
- host.docker.internal:host-gateway
tty: true
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432:5432"
###< doctrine/doctrine-bundle ###

View File

@@ -1,5 +1,10 @@
# Production environment override
services:
app:
image: tvdt/app:prod
php:
build:
dockerfile: containers/python/Containerfile.prod
context: .
target: frankenphp_prod
environment:
APP_SECRET: ${APP_SECRET}
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}

View File

@@ -1,25 +1,66 @@
services:
app:
build:
dockerfile: containers/python/Containerfile.dev
ports:
- "8000:8000"
php:
image: ${IMAGES_PREFIX:-}app-php
restart: unless-stopped
environment:
DATABASE_URL: postgres://tvdt:tvdt@db:5432/tvdt
DEBUG: true
depends_on:
- db
db:
image: postgres:17.2
environment:
POSTGRES_PASSWORD: tvdt
POSTGRES_USER: tvdt
POSTGRES_DB: tvdt
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# The two next lines can be removed after initial installation
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
STABILITY: ${STABILITY:-stable}
volumes:
- data:/var/lib/postgresql/data
- caddy_data:/data
- caddy_config:/config
ports:
- "5432:5432"
# HTTP
- target: 80
published: ${HTTP_PORT:-80}
protocol: tcp
# HTTPS
- target: 443
published: ${HTTPS_PORT:-443}
protocol: tcp
# HTTP/3
- target: 443
published: ${HTTP3_PORT:-443}
protocol: udp
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-16}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
data:
caddy_data:
caddy_config:
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

98
composer.json Normal file
View File

@@ -0,0 +1,98 @@
{
"name": "symfony/skeleton",
"type": "project",
"license": "MIT",
"description": "A minimal Symfony project recommended to create bare bones applications",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.3.15",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3",
"easycorp/easyadmin-bundle": "^4.23",
"runtime/frankenphp-symfony": "^0.2.0",
"symfony/asset": "7.2.*",
"symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*",
"symfony/flex": "^2.4.7",
"symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*",
"symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/uid": "7.2.*",
"symfony/yaml": "7.2.*",
"thecodingmachine/safe": "^2.5"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.0",
"friendsofphp/php-cs-fixer": "^3.65",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-doctrine": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-symfony": "^2.0",
"phpunit/phpunit": "^11",
"rector/rector": "^2.0",
"symfony/maker-bundle": "^1.62.1",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*",
"thecodingmachine/phpstan-safe-rule": "^1.3"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"phpstan/extension-installer": true,
"symfony/flex": true,
"symfony/runtime": true
},
"bump-after-update": true,
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.2.*",
"docker": true
}
}
}

9931
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
config/bundles.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
];

11
config/packages/csrf.yaml Normal file
View File

@@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View File

@@ -0,0 +1,52 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View File

@@ -0,0 +1,15 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View File

@@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View File

@@ -0,0 +1,52 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View File

@@ -0,0 +1,7 @@
framework:
default_locale: nl
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers:

View File

@@ -0,0 +1,7 @@
twig:
file_name_pattern: '*.twig'
form_themes: [ 'bootstrap_5_layout.html.twig' ]
when@test:
twig:
strict_variables: true

View File

@@ -0,0 +1,5 @@
twig_component:
anonymous_template_directory: 'components/'
defaults:
# Namespace & directory for components
App\Twig\Components\: 'components/'

4
config/packages/uid.yaml Normal file
View File

@@ -0,0 +1,4 @@
framework:
uid:
default_uuid_version: 7
time_based_uuid_version: 7

View File

@@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

View File

@@ -0,0 +1,17 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
only_exceptions: false
collect_serializer_data: true
when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

7
config/preload.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

5
config/routes.yaml Normal file
View File

@@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View File

@@ -0,0 +1,9 @@
controllers:
resource:
path: ../../src/Controller/
namespace: App\Controller
type: attribute
kernel:
resource: App\Kernel
type: attribute

View File

@@ -0,0 +1,3 @@
easyadmin:
resource: .
type: easyadmin.routes

View File

@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View File

@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

View File

@@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

24
config/services.yaml Normal file
View File

@@ -0,0 +1,24 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@@ -1,16 +0,0 @@
FROM python:3.13 AS dev
RUN apt-get update && apt-get install -y \
gettext
RUN pip install poetry~=1.8
WORKDIR /app
ENV POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
ENTRYPOINT ["python","manage.py", "runserver", "0.0.0.0:8000"]

View File

@@ -1,36 +0,0 @@
FROM python:3.13 AS builder
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
gettext
RUN pip install --no-cache-dir poetry==1.8
WORKDIR /app
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
COPY tvdt/pyproject.toml tvdt/poetry.lock ./
RUN poetry install --without dev
COPY ./tvdt/ .
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
RUN python manage.py compilemessages --ignore .venv \
&& python manage.py collectstatic
FROM python:3.13 AS runtime
WORKDIR /app
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
COPY --from=builder /app /app
ENTRYPOINT ["gunicorn", "-b", "0.0.0.0:8000", "tvdt.wsgi", ""]

57
frankenphp/Caddyfile Normal file
View File

@@ -0,0 +1,57 @@
{
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
log {
{$CADDY_SERVER_LOG_OPTIONS}
# Redact the authorization query parameter that can be set by Mercure
format filter {
request>uri query {
replace authorization REDACTED
}
}
}
root /app/public
encode zstd br gzip
mercure {
# Transport to use (default to Bolt)
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
# Publisher JWT key
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# Subscriber JWT key
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
# Allow anonymous subscribers (double-check that it's what you want)
anonymous
# Enable the subscription API (double-check that it's what you want)
subscriptions
# Extra directives
{$MERCURE_EXTRA_DIRECTIVES}
}
vulcain
{$CADDY_SERVER_EXTRA_DIRECTIVES}
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-topics=()"
@phpRoute {
not path /.well-known/mercure*
not file {path}
}
rewrite @phpRoute index.php
@frontController path index.php
php @frontController
file_server
}

View File

@@ -0,0 +1,13 @@
expose_php = 0
date.timezone = Europe/Amsterdam
apc.enable_cli = 1
session.use_strict_mode = 1
zend.detect_unicode = 0
; https://symfony.com/doc/current/performance.html
realpath_cache_size = 4096K
realpath_cache_ttl = 600
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.memory_consumption = 256
opcache.enable_file_override = 1

View File

@@ -0,0 +1,6 @@
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
; See https://github.com/docker/for-linux/issues/264
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
; Add `start_with_request=yes` to start debug session on each request
xdebug.client_host = host.docker.internal
memory_limit = 2048M

View File

@@ -0,0 +1,2 @@
opcache.preload_user = root
opcache.preload = /app/config/preload.php

42
frankenphp/docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/sh
set -e
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
composer install --prefer-dist --no-progress --no-interaction
fi
if grep -q ^DATABASE_URL= .env; then
echo 'Waiting for database to be ready...'
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
if [ $? -eq 255 ]; then
# If the Doctrine command exits with 255, an unrecoverable error occurred
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
break
fi
sleep 1
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
done
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
echo 'The database is not up or not reachable:'
echo "$DATABASE_ERROR"
exit 1
else
echo 'The database is now ready and reachable'
fi
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
fi
fi
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
echo 'PHP app ready!'
fi
exec docker-php-entrypoint "$@"

View File

@@ -0,0 +1,4 @@
worker {
file ./public/index.php
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241229195702 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE answer (id UUID NOT NULL, question_id UUID NOT NULL, text VARCHAR(255) NOT NULL, is_right_answer BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_DADD4A251E27F6BF ON answer (question_id)');
$this->addSql('COMMENT ON COLUMN answer.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN answer.question_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE answer_candidate (answer_id UUID NOT NULL, candidate_id UUID NOT NULL, PRIMARY KEY(answer_id, candidate_id))');
$this->addSql('CREATE INDEX IDX_F54D5192AA334807 ON answer_candidate (answer_id)');
$this->addSql('CREATE INDEX IDX_F54D519291BD8781 ON answer_candidate (candidate_id)');
$this->addSql('COMMENT ON COLUMN answer_candidate.answer_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN answer_candidate.candidate_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE candidate (id UUID NOT NULL, season_id UUID NOT NULL, name VARCHAR(16) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_C8B28E444EC001D1 ON candidate (season_id)');
$this->addSql('COMMENT ON COLUMN candidate.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN candidate.season_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE given_answer (id UUID NOT NULL, candidate_id UUID NOT NULL, quiz_id UUID NOT NULL, answer_id UUID NOT NULL, created TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_9AC61A3091BD8781 ON given_answer (candidate_id)');
$this->addSql('CREATE INDEX IDX_9AC61A30853CD175 ON given_answer (quiz_id)');
$this->addSql('CREATE INDEX IDX_9AC61A30AA334807 ON given_answer (answer_id)');
$this->addSql('COMMENT ON COLUMN given_answer.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN given_answer.candidate_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN given_answer.quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN given_answer.answer_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE question (id UUID NOT NULL, quiz_id UUID NOT NULL, question VARCHAR(255) NOT NULL, enabled BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_B6F7494E853CD175 ON question (quiz_id)');
$this->addSql('COMMENT ON COLUMN question.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN question.quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE quiz (id UUID NOT NULL, season_id UUID NOT NULL, name VARCHAR(64) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_A412FA924EC001D1 ON quiz (season_id)');
$this->addSql('COMMENT ON COLUMN quiz.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN quiz.season_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE season (id UUID NOT NULL, name VARCHAR(64) NOT NULL, season_code VARCHAR(5) NOT NULL, preregister_candidates BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN season.id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('COMMENT ON COLUMN "user".id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE answer ADD CONSTRAINT FK_DADD4A251E27F6BF FOREIGN KEY (question_id) REFERENCES question (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE answer_candidate ADD CONSTRAINT FK_F54D5192AA334807 FOREIGN KEY (answer_id) REFERENCES answer (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE answer_candidate ADD CONSTRAINT FK_F54D519291BD8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE candidate ADD CONSTRAINT FK_C8B28E444EC001D1 FOREIGN KEY (season_id) REFERENCES season (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE given_answer ADD CONSTRAINT FK_9AC61A3091BD8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE given_answer ADD CONSTRAINT FK_9AC61A30853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE given_answer ADD CONSTRAINT FK_9AC61A30AA334807 FOREIGN KEY (answer_id) REFERENCES answer (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494E853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE quiz ADD CONSTRAINT FK_A412FA924EC001D1 FOREIGN KEY (season_id) REFERENCES season (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE answer DROP CONSTRAINT FK_DADD4A251E27F6BF');
$this->addSql('ALTER TABLE answer_candidate DROP CONSTRAINT FK_F54D5192AA334807');
$this->addSql('ALTER TABLE answer_candidate DROP CONSTRAINT FK_F54D519291BD8781');
$this->addSql('ALTER TABLE candidate DROP CONSTRAINT FK_C8B28E444EC001D1');
$this->addSql('ALTER TABLE given_answer DROP CONSTRAINT FK_9AC61A3091BD8781');
$this->addSql('ALTER TABLE given_answer DROP CONSTRAINT FK_9AC61A30853CD175');
$this->addSql('ALTER TABLE given_answer DROP CONSTRAINT FK_9AC61A30AA334807');
$this->addSql('ALTER TABLE question DROP CONSTRAINT FK_B6F7494E853CD175');
$this->addSql('ALTER TABLE quiz DROP CONSTRAINT FK_A412FA924EC001D1');
$this->addSql('DROP TABLE answer');
$this->addSql('DROP TABLE answer_candidate');
$this->addSql('DROP TABLE candidate');
$this->addSql('DROP TABLE given_answer');
$this->addSql('DROP TABLE question');
$this->addSql('DROP TABLE quiz');
$this->addSql('DROP TABLE season');
$this->addSql('DROP TABLE "user"');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241229201314 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE season_user (season_id UUID NOT NULL, user_id UUID NOT NULL, PRIMARY KEY(season_id, user_id))');
$this->addSql('CREATE INDEX IDX_BDA4AD74EC001D1 ON season_user (season_id)');
$this->addSql('CREATE INDEX IDX_BDA4AD7A76ED395 ON season_user (user_id)');
$this->addSql('COMMENT ON COLUMN season_user.season_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN season_user.user_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE season_user ADD CONSTRAINT FK_BDA4AD74EC001D1 FOREIGN KEY (season_id) REFERENCES season (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE season_user ADD CONSTRAINT FK_BDA4AD7A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE season_user DROP CONSTRAINT FK_BDA4AD74EC001D1');
$this->addSql('ALTER TABLE season_user DROP CONSTRAINT FK_BDA4AD7A76ED395');
$this->addSql('DROP TABLE season_user');
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241229202103 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE correction (id UUID NOT NULL, candidate_id UUID NOT NULL, quiz_id UUID NOT NULL, amount DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_A29DA1B891BD8781 ON correction (candidate_id)');
$this->addSql('CREATE INDEX IDX_A29DA1B8853CD175 ON correction (quiz_id)');
$this->addSql('COMMENT ON COLUMN correction.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN correction.candidate_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN correction.quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE correction ADD CONSTRAINT FK_A29DA1B891BD8781 FOREIGN KEY (candidate_id) REFERENCES candidate (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE correction ADD CONSTRAINT FK_A29DA1B8853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE correction DROP CONSTRAINT FK_A29DA1B891BD8781');
$this->addSql('ALTER TABLE correction DROP CONSTRAINT FK_A29DA1B8853CD175');
$this->addSql('DROP TABLE correction');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241229202155 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE UNIQUE INDEX UNIQ_A29DA1B891BD8781853CD175 ON correction (candidate_id, quiz_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP INDEX UNIQ_A29DA1B891BD8781853CD175');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241229204335 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE given_answer ALTER answer_id DROP NOT NULL');
$this->addSql('ALTER TABLE given_answer ALTER created DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE given_answer ALTER answer_id SET NOT NULL');
$this->addSql('ALTER TABLE given_answer ALTER created SET NOT NULL');
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250303221227 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE given_answer ALTER created SET NOT NULL');
$this->addSql('ALTER TABLE season ADD active_quiz_id UUID DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN season.active_quiz_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE season ADD CONSTRAINT FK_F0E45BA96706D6B FOREIGN KEY (active_quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_F0E45BA96706D6B ON season (active_quiz_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE season DROP CONSTRAINT FK_F0E45BA96706D6B');
$this->addSql('DROP INDEX IDX_F0E45BA96706D6B');
$this->addSql('ALTER TABLE season DROP active_quiz_id');
$this->addSql('ALTER TABLE given_answer ALTER created DROP NOT NULL');
}
}

8
phpstan.dist.neon Normal file
View File

@@ -0,0 +1,8 @@
parameters:
level: 8
paths:
- bin/
- config/
- public/
- src/
- tests/

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

14
public/index.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static function (array $context): Kernel {
$appEnv = !empty($context['APP_ENV']) ? (string) $context['APP_ENV'] : 'prod';
$appDebug = !empty($context['APP_DEBUG']) ? filter_var($context['APP_DEBUG'], \FILTER_VALIDATE_BOOL) : 'prod' !== $appEnv;
return new Kernel($appEnv, $appDebug);
};

31
rector.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__.'/config',
__DIR__.'/public',
__DIR__.'/src',
__DIR__.'/tests',
])
->withPhpSets()
->withPreparedSets(
deadCode: true,
codeQuality: true,
codingStyle: true,
typeDeclarations: true,
privatization: true,
instanceOf: true,
earlyReturn: true,
strictBooleans: true,
rectorPreset: true,
phpunitCodeQuality: true,
doctrineCodeQuality: true,
symfonyCodeQuality: true,
)
->withComposerBased(twig: true, doctrine: true, phpunit: true)
->withAttributesSets()
;

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Answer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class AnswerCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Answer::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Candidate;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class CandidateCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Candidate::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Correction;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class CorrectionCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Correction::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\Correction;
use App\Entity\GivenAnswer;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
public function index(): Response
{
// return parent::index();
// Option 1. You can make your dashboard redirect to some common page of your backend
//
$adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
return $this->redirect($adminUrlGenerator->setController(SeasonCrudController::class)->generateUrl());
// Option 2. You can make your dashboard redirect to different pages depending on the user
//
// if ('jane' === $this->getUser()->getUsername()) {
// return $this->redirect('...');
// }
// Option 3. You can render some custom template to display a proper dashboard with widgets, etc.
// (tip: it's easier if your template extends from @EasyAdmin/page/content.html.twig)
//
// return $this->render('some/path/my-dashboard.html.twig');
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('TijdVoorDeTest');
}
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('Season', 'fas fa-list', Season::class);
yield MenuItem::linkToCrud('Quiz', 'fas fa-list', Quiz::class);
yield MenuItem::linkToCrud('Question', 'fas fa-list', Question::class);
yield MenuItem::linkToCrud('Candidate', 'fas fa-list', Candidate::class);
yield MenuItem::linkToCrud('Correction', 'fas fa-list', Correction::class);
yield MenuItem::linkToCrud('User', 'fas fa-list', User::class);
yield MenuItem::linkToCrud('Given Answer', 'fas fa-list', GivenAnswer::class);
yield MenuItem::linkToCrud('Answer', 'fas fa-list', Answer::class);
yield MenuItem::linkToLogout('Logout', 'fa fa-exit');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\GivenAnswer;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class GivenAnswerCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return GivenAnswer::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Question;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class QuestionCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Question::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Quiz;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class QuizCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Quiz::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Season;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class SeasonCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Season::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class UserCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return User::class;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Candidate;
use App\Entity\Season;
use App\Enum\FlashType;
use App\Form\EnterNameType;
use App\Form\SelectSeasonType;
use App\Helpers\Base64;
use App\Repository\CandidateRepository;
use App\Repository\QuestionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
#[AsController]
class QuizController extends AbstractController
{
public const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
private const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
#[Route(path: '/', name: 'select_season', methods: ['GET', 'POST'])]
public function selectSeason(Request $request): Response
{
$form = $this->createForm(SelectSeasonType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
return $this->redirectToRoute('enter_name', ['seasonCode' => $data['season_code']]);
}
return $this->render('quiz/select_season.html.twig', ['form' => $form]);
}
#[Route(path: '/{seasonCode}', name: 'enter_name', requirements: ['seasonCode' => self::SEASON_CODE_REGEX])]
public function enterName(
Request $request,
Season $season,
): Response {
$form = $this->createForm(EnterNameType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$name = $data['name'];
return $this->redirectToRoute('quiz_page', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => Base64::base64_url_encode($name)]);
}
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
}
#[Route(
path: '/{seasonCode}/{nameHash}',
name: 'quiz_page',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
)]
public function quizPage(
Season $season,
string $nameHash,
CandidateRepository $candidateRepository,
QuestionRepository $questionRepository,
): Response {
$candidate = $candidateRepository->getCandidateByHash($season, $nameHash);
if (!$candidate instanceof Candidate) {
if ($season->isPreregisterCandidates() === false) {
// create candidate
}
$this->addFlash(FlashType::Danger->value, "Candidate {${Base64::base64_url_decode($nameHash)}} not found");
return $this->redirectToRoute('enter_name', ['seasonCode' => $season->getSeasonCode()]);
}
$question = $questionRepository->findNextQuestionForCandidate($candidate);
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
}
}

View File

@@ -0,0 +1,343 @@
<?php
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Answer;
use App\Entity\Candidate;
use App\Entity\Question;
use App\Entity\Quiz;
use App\Entity\Season;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class KrtekFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$season = new Season();
$manager->persist($season);
$season->setName('Krtek Weekend')
->setSeasonCode('12345')
->setPreregisterCandidates(true)
->addCandidate(new Candidate('Claudia'))
->addCandidate(new Candidate('Eelco'))
->addCandidate(new Candidate('Elise'))
->addCandidate(new Candidate('Gert-Jan'))
->addCandidate(new Candidate('Iris'))
->addCandidate(new Candidate('Jari'))
->addCandidate(new Candidate('Lara'))
->addCandidate(new Candidate('Lotte'))
->addCandidate(new Candidate('Myrthe'))
->addCandidate(new Candidate('Philine'))
->addCandidate(new Candidate('Remy'))
->addCandidate(new Candidate('Robbert'))
->addCandidate(new Candidate('Tom'));
$quiz1 = $this->createQuiz1($season);
$season->addQuiz($quiz1)
->setActiveQuiz($quiz1)
->addQuiz($this->createQuiz2($season));
$manager->flush();
}
private function createQuiz1(Season $season): Quiz
{
return (new Quiz())
->setName('Quiz 1')
->setSeason($season)
->addQuestion((new Question())
->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Vrouw', true))
->addAnswer(new Answer('Man'))
)
->addQuestion((new Question())
->setQuestion('Hoeveel broers heeft de Krtek?')
->addAnswer(new Answer('Geen', true))
->addAnswer(new Answer('1'))
->addAnswer(new Answer('2'))
)
->addQuestion((new Question())
->setQuestion('Wat is de lievelingsfeestdag van de Krtek?')
->addAnswer(new Answer('Geen'))
->addAnswer(new Answer('Diens eigen verjaardag'))
->addAnswer(new Answer('Koningsdag'))
->addAnswer(new Answer('Kerst', true))
->addAnswer(new Answer('Oud en Nieuw'))
)
->addQuestion((new Question())
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
->addAnswer(new Answer('Met het OV', true))
->addAnswer(new Answer('Met de auto'))
)
->addQuestion((new Question())
->setQuestion('Met wie keek de Kretek video bij binnenkomst?')
->addAnswer(new Answer('Claudia'))
->addAnswer(new Answer('Eelco'))
->addAnswer(new Answer('Elise'))
->addAnswer(new Answer('Gert-Jan'))
->addAnswer(new Answer('Iris'))
->addAnswer(new Answer('Jari'))
->addAnswer(new Answer('Lara'))
->addAnswer(new Answer('Lotte'))
->addAnswer(new Answer('Myrthe'))
->addAnswer(new Answer('Philine'))
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom', true))
)
->addQuestion((new Question())
->setQuestion('Welk advies zou de Krtek zichzelf als kind geven?')
->addAnswer(new Answer('Geef je vader een knuffel.'))
->addAnswer(new Answer('Trek je wat minder aan van anderen.'))
->addAnswer(new Answer('Luister meer naar je eigen gevoel in plaats van naar wat anderen vinden.'))
->addAnswer(new Answer('Stel niet alles tot het laatste moment uit.'))
->addAnswer(new Answer('Altijd doorgaan.'))
->addAnswer(new Answer('Probeer ook eens buiten de lijntjes te kleuren', true))
->addAnswer(new Answer('Ga als je groot bent op groepsreis! '))
->addAnswer(new Answer('Trek minder aan van de mening van anderen, het is oké om anders te zijn.'))
)
->addQuestion((new Question())
->setQuestion('Wat voor soort schoenen droeg de Krtek bij het diner?')
->addAnswer(new Answer('Sneakers'))
->addAnswer(new Answer('Wandel-/bergschoenen', true))
->addAnswer(new Answer('Lederen schoenen'))
->addAnswer(new Answer('Pantoffels'))
->addAnswer(new Answer('Hakken'))
->addAnswer(new Answer('Geen schoenen, alleen sokken'))
)
->addQuestion((new Question())
->setQuestion('Met welk vervoersmiddel reist de Krtek het liefste?')
->addAnswer(new Answer('Fiets', true))
->addAnswer(new Answer('Auto'))
->addAnswer(new Answer('Trein'))
)
->addQuestion((new Question())
->setQuestion('Heeft de Krtek een eigen auto?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
)
->addQuestion((new Question())
->setQuestion('Van wie is de quote die de Krtek gepakt heeft')
->addAnswer(new Answer('Karen'))
->addAnswer(new Answer('Gilles de Coster'))
->addAnswer(new Answer('Kees Tol'))
->addAnswer(new Answer('Harry en John'))
->addAnswer(new Answer('Georgina Verbaan'))
->addAnswer(new Answer('Marc-Marie Huijbregts'))
->addAnswer(new Answer('Fresia Cousiño Arias, Rik van de Westelaken'))
->addAnswer(new Answer('Ellie Lust'))
->addAnswer(new Answer('Bouba'))
->addAnswer(new Answer('Jan Versteegh'))
->addAnswer(new Answer('Dick Jol'))
->addAnswer(new Answer('Karin de Groot'))
->addAnswer(new Answer('Pieter'))
->addAnswer(new Answer('Renée Fokker'))
->addAnswer(new Answer('Sam, Davy', true))
)
->addQuestion((new Question())
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topitos uit iemands rugzak stelen om te kunnen winnen?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
)
->addQuestion((new Question())
->setQuestion('In wat voor bed slaapt de Krtek dit weekend?')
->addAnswer(new Answer('Éénpersoons, losstaand bed'))
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
->addAnswer(new Answer('Tweepersoons bed'))
)
->addQuestion((new Question())
->setQuestion('Hoeveel jaar heeft de Krtek gedaan over de middelbare school?')
->addAnswer(new Answer('5'))
->addAnswer(new Answer('6', true))
->addAnswer(new Answer('7'))
->addAnswer(new Answer('8'))
)
->addQuestion((new Question())
->setQuestion('Waar zat de Krtek aan tafel bij het diner?')
->addAnswer(new Answer('Met de rug naar de accommodatie'))
->addAnswer(new Answer('Met de rug naar de buitenmuur', true))
)
->addQuestion((new Question())
->setQuestion('Wie is de Krtek?')
->addAnswer(new Answer('Claudia', true))
->addAnswer(new Answer('Eelco'))
->addAnswer(new Answer('Elise'))
->addAnswer(new Answer('Gert-Jan'))
->addAnswer(new Answer('Iris'))
->addAnswer(new Answer('Jari'))
->addAnswer(new Answer('Lara'))
->addAnswer(new Answer('Lotte'))
->addAnswer(new Answer('Myrthe'))
->addAnswer(new Answer('Philine'))
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom'))
)
;
}
private function createQuiz2(Season $season): Quiz
{
return (new Quiz())
->setName('Quiz 2')
->setSeason($season)
->addQuestion((new Question())
->setQuestion('Is de Krtek een man of een vrouw?')
->addAnswer(new Answer('Man'))
->addAnswer(new Answer('Vrouw', true))
)
->addQuestion((new Question())
->setQuestion('Heeft de Krtek dieetwensen of allergieën?')
->addAnswer(new Answer('nee'))
->addAnswer(new Answer('De Krtek is vegetariër', true))
->addAnswer(new Answer('De Krtek is flexitariër'))
->addAnswer(new Answer('De Krtek heeft een allergie'))
->addAnswer(new Answer('De Krtek heeft een intolerantie'))
->addAnswer(new Answer('De Krtek eet geen rundvlees'))
->addAnswer(new Answer('De Krtek eet geen waterdieren'))
)
->addQuestion((new Question())
->setQuestion('Hoe heet het huisdier/de huisdieren van de Krtek?')
->addAnswer(new Answer('Amy, Karel en Floyd'))
->addAnswer(new Answer('Flip en Majoor'))
->addAnswer(new Answer('Benji'))
->addAnswer(new Answer('Sini'))
->addAnswer(new Answer('Tom'))
->addAnswer(new Answer('De huisdieren van de Krtek hebben geen naam'))
->addAnswer(new Answer('De Krtek heeft geen huisdieren', true))
)
->addQuestion((new Question())
->setQuestion('Wat dronk de Krtek deze ochtend bij het ontbijt?')
->addAnswer(new Answer('Koffie'))
->addAnswer(new Answer('Thee'))
->addAnswer(new Answer('Water', true))
->addAnswer(new Answer('Melk'))
->addAnswer(new Answer('Sap'))
->addAnswer(new Answer('Niks'))
)
->addQuestion((new Question())
->setQuestion('Waar ging de eerste vakantie die de Krtek zich nog herinnert heen?')
->addAnswer(new Answer('Denemarken'))
->addAnswer(new Answer('Drenthe'))
->addAnswer(new Answer('Mallorca'))
->addAnswer(new Answer('Marokko'))
->addAnswer(new Answer('Oostenrijk'))
->addAnswer(new Answer('Turkije'))
->addAnswer(new Answer('Zweden', true))
)
->addQuestion((new Question())
->setQuestion('Met welk groepje ging de Krtek als eerste het Douanespel in?')
->addAnswer(new Answer('Het eerste groepje', true))
->addAnswer(new Answer('Het tweede groepje'))
->addAnswer(new Answer('Het derde groepje'))
->addAnswer(new Answer('Het vierde groepje'))
->addAnswer(new Answer('Het vijfde groepje'))
)
->addQuestion((new Question())
->setQuestion('Gelooft de Krtek ergens in?')
->addAnswer(new Answer('Nee'))
->addAnswer(new Answer('Het universum', true))
->addAnswer(new Answer('Toeval'))
->addAnswer(new Answer('De Krtek is hindoeïstisch'))
)
->addQuestion((new Question())
->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?')
->addAnswer(new Answer('Ja', true))
->addAnswer(new Answer('Nee'))
)
->addQuestion((new Question())
->setQuestion('Hoe laat ging de Krtek gisteravond naar bed?')
->addAnswer(new Answer('Tussen 0:00 en 0:59 uur'))
->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true))
->addAnswer(new Answer('Tussen 2:00 en 2:59 uur'))
->addAnswer(new Answer('Na 3:00'))
)
->addQuestion((new Question())
->setQuestion('Hoeveel batterijen heeft de Krtek naar het bord gebracht bij het douanespel?')
->addAnswer(new Answer('1'))
->addAnswer(new Answer('2'))
->addAnswer(new Answer('3'))
->addAnswer(new Answer('geen', true))
)
->addQuestion((new Question())
->setQuestion('Wat keek de Krtek als kind graag op TV?')
->addAnswer(new Answer('Digimon', true))
->addAnswer(new Answer('Floris'))
->addAnswer(new Answer('Het huis Anubis'))
->addAnswer(new Answer('Sesamstraat'))
->addAnswer(new Answer('Spongebob Squarepants'))
->addAnswer(new Answer('Teletubbies'))
)
->addQuestion((new Question())
->setQuestion('Waarin zat op de heenreis de bagage van de Krtek (voornamelijk)?')
->addAnswer(new Answer('In koffer(s)', true))
->addAnswer(new Answer('In losse tas(sen)'))
->addAnswer(new Answer('In een rugzak'))
)
->addQuestion((new Question())
->setQuestion('Van welk geluid gaan de haren van de Krtek overeind staan?')
->addAnswer(new Answer('Een vork die door een metalen pan krast '))
->addAnswer(new Answer('Smakkende mensen'))
->addAnswer(new Answer('Een vork die over een bord schraapt'))
->addAnswer(new Answer('Schuren met schuurpapier'))
->addAnswer(new Answer('Nagels op een krijtbord'))
->addAnswer(new Answer('Servies dat tegen elkaar klettert'))
->addAnswer(new Answer('Het geroekoe van een duif', true))
->addAnswer(new Answer('Piepschuim'))
)
->addQuestion((new Question())
->setQuestion('Wilde de Krtek penningmeester worden?')
->addAnswer(new Answer('Ja'))
->addAnswer(new Answer('Nee', true))
)
->addQuestion((new Question())
->setQuestion('Wie is de Krtek?')
->addAnswer(new Answer('Claudia', true))
->addAnswer(new Answer('Eelco'))
->addAnswer(new Answer('Elise'))
->addAnswer(new Answer('Gert-Jan'))
->addAnswer(new Answer('Iris'))
->addAnswer(new Answer('Jari'))
->addAnswer(new Answer('Lara'))
->addAnswer(new Answer('Lotte'))
->addAnswer(new Answer('Myrthe'))
->addAnswer(new Answer('Philine'))
->addAnswer(new Answer('Remy'))
->addAnswer(new Answer('Robbert'))
->addAnswer(new Answer('Tom'))
)
;
}
}

124
src/Entity/Answer.php Normal file
View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\AnswerRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: AnswerRepository::class)]
class Answer
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'answers')]
#[ORM\JoinColumn(nullable: false)]
private Question $question;
/** @var Collection<int, Candidate> */
#[ORM\ManyToMany(targetEntity: Candidate::class, inversedBy: 'answersOnCandidate')]
private Collection $candidates;
/** @var Collection<int, GivenAnswer> */
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'answer', orphanRemoval: true)]
private Collection $givenAnswers;
public function __construct(
#[ORM\Column(length: 255)]
private string $text,
#[ORM\Column]
private bool $isRightAnswer = false,
) {
$this->candidates = new ArrayCollection();
$this->givenAnswers = new ArrayCollection();
}
public function getId(): Uuid
{
return $this->id;
}
public function getText(): string
{
return $this->text;
}
public function setText(string $text): static
{
$this->text = $text;
return $this;
}
public function getQuestion(): Question
{
return $this->question;
}
public function setQuestion(Question $question): static
{
$this->question = $question;
return $this;
}
public function isRightAnswer(): ?bool
{
return $this->isRightAnswer;
}
public function setRightAnswer(bool $isRightAnswer): static
{
$this->isRightAnswer = $isRightAnswer;
return $this;
}
/** @return Collection<int, Candidate> */
public function getCandidates(): Collection
{
return $this->candidates;
}
public function addCandidate(Candidate $candidate): static
{
if (!$this->candidates->contains($candidate)) {
$this->candidates->add($candidate);
}
return $this;
}
public function removeCandidate(Candidate $candidate): static
{
$this->candidates->removeElement($candidate);
return $this;
}
/** @return Collection<int, GivenAnswer> */
public function getGivenAnswers(): Collection
{
return $this->givenAnswers;
}
public function addGivenAnswer(GivenAnswer $givenAnswer): static
{
if (!$this->givenAnswers->contains($givenAnswer)) {
$this->givenAnswers->add($givenAnswer);
$givenAnswer->setAnswer($this);
}
return $this;
}
}

140
src/Entity/Candidate.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Helpers\Base64;
use App\Repository\CandidateRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: CandidateRepository::class)]
class Candidate
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\ManyToOne(inversedBy: 'candidates')]
#[ORM\JoinColumn(nullable: false)]
private Season $season;
/** @var Collection<int, Answer> */
#[ORM\ManyToMany(targetEntity: Answer::class, mappedBy: 'candidates')]
private Collection $answersOnCandidate;
/** @var Collection<int, GivenAnswer> */
#[ORM\OneToMany(targetEntity: GivenAnswer::class, mappedBy: 'candidate', orphanRemoval: true)]
private Collection $givenAnswers;
/** @var Collection<int, Correction> */
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'candidate', orphanRemoval: true)]
private Collection $corrections;
public function __construct(
#[ORM\Column(length: 16)]
private string $name,
) {
$this->answersOnCandidate = new ArrayCollection();
$this->givenAnswers = new ArrayCollection();
$this->corrections = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getSeason(): Season
{
return $this->season;
}
public function setSeason(Season $season): static
{
$this->season = $season;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/** @return Collection<int, Answer> */
public function getAnswersOnCandidate(): Collection
{
return $this->answersOnCandidate;
}
public function addAnswersOnCandidate(Answer $answersOnCandidate): static
{
if (!$this->answersOnCandidate->contains($answersOnCandidate)) {
$this->answersOnCandidate->add($answersOnCandidate);
$answersOnCandidate->addCandidate($this);
}
return $this;
}
public function removeAnswersOnCandidate(Answer $answersOnCandidate): static
{
if ($this->answersOnCandidate->removeElement($answersOnCandidate)) {
$answersOnCandidate->removeCandidate($this);
}
return $this;
}
/** @return Collection<int, GivenAnswer> */
public function getGivenAnswers(): Collection
{
return $this->givenAnswers;
}
public function addGivenAnswer(GivenAnswer $givenAnswer): static
{
if (!$this->givenAnswers->contains($givenAnswer)) {
$this->givenAnswers->add($givenAnswer);
$givenAnswer->setCandidate($this);
}
return $this;
}
/** @return Collection<int, Correction> */
public function getCorrections(): Collection
{
return $this->corrections;
}
public function addCorrection(Correction $correction): static
{
if (!$this->corrections->contains($correction)) {
$this->corrections->add($correction);
$correction->setCandidate($this);
}
return $this;
}
public function getNameHash(): string
{
return Base64::base64_url_encode($this->name);
}
}

74
src/Entity/Correction.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\CorrectionRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: CorrectionRepository::class)]
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
class Correction
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\ManyToOne(inversedBy: 'corrections')]
#[ORM\JoinColumn(nullable: false)]
private Candidate $candidate;
#[ORM\ManyToOne(inversedBy: 'corrections')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
#[ORM\Column]
private float $amount = 0;
public function getId(): ?Uuid
{
return $this->id;
}
public function getCandidate(): Candidate
{
return $this->candidate;
}
public function setCandidate(Candidate $candidate): static
{
$this->candidate = $candidate;
return $this;
}
public function getQuiz(): Quiz
{
return $this->quiz;
}
public function setQuiz(Quiz $quiz): static
{
$this->quiz = $quiz;
return $this;
}
public function getAmount(): ?float
{
return $this->amount;
}
public function setAmount(float $amount): static
{
$this->amount = $amount;
return $this;
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\GivenAnswerRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Safe\DateTimeImmutable;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: GivenAnswerRepository::class)]
#[ORM\HasLifecycleCallbacks]
class GivenAnswer
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)]
private Candidate $candidate;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: true)]
private ?Answer $answer = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: false)]
private \DateTimeInterface $created;
public function getId(): ?Uuid
{
return $this->id;
}
public function getCandidate(): Candidate
{
return $this->candidate;
}
public function setCandidate(Candidate $candidate): static
{
$this->candidate = $candidate;
return $this;
}
public function getQuiz(): ?Quiz
{
return $this->quiz;
}
public function setQuiz(Quiz $quiz): static
{
$this->quiz = $quiz;
return $this;
}
public function getAnswer(): ?Answer
{
return $this->answer;
}
public function setAnswer(?Answer $answer): static
{
$this->answer = $answer;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->created = new DateTimeImmutable();
}
}

99
src/Entity/Question.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\QuestionRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: QuestionRepository::class)]
class Question
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 255, nullable: false)]
private string $question;
#[ORM\ManyToOne(inversedBy: 'questions')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
#[ORM\Column]
private bool $enabled = true;
/** @var Collection<int, Answer> */
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', cascade: ['persist'], orphanRemoval: true)]
private Collection $answers;
public function __construct()
{
$this->answers = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getQuestion(): string
{
return $this->question;
}
public function setQuestion(string $question): static
{
$this->question = $question;
return $this;
}
public function getQuiz(): Quiz
{
return $this->quiz;
}
public function setQuiz(Quiz $quiz): static
{
$this->quiz = $quiz;
return $this;
}
public function isEnabled(): ?bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): static
{
$this->enabled = $enabled;
return $this;
}
/** @return Collection<int, Answer> */
public function getAnswers(): Collection
{
return $this->answers;
}
public function addAnswer(Answer $answer): static
{
if (!$this->answers->contains($answer)) {
$this->answers->add($answer);
$answer->setQuestion($this);
}
return $this;
}
}

105
src/Entity/Quiz.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\QuizRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: QuizRepository::class)]
class Quiz
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 64)]
private string $name;
#[ORM\ManyToOne(inversedBy: 'quizzes')]
#[ORM\JoinColumn(nullable: false)]
private Season $season;
/** @var Collection<int, Question> */
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
private Collection $questions;
/** @var Collection<int, Correction> */
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
private Collection $corrections;
public function __construct()
{
$this->questions = new ArrayCollection();
$this->corrections = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getSeason(): Season
{
return $this->season;
}
public function setSeason(Season $season): static
{
$this->season = $season;
return $this;
}
/** @return Collection<int, Question> */
public function getQuestions(): Collection
{
return $this->questions;
}
public function addQuestion(Question $question): static
{
if (!$this->questions->contains($question)) {
$this->questions->add($question);
$question->setQuiz($this);
}
return $this;
}
/** @return Collection<int, Correction> */
public function getCorrections(): Collection
{
return $this->corrections;
}
public function addCorrection(Correction $correction): static
{
if (!$this->corrections->contains($correction)) {
$this->corrections->add($correction);
$correction->setQuiz($this);
}
return $this;
}
}

161
src/Entity/Season.php Normal file
View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\SeasonRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: SeasonRepository::class)]
class Season
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 64)]
private string $name;
#[ORM\Column(length: 5)]
private string $seasonCode;
#[ORM\Column]
private bool $preregisterCandidates;
/** @var Collection<int, Quiz> */
#[ORM\OneToMany(targetEntity: Quiz::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
private Collection $quizzes;
/** @var Collection<int, Candidate> */
#[ORM\OneToMany(targetEntity: Candidate::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)]
private Collection $candidates;
/** @var Collection<int, User> */
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'seasons')]
private Collection $owners;
#[ORM\ManyToOne]
private ?Quiz $ActiveQuiz = null;
public function __construct()
{
$this->quizzes = new ArrayCollection();
$this->candidates = new ArrayCollection();
$this->owners = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getSeasonCode(): ?string
{
return $this->seasonCode;
}
public function setSeasonCode(string $seasonCode): static
{
$this->seasonCode = $seasonCode;
return $this;
}
public function isPreregisterCandidates(): bool
{
return $this->preregisterCandidates;
}
public function setPreregisterCandidates(bool $preregisterCandidates): static
{
$this->preregisterCandidates = $preregisterCandidates;
return $this;
}
/** @return Collection<int, Quiz> */
public function getQuizzes(): Collection
{
return $this->quizzes;
}
public function addQuiz(Quiz $quiz): static
{
if (!$this->quizzes->contains($quiz)) {
$this->quizzes->add($quiz);
$quiz->setSeason($this);
}
return $this;
}
/** @return Collection<int, Candidate> */
public function getCandidates(): Collection
{
return $this->candidates;
}
public function addCandidate(Candidate $candidate): static
{
if (!$this->candidates->contains($candidate)) {
$this->candidates->add($candidate);
$candidate->setSeason($this);
}
return $this;
}
/** @return Collection<int, User> */
public function getOwners(): Collection
{
return $this->owners;
}
public function addOwner(User $owner): static
{
if (!$this->owners->contains($owner)) {
$this->owners->add($owner);
}
return $this;
}
public function removeOwner(User $owner): static
{
$this->owners->removeElement($owner);
return $this;
}
public function getActiveQuiz(): ?Quiz
{
return $this->ActiveQuiz;
}
public function setActiveQuiz(?Quiz $ActiveQuiz): static
{
$this->ActiveQuiz = $ActiveQuiz;
return $this;
}
}

143
src/Entity/User.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private ?Uuid $id = null;
#[ORM\Column(length: 180)]
private string $email;
/** @var list<string> The user roles */
#[ORM\Column]
private array $roles = [];
/** @var string The hashed password */
#[ORM\Column]
private string $password;
/** @var Collection<int, Season> */
#[ORM\ManyToMany(targetEntity: Season::class, mappedBy: 'owners')]
private Collection $seasons;
public function __construct()
{
$this->seasons = new ArrayCollection();
}
public function getId(): ?Uuid
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*
* @return non-empty-string
*/
public function getUserIdentifier(): string
{
return $this->email;
}
/**
* @see UserInterface
*
* @return non-empty-list<string>
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/** @param list<string> $roles */
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/** @see PasswordAuthenticatedUserInterface */
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
/** @see UserInterface */
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/** @return Collection<int, Season> */
public function getSeasons(): Collection
{
return $this->seasons;
}
public function addSeason(Season $season): static
{
if (!$this->seasons->contains($season)) {
$this->seasons->add($season);
$season->addOwner($this);
}
return $this;
}
public function removeSeason(Season $season): static
{
if ($this->seasons->removeElement($season)) {
$season->removeOwner($this);
}
return $this;
}
}

17
src/Enum/FlashType.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Enum;
enum FlashType: string
{
case Primary = 'primary';
case Secondary = 'secondary';
case Success = 'success';
case Danger = 'danger';
case Warning = 'warning';
case Info = 'info';
case Ligt = 'light';
case Dark = 'dark';
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class EnterNameType extends AbstractType
{
public function __construct(private TranslatorInterface $translator)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class,
['required' => true, 'label' => $this->translator->trans('Enter your name')],
)
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Regex;
class SelectSeasonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('season_code', TextType::class,
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/")],
)
// ->add('submit', SubmitType::class, ['label' => 'Start quiz'])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}

34
src/Helpers/Base64.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Helpers;
use Safe\Exceptions\UrlException;
use function rtrim;
class Base64
{
private function __construct()
{
}
/**
* @param string $name name to hash
* @return string hashed name
*/
public static function base64_url_encode(string $name): string
{
return rtrim(strtr(base64_encode($name), '+/', '-_'), '=');
}
/**
* @param string $hash hashed name
* @return string plaintext name
* @throws UrlException
*/
public static function base64_url_decode(string $hash): string
{
return \Safe\base64_decode(strtr($hash, '-_', '+/'), true);
}
}

13
src/Kernel.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Answer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Answer>
*/
class AnswerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Answer::class);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Candidate;
use App\Entity\Season;
use App\Helpers\Base64;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Safe\Exceptions\UrlException;
/**
* @extends ServiceEntityRepository<Candidate>
*/
class CandidateRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Candidate::class);
}
public function getCandidateByHash(Season $season, string $hash): ?Candidate
{
try {
$name = Base64::base64_url_decode($hash);
} catch (UrlException) {
return null;
}
return $this->createQueryBuilder('c')
->where('c.season = :season')
->andWhere('lower(c.name) = lower(:name)')
->setParameter('season', $season)
->setParameter('name', $name)
->getQuery()->getOneOrNullResult();
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Correction;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Correction>
*/
class CorrectionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Correction::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\GivenAnswer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<GivenAnswer>
*/
class GivenAnswerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, GivenAnswer::class);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Candidate;
use App\Entity\GivenAnswer;
use App\Entity\Question;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Question>
*/
class QuestionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Question::class);
}
public function findNextQuestionForCandidate(Candidate $candidate): Question
{
$qb = $this->createQueryBuilder('q');
return $qb->join('q.quiz', 'qz')
->andWhere($qb->expr()->notIn('q.id', $this->getEntityManager()->createQueryBuilder()
->select('ga.id')
->from(GivenAnswer::class, 'ga')
->join('ga.answer', 'a')
->join('a.question', 'q1')
->andWhere($qb->expr()->isNotNull('ga.answer'))
->andWhere('ga.candidate = :candidate')
->andWhere('q1.quiz = :quiz')
->getDQL()))
->andWhere('qz = :quiz')
->setMaxResults(1)
->setParameter('candidate', $candidate)
->setParameter('quiz', $candidate->getSeason()->getActiveQuiz())
->getQuery()->getSingleResult();
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Quiz;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Quiz>
*/
class QuizRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Quiz::class);
}
}

Some files were not shown because too many files have changed in this diff Show More