Some settings
This commit is contained in:
11
.env
11
.env
@@ -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
3
.idea/escapepage.iml
generated
@@ -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
8
.idea/laravel-idea.xml
generated
Normal 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>
|
||||||
5
.idea/php.xml
generated
5
.idea/php.xml
generated
@@ -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">
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -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.
|
||||||
|
|
||||||
|
## Real‑time updates with Mercure
|
||||||
|
We use a Mercure hub (Docker service) to push server updates to browsers via Server‑Sent 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).
|
||||||
|
|||||||
@@ -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
7
compose.override.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
services:
|
||||||
|
###> symfony/mercure-bundle ###
|
||||||
|
mercure:
|
||||||
|
ports:
|
||||||
|
- "80"
|
||||||
|
###< symfony/mercure-bundle ###
|
||||||
31
compose.yaml
Normal file
31
compose.yaml
Normal 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 ###
|
||||||
@@ -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
244
composer.lock
generated
@@ -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": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
6
config/packages/mercure.yaml
Normal file
6
config/packages/mercure.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mercure:
|
||||||
|
hubs:
|
||||||
|
default:
|
||||||
|
url: '%env(MERCURE_URL)%'
|
||||||
|
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||||
|
jwt: '%env(MERCURE_JWT_SECRET)%'
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
This document is for Junie (and humans) to keep our code style consistent and to quickly find the files we’ll 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 we’ll 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`.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
65
src/Command/MercurePublishCommand.php
Normal file
65
src/Command/MercurePublishCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/Game/Controller/GameApiController.php
Normal file
49
src/Game/Controller/GameApiController.php
Normal 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'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
symfony.lock
12
symfony.lock
@@ -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": {
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
Reference in New Issue
Block a user