3 Commits
Author SHA1 Message Date
Frank af61a3b920 Some settings 2026-01-03 13:16:58 +01:00
Frank 0d6628e7c9 Startup 2026-01-02 20:27:56 +01:00
Frank 534175efb3 Setup 2025-09-06 16:50:16 +02:00
91 changed files with 24218 additions and 50 deletions
+17
View File
@@ -0,0 +1,17 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+63
View File
@@ -0,0 +1,63 @@
# 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_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
DATABASE_URL="mysql://app:!ChangeMe!@database:3306/app?serverVersion=8.0.32&charset=utf8mb4"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> symfony/mailer ###
# Development: use Mailpit (docker compose override provides service `mailer` on port 1025)
MAILER_DSN=smtp://mailer:1025
# Production/Stage (uncomment and set SENDGRID_API_KEY in real env or secrets):
# MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%
# Alternatively, via SMTP (no extra package needed):
# MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"
# Optional default sender (used by test command if --from not passed):
# MAILER_FROM=no-reply@your-domain.tld
# SENDGRID_API_KEY=your_real_key_goes_here # Do NOT commit this; set in .env.local or deployment env
###< symfony/mailer ###
###> symfony/sendgrid-mailer ###
# MAILER_DSN=sendgrid://KEY@default
###< symfony/sendgrid-mailer ###
###> mercure ###
# Internal hub URL used by the PHP app (reachable from the php container)
MERCURE_URL=http://mercure/.well-known/mercure
# Public hub URL used by browsers
MERCURE_PUBLIC_URL=http://localhost:8090/.well-known/mercure
# Shared secret for signing JWTs (dev only). In prod, set via real env/secrets.
MERCURE_JWT_SECRET=!ChangeThisMercureJWT!
# Base URL for Mercure topics. Use .dev in development; override to .com in prod via .env.prod or real env.
MERCURE_TOPIC_BASE=https://escapepage.dev
###< mercure ###
+4
View File
@@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=620e9ce5f88a714b636179eb39d5be4f
###< symfony/framework-bundle ###
+9
View File
@@ -0,0 +1,9 @@
### Compiled or real environment variables should be used in production.
### Configure MAILER_DSN to use SendGrid API transport.
### Prefer storing SENDGRID_API_KEY using Symfony Secrets or real env vars.
###> symfony/mailer ###
# Example using SendGrid API key (replace with real secret via vault/secrets):
# SENDGRID_API_KEY=SG.xxxxx
MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default
###< symfony/mailer ###
+3
View File
@@ -0,0 +1,3 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
+21 -48
View File
@@ -1,54 +1,27 @@
# ---> Symfony
# Cache and logs (Symfony2)
/app/cache/*
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep
# Email spool folder ###> symfony/framework-bundle ###
/app/spool/* /.env.local
/.env.local.php
# Cache, session files and logs (Symfony3) /.env.*.local
/var/cache/* /config/secrets/prod/prod.decrypt.private.php
/var/logs/* /public/bundles/
/var/sessions/* /var/
!var/cache/.gitkeep
!var/logs/.gitkeep
!var/sessions/.gitkeep
# Logs (Symfony4)
/var/log/*
!var/log/.gitkeep
# Parameters
/app/config/parameters.yml
/app/config/parameters.ini
# Managed by Composer
/app/bootstrap.php.cache
/var/bootstrap.php.cache
/bin/*
!bin/console
!bin/symfony_requirements
/vendor/ /vendor/
###< symfony/framework-bundle ###
# Assets and user uploads ###> phpunit/phpunit ###
/web/bundles/
/web/uploads/
# PHPUnit
/app/phpunit.xml
/phpunit.xml /phpunit.xml
/.phpunit.cache/
###< phpunit/phpunit ###
# Build data ###> symfony/asset-mapper ###
/build/ /public/assets/
/assets/vendor/
# Composer PHAR ###< symfony/asset-mapper ###
/composer.phar
# Backup entities generated with doctrine:generate:entities command
**/Entity/*~
# Embedded web-server pid file
/.web-server-pid
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="framework" type="frameworkType"/>
<xs:complexType name="commandType">
<xs:all>
<xs:element type="xs:string" name="name" minOccurs="1" maxOccurs="1"/>
<xs:element type="xs:string" name="params" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
<xs:element type="optionsBeforeType" name="optionsBefore" minOccurs="0" maxOccurs="1"/>
</xs:all>
</xs:complexType>
<xs:complexType name="frameworkType">
<xs:sequence>
<xs:element type="xs:string" name="extraData" minOccurs="0" maxOccurs="1"/>
<xs:element type="commandType" name="command" maxOccurs="unbounded" minOccurs="0"/>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="invoke" use="required"/>
<xs:attribute type="xs:string" name="alias" use="required"/>
<xs:attribute type="xs:boolean" name="enabled" use="required"/>
<xs:attribute type="xs:integer" name="version" use="required"/>
<xs:attribute type="xs:string" name="frameworkId" use="optional"/>
</xs:complexType>
<xs:complexType name="optionsBeforeType">
<xs:sequence>
<xs:element type="optionType" name="option" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="optionType">
<xs:sequence>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="shortcut" use="optional"/>
<xs:attribute name="pattern" use="optional">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="space"/>
<xs:enumeration value="equals"/>
<xs:enumeration value="unknown"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:schema>
+145
View File
@@ -0,0 +1,145 @@
<?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/composer" />
<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/egulias/email-validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
<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/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
<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/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/link" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<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/asset" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset-mapper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/debug-bundle" />
<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/doctrine-messenger" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
<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/expression-language" />
<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/form" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/framework-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
<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/intl" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/messenger" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/notifier" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-icu" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<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/polyfill-php84" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<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/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
<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/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stimulus-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
<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/ux-turbo" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/validator" />
<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/web-link" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-profiler-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/sendgrid-mailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="InertiaPackage">
<option name="directoryPaths">
<list />
</option>
</component>
</project>
+8
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/escapepage.iml" filepath="$PROJECT_DIR$/.idea/escapepage.iml" />
</modules>
</component>
</project>
Generated
+180
View File
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="5505d524-8d4c-4fe7-a2cb-82e334156ed6" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/psr/link" />
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/symfony/asset-mapper" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-messenger" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/symfony/debug-bundle" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/symfony/notifier" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/symfony/form" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/symfony/messenger" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/symfony/sendgrid-mailer" />
<path value="$PROJECT_DIR$/vendor/symfony/webpack-encore-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.5.1">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="5505d524-8d4c-4fe7-a2cb-82e334156ed6" timeout="60000" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<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="5505d524-8d4c-4fe7-a2cb-82e334156ed6" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+118 -2
View File
@@ -1,3 +1,119 @@
# Escapepage # EscapePage — Online Escape Room
De Escapepage This repository contains a Symfony 7.3 (PHP >= 8.5.1) application for a collaborative online escape room experience.
- Start here: doc/FILES.md — quick file index.
- Development standards and workflows: doc/CONTRIBUTING.md
- All documentation is located under the doc/ directory.
## Getting Started (brief)
- Local machine (no Docker):
1. Copy `.env` to `.env.local` and set database and `APP_SECRET`.
2. Install PHP deps: `composer install`
3. Create database and run migrations (when present): `php bin/console doctrine:database:create --if-not-exists && php bin/console doctrine:migrations:migrate -n`
4. Install/import JS deps: `php bin/console importmap:install`
5. Run the server: `symfony server:start -d` (or your preferred web server)
6. Run tests: `vendor/bin/phpunit`
- With Docker:
1. From `docker/`: `docker compose up -d`
2. Install vendors inside the PHP container:
- `docker compose exec php bash`
- `composer install`
3. Initialize DB:
- `php bin/console doctrine:database:create --if-not-exists`
- `php bin/console doctrine:migrations:migrate -n`
4. App is at http://localhost:8080
## Email (Mailpit in dev, SendGrid for prod)
- Dev: a `mailer` service (Mailpit) runs in Docker.
- SMTP DSN in `.env`: `MAILER_DSN=smtp://mailer:1025`
- Mailpit UI: http://localhost:8025
- Send a test mail: `php bin/console app:mail:test you@example.com`
- Staging/Prod: use SendGrid.
- Require package (already in composer): `symfony/sendgrid-mailer`.
- Set environment variables (do NOT commit secrets):
- `MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%`
- `SENDGRID_API_KEY=YOUR_REAL_KEY`
- Optional: `MAILER_FROM=no-reply@your-domain.tld`
- Alternatively via SMTP (no extra package):
- `MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"`
Troubleshooting:
- If emails dont appear in dev, open Mailpit at http://localhost:8025 and verify messages.
- In prod, check logs for HTTP 2xx responses from SendGrid and verify sender domain is verified in SendGrid.
## Frontend assets with Webpack Encore
We use Webpack Encore to build and minify JS/CSS from the `assets/` directory into `public/build/`.
Install Node dependencies (on your host machine):
```
npm install
```
Common commands:
```
# One-time dev build
npm run dev
# Watch & rebuild on changes
npm run watch
# Production build (minified, versioned filenames)
npm run build
```
How its wired:
- Entry file: `assets/app.js` (imports `assets/styles/app.css`).
- Webpack config: `webpack.config.js` outputs to `public/build/`.
- Twig template includes built assets via Encore:
- In `templates/base.html.twig`:
- `{{ encore_entry_link_tags('app') }}` (CSS)
- `{{ encore_entry_script_tags('app') }}` (JS)
Notes:
- The PHP Docker image does not include Node. Run the build commands on your host, or ask to add a Node build container if you prefer fully containerized builds.
- Built files are ignored by git except for `public/build/.gitignore` to keep the directory.
See doc/CONTRIBUTING.md for code style and more details.
## Realtime updates with Mercure
We use a Mercure hub (Docker service) to push server updates to browsers via ServerSent Events (SSE).
Quick start (dev):
1. Start Docker stack from `docker/`:
```
docker compose up -d
```
This starts `mercure` at http://localhost:8090 and the app at http://localhost:8080.
2. Install PHP deps inside the PHP container if you haven't yet:
```
docker compose exec php bash
composer install
```
3. Open the Game Hub page in your browser: http://localhost:8080/game
- The page subscribes to a demo topic and logs messages in the console.
4. Publish a test update (in the PHP container):
```
php bin/console app:mercure:publish
```
You should see a console log like `[Mercure] Update received: { ... }` on the Game Hub page.
Configuration:
- Env vars (defined in `.env`, override in `.env.local` as needed):
- `MERCURE_URL=http://mercure/.well-known/mercure` (internal URL from PHP to the hub)
- `MERCURE_PUBLIC_URL=http://localhost:8090/.well-known/mercure` (browser URL)
- `MERCURE_JWT_SECRET=!ChangeThisMercureJWT!` (dev secret; do not use in prod)
- `MERCURE_TOPIC_BASE=https://escapepage.dev` (base topic URL)
- Docker service `mercure` is based on `dunglas/mercure` and allows anonymous subscribers in dev.
Topics:
- Topics must be URLs. We use `MERCURE_TOPIC_BASE` to control the domain per environment.
- Dev: `.env` sets `https://escapepage.dev`
- Prod: set `MERCURE_TOPIC_BASE=https://escapepage.com`
- The Game Hub demo topic is `${MERCURE_TOPIC_BASE}/game/hub`.
Production notes:
- Disable anonymous subscribers and require JWTs for subscriptions (configure the Mercure container accordingly).
- Use a strong, rotated `MERCURE_JWT_SECRET` and keep it out of VCS (use real env/secrets).
- Serve Mercure over HTTPS and set proper CORS/allowed origins for your production domain(s).
+6
View File
@@ -0,0 +1,6 @@
/*
* Welcome to your app's main JavaScript file!
*/
import './styles/app.css';
console.log('This log comes from assets/app.js built by Webpack Encore! 🎉');
+5
View File
@@ -0,0 +1,5 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
+15
View File
@@ -0,0 +1,15 @@
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": []
}
@@ -0,0 +1,79 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
export function generateCsrfHeaders (formElement) {
const headers = {};
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';
+16
View File
@@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}
+100
View File
@@ -0,0 +1,100 @@
/* Game1 entry point built with Webpack Encore */
import './styles/game1.css';
function subscribeToMercure(mercurePublicUrl, topic) {
try {
const url = mercurePublicUrl + '?topic=' + encodeURIComponent(topic);
const es = new EventSource(url);
es.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('[Mercure][game1] Update:', data);
} catch (e) {
console.log('[Mercure][game1] Raw event:', event.data);
}
};
es.onerror = (err) => {
console.warn('[Mercure][game1] EventSource error:', err);
};
console.log('[Mercure][game1] Subscribed to', url);
} catch (e) {
console.error('[Mercure][game1] Failed to subscribe:', e);
}
}
async function fetchJson(url, options = {}) {
const opts = { ...options };
const headers = new Headers(opts.headers || {});
headers.set('Accept', 'application/json');
if (opts.body !== undefined && typeof opts.body !== 'string') {
headers.set('Content-Type', 'application/json');
opts.body = JSON.stringify(opts.body);
}
// Useful convention for server-side checks
if (!headers.has('X-Requested-With')) {
headers.set('X-Requested-With', 'XMLHttpRequest');
}
opts.headers = headers;
const res = await fetch(url, opts);
const text = await res.text();
let data;
try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
if (!res.ok) {
const err = new Error('HTTP ' + res.status + ' ' + res.statusText);
console.error('[API][game1]', err, data);
throw err;
}
return data;
}
document.addEventListener('DOMContentLoaded', async () => {
// Simple boot log so you can verify it in the browser console
// and confirm this specific bundle is loaded on the Game Hub page.
console.log('Game1 bundle loaded');
// Example: add a CSS class to <body> so page-specific styles can apply
document.body.classList.add('game1-page');
// Look for config injected by Twig in the page
const cfgEl = document.getElementById('mercure-config');
if (!cfgEl) {
console.warn('[Mercure][game1] #mercure-config element not found on page');
return;
}
const mercurePublicUrl = cfgEl.dataset.mercurePublicUrl;
const topic = cfgEl.dataset.topic;
const apiPingUrl = cfgEl.dataset.apiPingUrl;
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
if (mercurePublicUrl && topic) {
subscribeToMercure(mercurePublicUrl, topic);
} else {
console.warn('[Mercure][game1] Missing data attributes on #mercure-config');
}
// Demo API calls
try {
if (apiPingUrl) {
const ping = await fetchJson(apiPingUrl);
console.log('[API][game1] ping →', ping);
} else {
console.warn('[API][game1] data-api-ping-url missing');
}
if (apiEchoUrl) {
const echo = await fetchJson(apiEchoUrl, {
method: 'POST',
body: { hello: 'from game1.js', ts: new Date().toISOString() },
});
console.log('[API][game1] echo →', echo);
} else {
console.warn('[API][game1] data-api-echo-url missing');
}
} catch (e) {
console.error('[API][game1] Request failed:', e);
}
});
+3
View File
@@ -0,0 +1,3 @@
body {
background-color: skyblue;
}
+17
View File
@@ -0,0 +1,17 @@
/* Styles specific to Game1 */
/* page-level indicator to confirm CSS is loaded */
body.game1-page {
/* subtle background tint so you can visually confirm on /game */
background-color: #f9fbff;
}
/* example component style */
.game1-banner {
padding: 1rem 1.25rem;
border: 1px solid #cfe2ff;
background: #e9f2ff;
color: #0b5ed7;
border-radius: 8px;
margin: 1rem 0;
}
+21
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);
};
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}
+7
View File
@@ -0,0 +1,7 @@
services:
###> symfony/mercure-bundle ###
mercure:
ports:
- "80"
###< symfony/mercure-bundle ###
+31
View File
@@ -0,0 +1,31 @@
services:
###> symfony/mercure-bundle ###
mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
# Uncomment the following line to disable HTTPS,
#SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://127.0.0.1:8000
# Comment the following line to disable the development mode
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
healthcheck:
test: ["CMD", "curl", "-f", "https://localhost/healthz"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- mercure_data:/data
- mercure_config:/config
###< symfony/mercure-bundle ###
volumes:
###> symfony/mercure-bundle ###
mercure_data:
mercure_config:
###< symfony/mercure-bundle ###
+111
View File
@@ -0,0 +1,111 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.5.1",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.16",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.5",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*",
"symfony/flex": "^2",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/intl": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/mime": "7.3.*",
"symfony/sendgrid-mailer": "7.3.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.3.*",
"symfony/process": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/stimulus-bundle": "^2.30",
"symfony/string": "7.3.*",
"symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.3.*",
"symfony/ux-turbo": "^2.30",
"symfony/validator": "7.3.*",
"symfony/web-link": "7.3.*",
"symfony/yaml": "7.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/mercure-bundle": "^0.3"
},
"config": {
"allow-plugins": {
"php-http/discovery": 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",
"importmap:install": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.3.*"
}
},
"require-dev": {
"phpunit/phpunit": "^11.5",
"symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.3.*",
"symfony/debug-bundle": "7.3.*",
"symfony/maker-bundle": "^1.0",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
}
}
Generated
+10511
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
<?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\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
];
+11
View File
@@ -0,0 +1,11 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn
+19
View File
@@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
+11
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
+5
View File
@@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
+50
View File
@@ -0,0 +1,50 @@
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
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src'
prefix: 'App'
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
+6
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
+20
View File
@@ -0,0 +1,20 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
default_locale: 'en'
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks: ['en', 'nl']
# 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
+3
View File
@@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'
+6
View File
@@ -0,0 +1,6 @@
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt: '%env(MERCURE_JWT_SECRET)%'
+29
View File
@@ -0,0 +1,29 @@
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
default_bus: messenger.bus.default
buses:
messenger.bus.default: []
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
# 'App\Message\YourMessage': async
+62
View File
@@ -0,0 +1,62 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.json
+13
View File
@@ -0,0 +1,13 @@
framework:
notifier:
chatter_transports:
texter_transports:
sendgrid: '%env(MAILER_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }
+3
View File
@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true
+10
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
+39
View File
@@ -0,0 +1,39 @@
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:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# 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
+5
View File
@@ -0,0 +1,5 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
providers:
+4
View File
@@ -0,0 +1,4 @@
twig:
globals:
mercure_public_url: '%env(MERCURE_PUBLIC_URL)%'
mercure_topic_base: '%env(MERCURE_TOPIC_BASE)%'
+4
View File
@@ -0,0 +1,4 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
csrf_protection:
check_header: true
+11
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
+13
View File
@@ -0,0 +1,13 @@
when@dev:
web_profiler:
toolbar: true
framework:
profiler:
collect_serializer_data: true
when@test:
framework:
profiler:
collect: false
collect_serializer_data: true
+45
View File
@@ -0,0 +1,45 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
#when@prod:
# webpack_encore:
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# # Available in version 1.2
# cache: true
#when@test:
# webpack_encore:
# strict_mode: false
+5
View File
@@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}
+23
View File
@@ -0,0 +1,23 @@
# Attribute-based routing imports for controllers in subnamespaces
website_controllers:
resource:
path: ../src/Website/Controller/
namespace: App\Website\Controller
type: attribute
game_controllers:
resource:
path: ../src/Game/Controller/
namespace: App\Game\Controller
type: attribute
prefix: /game
# Uncomment when you add base controllers
# base_controllers:
# resource:
# path: ../src/Base/Controller/
# namespace: App\Base\Controller
# type: attribute
# # Set a prefix if desired, e.g., "/base" or leave empty to mount at root
# # prefix: /base
+13
View File
@@ -0,0 +1,13 @@
website:
resource: ../../src/Website/Controller/
type: attribute
prefix:
en: '/'
nl: '/nl'
game:
resource: ../../src/Game/Controller/
type: attribute
prefix:
en: '/game'
nl: '/nl/game'
+4
View File
@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error
+3
View File
@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service
+8
View File
@@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler
+20
View File
@@ -0,0 +1,20 @@
# 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/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
+138
View File
@@ -0,0 +1,138 @@
# Contribution & Code Style Guide
This document is for Junie (and humans) to keep our code style consistent and to quickly find the files well reference during development of the Online Escape Room platform.
Project type: Symfony 7.3, PHP >= 8.5.1, Doctrine ORM 3, Twig, Stimulus (UX), Importmap/Asset Mapper, PHPUnit 11.
## 1. Repository Conventions
- PHP version: 8.5.1+ (composer.json enforces ">=8.5.1").
- Framework: Symfony 7.3.* (see composer.json).
- Architecture: MVC with Controllers in `src/Controller`, Entities in `src/Entity`, Repositories in `src/Repository`, Templates in `templates`.
- Env files: `.env`, `.env.local` (ignored), and environment overrides like `.env.test.local`.
- Routes: annotation/attributes in controllers or YAML/PHP in `config/routes`.
- Documentation location: All documentation must live under the `doc/` directory. New docs requested by the team will be added there.
## 2. Coding Standards
### 2.1 PHP
- Standard: PSR-12 (line length soft cap 120; prefer multi-line for long signatures/arrays).
- Strict types at top of new files: `declare(strict_types=1);`
- Type-hint everything (params, returns, properties). Prefer readonly where applicable.
- Visibility: declare on all properties and methods.
- Exceptions: throw domain-specific exceptions for game logic; do not return null where an exception is appropriate.
- Controllers: keep thin; delegate to services. Use DTOs/Form types to validate input.
- Dependency Injection: constructor injection; avoid container-aware services.
- Naming: Services suffixed with `...Service`, commands with `...Command`, event listeners with `...Listener`/`...Subscriber`.
- Logging: use `Psr\Log\LoggerInterface` where meaningful.
### 2.2 Twig
- Keep templates lean; no heavy logic. Use filters/functions and view models if needed.
- Use `trans` for strings that will be localized.
- Partial templates/components: place in `templates/_partials` or `templates/components`.
### 2.3 JavaScript (Stimulus / vanilla)
- Use ES modules via Importmap/Asset Mapper.
- Controllers in `assets/controllers`. Name as `something_controller.js` following Stimulus conventions.
- Prefer small, focused controllers. Keep DOM queries scoped to controller element.
- Avoid global state; communicate via events or Turbo streams when appropriate.
### 2.4 Styles (CSS/SCSS)
- Keep styles in `assets/styles`. Use BEM naming for classes.
- Prefer CSS variables for theme colors, spacing scale, z-index scale.
### 2.5 YAML / Config
- 2-space indentation, no tabs.
- Use parameters and env vars (`env()`) instead of hardcoding secrets.
### 2.6 Git & Commits
- Branch naming: `feature/<short-name>`, `fix/<short-name>`, `chore/<short-name>`.
- Commit messages:
- Conventional commits style: `feat: ...`, `fix: ...`, `chore: ...`, `docs: ...`, `test: ...`, `refactor: ...`.
- First line ≤ 72 chars; add body when needed with rationale.
## 3. Linting, Tests, and Quality
### 3.1 PHP
- Use PHP-CS-Fixer or PHP_CodeSniffer (PHPCS) with PSR-12. If not yet installed, proposed composer scripts (to add later):
- `composer require --dev friendsofphp/php-cs-fixer`
- Script: `php-cs-fixer fix --allow-risky=yes`
- Static analysis: PHPStan level 68 recommended:
- `composer require --dev phpstan/phpstan`
- Run: `vendor/bin/phpstan analyse src tests` (configure `phpstan.neon` later)
### 3.2 Symfony
- Cache and debug:
- `php bin/console cache:clear`
- `php bin/console debug:router`
- `php bin/console debug:container`
### 3.3 Tests
- PHPUnit (already required):
- Run tests: `vendor/bin/phpunit`
- Tests location: `tests/`
- Config: `phpunit.dist.xml`
### 3.4 Frontend
- Stimulus/UX: controllers auto-registered via `symfony/stimulus-bundle`.
- Asset Mapper/Importmap:
- Install deps: `php bin/console importmap:install`
- Dev server (if using symfony local server): `symfony server:start -d`
## 4. Project File Map (Quick Reference)
- App kernel: `src/Kernel.php`
- Controllers: `src/Controller/`
- Domain entities: `src/Entity/`
- Repositories: `src/Repository/`
- Migrations: `migrations/`
- Templates: `templates/`
- Translations: `translations/`
- Assets entry: `assets/app.js`, styles in `assets/styles/`
- Stimulus controllers: `assets/controllers/`
- Routes: `config/routes/`
- Packages config: `config/packages/`
- Env vars: `.env` (base), `.env.local` (local overrides)
- Public web root: `public/`
- Tests: `tests/`
## 5. How We Build Features (Checklist)
1) Create branch: `feature/<name>`.
2) Describe the task in an issue with acceptance criteria.
3) Implement backend (entities/services/controllers) with tests.
4) Implement templates and Stimulus controller if interactive.
5) Add/adjust routes and translations.
6) Run: linters, phpstan, phpunit; ensure green.
7) Open PR; request review.
## 6. Escape Room Domain Notes (early)
- Core concepts likely: Game, Room, Puzzle, Session, Player, Team, Hint, Timer.
- Consider events (Domain Events) for puzzle solved, hint requested, time warnings.
- Real-time collaboration options: Symfony Mercure, WebSockets, or Turbo Streams.
- Persist immutable audit trail for gameplay.
## 7. Editor Configuration
- Recommend EditorConfig. If we add `.editorconfig` later, set:
- Indent 4 spaces for PHP, 2 for YAML/Twig/JS/CSS.
- LF line endings, UTF-8, insert final newline, trim trailing whitespace.
## 8. Security & Secrets
- Never commit secrets. Use environment variables or Symfony Vault.
- Review `APP_ENV`, `APP_SECRET`, database DSN in `.env` and `.env.local`.
## 9. Release & Environments
- Envs: `dev`, `test`, `prod`.
- Build steps: run migrations, warmup cache, compile assets if using.
## 10. References
- Symfony Best Practices: https://symfony.com/doc/current/best_practices.html
- Doctrine ORM 3 Docs: https://www.doctrine-project.org/projects/doctrine-orm/en/current/
- Stimulus: https://stimulus.hotwired.dev/
+51
View File
@@ -0,0 +1,51 @@
# Project File Index (Quick Reference)
Use this index to quickly locate files and directories during development and in discussions.
## Top-Level
- docker/compose.yaml / docker/compose.override.yaml — Docker services.
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost, compose files).
- composer.json / composer.lock — Dependencies and scripts.
- importmap.php — Importmap configuration for JS dependencies.
- phpunit.dist.xml — PHPUnit configuration.
- public/ — Web root (index.php, assets, static files).
- var/ — Cache and logs.
- vendor/ — Composer dependencies.
## Application Source (src/)
- src/Kernel.php — Symfony Kernel bootstrapping.
- src/Website/ — Marketing/public website area (controllers, templates under templates/website/).
- src/Game/ — Game area (controllers, templates under templates/game/).
- src/Entity/ — Doctrine ORM entities.
- src/Repository/ — Doctrine repositories.
## Configuration (config/)
- config/packages/ — Symfony bundles configuration (framework.yaml, cache.yaml, etc.).
- config/routes/ — Routing configuration files.
- config/routes/app.yaml — Imports attribute routes for both sites (Website and Game).
## Data & DB
- migrations/ — Doctrine migrations.
## Presentation
- templates/ — Twig templates.
- templates/website — Views for the public website.
- templates/game — Views for the game area.
- translations/ — i18n message files.
## Frontend Assets
- assets/app.js — Main JS entry (imports Stimulus, styles, etc.).
- assets/controllers/ — Stimulus controllers.
- assets/styles/ — Global styles.
- assets/vendor/ — Vendor frontend assets if any.
## Tests
- tests/ — Test suites for PHPUnit.
## Environment
- .env — Base environment configuration.
- .env.local — Local overrides (ignored).
## Notes
- Follow doc/CONTRIBUTING.md for code style and workflows.
- Email setup: see doc/email.md for dev Mailpit and production SendGrid configuration.
+10
View File
@@ -0,0 +1,10 @@
# Project Documentation
This directory contains all project documentation for EscapePage.
- Code Style & Contribution Guide: CONTRIBUTING.md
- Project File Index: FILES.md
Policy: All new and existing documentation must be placed in this doc/ directory. If you ask Junie to add docs in the future, they will be created under doc/.
Additional docs can be added here as the project grows (architecture decisions, API docs, gameplay design, onboarding, etc.).
+60
View File
@@ -0,0 +1,60 @@
# Docker Setup
This app can run fully in Docker using docker compose with PHP-FPM, Nginx and MySQL.
## Services
- php: PHP 8.5.1 FPM with required extensions and Composer
- nginx: Serves the Symfony app from public/ and proxies PHP to php-fpm
- database: MySQL 8.0 (data persisted in a volume)
- mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI)
## Prerequisites
- Docker and Docker Compose (v2)
## Usage
### 1) Build and start
```
./docker/setup.sh
```
App will be served at http://localhost:8080
Alternatively (manual):
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml up -d --build
```
### 2) Install dependencies
The setup script already runs composer install. To run manually:
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php composer install
```
### 3) Prepare DB
The setup script already prepares the DB. To run manually:
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:database:create --if-not-exists
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:migrations:migrate -n
```
### 4) Run tests
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php vendor/bin/phpunit
```
### 5) Logs
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f nginx
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f php
```
### 6) Stop
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml down
```
## Notes
- .env already points DATABASE_URL to the `database` service hostname.
- Nginx listens on port 8080 (mapped from container 80) to avoid conflicts.
- Source is bind-mounted; changes on host reflect inside containers.
- For production images, create a separate Dockerfile with build steps (composer install --no-dev, cache warmup) and avoid bind mounts.
+45
View File
@@ -0,0 +1,45 @@
# Email Delivery: Dev Mailcatcher & Production SendGrid
This application uses Symfony Mailer. We separate development and production delivery:
- Development: Mailpit (mailcatcher) via SMTP in Docker.
- Production: SendGrid via API transport.
## Development (Mailpit)
- Service is defined in `compose.override.yaml` as `mailer` (axllent/mailpit).
- Ports:
- SMTP: 1025 (mapped to host 1025)
- Web UI: 8025 (mapped to host 8025)
- Default DSN for dev is set in `.env`:
```
MAILER_DSN=smtp://mailer:1025
```
- Usage:
1. Start stack: `docker compose up -d`
2. Send an email from the app.
3. Open http://localhost:8025 to view captured emails.
## Production (SendGrid)
Use the SendGrid API transport. Do not commit secrets.
- Example configuration is in `.env.prod`:
```
MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default
```
- Provide `SENDGRID_API_KEY` via:
- Real environment variable on the server/container, or
- Symfony secrets: `php bin/console secrets:set SENDGRID_API_KEY` (and dump for prod), or
- Orchestration secret stores (e.g., Docker/K8s).
### Notes
- No Mailpit container is defined in the base `compose.yaml`, only in `compose.override.yaml`. This ensures it is used in development only.
- To test email locally without Docker, you can:
- Run Mailpit on your host (ports 1025/8025) and set `MAILER_DSN=smtp://127.0.0.1:1025` in `.env.local`.
- If you need to use SendGrid SMTP instead of API, a DSN example:
`smtp://apikey:YOUR_SENDGRID_API_KEY@smtp.sendgrid.net:587`.
+26
View File
@@ -0,0 +1,26 @@
services:
php:
environment:
XDEBUG_MODE: off
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- mailer
###> doctrine/doctrine-bundle ###
database:
ports:
- "3306"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
- "1025:1025"
- "8025:8025"
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
###< symfony/mailer ###
+96
View File
@@ -0,0 +1,96 @@
version: '3.7'
services:
php:
build:
context: ..
dockerfile: php/Dockerfile
container_name: escapepage-php
volumes:
- ../:/var/www/html:delegated
environment:
APP_ENV: dev
depends_on:
- database
- mercure
networks:
- backend
restart: unless-stopped
nginx:
image: nginx:1.29.4-alpine
container_name: escapepage-nginx
ports:
- "8080:80"
volumes:
- ../:/var/www/html:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
networks:
- backend
restart: unless-stopped
mailer:
image: axllent/mailpit:latest
container_name: escapepage-mailer
ports:
- "8025:8025"
networks:
- backend
restart: unless-stopped
mercure:
image: dunglas/mercure:v0.21
container_name: escapepage-mercure
environment:
SERVER_NAME: ":80"
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureJWT!}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureJWT!}
MERCURE_CORS_ALLOWED_ORIGINS: http://localhost:8080
MERCURE_PUBLISH_ALLOWED_ORIGINS: http://localhost:8080
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://localhost:8080
# Allow anonymous subscribers in dev only
anonymous
ports:
- "8090:80"
networks:
- backend
restart: unless-stopped
###> doctrine/doctrine-bundle ###
database:
image: mysql:8.0
container_name: escapepage-db
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
MYSQL_USER: ${MYSQL_USER:-app}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
command: ["--default-authentication-plugin=mysql_native_password", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
volumes:
- database_data:/var/lib/mysql:rw
# Uncomment the two lines below if you need to access MySQL from your host (workbench, etc.)
# ports:
# - "3306:3306"
networks:
- backend
restart: unless-stopped
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###
networks:
backend:
driver: bridge
+28
View File
@@ -0,0 +1,28 @@
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php index.html;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass php:9000;
fastcgi_read_timeout 120;
}
location ~ /\.ht {
deny all;
}
client_max_body_size 32m;
}
+21
View File
@@ -0,0 +1,21 @@
FROM php:8.5.1-fpm-alpine3.23
# Install system deps
RUN apk add --no-cache bash git icu-dev libzip-dev oniguruma-dev
# Install PHP extensions
RUN docker-php-ext-configure intl \
&& docker-php-ext-install -j$(nproc) intl pdo pdo_mysql opcache
# Install composer
ENV COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_HOME=/tmp/composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Configure PHP
COPY php.ini $PHP_INI_DIR/conf.d/zz-custom.ini
WORKDIR /var/www/html
# Default command
CMD ["php-fpm"]
+9
View File
@@ -0,0 +1,9 @@
memory_limit=512M
post_max_size=32M
upload_max_filesize=32M
max_execution_time=60
; For Symfony dev
opcache.enable=1
opcache.enable_cli=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
set -euo pipefail
# Simple setup script to bootstrap the Dockerized dev stack for this project.
# - Builds and starts containers (php, nginx, mariadb, mailpit via override)
# - Installs composer dependencies
# - Ensures APP_SECRET is set (generates if empty)
# - Creates and migrates the database
# - Installs/imports JS dependencies
# - Prints helpful info on success
# Usage:
# ./docker/setup.sh # full setup
# ./docker/setup.sh --no-build # skip image rebuild
# ./docker/setup.sh --down # stop and remove containers (down)
# ./docker/setup.sh --recreate # force recreate containers
ROOT_DIR=$(cd "$(dirname "$0")"/.. && pwd)
DOCKER_DIR="$ROOT_DIR/docker"
# Determine the docker compose command (V2 'docker compose' or V1 'docker-compose')
if docker compose version >/dev/null 2>&1; then
DOCKER_COMPOSE="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE="docker-compose"
else
echo "Error: Neither 'docker compose' nor 'docker-compose' was found. Please install Docker Compose." >&2
exit 1
fi
# Helper to run docker compose from the docker/ directory
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -f compose.yaml "$@"); }
REBUILD=1
RECREATE=0
DOWN_ONLY=0
for arg in "$@"; do
case "$arg" in
--no-build) REBUILD=0 ;;
--recreate) RECREATE=1 ;;
--down) DOWN_ONLY=1 ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
need() { command -v "$1" >/dev/null 2>&1 || { echo "Error: '$1' is required but not installed." >&2; exit 1; }; }
need docker
if [ "$DOWN_ONLY" -eq 1 ]; then
dc down
exit 0
fi
BUILD_ARGS=()
if [ "$REBUILD" -eq 1 ]; then
BUILD_ARGS+=("--build")
fi
if [ "$RECREATE" -eq 1 ]; then
BUILD_ARGS+=("--force-recreate")
fi
# Start stack
dc up "${BUILD_ARGS[@]}"
# Helper to run commands in php container
pexec() { dc exec -T php "$@"; }
# Wait for database to be healthy (mariadb)
printf "Waiting for database to be healthy..."
# Use docker inspect health status
DB_HEALTH=""
for i in {1..60}; do
DB_HEALTH=$(docker inspect -f '{{.State.Health.Status}}' "$(docker ps --filter name=_database_ --format '{{.ID}}' | head -n1)" 2>/dev/null || true)
if [ "$DB_HEALTH" = "healthy" ]; then
echo " OK"
break
fi
printf "."
sleep 2
if [ "$i" -eq 60 ]; then
echo "\nWarning: database health check not healthy yet, continuing anyway."
fi
done
# Ensure composer is available and install dependencies
pexec composer install --no-interaction
# Ensure APP_SECRET is set
if grep -q '^APP_SECRET=$' "$ROOT_DIR/.env" 2>/dev/null; then
echo "Generating APP_SECRET in .env.local..."
mkdir -p "$ROOT_DIR"
SECRET=$(openssl rand -hex 16)
# Write to .env.local so we don't commit it
if [ ! -f "$ROOT_DIR/.env.local" ]; then
printf "APP_SECRET=%s\n" "$SECRET" > "$ROOT_DIR/.env.local"
elif ! grep -q '^APP_SECRET=' "$ROOT_DIR/.env.local"; then
printf "APP_SECRET=%s\n" "$SECRET" >> "$ROOT_DIR/.env.local"
fi
fi
# Prepare DB
pexec php bin/console doctrine:database:create --if-not-exists || true
pexec php bin/console doctrine:migrations:migrate -n || true
# Import JS deps (Importmap/Asset Mapper)
pexec php bin/console importmap:install || true
APP_URL=http://localhost:8080
MAILPIT_URL=http://localhost:8025
cat <<EOT
Setup complete!
Open the app: $APP_URL
Mailpit (dev): $MAILPIT_URL
Common commands:
(cd docker && $DOCKER_COMPOSE logs -f nginx)
(cd docker && $DOCKER_COMPOSE logs -f php)
(cd docker && $DOCKER_COMPOSE exec php bash)
(cd docker && $DOCKER_COMPOSE down)
You can re-run this script any time. Use --no-build to skip rebuilding images.
EOT
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
];
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File
+8205
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "escapepage",
"private": true,
"version": "1.0.0",
"description": "EscapePage Symfony app assets built with Webpack Encore",
"license": "UNLICENSED",
"scripts": {
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production"
},
"devDependencies": {
"@babel/core": "^7.25.0",
"@babel/preset-env": "^7.25.0",
"@symfony/webpack-encore": "^4.6.1",
"babel-loader": "^9.1.3",
"core-js": "^3.37.1",
"css-loader": "^7.1.2",
"mini-css-extract-plugin": "^2.9.2",
"regenerator-runtime": "^0.14.1",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-notifier": "^1.15.0"
}
}
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<method>Doctrine\Deprecations\Deprecation::trigger</method>
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
<extensions>
</extensions>
</phpunit>
+9
View File
@@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
#[AsCommand(
name: 'app:mercure:publish',
description: 'Publishes a test update to the Mercure hub.'
)]
final class MercurePublishCommand extends Command
{
public function __construct(private readonly HubInterface $hub)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('topic', InputArgument::OPTIONAL, 'Topic URL to publish to', $_ENV['MERCURE_TOPIC_BASE'] . '/game/hub')
->addOption('type', null, InputOption::VALUE_REQUIRED, 'Update type (for clients to filter)', 'game.event')
->addOption('data', null, InputOption::VALUE_REQUIRED, 'JSON payload to send', '{"message":"Hello from Mercure!"}')
->addOption('private', null, InputOption::VALUE_NONE, 'Mark the update as private');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$topic = (string) $input->getArgument('topic');
$type = (string) $input->getOption('type');
$data = (string) $input->getOption('data');
$isPrivate = (bool) $input->getOption('private');
// Validate JSON
$decoded = json_decode($data, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
$output->writeln('<error>Invalid JSON provided for --data.</error>');
return Command::FAILURE;
}
$update = new Update(
topics: $topic,
data: json_encode([
'type' => $type,
'payload' => $decoded,
'ts' => date('c'),
], JSON_THROW_ON_ERROR),
private: $isPrivate
);
$this->hub->publish($update);
$output->writeln('<info>Published update to topic:</info> ' . $topic);
return Command::SUCCESS;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
#[AsCommand(
name: 'app:mail:test',
description: 'Sends a simple test email using the configured mail transport (SendGrid in prod).'
)]
final class TestEmailCommand extends Command
{
public function __construct(private readonly MailerInterface $mailer)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('to', InputArgument::REQUIRED, 'Recipient email address')
->addOption('subject', null, InputOption::VALUE_REQUIRED, 'Email subject', 'EscapePage mailer test')
->addOption('from', null, InputOption::VALUE_REQUIRED, 'Sender email address (defaults to MAILER_FROM if set)')
->addOption('from-name', null, InputOption::VALUE_REQUIRED, 'Sender name', 'EscapePage');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$to = (string) $input->getArgument('to');
$subject = (string) $input->getOption('subject');
$fromEmail = (string) ($input->getOption('from') ?? ($_ENV['MAILER_FROM'] ?? $_SERVER['MAILER_FROM'] ?? 'no-reply@example.com'));
$fromName = (string) $input->getOption('from-name');
$email = (new Email())
->from(new Address($fromEmail, $fromName))
->to($to)
->subject($subject)
->html('<p>This is a test email sent at ' . date('c') . '.</p><p>If you see this, your mailer setup works.</p>')
->text('This is a test email sent at ' . date('c') . ". If you see this, your mailer setup works.");
$this->mailer->send($email);
$output->writeln('<info>Test email sent to ' . $to . '.</info>');
return Command::SUCCESS;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Game\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/game/api', name: 'game_api_')]
final class GameApiController extends AbstractController
{
#[Route('/ping', name: 'ping', methods: ['GET'])]
public function ping(): JsonResponse
{
return $this->json([
'ok' => true,
'service' => 'game-api',
'ts' => date('c'),
]);
}
#[Route('/echo', name: 'echo', methods: ['POST'])]
public function echo(Request $request): JsonResponse
{
$raw = (string) $request->getContent();
$data = null;
if ($raw !== '') {
try {
/** @var array<string,mixed>|null $decoded */
$decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
$data = $decoded;
} catch (\Throwable $e) {
return $this->json([
'ok' => false,
'error' => 'Invalid JSON: ' . $e->getMessage(),
], Response::HTTP_BAD_REQUEST);
}
}
return $this->json([
'ok' => true,
'received' => $data,
'ts' => date('c'),
]);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Game\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class GameController extends AbstractController
{
#[Route(path: '', name: 'game')]
public function index(): Response
{
return $this->render('game/index.html.twig');
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Website\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomeController extends AbstractController
{
#[Route(path: '', name: 'website_home')]
public function index(): Response
{
return $this->render(
'website/home/index.html.twig');
}
}
+362
View File
@@ -0,0 +1,362 @@
{
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
}
},
"doctrine/doctrine-bundle": {
"version": "2.16",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "620b57f496f2e599a6015a9fa222c2ee0a32adcb"
},
"files": [
"config/packages/doctrine.yaml",
"src/Entity/.gitignore",
"src/Repository/.gitignore"
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.1",
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
},
"files": [
"config/packages/doctrine_migrations.yaml",
"migrations/.gitignore"
]
},
"phpunit/phpunit": {
"version": "11.5",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "11.1",
"ref": "c6658a60fc9d594805370eacdf542c3d6b5c0869"
},
"files": [
".env.test",
"phpunit.dist.xml",
"tests/bootstrap.php",
"bin/phpunit"
]
},
"symfony/asset-mapper": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
]
},
"symfony/console": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
},
"files": [
"bin/console"
]
},
"symfony/debug-bundle": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
},
"files": [
"config/packages/debug.yaml"
]
},
"symfony/flex": {
"version": "2.8",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env",
".env.dev"
]
},
"symfony/form": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
},
"files": [
"config/packages/cache.yaml",
"config/packages/framework.yaml",
"config/preload.php",
"config/routes/framework.yaml",
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php",
".editorconfig"
]
},
"symfony/mailer": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "4.3",
"ref": "09051cfde49476e3c12cd3a0e44289ace1c75a4f"
},
"files": [
"config/packages/mailer.yaml"
]
},
"symfony/maker-bundle": {
"version": "1.64",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/mercure-bundle": {
"version": "0.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.3",
"ref": "528285147494380298f8f991ee8c47abebaf79db"
},
"files": [
"./config/packages/mercure.yaml"
]
},
"symfony/messenger": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.0",
"ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee"
},
"files": [
"config/packages/messenger.yaml"
]
},
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
},
"files": [
"config/packages/monolog.yaml"
]
},
"symfony/notifier": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.0",
"ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
},
"files": [
"config/packages/notifier.yaml"
]
},
"symfony/property-info": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"config/packages/property_info.yaml"
]
},
"symfony/routing": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
},
"files": [
"config/packages/routing.yaml",
"config/routes.yaml"
]
},
"symfony/security-bundle": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
},
"files": [
"config/packages/security.yaml",
"config/routes/security.yaml"
]
},
"symfony/sendgrid-mailer": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "4.4",
"ref": "224aedffb66812dc2b0965dabc14d5f800941da6"
}
},
"symfony/stimulus-bundle": {
"version": "2.30",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.20",
"ref": "c30f782a8910ae9f12e7a1399db607d172d23da6"
},
"files": [
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/csrf_protection_controller.js",
"assets/controllers/hello_controller.js"
]
},
"symfony/translation": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "620a1b84865ceb2ba304c8f8bf2a185fbf32a843"
},
"files": [
"config/packages/translation.yaml",
"translations/.gitignore"
]
},
"symfony/twig-bundle": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
},
"files": [
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"symfony/ux-turbo": {
"version": "2.30",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.20",
"ref": "287f7c6eb6e9b65e422d34c00795b360a787380b"
},
"files": [
"config/packages/ux_turbo.yaml"
]
},
"symfony/validator": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
},
"files": [
"config/packages/validator.yaml"
]
},
"symfony/web-profiler-bundle": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "a363460c1b0b4a4d0242f2ce1a843ca0f6ac9026"
},
"files": [
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"symfony/webapp-pack": {
"version": "1.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "7d5c5e282f7e2c36a2c3bbb1504f78456c352407"
},
"files": [
"config/packages/messenger.yaml"
]
},
"symfony/webpack-encore-bundle": {
"version": "2.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.0",
"ref": "719f6110345acb6495e496601fc1b4977d7102b3"
},
"files": [
"./assets/app.js",
"./assets/styles/app.css",
"./config/packages/webpack_encore.yaml",
"./package.json",
"./webpack.config.js"
]
},
"twig/extra-bundle": {
"version": "v3.21.0"
}
}
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ app.request.locale|default(app.request.defaultLocale|default('en')) }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ 'site.name'|trans }}{% endblock %}</title>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
<body>
<header>
<nav>
{% set pathinfo = app.request.pathinfo %}
<a href="/">{{ 'nav.home'|trans }}</a> |
<a href="/game">{{ 'nav.game'|trans }}</a>
</nav>
</header>
<main>
{% block body %}{% endblock %}
</main>
<footer>
<small>&copy; {{ "now"|date("Y") }} {{ 'site.name'|trans }}</small>
</footer>
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
{% extends 'base.html.twig' %}
{% block title %}{{ 'game.title'|trans({'%site%': ('site.name'|trans)}) }}{% endblock %}
{# Include Game1-specific CSS in addition to the base app assets #}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('game1') }}
{% endblock %}
{% block body %}
<h1>{{ 'game.h1'|trans }}</h1>
<p>{{ 'game.description'|trans }}</p>
<div class="game1-banner">Game 1 assets are active. Enjoy the challenge!</div>
{# Hidden config element read by assets/game1.js #}
<div id="mercure-config"
data-mercure-public-url="{{ mercure_public_url|e('html_attr') }}"
data-topic="{{ (mercure_topic_base ~ '/game/hub')|e('html_attr') }}"
data-api-ping-url="{{ path('game_api_ping')|e('html_attr') }}"
data-api-echo-url="{{ path('game_api_echo')|e('html_attr') }}"
style="display:none">
</div>
<p><a href="{{ path('website_home') }}">{{ 'link.back_to_website'|trans }}</a></p>
{% endblock %}
{# Include Game1-specific JS in addition to the base app assets #}
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('game1') }}
{% endblock %}
+9
View File
@@ -0,0 +1,9 @@
{% extends 'base.html.twig' %}
{% block title %}{{ 'home.title'|trans({'%site%': ('site.name'|trans)}) }}{% endblock %}
{% block body %}
<h1>{{ 'home.h1'|trans({'%site%': ('site.name'|trans)}) }}</h1>
<p>{{ 'home.description'|trans }}</p>
<p><a href="{{ path('game_hub') }}">{{ 'link.enter_game'|trans }}</a></p>
{% endblock %}
+13
View File
@@ -0,0 +1,13 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG']) {
umask(0000);
}
View File
+16
View File
@@ -0,0 +1,16 @@
# General
site.name: EscapePage
nav.home: Home
nav.game: Game
link.enter_game: Enter the Game Area
link.back_to_website: Back to Website
# Home page
home.title: "Welcome | EscapePage"
home.h1: "Welcome to EscapePage"
home.description: "This is the public website. Minimal interaction, information about the game, and links."
# Game hub
game.title: "Game Hub | EscapePage"
game.h1: "Game Area"
game.description: "This is the game hub. Interactive components will be built here."
+16
View File
@@ -0,0 +1,16 @@
# Algemeen
site.name: EscapePage
nav.home: Start
nav.game: Spel
link.enter_game: Ga naar het Speelgedeelte
link.back_to_website: Terug naar Website
# Home pagina
home.title: "Welkom | %{site}%"
home.h1: "Welkom bij %{site}%"
home.description: "Dit is de publieke website. Beperkte interactie, informatie over het spel en links."
# Spel hub
game.title: "Spel Hub | %{site}%"
game.h1: "Speelgedeelte"
game.description: "Dit is de spelhub. Interactieve componenten worden hier later gebouwd."
+34
View File
@@ -0,0 +1,34 @@
const Encore = require('@symfony/webpack-encore');
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// clean output before build
.cleanupOutputBeforeBuild()
// main entry for your app
.addEntry('app', './assets/app.js')
.addEntry('game1', './assets/game1.js')
// split entry chunks for better caching
.splitEntryChunks()
// will require an extra script tag for runtime.js
.enableSingleRuntimeChunk()
// features
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
// Babel config for wide browser support
.configureBabelPresetEnv((options) => {
options.useBuiltIns = 'usage';
options.corejs = 3;
options.bugfixes = true;
})
.autoProvidejQuery(false)
.enableBuildNotifications()
;
module.exports = Encore.getWebpackConfig();