Some settings

This commit is contained in:
Frank
2026-01-03 13:16:58 +01:00
parent 0d6628e7c9
commit af61a3b920
23 changed files with 616 additions and 23 deletions

11
.env
View File

@@ -50,3 +50,14 @@ MAILER_DSN=smtp://mailer:1025
###> symfony/sendgrid-mailer ### ###> symfony/sendgrid-mailer ###
# MAILER_DSN=sendgrid://KEY@default # MAILER_DSN=sendgrid://KEY@default
###< symfony/sendgrid-mailer ### ###< 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 ###

3
.idea/escapepage.iml generated
View File

@@ -133,6 +133,9 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" /> <excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" /> <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/sendgrid-mailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" />
</content> </content>

8
.idea/laravel-idea.xml generated Normal file
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>

7
.idea/php.xml generated
View File

@@ -148,9 +148,12 @@
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" /> <path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/symfony/sendgrid-mailer" /> <path value="$PROJECT_DIR$/vendor/symfony/sendgrid-mailer" />
<path value="$PROJECT_DIR$/vendor/symfony/webpack-encore-bundle" /> <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> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2"> <component name="PhpProjectSharedConfiguration" php_language_level="8.5.1">
<option name="suggestChangeDefaultLanguageLevel" value="false" /> <option name="suggestChangeDefaultLanguageLevel" value="false" />
</component> </component>
<component name="PhpStan"> <component name="PhpStan">
@@ -174,4 +177,4 @@
<component name="PsalmOptionsConfiguration"> <component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
</project> </project>

View File

@@ -1,6 +1,6 @@
# EscapePage — Online Escape Room # EscapePage — Online Escape Room
This repository contains a Symfony 7.3 (PHP >= 8.2) application for a collaborative online escape room experience. 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. - Start here: doc/FILES.md — quick file index.
- Development standards and workflows: doc/CONTRIBUTING.md - Development standards and workflows: doc/CONTRIBUTING.md
@@ -76,3 +76,44 @@ Notes:
- Built files are ignored by git except for `public/build/.gitignore` to keep the directory. - 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. 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).

View File

@@ -1,11 +1,100 @@
/* Game1 entry point built with Webpack Encore */ /* Game1 entry point built with Webpack Encore */
import './styles/game1.css'; import './styles/game1.css';
document.addEventListener('DOMContentLoaded', () => { 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 // Simple boot log so you can verify it in the browser console
// and confirm this specific bundle is loaded on the Game Hub page. // and confirm this specific bundle is loaded on the Game Hub page.
console.log('Game1 bundle loaded'); console.log('Game1 bundle loaded');
// Example: add a CSS class to <body> so page-specific styles can apply // Example: add a CSS class to <body> so page-specific styles can apply
document.body.classList.add('game1-page'); 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);
}
}); });

7
compose.override.yaml Normal file
View File

@@ -0,0 +1,7 @@
services:
###> symfony/mercure-bundle ###
mercure:
ports:
- "80"
###< symfony/mercure-bundle ###

31
compose.yaml Normal file
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 ###

View File

@@ -4,7 +4,7 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"require": { "require": {
"php": ">=8.2", "php": ">=8.5.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"doctrine/dbal": "^3", "doctrine/dbal": "^3",
@@ -45,7 +45,8 @@
"symfony/yaml": "7.3.*", "symfony/yaml": "7.3.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0", "twig/twig": "^2.12|^3.0",
"symfony/webpack-encore-bundle": "^2.1" "symfony/webpack-encore-bundle": "^2.1",
"symfony/mercure-bundle": "^0.3"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {

244
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "385b5387c2854aefde2b8eea7c1e39cb", "content-hash": "7b090cdc9768a74bdf1ef02cc14e5d8c",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
@@ -1271,6 +1271,79 @@
], ],
"time": "2025-03-06T22:45:56+00:00" "time": "2025-03-06T22:45:56+00:00"
}, },
{
"name": "lcobucci/jwt",
"version": "5.6.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-sodium": "*",
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/clock": "^1.0"
},
"require-dev": {
"infection/infection": "^0.29",
"lcobucci/clock": "^3.2",
"lcobucci/coding-standard": "^11.0",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.10.7",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-phpunit": "^1.3.10",
"phpstan/phpstan-strict-rules": "^1.5.0",
"phpunit/phpunit": "^11.1"
},
"suggest": {
"lcobucci/clock": ">= 3.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
"source": "https://github.com/lcobucci/jwt/tree/5.6.0"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2025-10-17T11:30:53+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.10.0", "version": "3.10.0",
@@ -4238,6 +4311,173 @@
], ],
"time": "2025-12-16T07:50:38+00:00" "time": "2025-12-16T07:50:38+00:00"
}, },
{
"name": "symfony/mercure",
"version": "v0.6.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mercure.git",
"reference": "304cf84609ef645d63adc65fc6250292909a461b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mercure/zipball/304cf84609ef645d63adc65fc6250292909a461b",
"reference": "304cf84609ef645d63adc65fc6250292909a461b",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/deprecation-contracts": "^2.0|^3.0|^4.0",
"symfony/http-client": "^4.4|^5.0|^6.0|^7.0",
"symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0",
"symfony/polyfill-php80": "^1.22",
"symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"lcobucci/jwt": "^3.4|^4.0|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0|^6.0|^7.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
"symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
"symfony/stopwatch": "^4.4|^5.0|^6.0|^7.0",
"twig/twig": "^2.0|^3.0|^4.0"
},
"suggest": {
"symfony/stopwatch": "Integration with the profiler performances"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/dunglas/mercure",
"name": "dunglas/mercure"
},
"branch-alias": {
"dev-main": "0.6.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Mercure\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Mercure Component",
"homepage": "https://symfony.com",
"keywords": [
"mercure",
"push",
"sse",
"updates"
],
"support": {
"issues": "https://github.com/symfony/mercure/issues",
"source": "https://github.com/symfony/mercure/tree/v0.6.5"
},
"funding": [
{
"url": "https://github.com/dunglas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/mercure",
"type": "tidelift"
}
],
"time": "2024-04-08T12:51:34+00:00"
},
{
"name": "symfony/mercure-bundle",
"version": "v0.3.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/mercure-bundle.git",
"reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/77435d740b228e9f5f3f065b6db564f85f2cdb64",
"reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64",
"shasum": ""
},
"require": {
"lcobucci/jwt": "^3.4|^4.0|^5.0",
"php": ">=7.1.3",
"symfony/config": "^4.4|^5.0|^6.0|^7.0",
"symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
"symfony/mercure": "^0.6.1",
"symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.3.7|^5.0|^6.0|^7.0",
"symfony/stopwatch": "^4.3.7|^5.0|^6.0|^7.0",
"symfony/ux-turbo": "*",
"symfony/var-dumper": "^4.3.7|^5.0|^6.0|^7.0"
},
"suggest": {
"symfony/messenger": "To use the Messenger integration"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-main": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MercureBundle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MercureBundle",
"homepage": "https://symfony.com",
"keywords": [
"mercure",
"push",
"sse",
"updates"
],
"support": {
"issues": "https://github.com/symfony/mercure-bundle/issues",
"source": "https://github.com/symfony/mercure-bundle/tree/v0.3.9"
},
"funding": [
{
"url": "https://github.com/dunglas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle",
"type": "tidelift"
}
],
"time": "2024-05-31T09:07:18+00:00"
},
{ {
"name": "symfony/messenger", "name": "symfony/messenger",
"version": "v7.3.9", "version": "v7.3.9",
@@ -10262,7 +10502,7 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=8.2", "php": ">=8.5.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*" "ext-iconv": "*"
}, },

View File

@@ -14,4 +14,5 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
]; ];

View File

@@ -22,8 +22,6 @@ doctrine:
dir: '%kernel.project_dir%/src' dir: '%kernel.project_dir%/src'
prefix: 'App' prefix: 'App'
alias: App alias: App
controller_resolver:
auto_mapping: false
when@test: when@test:
doctrine: doctrine:

View File

@@ -0,0 +1,6 @@
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt: '%env(MERCURE_JWT_SECRET)%'

View File

@@ -1,6 +1,4 @@
twig: twig:
file_name_pattern: '*.twig' globals:
mercure_public_url: '%env(MERCURE_PUBLIC_URL)%'
when@test: mercure_topic_base: '%env(MERCURE_TOPIC_BASE)%'
twig:
strict_variables: true

View File

@@ -2,11 +2,11 @@
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. 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.2, Doctrine ORM 3, Twig, Stimulus (UX), Importmap/Asset Mapper, PHPUnit 11. Project type: Symfony 7.3, PHP >= 8.5.1, Doctrine ORM 3, Twig, Stimulus (UX), Importmap/Asset Mapper, PHPUnit 11.
## 1. Repository Conventions ## 1. Repository Conventions
- PHP version: 8.2+ (composer.json enforces ">=8.2"). - PHP version: 8.5.1+ (composer.json enforces ">=8.5.1").
- Framework: Symfony 7.3.* (see composer.json). - 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`. - 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`. - Env files: `.env`, `.env.local` (ignored), and environment overrides like `.env.test.local`.

View File

@@ -3,7 +3,7 @@
This app can run fully in Docker using docker compose with PHP-FPM, Nginx and MySQL. This app can run fully in Docker using docker compose with PHP-FPM, Nginx and MySQL.
## Services ## Services
- php: PHP 8.2 FPM with required extensions and Composer - php: PHP 8.5.1 FPM with required extensions and Composer
- nginx: Serves the Symfony app from public/ and proxies PHP to php-fpm - nginx: Serves the Symfony app from public/ and proxies PHP to php-fpm
- database: MySQL 8.0 (data persisted in a volume) - database: MySQL 8.0 (data persisted in a volume)
- mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI) - mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI)

View File

@@ -13,12 +13,13 @@ services:
APP_ENV: dev APP_ENV: dev
depends_on: depends_on:
- database - database
- mercure
networks: networks:
- backend - backend
restart: unless-stopped restart: unless-stopped
nginx: nginx:
image: nginx:1.27-alpine image: nginx:1.29.4-alpine
container_name: escapepage-nginx container_name: escapepage-nginx
ports: ports:
- "8080:80" - "8080:80"
@@ -40,6 +41,25 @@ services:
- backend - backend
restart: unless-stopped 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 ### ###> doctrine/doctrine-bundle ###
database: database:
image: mysql:8.0 image: mysql:8.0

View File

@@ -1,4 +1,4 @@
FROM php:8.2-fpm-alpine FROM php:8.5.1-fpm-alpine3.23
# Install system deps # Install system deps
RUN apk add --no-cache bash git icu-dev libzip-dev oniguruma-dev RUN apk add --no-cache bash git icu-dev libzip-dev oniguruma-dev

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;
}
}

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'),
]);
}
}

View File

@@ -7,11 +7,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
final class HubController extends AbstractController final class GameController extends AbstractController
{ {
#[Route(path: '', name: 'game_hub')] #[Route(path: '', name: 'game')]
public function index(): Response public function index(): Response
{ {
return $this->render('game/hub/index.html.twig'); return $this->render('game/index.html.twig');
} }
} }

View File

@@ -155,6 +155,18 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" "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": { "symfony/messenger": {
"version": "7.3", "version": "7.3",
"recipe": { "recipe": {

View File

@@ -12,6 +12,16 @@
<h1>{{ 'game.h1'|trans }}</h1> <h1>{{ 'game.h1'|trans }}</h1>
<p>{{ 'game.description'|trans }}</p> <p>{{ 'game.description'|trans }}</p>
<div class="game1-banner">Game 1 assets are active. Enjoy the challenge!</div> <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> <p><a href="{{ path('website_home') }}">{{ 'link.back_to_website'|trans }}</a></p>
{% endblock %} {% endblock %}