diff --git a/.env b/.env index 59c349f..3a6f8fb 100644 --- a/.env +++ b/.env @@ -50,3 +50,14 @@ MAILER_DSN=smtp://mailer:1025 ###> 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 ### diff --git a/.idea/escapepage.iml b/.idea/escapepage.iml index c8f8089..beb36aa 100644 --- a/.idea/escapepage.iml +++ b/.idea/escapepage.iml @@ -133,6 +133,9 @@ + + + diff --git a/.idea/laravel-idea.xml b/.idea/laravel-idea.xml new file mode 100644 index 0000000..bd941a4 --- /dev/null +++ b/.idea/laravel-idea.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index 45f9711..6852c14 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -148,9 +148,12 @@ + + + - + @@ -174,4 +177,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index f8d3ac6..f65617e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. - 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. 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). diff --git a/assets/game1.js b/assets/game1.js index 8556c64..e6d5ffe 100644 --- a/assets/game1.js +++ b/assets/game1.js @@ -1,11 +1,100 @@ /* Game1 entry point built with Webpack Encore */ 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 // and confirm this specific bundle is loaded on the Game Hub page. console.log('Game1 bundle loaded'); // Example: add a CSS class to 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); + } }); diff --git a/compose.override.yaml b/compose.override.yaml new file mode 100644 index 0000000..187de4f --- /dev/null +++ b/compose.override.yaml @@ -0,0 +1,7 @@ + +services: +###> symfony/mercure-bundle ### + mercure: + ports: + - "80" +###< symfony/mercure-bundle ### diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..c4fa3b1 --- /dev/null +++ b/compose.yaml @@ -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 ### diff --git a/composer.json b/composer.json index 2a1acf1..15f4edf 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "minimum-stability": "stable", "prefer-stable": true, "require": { - "php": ">=8.2", + "php": ">=8.5.1", "ext-ctype": "*", "ext-iconv": "*", "doctrine/dbal": "^3", @@ -45,7 +45,8 @@ "symfony/yaml": "7.3.*", "twig/extra-bundle": "^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": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index aa3db5b..b918f0b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "385b5387c2854aefde2b8eea7c1e39cb", + "content-hash": "7b090cdc9768a74bdf1ef02cc14e5d8c", "packages": [ { "name": "composer/semver", @@ -1271,6 +1271,79 @@ ], "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", "version": "3.10.0", @@ -4238,6 +4311,173 @@ ], "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", "version": "v7.3.9", @@ -10262,7 +10502,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.2", + "php": ">=8.5.1", "ext-ctype": "*", "ext-iconv": "*" }, diff --git a/config/bundles.php b/config/bundles.php index aa107c6..003cd9c 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -14,4 +14,5 @@ return [ 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], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 1a48c4b..8e0dbc1 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -22,8 +22,6 @@ doctrine: dir: '%kernel.project_dir%/src' prefix: 'App' alias: App - controller_resolver: - auto_mapping: false when@test: doctrine: diff --git a/config/packages/mercure.yaml b/config/packages/mercure.yaml new file mode 100644 index 0000000..11dbcda --- /dev/null +++ b/config/packages/mercure.yaml @@ -0,0 +1,6 @@ +mercure: + hubs: + default: + url: '%env(MERCURE_URL)%' + public_url: '%env(MERCURE_PUBLIC_URL)%' + jwt: '%env(MERCURE_JWT_SECRET)%' diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 3f795d9..c14d8e1 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,6 +1,4 @@ twig: - file_name_pattern: '*.twig' - -when@test: - twig: - strict_variables: true + globals: + mercure_public_url: '%env(MERCURE_PUBLIC_URL)%' + mercure_topic_base: '%env(MERCURE_TOPIC_BASE)%' diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 0f5ee23..9cd5af1 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -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. -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 -- 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). - 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`. diff --git a/doc/docker.md b/doc/docker.md index 840ecec..ea506ca 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -3,7 +3,7 @@ This app can run fully in Docker using docker compose with PHP-FPM, Nginx and MySQL. ## 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 - database: MySQL 8.0 (data persisted in a volume) - mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI) diff --git a/docker/compose.yaml b/docker/compose.yaml index 35861cd..8b103ef 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -13,12 +13,13 @@ services: APP_ENV: dev depends_on: - database + - mercure networks: - backend restart: unless-stopped nginx: - image: nginx:1.27-alpine + image: nginx:1.29.4-alpine container_name: escapepage-nginx ports: - "8080:80" @@ -40,6 +41,25 @@ services: - 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 diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 984a452..41dfc85 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-fpm-alpine +FROM php:8.5.1-fpm-alpine3.23 # Install system deps RUN apk add --no-cache bash git icu-dev libzip-dev oniguruma-dev diff --git a/src/Command/MercurePublishCommand.php b/src/Command/MercurePublishCommand.php new file mode 100644 index 0000000..400062b --- /dev/null +++ b/src/Command/MercurePublishCommand.php @@ -0,0 +1,65 @@ +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('Invalid JSON provided for --data.'); + 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('Published update to topic: ' . $topic); + return Command::SUCCESS; + } +} diff --git a/src/Game/Controller/GameApiController.php b/src/Game/Controller/GameApiController.php new file mode 100644 index 0000000..04da718 --- /dev/null +++ b/src/Game/Controller/GameApiController.php @@ -0,0 +1,49 @@ +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|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'), + ]); + } +} diff --git a/src/Game/Controller/HubController.php b/src/Game/Controller/GameController.php similarity index 64% rename from src/Game/Controller/HubController.php rename to src/Game/Controller/GameController.php index db494c3..dbca841 100644 --- a/src/Game/Controller/HubController.php +++ b/src/Game/Controller/GameController.php @@ -7,11 +7,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; 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 { - return $this->render('game/hub/index.html.twig'); + return $this->render('game/index.html.twig'); } } diff --git a/symfony.lock b/symfony.lock index 9e08978..cdf33ac 100644 --- a/symfony.lock +++ b/symfony.lock @@ -155,6 +155,18 @@ "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": { diff --git a/templates/game/hub/index.html.twig b/templates/game/index.html.twig similarity index 64% rename from templates/game/hub/index.html.twig rename to templates/game/index.html.twig index 4e026f5..7433ccc 100644 --- a/templates/game/hub/index.html.twig +++ b/templates/game/index.html.twig @@ -12,6 +12,16 @@

{{ 'game.h1'|trans }}

{{ 'game.description'|trans }}

Game 1 assets are active. Enjoy the challenge!
+ +{# Hidden config element read by assets/game1.js #} + +

{{ 'link.back_to_website'|trans }}

{% endblock %}