From 7219471a8615f305b4570395828adbafd072b7cc Mon Sep 17 00:00:00 2001
From: Lang <7system7@gmail.com>
Date: Thu, 9 Apr 2026 22:00:53 +0200
Subject: [PATCH] chg: dev: replace the legacy gos/web-socket-bundle & replace
it with Mercure protocol #4
---
.env.dist | 10 +
assets/js/mine-seeker.js | 1 -
assets/js/mine-seeker/app.js | 409 ++--
assets/js/mine-seeker/grid/grid-control.js | 7 +-
composer.json | 5 +-
composer.lock | 1790 +++++------------
config/bundles.php | 3 +-
config/packages/gos_web_socket.yaml | 28 -
config/packages/mercure.yaml | 8 +
config/pubsub/routing.yaml | 14 -
config/routes/mineseeker.yaml | 28 +-
config/services.yaml | 43 +-
package.json | 1 -
src/Command/WebsocketServerCommand.php | 79 -
src/Controller/GameController.php | 13 +-
src/Controller/MercureController.php | 98 +
src/Entity/PlayedGame.php | 187 +-
src/Entity/Step.php | 2 +-
.../MineseekerClientEventListener.php | 60 -
src/Interfaces/TopicManagerInterface.php | 9 +-
src/Interfaces/WebsocketManagerInterface.php | 28 -
src/Migrations/.gitignore | 0
.../2026/04/Version20260409194708.php | 42 +
src/Periodic/MinePeriodic.php | 51 -
src/Rpc/MineseekerRpc.php | 69 -
src/Topic/MineseekerTopic.php | 95 -
src/Util/RpcManager.php | 39 +-
src/Util/TopicManager.php | 261 +--
src/Util/WebsocketManager.php | 47 -
symfony.lock | 60 +-
templates/Game/play.html.twig | 11 +-
templates/Recent/facebook.html.twig | 23 -
templates/base.html.twig | 1 -
33 files changed, 1198 insertions(+), 2324 deletions(-)
delete mode 100644 config/packages/gos_web_socket.yaml
create mode 100644 config/packages/mercure.yaml
delete mode 100644 config/pubsub/routing.yaml
delete mode 100644 src/Command/WebsocketServerCommand.php
create mode 100644 src/Controller/MercureController.php
delete mode 100644 src/EventListener/MineseekerClientEventListener.php
delete mode 100644 src/Interfaces/WebsocketManagerInterface.php
delete mode 100644 src/Migrations/.gitignore
create mode 100644 src/Migrations/2026/04/Version20260409194708.php
delete mode 100644 src/Periodic/MinePeriodic.php
delete mode 100644 src/Rpc/MineseekerRpc.php
delete mode 100644 src/Topic/MineseekerTopic.php
delete mode 100644 src/Util/WebsocketManager.php
delete mode 100644 templates/Recent/facebook.html.twig
diff --git a/.env.dist b/.env.dist
index f522612..5acf959 100644
--- a/.env.dist
+++ b/.env.dist
@@ -18,3 +18,13 @@ DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
###> symfony/mailer ###
# MAILER_DSN=smtp://localhost
###< symfony/mailer ###
+
+###> symfony/mercure-bundle ###
+# See https://symfony.com/doc/current/mercure.html#configuration
+# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
+MERCURE_URL=https://example.com/.well-known/mercure
+# The public URL of the Mercure hub, used by the browser to connect
+MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
+# The secret used to sign the JWTs
+MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
+###< symfony/mercure-bundle ###
diff --git a/assets/js/mine-seeker.js b/assets/js/mine-seeker.js
index 41d245e..cfb2252 100644
--- a/assets/js/mine-seeker.js
+++ b/assets/js/mine-seeker.js
@@ -6,7 +6,6 @@ ReactDOM.render(
,
document.getElementById("mine-wrapper"),
);
diff --git a/assets/js/mine-seeker/app.js b/assets/js/mine-seeker/app.js
index c0118a8..c142249 100644
--- a/assets/js/mine-seeker/app.js
+++ b/assets/js/mine-seeker/app.js
@@ -11,16 +11,22 @@ class MineSeeker extends React.Component {
this.state = {
env: props.env,
- ssl: props.ssl,
gameInherited: '' !== props.gameId,
gameAssoc: gameAssoc,
channel: channel,
- session: null,
- createGrid: false,
stepCache: [],
connectionLost: false,
end: false,
};
+
+ /** SSE connection (not React state — no re-render needed) */
+ this.eventSource = null;
+
+ /**
+ * Users from GET /api/game/connect (inherited game).
+ * Passed to wSubscribe so it can determine the local player's colour.
+ */
+ this.rpcUsers = null;
}
currectGridSize() {
@@ -37,9 +43,6 @@ class MineSeeker extends React.Component {
/**
* STEP
- *
- * @param coords
- * @returns {{red: *, blue: *}}
*/
makePointsCalcAndStep(coords) {
let users = this.refs.gridControl.refs.userControl,
@@ -63,14 +66,10 @@ class MineSeeker extends React.Component {
/**
* START
- *
- * @param payload
*/
makeGameStart(payload) {
- /** every time the blue starts */
this.refs.gridControl.refs.userControl.setState({ activePlayer: 1 });
- /** Set up player names w/ server data */
this.refs.gridControl.refs.userControl.refs.red.setState({
name: '' !== payload.users.red ? payload.users.red : payload.users.redAnon,
});
@@ -88,10 +87,6 @@ class MineSeeker extends React.Component {
/**
* THE END
- *
- * @param bluePoints
- * @param redPoints
- * @param resign
*/
makeGameEndIfItEnds(bluePoints, redPoints, resign = false) {
let redWins = 25 < redPoints,
@@ -109,7 +104,6 @@ class MineSeeker extends React.Component {
}
this.refs.gridControl.showLeftMines();
-
this.refs.gridControl.refs.userControl.setState({ activePlayer: false });
this.refs.gridControl.refs.userControl.refs.red.setState({ desc: '' });
this.refs.gridControl.refs.userControl.refs.blue.setState({ desc: '' });
@@ -128,25 +122,19 @@ class MineSeeker extends React.Component {
});
this.setState({ end: true });
-
this.makeGameEndIfItEnds(0, 0, true);
}
clickResign() {
- /** PUBLISH */
- this.state.session.publish(this.state.channel, {
- 'resign': this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red',
- });
+ let resignColor = this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red';
+ this.publishStep({ resign: resignColor });
this.resignProcess(this.refs.gridControl.state.webPlayer);
}
clickResignCancel() {
- this.refs.gridControl.setState({
- overlay: false,
- });
+ this.refs.gridControl.setState({ overlay: false });
}
- /** RESIGN */
resign() {
let users = this.refs.gridControl.refs.userControl,
activePlayer = users.state.activePlayer ? 'blue' : 'red';
@@ -165,11 +153,133 @@ class MineSeeker extends React.Component {
}
}
- wInit(session, gridServer, gridClient) {
- this.setState({ session: session });
+ // ------------------------------------------------------------------ //
+ // Mercure message handlers (same logic as former WAMP callbacks)
+ // ------------------------------------------------------------------ //
- /** save session to GridControl */
- /** render grid fields - @see #12 */
+ wSubscribe(payload, rpcUsers = null) {
+ 'dev' === this.state.env && console.info(
+ ('undefined' !== typeof payload.user ? payload.user : 'user') + ' has been subscribed to the channel!',
+ );
+
+ let firstUser = !rpcUsers;
+
+ null === this.refs.gridControl.state.webPlayer && this.refs.gridControl.setState({
+ webPlayer: payload.user === payload.users.blue
+ || (
+ firstUser && '' !== payload.users.blueAnon
+ || !firstUser && ('' === rpcUsers.blueAnon && '' === rpcUsers.blue)
+ )
+ ? 'blue' : 'red',
+ });
+
+ (900 > $(document).width()) && this.currectGridSize();
+
+ if (
+ 2 === payload.userCnt
+ && (
+ !this.state.connectionLost
+ || this.state.connectionLost && false === this.refs.gridControl.refs.userControl.state.activePlayer && !this.state.end
+ )
+ ) {
+ this.makeGameStart(payload);
+ }
+ }
+
+ wUnsubscribe(payload) {
+ 'dev' === this.state.env && console.info(payload.msg);
+
+ this.refs.gridControl.setState({
+ overlay: true,
+ overlayTitle: 'The connection has been lost w/ your friend...',
+ overlaySubTitle: 'Please, restart the game!',
+ });
+ }
+
+ wTopic(payload) {
+ if (this.refs.gridControl.state.webPlayer !== payload.data.player) {
+ if (null === payload.data.resign) {
+ 'dev' === this.state.env && console.warn(payload.user + ' has been stepped to coords: ' + payload.data.coords[0] + ', ' + payload.data.coords[1]);
+ 'dev' === this.state.env && console.warn('Opponent stepped: Auto-Step process');
+
+ this.refs.gridControl.refs.userControl.setState({ bombSelected: payload.data.bomb });
+
+ let points = this.makePointsCalcAndStep(payload.data.coords);
+ this.makeGameEndIfItEnds(points.blue, points.red);
+ } else {
+ this.resignProcess(payload.data.resign);
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------ //
+ // Mercure / SSE connection
+ // ------------------------------------------------------------------ //
+
+ /**
+ * Dispatches every incoming SSE message.
+ * Distinguishes subscription events, game-step events, and disconnect events
+ * using the same payload shape as the former WAMP broadcast.
+ */
+ handleMercureMessage(payload) {
+ let isTopicEvent = 'undefined' !== typeof payload.data;
+ let isNotUnsubscribe = 'undefined' === typeof payload.msg;
+
+ if (isTopicEvent) {
+ this.wTopic(payload);
+ } else if (isNotUnsubscribe) {
+ this.wSubscribe(payload, this.rpcUsers);
+ } else {
+ this.wUnsubscribe(payload);
+ }
+
+ /** Reconnection: replay cached steps once both players are back */
+ if (2 === payload.userCnt && this.state.connectionLost) {
+ 'dev' === this.state.env && console.info('Reconnection process');
+
+ let cache = this.state.stepCache;
+ cache.forEach(item => this.publishStep(item));
+ this.setState({ connectionLost: false, stepCache: [] });
+ }
+ }
+
+ openEventSource() {
+ const wrapper = document.getElementById('mine-wrapper');
+ const hubUrl = wrapper.dataset.mercureHubUrl;
+ const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
+
+ const url = new URL(hubUrl, window.location.origin);
+ url.searchParams.append('topic', this.state.channel);
+ if (subscriberJwt) {
+ url.searchParams.append('authorization', subscriberJwt);
+ }
+
+ if (this.eventSource) {
+ this.eventSource.close();
+ }
+
+ this.eventSource = new EventSource(url.toString());
+
+ this.eventSource.onmessage = event => {
+ this.handleMercureMessage(JSON.parse(event.data));
+ };
+
+ this.eventSource.onopen = () => {
+ 'dev' === this.state.env && console.info('SSE connection opened');
+
+ if (this.state.connectionLost) {
+ 'dev' === this.state.env && console.info('SSE reconnected — rejoining channel');
+ this.joinGame();
+ }
+ };
+
+ this.eventSource.onerror = () => {
+ 'dev' === this.state.env && console.error('SSE connection error / lost');
+ this.setState({ connectionLost: true });
+ };
+ }
+
+ wInit(gridServer, gridClient) {
this.refs.gridControl.setState({
grid: this.state.gameInherited ? gridServer : gridClient,
channel: this.state.channel,
@@ -207,177 +317,86 @@ class MineSeeker extends React.Component {
});
}
- wSubscribe(payload, rpcUsers = null) {
- 'dev' === this.state.env && console.info(
- ('undefined' !== typeof payload.user ? payload.user : 'user') + ' has been subscribed to the channel!',
- );
-
- let firstUser = !rpcUsers;
-
- null === this.refs.gridControl.state.webPlayer && this.refs.gridControl.setState({
- webPlayer: payload.user === payload.users.blue
- || (
- firstUser && '' !== payload.users.blueAnon
- || !firstUser && ('' === rpcUsers.blueAnon && '' === rpcUsers.blue)
- )
- ? 'blue' : 'red',
- });
-
- /** rwd */
- (900 > $(document).width()) && this.currectGridSize();
-
- /** every user has been came */
- if (
- 2 === payload.userCnt
- && (
- !this.state.connectionLost
- || this.state.connectionLost && false === this.refs.gridControl.refs.userControl.state.activePlayer && !this.state.end
- )
- ) {
- this.makeGameStart(payload);
- }
+ /** POST /api/game/join — register this player, broadcast subscription event via Mercure */
+ joinGame() {
+ return fetch('/api/game/join/' + this.state.gameAssoc, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ }).catch(e => 'dev' === this.state.env && console.error('Join error', e));
}
- wUnsubscribe(payload) {
- 'dev' === this.state.env && console.info(payload.msg);
-
- this.refs.gridControl.setState({
- overlay: true,
- overlayTitle: 'The connection has been lost w/ your friend...',
- overlaySubTitle: 'Please, restart the game!',
- });
+ /** POST /api/game/step — persist a move and fan it out via Mercure */
+ publishStep(dataPack) {
+ return fetch('/api/game/step/' + this.state.gameAssoc, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(dataPack),
+ }).catch(e => 'dev' === this.state.env && console.error('Step error', e));
}
- wTopic(payload) {
- /** Auto-Step if this player is not the current user */
- if (this.refs.gridControl.state.webPlayer !== payload.data.player) {
- if (null === payload.data.resign) {
- 'dev' === this.state.env && console.warn(payload.user + ' has been stepped to coords: ' + payload.data.coords[0] + ', ' + payload.data.coords[1]);
- 'dev' === this.state.env && console.warn('Opponent stepped: Auto-Step process');
+ // ------------------------------------------------------------------ //
+ // Lifecycle
+ // ------------------------------------------------------------------ //
- this.refs.gridControl.refs.userControl.setState({ bombSelected: payload.data.bomb });
+ async componentDidMount() {
+ if (!this.state.connectionLost) {
+ let gridClient = this.state.gameInherited ? null : new Grid().state.grid;
- /** STEP */
- let points = this.makePointsCalcAndStep(payload.data.coords);
+ try {
+ if (this.state.gameInherited) {
+ /** Fetch existing grid and player info */
+ const resp = await fetch('/api/game/connect/' + this.state.gameAssoc);
+ const b64 = await resp.text();
+ const serverData = JSON.parse(window.atob(b64));
- /** THE END */
- this.makeGameEndIfItEnds(points.blue, points.red);
- } else {
- /** RESIGN */
- /** THE END */
- this.resignProcess(payload.data.resign);
- }
- }
- }
-
- /** Connect - Subscribe */
- subscribe(rpcUsers = null) {
- this.state.session.subscribe(
- this.state.channel,
- (uri, payload) => {
- let isTopicEvent = 'undefined' !== typeof payload.data,
- isNotUnsubscribe = 'undefined' === typeof payload.msg;
-
- /** CONNECTION */
- if (isTopicEvent) {
- this.wTopic(payload);
- } else {
- if (isNotUnsubscribe) {
- this.wSubscribe(payload, rpcUsers);
- } else {
- this.wUnsubscribe(payload);
+ if ('undefined' === typeof serverData.grid || null === serverData.grid) {
+ this.refs.gridControl.setState({
+ overlay: true,
+ overlayTitle: 'This channel does not exists!',
+ overlaySubTitle: Restart game!,
+ });
+ console.error('This channel does not exists!');
+ return;
}
+
+ this.rpcUsers = serverData.users;
+ this.openEventSource();
+ this.wInit(serverData.grid, null);
+
+ } else {
+ /** Create the game record with this client's grid */
+ await fetch('/api/game/start', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ grid: window.btoa(JSON.stringify(gridClient)),
+ gameAssoc: this.state.gameAssoc,
+ }),
+ });
+
+ this.openEventSource();
+ this.wInit(null, gridClient);
}
- /** RECONNECTION */
- if (2 === payload.userCnt && this.state.connectionLost) {
- 'dev' === this.state.env && console.info('Reconnection process');
+ 'dev' === this.state.env && console.info('Connection initialised — joining channel');
+ await this.joinGame();
- /** PUBLISH */
- let cache = this.state.stepCache;
- cache.forEach(item => this.state.session.publish(this.state.channel, item));
- this.setState({ connectionLost: false, stepCache: [] });
- }
- });
- }
-
- connectWithWebsocket() {
- /** Create Websocket w/ Bahnhof.js */
- let websocket = WS.connect(
- ('true' === this.state.ssl ? 'wss' : 'ws') + '://' + window.location.hostname + '/ws/',
- );
-
- /**
- * Connect
- * Session is an Autobahn JS WAMP session.
- */
- websocket.on('socket/connect', session => {
- 'dev' === this.state.env && console.info('Successfully connected to the Server!');
-
- if (!this.state.connectionLost) {
- let gridClient = this.state.gameInherited || new Grid().state.grid;
-
- /**
- * Connect - RPC
- * Send grid information to the server
- */
- session
- .call(
- this.state.gameInherited ? 'mineseeker-rpc/connectGame' : 'mineseeker-rpc/startGame',
- this.state.gameInherited ? this.state.gameAssoc : [window.btoa(JSON.stringify(gridClient)), this.state.gameAssoc],
- )
- .then(
- data => {
- 'dev' === this.state.env && console.info('RPC has been called');
-
- let serverData = true !== data[0]
- ? JSON.parse(window.atob(data))
- : data;
-
- /** Check the grid if the user is inherited @see #30 */
- if ((this.state.gameInherited && 'undefined' !== typeof serverData.grid) || !this.state.gameInherited) {
- this.wInit(session, serverData.grid, gridClient);
- this.subscribe(this.state.gameInherited && serverData.users);
- } else {
- this.refs.gridControl.setState({
- overlay: true,
- overlayTitle: 'This channel does not exists!',
- overlaySubTitle: Restart game!,
- });
-
- console.error('This channel does not exists!');
- }
- },
- (error, desc) => 'dev' === this.state.env && console.error(['RPC Error', error, desc]),
- );
- } else {
- this.setState({ session: session });
- this.subscribe();
+ } catch (e) {
+ 'dev' === this.state.env && console.error('Connection error', e);
+ setTimeout(() => this.componentDidMount(), 500);
}
- });
- /**
- * DisConnect
- * Error provides us with some insight into the disconnection: error.reason and error.code
- */
- websocket.on('socket/disconnect', error => {
- 'dev' === this.state.env && console.error('Disconnected for ' + error.reason + ' with code ' + error.code);
+ } else {
+ /** Hard-reconnect path */
+ this.openEventSource();
+ }
- 6 === error.code && this.setState({ connectionLost: true });
- 3 === error.code && setTimeout(this.componentDidMount.bind(this), 500);
+ /** Notify the server when the player closes / navigates away */
+ window.addEventListener('pagehide', () => {
+ navigator.sendBeacon('/api/game/leave/' + this.state.gameAssoc);
});
}
- /** After rendering */
- componentDidMount() {
- this.connectWithWebsocket();
- }
-
- /**
- * Cache the steps unless reconnection
- *
- * @param dataPack
- */
cachePublish(dataPack) {
let cache = this.state.stepCache;
cache.push(dataPack);
@@ -387,30 +406,24 @@ class MineSeeker extends React.Component {
onClick(coords) {
let activePlayer = this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red';
- /** if the clicked field is NEVER CLICKED */
if (this.refs.gridControl.checkFieldHasBeenNeverClicked(coords[0], coords[1])) {
- /** Player step and it is the current player */
if (activePlayer === this.refs.gridControl.state.webPlayer) {
- /** STEP */
let points = this.makePointsCalcAndStep(coords);
-
- /** THE END */
this.makeGameEndIfItEnds(points.blue, points.red);
let dataPack = {
- 'coords': coords,
- 'player': activePlayer,
- 'bomb': this.refs.gridControl.refs.userControl.state.bombSelected,
- 'redPoints': points.red,
- 'bluePoints': points.blue,
- 'resign': null,
- 'redExplodedBomb': 'red' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
- 'blueExplodedBomb': 'blue' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
+ coords: coords,
+ player: activePlayer,
+ bomb: this.refs.gridControl.refs.userControl.state.bombSelected,
+ redPoints: points.red,
+ bluePoints: points.blue,
+ resign: null,
+ redExplodedBomb: 'red' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
+ blueExplodedBomb: 'blue' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
};
- /** PUBLISH */
!this.state.connectionLost
- ? this.state.session.publish(this.state.channel, dataPack)
+ ? this.publishStep(dataPack)
: this.cachePublish(dataPack);
}
}
@@ -428,4 +441,4 @@ class MineSeeker extends React.Component {
}
}
-export default MineSeeker;
+export default MineSeeker;
\ No newline at end of file
diff --git a/assets/js/mine-seeker/grid/grid-control.js b/assets/js/mine-seeker/grid/grid-control.js
index 3ad8677..7a7de85 100644
--- a/assets/js/mine-seeker/grid/grid-control.js
+++ b/assets/js/mine-seeker/grid/grid-control.js
@@ -173,8 +173,11 @@ class GridControl extends React.Component {
inactivePlayer = userControl.state.activePlayer ? 'red' : 'blue';
if (
- userControl.state.bombSelected && idx === (max - 1)
- || !idx && !userControl.state.bombSelected && 'm' !== currentObject
+ userControl.state.bombSelected
+ && idx === (max - 1)
+ || !idx
+ && !userControl.state.bombSelected
+ && 'm' !== currentObject
) {
userControl.setState({
activePlayer: userControl.state.activePlayer ? 0 : 1,
diff --git a/composer.json b/composer.json
index 6fb2dc6..f502655 100644
--- a/composer.json
+++ b/composer.json
@@ -9,11 +9,13 @@
"doctrine/doctrine-bundle": ">=2.11 <2.14",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.6",
- "gos/web-socket-bundle": "^3.0",
"symfony/console": "6.4.*",
"symfony/flex": "^2.10.0",
"symfony/framework-bundle": "6.4.*",
+ "symfony/http-client": "6.4.*",
"symfony/mailer": "6.4.*",
+ "symfony/mercure": "^0.6",
+ "symfony/mercure-bundle": "*",
"symfony/monolog-bundle": "^3.8",
"symfony/security-bundle": "6.4.*",
"symfony/translation": "6.4.*",
@@ -22,6 +24,7 @@
"symfony/yaml": "6.4.*"
},
"require-dev": {
+ "firebase/php-jwt": "^7.0",
"roave/security-advisories": "dev-master",
"symfony/dotenv": "6.4.*",
"symfony/maker-bundle": "^1.5",
diff --git a/composer.lock b/composer.lock
index e47e3c3..9924347 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,71 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ca1c9aca666ca6afdb45abc43022ffa5",
+ "content-hash": "c77d12baceb4246bf494147134d62712",
"packages": [
- {
- "name": "cboden/ratchet",
- "version": "v0.4.4",
- "source": {
- "type": "git",
- "url": "https://github.com/ratchetphp/Ratchet.git",
- "reference": "5012dc954541b40c5599d286fd40653f5716a38f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/5012dc954541b40c5599d286fd40653f5716a38f",
- "reference": "5012dc954541b40c5599d286fd40653f5716a38f",
- "shasum": ""
- },
- "require": {
- "guzzlehttp/psr7": "^1.7|^2.0",
- "php": ">=5.4.2",
- "ratchet/rfc6455": "^0.3.1",
- "react/event-loop": ">=0.4",
- "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5",
- "symfony/http-foundation": "^2.6|^3.0|^4.0|^5.0|^6.0",
- "symfony/routing": "^2.6|^3.0|^4.0|^5.0|^6.0"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.8"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Ratchet\\": "src/Ratchet"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "role": "Developer"
- },
- {
- "name": "Matt Bonneau",
- "role": "Developer"
- }
- ],
- "description": "PHP WebSocket library",
- "homepage": "http://socketo.me",
- "keywords": [
- "Ratchet",
- "WebSockets",
- "server",
- "sockets",
- "websocket"
- ],
- "support": {
- "chat": "https://gitter.im/reactphp/reactphp",
- "issues": "https://github.com/ratchetphp/Ratchet/issues",
- "source": "https://github.com/ratchetphp/Ratchet/tree/v0.4.4"
- },
- "time": "2021-12-14T00:20:41+00:00"
- },
{
"name": "doctrine/cache",
"version": "2.2.0",
@@ -1457,423 +1394,77 @@
"time": "2025-03-06T22:45:56+00:00"
},
{
- "name": "evenement/evenement",
- "version": "v3.0.2",
+ "name": "lcobucci/jwt",
+ "version": "5.6.0",
"source": {
"type": "git",
- "url": "https://github.com/igorw/evenement.git",
- "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
+ "url": "https://github.com/lcobucci/jwt.git",
+ "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
- "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
+ "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "ext-openssl": "*",
+ "ext-sodium": "*",
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/clock": "^1.0"
},
"require-dev": {
- "phpunit/phpunit": "^9 || ^6"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Evenement\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Igor Wiedler",
- "email": "igor@wiedler.ch"
- }
- ],
- "description": "Événement is a very simple event dispatching library for PHP",
- "keywords": [
- "event-dispatcher",
- "event-emitter"
- ],
- "support": {
- "issues": "https://github.com/igorw/evenement/issues",
- "source": "https://github.com/igorw/evenement/tree/v3.0.2"
- },
- "time": "2023-08-08T05:53:35+00:00"
- },
- {
- "name": "gos/pubsub-router-bundle",
- "version": "v2.8.0",
- "source": {
- "type": "git",
- "url": "https://github.com/GeniusesOfSymfony/PubSubRouterBundle.git",
- "reference": "f91b170d521c19e5fc3ec1c401652536c2d0f007"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/GeniusesOfSymfony/PubSubRouterBundle/zipball/f91b170d521c19e5fc3ec1c401652536c2d0f007",
- "reference": "f91b170d521c19e5fc3ec1c401652536c2d0f007",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0",
- "symfony/config": "^4.4.42 || ^5.4 || ^6.0",
- "symfony/console": "^4.4.42 || ^5.4 || ^6.0",
- "symfony/dependency-injection": "^4.4.42 || ^5.4 || ^6.0",
- "symfony/deprecation-contracts": "^2.1 || ^3.0",
- "symfony/http-foundation": "^4.4.42 || ^5.4 || ^6.0",
- "symfony/http-kernel": "^4.4.42 || ^5.4 || ^6.0",
- "symfony/polyfill-php80": "^1.22",
- "symfony/yaml": "^4.4.42 || ^5.4 || ^6.0"
- },
- "require-dev": {
- "matthiasnoback/symfony-dependency-injection-test": "^4.1.2",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "1.8.5",
- "phpstan/phpstan-phpunit": "1.1.1",
- "phpstan/phpstan-symfony": "1.2.13",
- "phpunit/phpunit": "^8.5 || ^9.3",
- "psr/container": "^1.0 || ^2.0",
- "symfony/phpunit-bridge": "^5.4 || ^6.0"
- },
- "type": "symfony-bundle",
- "autoload": {
- "psr-4": {
- "Gos\\Bundle\\PubSubRouterBundle\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Johann Saunier",
- "email": "johann_27@hotmail.fr"
- }
- ],
- "description": "Symfony PubSub Router Bundle",
- "homepage": "https://github.com/GeniusesOfSymfony/PubSubRouterBundle",
- "keywords": [
- "PubSub Bundle",
- "WAMP",
- "bundle",
- "pubsub",
- "redis",
- "zmq"
- ],
- "support": {
- "issues": "https://github.com/GeniusesOfSymfony/PubSubRouterBundle/issues",
- "source": "https://github.com/GeniusesOfSymfony/PubSubRouterBundle/tree/v2.8.0"
- },
- "funding": [
- {
- "url": "https://github.com/mbabker",
- "type": "github"
- }
- ],
- "time": "2022-09-20T01:46:13+00:00"
- },
- {
- "name": "gos/web-socket-bundle",
- "version": "v3.15.0",
- "source": {
- "type": "git",
- "url": "https://github.com/GeniusesOfSymfony/WebSocketBundle.git",
- "reference": "1c718de6434b87e28c65376fa61b159d24053aae"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/GeniusesOfSymfony/WebSocketBundle/zipball/1c718de6434b87e28c65376fa61b159d24053aae",
- "reference": "1c718de6434b87e28c65376fa61b159d24053aae",
- "shasum": ""
- },
- "require": {
- "cboden/ratchet": "^0.4.4",
- "gos/pubsub-router-bundle": "^2.2",
- "gos/websocket-client": "^1.1",
- "php": "^7.4 || ^8.0",
- "psr/log": "^1.1 || ^2.0 || ^3.0",
- "react/event-loop": "^1.2",
- "react/socket": "^1.9",
- "symfony/config": "^4.4 || ^5.4 || ^6.0",
- "symfony/console": "^4.4 || ^5.4 || ^6.0",
- "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0",
- "symfony/deprecation-contracts": "^2.1 || ^3.0",
- "symfony/event-dispatcher": "^4.4 || ^5.4 || ^6.0",
- "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0",
- "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0",
- "symfony/polyfill-php80": "^1.15",
- "symfony/security-core": "^4.4 || ^5.4 || ^6.0",
- "symfony/serializer": "^4.4 || ^5.4 || ^6.0",
- "symfony/string": "^5.4 || ^6.0",
- "symfony/yaml": "^4.4 || ^5.4 || ^6.0"
- },
- "conflict": {
- "doctrine/cache": "<1.11",
- "doctrine/dbal": "<2.13.1 || ~3.0.0",
- "gos/react-amqp": "<0.3",
- "symfony/cache": "<4.4 || >=5.0,<5.3",
- "symfony/monolog-bundle": "<3.0",
- "symfony/options-resolver": "<4.4 || >=5.0,<5.3",
- "symfony/stopwatch": "<4.4 || >=5.0,<5.3",
- "symfony/twig-bundle": "<4.4 || >=5.0,<5.3",
- "symfony/web-profiler-bundle": "<4.4 || >=5.0,<5.3",
- "twig/twig": "<1.36 || >=2.0,<2.6"
- },
- "require-dev": {
- "doctrine/cache": "^1.11 || ^2.0",
- "doctrine/dbal": "^2.13.1 || ^3.1",
- "matthiasnoback/symfony-dependency-injection-test": "^4.2",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "1.8.5",
- "phpstan/phpstan-phpunit": "1.1.1",
- "phpstan/phpstan-symfony": "1.2.13",
- "phpunit/phpunit": "^9.5",
- "symfony/cache": "^4.4 || ^5.4 || ^6.0",
- "symfony/options-resolver": "^4.4 || ^5.4 || ^6.0",
- "symfony/phpunit-bridge": "^5.4 || ^6.0",
- "symfony/stopwatch": "^4.4 || ^5.4 || ^6.0",
- "symfony/twig-bundle": "^4.4 || ^5.4 || ^6.0",
- "symfony/web-profiler-bundle": "^4.4 || ^5.4 || ^6.0"
+ "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": {
- "doctrine/cache": "to use doctrine/cache as a client driver",
- "doctrine/dbal": "to use Doctrine ping services",
- "ext-amqp": "* to use the amqp pusher",
- "ext-pcntl": "* to handle process signals",
- "ext-pdo": "* to use PDO ping services",
- "gos/react-amqp": "to use the amqp server push handler",
- "symfony/cache": "to use symfony/cache as an authentication storage driver",
- "symfony/options-resolver": "to use the pushers",
- "symfony/serializer": "to use the pushers",
- "symfony/stopwatch": "to use the data collectors"
- },
- "type": "symfony-bundle",
- "autoload": {
- "psr-4": {
- "Gos\\Bundle\\WebSocketBundle\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jeremy Dare",
- "email": "jeremy.d.dare@gmail.com"
- },
- {
- "name": "Johann Saunier",
- "email": "johann_27@hotmail.fr"
- }
- ],
- "description": "Symfony Web Socket Bundle",
- "homepage": "https://github.com/GeniusesOfSymfony/WebSocketBundle",
- "keywords": [
- "Ratchet",
- "WAMP",
- "Web Socket Bundle",
- "io",
- "websocket"
- ],
- "support": {
- "issues": "https://github.com/GeniusesOfSymfony/WebSocketBundle/issues",
- "source": "https://github.com/GeniusesOfSymfony/WebSocketBundle/tree/v3.15.0"
- },
- "funding": [
- {
- "url": "https://github.com/mbabker",
- "type": "github"
- }
- ],
- "time": "2022-09-20T02:03:09+00:00"
- },
- {
- "name": "gos/websocket-client",
- "version": "v1.5.0",
- "source": {
- "type": "git",
- "url": "https://github.com/GeniusesOfSymfony/WebSocketPhpClient.git",
- "reference": "97354f2b970f59cb6d511307ef7af5f761b684ff"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/GeniusesOfSymfony/WebSocketPhpClient/zipball/97354f2b970f59cb6d511307ef7af5f761b684ff",
- "reference": "97354f2b970f59cb6d511307ef7af5f761b684ff",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "php": "^7.2 || ^8.0",
- "psr/log": "^1.0 || ^2.0 || ^3.0",
- "symfony/deprecation-contracts": "^2.1 || ^3.0",
- "symfony/options-resolver": "^4.4 || ^5.3 || ^6.0"
- },
- "require-dev": {
- "cboden/ratchet": "^0.4.3",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "1.2.0",
- "phpstan/phpstan-phpunit": "1.0.0",
- "phpunit/phpunit": "^8.5 || ^9.5"
+ "lcobucci/clock": ">= 3.2"
},
"type": "library",
"autoload": {
"psr-4": {
- "Gos\\Component\\WebSocketClient\\": "src/"
+ "Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "MIT"
+ "BSD-3-Clause"
],
"authors": [
{
- "name": "Martin Bažík",
- "email": "martin@bazo.sk"
- },
- {
- "name": "Johann Saunier",
- "email": "johann_27@hotmail.fr"
+ "name": "Luís Cobucci",
+ "email": "lcobucci@gmail.com",
+ "role": "Developer"
}
],
- "description": "WAMP client in PHP",
+ "description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
- "Ratchet",
- "WAMP",
- "websocket"
+ "JWS",
+ "jwt"
],
"support": {
- "issues": "https://github.com/GeniusesOfSymfony/WebSocketPhpClient/issues",
- "source": "https://github.com/GeniusesOfSymfony/WebSocketPhpClient/tree/v1.5.0"
+ "issues": "https://github.com/lcobucci/jwt/issues",
+ "source": "https://github.com/lcobucci/jwt/tree/5.6.0"
},
"funding": [
{
- "url": "https://github.com/mbabker",
- "type": "github"
- }
- ],
- "time": "2021-11-24T03:24:48+00:00"
- },
- {
- "name": "guzzlehttp/psr7",
- "version": "2.9.0",
- "source": {
- "type": "git",
- "url": "https://github.com/guzzle/psr7.git",
- "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884",
- "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884",
- "shasum": ""
- },
- "require": {
- "php": "^7.2.5 || ^8.0",
- "psr/http-factory": "^1.0",
- "psr/http-message": "^1.1 || ^2.0",
- "ralouphie/getallheaders": "^3.0"
- },
- "provide": {
- "psr/http-factory-implementation": "1.0",
- "psr/http-message-implementation": "1.0"
- },
- "require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.2",
- "http-interop/http-factory-tests": "0.9.0",
- "jshttp/mime-db": "1.54.0.1",
- "phpunit/phpunit": "^8.5.44 || ^9.6.25"
- },
- "suggest": {
- "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
- },
- "type": "library",
- "extra": {
- "bamarni-bin": {
- "bin-links": true,
- "forward-command": false
- }
- },
- "autoload": {
- "psr-4": {
- "GuzzleHttp\\Psr7\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Graham Campbell",
- "email": "hello@gjcampbell.co.uk",
- "homepage": "https://github.com/GrahamCampbell"
- },
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- },
- {
- "name": "George Mponos",
- "email": "gmponos@gmail.com",
- "homepage": "https://github.com/gmponos"
- },
- {
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com",
- "homepage": "https://github.com/Nyholm"
- },
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com",
- "homepage": "https://github.com/sagikazarmark"
- },
- {
- "name": "Tobias Schultze",
- "email": "webmaster@tubo-world.de",
- "homepage": "https://github.com/Tobion"
- },
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com",
- "homepage": "https://sagikazarmark.hu"
- }
- ],
- "description": "PSR-7 message implementation that also provides common utility methods",
- "keywords": [
- "http",
- "message",
- "psr-7",
- "request",
- "response",
- "stream",
- "uri",
- "url"
- ],
- "support": {
- "issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.9.0"
- },
- "funding": [
- {
- "url": "https://github.com/GrahamCampbell",
+ "url": "https://github.com/lcobucci",
"type": "github"
},
{
- "url": "https://github.com/Nyholm",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
- "type": "tidelift"
+ "url": "https://www.patreon.com/lcobucci",
+ "type": "patreon"
}
],
- "time": "2026-03-10T16:41:02+00:00"
+ "time": "2025-10-17T11:30:53+00:00"
},
{
"name": "monolog/monolog",
@@ -2179,76 +1770,24 @@
"time": "2019-01-08T18:20:26+00:00"
},
{
- "name": "psr/http-factory",
- "version": "1.1.0",
+ "name": "psr/link",
+ "version": "2.0.1",
"source": {
"type": "git",
- "url": "https://github.com/php-fig/http-factory.git",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ "url": "https://github.com/php-fig/link.git",
+ "reference": "84b159194ecfd7eaa472280213976e96415433f7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "url": "https://api.github.com/repos/php-fig/link/zipball/84b159194ecfd7eaa472280213976e96415433f7",
+ "reference": "84b159194ecfd7eaa472280213976e96415433f7",
"shasum": ""
},
"require": {
- "php": ">=7.1",
- "psr/http-message": "^1.0 || ^2.0"
+ "php": ">=8.0.0"
},
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
- "keywords": [
- "factory",
- "http",
- "message",
- "psr",
- "psr-17",
- "psr-7",
- "request",
- "response"
- ],
- "support": {
- "source": "https://github.com/php-fig/http-factory"
- },
- "time": "2024-04-15T12:06:14+00:00"
- },
- {
- "name": "psr/http-message",
- "version": "2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-message.git",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
+ "suggest": {
+ "fig/link-util": "Provides some useful PSR-13 utilities"
},
"type": "library",
"extra": {
@@ -2258,7 +1797,7 @@
},
"autoload": {
"psr-4": {
- "Psr\\Http\\Message\\": "src/"
+ "Psr\\Link\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2268,23 +1807,23 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
+ "homepage": "http://www.php-fig.org/"
}
],
- "description": "Common interface for HTTP messages",
- "homepage": "https://github.com/php-fig/http-message",
+ "description": "Common interfaces for HTTP links",
+ "homepage": "https://github.com/php-fig/link",
"keywords": [
"http",
- "http-message",
+ "http-link",
+ "link",
"psr",
- "psr-7",
- "request",
- "response"
+ "psr-13",
+ "rest"
],
"support": {
- "source": "https://github.com/php-fig/http-message/tree/2.0"
+ "source": "https://github.com/php-fig/link/tree/2.0.1"
},
- "time": "2023-04-04T09:54:51+00:00"
+ "time": "2021-03-11T23:00:27+00:00"
},
{
"name": "psr/log",
@@ -2336,558 +1875,6 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
- {
- "name": "ralouphie/getallheaders",
- "version": "3.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/ralouphie/getallheaders.git",
- "reference": "120b605dfeb996808c31b6477290a714d356e822"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
- "reference": "120b605dfeb996808c31b6477290a714d356e822",
- "shasum": ""
- },
- "require": {
- "php": ">=5.6"
- },
- "require-dev": {
- "php-coveralls/php-coveralls": "^2.1",
- "phpunit/phpunit": "^5 || ^6.5"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/getallheaders.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Ralph Khattar",
- "email": "ralph.khattar@gmail.com"
- }
- ],
- "description": "A polyfill for getallheaders.",
- "support": {
- "issues": "https://github.com/ralouphie/getallheaders/issues",
- "source": "https://github.com/ralouphie/getallheaders/tree/develop"
- },
- "time": "2019-03-08T08:55:37+00:00"
- },
- {
- "name": "ratchet/rfc6455",
- "version": "v0.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/ratchetphp/RFC6455.git",
- "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb",
- "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb",
- "shasum": ""
- },
- "require": {
- "guzzlehttp/psr7": "^2 || ^1.7",
- "php": ">=5.4.2"
- },
- "require-dev": {
- "phpunit/phpunit": "^5.7",
- "react/socket": "^1.3"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Ratchet\\RFC6455\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "role": "Developer"
- },
- {
- "name": "Matt Bonneau",
- "role": "Developer"
- }
- ],
- "description": "RFC6455 WebSocket protocol handler",
- "homepage": "http://socketo.me",
- "keywords": [
- "WebSockets",
- "rfc6455",
- "websocket"
- ],
- "support": {
- "chat": "https://gitter.im/reactphp/reactphp",
- "issues": "https://github.com/ratchetphp/RFC6455/issues",
- "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1"
- },
- "time": "2021-12-09T23:20:49+00:00"
- },
- {
- "name": "react/cache",
- "version": "v1.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/reactphp/cache.git",
- "reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
- "reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0",
- "react/promise": "^3.0 || ^2.0 || ^1.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "React\\Cache\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering",
- "homepage": "https://clue.engineering/"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "reactphp@ceesjankiewiet.nl",
- "homepage": "https://wyrihaximus.net/"
- },
- {
- "name": "Jan Sorgalla",
- "email": "jsorgalla@gmail.com",
- "homepage": "https://sorgalla.com/"
- },
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "homepage": "https://cboden.dev/"
- }
- ],
- "description": "Async, Promise-based cache interface for ReactPHP",
- "keywords": [
- "cache",
- "caching",
- "promise",
- "reactphp"
- ],
- "support": {
- "issues": "https://github.com/reactphp/cache/issues",
- "source": "https://github.com/reactphp/cache/tree/v1.2.0"
- },
- "funding": [
- {
- "url": "https://opencollective.com/reactphp",
- "type": "open_collective"
- }
- ],
- "time": "2022-11-30T15:59:55+00:00"
- },
- {
- "name": "react/dns",
- "version": "v1.14.0",
- "source": {
- "type": "git",
- "url": "https://github.com/reactphp/dns.git",
- "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3",
- "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0",
- "react/cache": "^1.0 || ^0.6 || ^0.5",
- "react/event-loop": "^1.2",
- "react/promise": "^3.2 || ^2.7 || ^1.2.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
- "react/async": "^4.3 || ^3 || ^2",
- "react/promise-timer": "^1.11"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "React\\Dns\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering",
- "homepage": "https://clue.engineering/"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "reactphp@ceesjankiewiet.nl",
- "homepage": "https://wyrihaximus.net/"
- },
- {
- "name": "Jan Sorgalla",
- "email": "jsorgalla@gmail.com",
- "homepage": "https://sorgalla.com/"
- },
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "homepage": "https://cboden.dev/"
- }
- ],
- "description": "Async DNS resolver for ReactPHP",
- "keywords": [
- "async",
- "dns",
- "dns-resolver",
- "reactphp"
- ],
- "support": {
- "issues": "https://github.com/reactphp/dns/issues",
- "source": "https://github.com/reactphp/dns/tree/v1.14.0"
- },
- "funding": [
- {
- "url": "https://opencollective.com/reactphp",
- "type": "open_collective"
- }
- ],
- "time": "2025-11-18T19:34:28+00:00"
- },
- {
- "name": "react/event-loop",
- "version": "v1.6.0",
- "source": {
- "type": "git",
- "url": "https://github.com/reactphp/event-loop.git",
- "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
- "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
- },
- "suggest": {
- "ext-pcntl": "For signal handling support when using the StreamSelectLoop"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "React\\EventLoop\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering",
- "homepage": "https://clue.engineering/"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "reactphp@ceesjankiewiet.nl",
- "homepage": "https://wyrihaximus.net/"
- },
- {
- "name": "Jan Sorgalla",
- "email": "jsorgalla@gmail.com",
- "homepage": "https://sorgalla.com/"
- },
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "homepage": "https://cboden.dev/"
- }
- ],
- "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
- "keywords": [
- "asynchronous",
- "event-loop"
- ],
- "support": {
- "issues": "https://github.com/reactphp/event-loop/issues",
- "source": "https://github.com/reactphp/event-loop/tree/v1.6.0"
- },
- "funding": [
- {
- "url": "https://opencollective.com/reactphp",
- "type": "open_collective"
- }
- ],
- "time": "2025-11-17T20:46:25+00:00"
- },
- {
- "name": "react/promise",
- "version": "v3.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/reactphp/promise.git",
- "reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
- "reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1.0"
- },
- "require-dev": {
- "phpstan/phpstan": "1.12.28 || 1.4.10",
- "phpunit/phpunit": "^9.6 || ^7.5"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions_include.php"
- ],
- "psr-4": {
- "React\\Promise\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jan Sorgalla",
- "email": "jsorgalla@gmail.com",
- "homepage": "https://sorgalla.com/"
- },
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering",
- "homepage": "https://clue.engineering/"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "reactphp@ceesjankiewiet.nl",
- "homepage": "https://wyrihaximus.net/"
- },
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "homepage": "https://cboden.dev/"
- }
- ],
- "description": "A lightweight implementation of CommonJS Promises/A for PHP",
- "keywords": [
- "promise",
- "promises"
- ],
- "support": {
- "issues": "https://github.com/reactphp/promise/issues",
- "source": "https://github.com/reactphp/promise/tree/v3.3.0"
- },
- "funding": [
- {
- "url": "https://opencollective.com/reactphp",
- "type": "open_collective"
- }
- ],
- "time": "2025-08-19T18:57:03+00:00"
- },
- {
- "name": "react/socket",
- "version": "v1.17.0",
- "source": {
- "type": "git",
- "url": "https://github.com/reactphp/socket.git",
- "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08",
- "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08",
- "shasum": ""
- },
- "require": {
- "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
- "php": ">=5.3.0",
- "react/dns": "^1.13",
- "react/event-loop": "^1.2",
- "react/promise": "^3.2 || ^2.6 || ^1.2.1",
- "react/stream": "^1.4"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
- "react/async": "^4.3 || ^3.3 || ^2",
- "react/promise-stream": "^1.4",
- "react/promise-timer": "^1.11"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "React\\Socket\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering",
- "homepage": "https://clue.engineering/"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "reactphp@ceesjankiewiet.nl",
- "homepage": "https://wyrihaximus.net/"
- },
- {
- "name": "Jan Sorgalla",
- "email": "jsorgalla@gmail.com",
- "homepage": "https://sorgalla.com/"
- },
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "homepage": "https://cboden.dev/"
- }
- ],
- "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
- "keywords": [
- "Connection",
- "Socket",
- "async",
- "reactphp",
- "stream"
- ],
- "support": {
- "issues": "https://github.com/reactphp/socket/issues",
- "source": "https://github.com/reactphp/socket/tree/v1.17.0"
- },
- "funding": [
- {
- "url": "https://opencollective.com/reactphp",
- "type": "open_collective"
- }
- ],
- "time": "2025-11-19T20:47:34+00:00"
- },
- {
- "name": "react/stream",
- "version": "v1.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/reactphp/stream.git",
- "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
- "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
- "shasum": ""
- },
- "require": {
- "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
- "php": ">=5.3.8",
- "react/event-loop": "^1.2"
- },
- "require-dev": {
- "clue/stream-filter": "~1.2",
- "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "React\\Stream\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering",
- "homepage": "https://clue.engineering/"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "reactphp@ceesjankiewiet.nl",
- "homepage": "https://wyrihaximus.net/"
- },
- {
- "name": "Jan Sorgalla",
- "email": "jsorgalla@gmail.com",
- "homepage": "https://sorgalla.com/"
- },
- {
- "name": "Chris Boden",
- "email": "cboden@gmail.com",
- "homepage": "https://cboden.dev/"
- }
- ],
- "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
- "keywords": [
- "event-driven",
- "io",
- "non-blocking",
- "pipe",
- "reactphp",
- "readable",
- "stream",
- "writable"
- ],
- "support": {
- "issues": "https://github.com/reactphp/stream/issues",
- "source": "https://github.com/reactphp/stream/tree/v1.4.0"
- },
- "funding": [
- {
- "url": "https://opencollective.com/reactphp",
- "type": "open_collective"
- }
- ],
- "time": "2024-06-11T12:45:25+00:00"
- },
{
"name": "symfony/asset",
"version": "v6.4.34",
@@ -4259,6 +3246,182 @@
],
"time": "2026-03-25T17:41:29+00:00"
},
+ {
+ "name": "symfony/http-client",
+ "version": "v6.4.36",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "1baea3a592ec5ee1f58de6548a034268d4946db6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/1baea3a592ec5ee1f58de6548a034268d4946db6",
+ "reference": "1baea3a592ec5ee1f58de6548a034268d4946db6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-client-contracts": "~3.4.4|^3.5.2",
+ "symfony/polyfill-php83": "^1.29",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "php-http/discovery": "<1.15",
+ "symfony/http-foundation": "<6.3"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "3.0"
+ },
+ "require-dev": {
+ "amphp/amp": "^2.5",
+ "amphp/http-client": "^4.2.1",
+ "amphp/http-tunnel": "^1.0",
+ "amphp/socket": "^1.1",
+ "guzzlehttp/promises": "^1.4|^2.0",
+ "nyholm/psr7": "^1.0",
+ "php-http/httplug": "^1.0|^2.0",
+ "psr/http-client": "^1.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v6.4.36"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-23T20:48:09+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-29T11:18:49+00:00"
+ },
{
"name": "symfony/http-foundation",
"version": "v6.4.35",
@@ -4542,6 +3705,173 @@
],
"time": "2026-02-24T09:34:36+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.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mercure-bundle.git",
+ "reference": "eae8bf5a75b4e1203bd9aa4181c7950a4df4b3e3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/eae8bf5a75b4e1203bd9aa4181c7950a4df4b3e3",
+ "reference": "eae8bf5a75b4e1203bd9aa4181c7950a4df4b3e3",
+ "shasum": ""
+ },
+ "require": {
+ "lcobucci/jwt": "^3.4|^4.0|^5.0",
+ "php": ">=8.1",
+ "symfony/config": "^6.4|^7.3|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.3|^8.0",
+ "symfony/http-kernel": "^6.4|^7.3|^8.0",
+ "symfony/mercure": "*",
+ "symfony/web-link": "^6.4|^7.3|^8.0"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^7.3.4|^8.0",
+ "symfony/stopwatch": "^6.4|^7.3|^8.0",
+ "symfony/ux-turbo": "*",
+ "symfony/var-dumper": "^6.4|^7.3|^8.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.4.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dunglas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-25T12:51:49+00:00"
+ },
{
"name": "symfony/mime",
"version": "v6.4.36",
@@ -4794,77 +4124,6 @@
],
"time": "2026-04-02T18:23:01+00:00"
},
- {
- "name": "symfony/options-resolver",
- "version": "v6.4.30",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/options-resolver.git",
- "reference": "eeaa8cabe54c7b3516938c72a4a161c0cc80a34f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/eeaa8cabe54c7b3516938c72a4a161c0cc80a34f",
- "reference": "eeaa8cabe54c7b3516938c72a4a161c0cc80a34f",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\OptionsResolver\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Provides an improved replacement for the array_replace PHP function",
- "homepage": "https://symfony.com",
- "keywords": [
- "config",
- "configuration",
- "options"
- ],
- "support": {
- "source": "https://github.com/symfony/options-resolver/tree/v6.4.30"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-11-12T13:06:53+00:00"
- },
{
"name": "symfony/password-hasher",
"version": "v6.4.32",
@@ -6152,108 +5411,6 @@
],
"time": "2026-02-16T20:44:03+00:00"
},
- {
- "name": "symfony/serializer",
- "version": "v6.4.36",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/serializer.git",
- "reference": "90e4e0187dca57331ea301506545aa26895b7787"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/serializer/zipball/90e4e0187dca57331ea301506545aa26895b7787",
- "reference": "90e4e0187dca57331ea301506545aa26895b7787",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
- "symfony/polyfill-ctype": "~1.8"
- },
- "conflict": {
- "doctrine/annotations": "<1.12",
- "phpdocumentor/reflection-docblock": "<3.2.2",
- "phpdocumentor/type-resolver": "<1.4.0",
- "symfony/dependency-injection": "<5.4",
- "symfony/property-access": "<5.4",
- "symfony/property-info": "<5.4.24|>=6,<6.2.11",
- "symfony/uid": "<5.4",
- "symfony/validator": "<6.4",
- "symfony/yaml": "<5.4"
- },
- "require-dev": {
- "doctrine/annotations": "^1.12|^2",
- "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
- "seld/jsonlint": "^1.10",
- "symfony/cache": "^5.4|^6.0|^7.0",
- "symfony/config": "^5.4|^6.0|^7.0",
- "symfony/console": "^5.4|^6.0|^7.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/error-handler": "^5.4|^6.0|^7.0",
- "symfony/filesystem": "^5.4|^6.0|^7.0",
- "symfony/form": "^5.4|^6.0|^7.0",
- "symfony/http-foundation": "^5.4|^6.0|^7.0",
- "symfony/http-kernel": "^5.4|^6.0|^7.0",
- "symfony/messenger": "^5.4|^6.0|^7.0",
- "symfony/mime": "^5.4|^6.0|^7.0",
- "symfony/property-access": "^5.4.26|^6.3|^7.0",
- "symfony/property-info": "^5.4.24|^6.2.11|^7.0",
- "symfony/translation-contracts": "^2.5|^3",
- "symfony/uid": "^5.4|^6.0|^7.0",
- "symfony/validator": "^6.4|^7.0",
- "symfony/var-dumper": "^5.4|^6.0|^7.0",
- "symfony/var-exporter": "^5.4|^6.0|^7.0",
- "symfony/yaml": "^5.4|^6.0|^7.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Serializer\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
- "homepage": "https://symfony.com",
- "support": {
- "source": "https://github.com/symfony/serializer/tree/v6.4.36"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2026-03-30T15:37:17+00:00"
- },
{
"name": "symfony/service-contracts",
"version": "v3.6.1",
@@ -7047,6 +6204,93 @@
],
"time": "2026-03-10T15:06:19+00:00"
},
+ {
+ "name": "symfony/web-link",
+ "version": "v6.4.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/web-link.git",
+ "reference": "636d5e34cd5c4a2538b02ba48a3c02989bfdf06b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/web-link/zipball/636d5e34cd5c4a2538b02ba48a3c02989bfdf06b",
+ "reference": "636d5e34cd5c4a2538b02ba48a3c02989bfdf06b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/link": "^1.1|^2.0"
+ },
+ "conflict": {
+ "symfony/http-kernel": "<5.4"
+ },
+ "provide": {
+ "psr/link-implementation": "1.0|2.0"
+ },
+ "require-dev": {
+ "symfony/http-kernel": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\WebLink\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Manages links between resources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dns-prefetch",
+ "http",
+ "http2",
+ "link",
+ "performance",
+ "prefetch",
+ "preload",
+ "prerender",
+ "psr13",
+ "push"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/web-link/tree/v6.4.32"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-01T13:45:34+00:00"
+ },
{
"name": "symfony/webpack-encore-bundle",
"version": "v1.17.2",
@@ -7278,6 +6522,70 @@
}
],
"packages-dev": [
+ {
+ "name": "firebase/php-jwt",
+ "version": "v7.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380",
+ "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.4",
+ "phpfastcache/phpfastcache": "^9.2",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^2.0||^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v7.0.5"
+ },
+ "time": "2026-04-01T20:38:03+00:00"
+ },
{
"name": "nikic/php-parser",
"version": "v5.7.0",
diff --git a/config/bundles.php b/config/bundles.php
index fff3113..c8419b9 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -8,8 +8,7 @@ return [
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
- Gos\Bundle\PubSubRouterBundle\GosPubSubRouterBundle::class => ['all' => true],
- Gos\Bundle\WebSocketBundle\GosWebSocketBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
+ Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
];
diff --git a/config/packages/gos_web_socket.yaml b/config/packages/gos_web_socket.yaml
deleted file mode 100644
index 8c0f86c..0000000
--- a/config/packages/gos_web_socket.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-#doctrine_cache:
-# providers:
-# redis_cache:
-# redis:
-# host: localhost
-# port: 6379
-# database: 3
-# websocket_cache_client:
-# type: redis
-# alias: gos_web_socket.client_storage.driver.redis
-
-gos_web_socket:
- server:
- host: 0.0.0.0
- port: "%mineseeker.websocket%"
- router:
- resources:
- - '%kernel.project_dir%/config/pubsub/routing.yaml'
- client:
- firewall: secured_area
- # session_handler: "@session.handler.pdo"
- # storage:
- # driver: "@gos_web_socket.client_storage.driver.predis"
- # ttl: 28800 #(optionally) time to live if you use redis driver
- # prefix: client #(optionally) prefix if you use redis driver, create key "client:1" instead key "1"
- ping:
- services:
- - { name: doctrine.dbal.default_connection, type: doctrine, interval: 300 }
diff --git a/config/packages/mercure.yaml b/config/packages/mercure.yaml
new file mode 100644
index 0000000..9fa41b2
--- /dev/null
+++ b/config/packages/mercure.yaml
@@ -0,0 +1,8 @@
+mercure:
+ hubs:
+ default:
+ url: '%env(MERCURE_URL)%'
+ public_url: '%env(MERCURE_PUBLIC_URL)%'
+ jwt:
+ secret: '%env(MERCURE_JWT_SECRET)%'
+ publish: '*'
diff --git a/config/pubsub/routing.yaml b/config/pubsub/routing.yaml
deleted file mode 100644
index 52b2d81..0000000
--- a/config/pubsub/routing.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-## Topic Configuration
-mineseeker_topic:
- channel: mineseeker/channel/{game}
- callback: 'mineseeker.topic'
-# requirements:
-# method:
-# path: '[a-z1-9A-Z]+'
-
-# Remote Procedure Call Configuration
-mineseeker_rpc:
- channel: mineseeker-rpc/{method}
- callback: 'mineseeker.rpc'
- requirements:
- method: "[a-zA-Z_]+"
diff --git a/config/routes/mineseeker.yaml b/config/routes/mineseeker.yaml
index 657bc8a..7fab5bd 100644
--- a/config/routes/mineseeker.yaml
+++ b/config/routes/mineseeker.yaml
@@ -24,4 +24,30 @@ MineSeekerBundle_contact:
MineSeekerBundle_landing:
path: /landing-page
- controller: App\Controller\GameController::landing
\ No newline at end of file
+ controller: App\Controller\GameController::landing
+
+MineSeekerBundle_api_game_start:
+ path: /api/game/start
+ controller: App\Controller\MercureController::start
+ methods: [POST]
+
+MineSeekerBundle_api_game_connect:
+ path: /api/game/connect/{gameAssoc}
+ controller: App\Controller\MercureController::connect
+ methods: [GET]
+
+MineSeekerBundle_api_game_join:
+ path: /api/game/join/{gameAssoc}
+ controller: App\Controller\MercureController::join
+ methods: [POST]
+
+MineSeekerBundle_api_game_step:
+ path: /api/game/step/{gameAssoc}
+ controller: App\Controller\MercureController::step
+ methods: [POST]
+
+MineSeekerBundle_api_game_leave:
+ path: /api/game/leave/{gameAssoc}
+ controller: App\Controller\MercureController::leave
+ methods: [POST]
+
diff --git a/config/services.yaml b/config/services.yaml
index b53c5c8..8020794 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -3,11 +3,6 @@
parameters:
locale: 'en'
jotunheimr.version: 1.1.0-20191026
- facebook.api: 320599508311862
- facebook.api-secret: 18d4f48cdd274bccee2678e5eff3f557
- facebook.version: 'v2.8'
- facebook.scope: 'public_profile,email,user_friends'
- mineseeker.websocket: 6450
services:
# default configuration for services in *this* file
@@ -16,50 +11,16 @@ services:
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
- # The best practice is to be explicit about your dependencies anyway.
+ # The best practice is to be explicit about your dependencies anyway.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
- exclude: '../src/{Command/WebsocketServerCommand.php,Entity,Migrations,Tests,Kernel.php,Periodic}'
+ exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: [ 'controller.service_arguments' ]
-
- # add more service definitions when explicit configuration is needed
- # please note that last definitions always *replace* previous ones
-
- mineseeker.topic_sample_service:
- class: App\Topic\MineseekerTopic
- tags:
- - { name: gos_web_socket.topic }
-
- mineseeker.rpc_sample_service:
- class: App\Rpc\MineseekerRpc
- public: true
- tags:
- - { name: gos_web_socket.rpc }
-
- # Override gos WebsocketServerCommand to avoid --profile conflict with Symfony 6.4 global option
- gos_web_socket.command.websocket_server:
- class: App\Command\WebsocketServerCommand
- arguments:
- - '@gos_web_socket.server.launcher'
- - '%gos_web_socket.server.host%'
- - '%gos_web_socket.server.port%'
- - '@gos_web_socket.registry.server'
- tags:
- - { name: console.command, command: 'gos:websocket:server' }
-
- gos_web_socket_server.client_event.listener:
- class: App\EventListener\MineseekerClientEventListener
- tags:
- - { name: kernel.event_listener, event: 'gos_web_socket.client_connected', method: onClientConnect }
- - { name: kernel.event_listener, event: 'gos_web_socket.client_disconnected', method: onClientDisconnect }
- - { name: kernel.event_listener, event: 'gos_web_socket.client_error', method: onClientError }
- - { name: kernel.event_listener, event: 'gos_web_socket.server_launched', method: onServerStart }
- - { name: kernel.event_listener, event: 'gos_web_socket.client_rejected', method: onClientRejected }
diff --git a/package.json b/package.json
index 1f2f2fa..81a2ffa 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,6 @@
"private": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^5.2.0",
- "autobahn": "^19.10.1",
"bootstrap": "3",
"buffer": "^5.4.3",
"howler": "^2.1.2",
diff --git a/src/Command/WebsocketServerCommand.php b/src/Command/WebsocketServerCommand.php
deleted file mode 100644
index fef104c..0000000
--- a/src/Command/WebsocketServerCommand.php
+++ /dev/null
@@ -1,79 +0,0 @@
-addArgument('name', InputArgument::OPTIONAL, 'Name of the server to start, launches the first registered server if not specified')
- ->addOption('ws-profile', 'm', InputOption::VALUE_NONE, 'Enable profiling of the websocket server')
- ->addOption('host', 'a', InputOption::VALUE_OPTIONAL, 'The hostname of the websocket server')
- ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'The port of the websocket server');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- /** @var string $name */
- $name = $input->getArgument('name');
-
- /** @var string $host */
- $host = null === $input->getOption('host') ? $this->host : $input->getOption('host');
-
- /** @var int|string $port */
- $port = null === $input->getOption('port') ? $this->port : $input->getOption('port');
-
- if (!is_numeric($port)) {
- throw new InvalidArgumentException('The port option must be a numeric value.');
- }
-
- /** @var bool $profile */
- $profile = (bool) $input->getOption('ws-profile');
-
- $this->serverLauncher->launch($name, $host, (int) $port, $profile);
-
- return 0;
- }
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- if ($input->mustSuggestArgumentValuesFor('name') && null !== $this->serverRegistry) {
- $suggestions->suggestValues(array_keys($this->serverRegistry->getServers()));
- }
- }
-}
\ No newline at end of file
diff --git a/src/Controller/GameController.php b/src/Controller/GameController.php
index 12b3d19..251ca8d 100644
--- a/src/Controller/GameController.php
+++ b/src/Controller/GameController.php
@@ -12,7 +12,6 @@ namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
-use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
/**
@@ -29,8 +28,11 @@ class GameController extends AbstractController
{
public function __construct(
#[Autowire(env: 'APP_ENV')]
- private readonly string $env,
- private readonly RequestStack $request,
+ private readonly string $env,
+ #[Autowire(env: 'MERCURE_PUBLIC_URL')]
+ private readonly string $mercurePublicUrl,
+ #[Autowire(env: 'MERCURE_SUBSCRIBER_JWT')]
+ private readonly string $mercureSubscriberJwt,
) {
}
@@ -42,8 +44,9 @@ class GameController extends AbstractController
public function play(): Response
{
return $this->render('Game/play.html.twig', [
- 'env' => $this->env,
- 'ssl' => $this->request->getCurrentRequest()->isSecure() ? 'true' : 'false',
+ 'env' => $this->env,
+ 'mercure_hub_url' => $this->mercurePublicUrl,
+ 'mercure_subscriber_jwt' => $this->mercureSubscriberJwt,
]);
}
diff --git a/src/Controller/MercureController.php b/src/Controller/MercureController.php
new file mode 100644
index 0000000..af1f6c0
--- /dev/null
+++ b/src/Controller/MercureController.php
@@ -0,0 +1,98 @@
+
+ * @category Class
+ * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
+ * @link www.splendidbear.org
+ * @since 2026. 04. 09.
+ */
+class MercureController extends AbstractController
+{
+ public function __construct(
+ private readonly TopicManager $topicManager,
+ private readonly RpcManager $rpcManager,
+ ) {
+ }
+
+ /** POST /api/game/start — save the grid and create the PlayedGame record */
+ public function start(Request $request): JsonResponse
+ {
+ $data = $request->toArray();
+ $result = $this->rpcManager->saveGrid([$data['grid'], $data['gameAssoc']]);
+
+ return $this->json(['success' => $result]);
+ }
+
+ /** GET /api/game/connect/{gameAssoc} — return grid + current user info (base64 JSON) */
+ public function connect(string $gameAssoc): Response
+ {
+ $payload = $this->rpcManager->getConnectInformation($gameAssoc);
+
+ return new Response($payload, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
+ }
+
+ /** POST /api/game/join/{gameAssoc} — register the player, broadcast subscription event via Mercure */
+ public function join(string $gameAssoc, Request $request): JsonResponse
+ {
+ $this->topicManager->subscribe($gameAssoc, $this->resolveUserName($request), $this->getUser());
+
+ return $this->json(['success' => true]);
+ }
+
+ /** POST /api/game/step/{gameAssoc} — persist the step and broadcast game event via Mercure */
+ public function step(string $gameAssoc, Request $request): JsonResponse
+ {
+ $this->topicManager->publish($gameAssoc, $this->resolveUserName($request), $request->toArray());
+
+ return $this->json(['success' => true]);
+ }
+
+ /** POST /api/game/leave/{gameAssoc} — broadcast disconnect event via Mercure */
+ public function leave(string $gameAssoc, Request $request): JsonResponse
+ {
+ $this->topicManager->unSubscribe($gameAssoc, $this->resolveUserName($request));
+
+ return $this->json(['success' => true]);
+ }
+
+ private function resolveUserName(Request $request): string
+ {
+ $user = $this->getUser();
+
+ if (null !== $user) {
+ return $user->getUserIdentifier();
+ }
+
+ $sessionId = $request->getSession()->getId();
+ if (empty($sessionId)) {
+ $sessionId = bin2hex(random_bytes(16));
+ }
+
+ return 'anon_' . $sessionId;
+ }
+}
diff --git a/src/Entity/PlayedGame.php b/src/Entity/PlayedGame.php
index 48174b4..4c00c13 100644
--- a/src/Entity/PlayedGame.php
+++ b/src/Entity/PlayedGame.php
@@ -12,6 +12,8 @@ namespace App\Entity;
use App\Repository\PlayedGameRepository;
use DateTime;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
@@ -19,6 +21,7 @@ use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
/**
@@ -80,107 +83,28 @@ class PlayedGame
#[JoinColumn(name: 'blue_anon', referencedColumnName: 'id', nullable: true)]
private ?Gamer $blueAnon = null;
- #[OneToOne(mappedBy: 'playedGame')]
- private ?Step $step = null;
+ #[OneToMany(mappedBy: 'playedGame', targetEntity: Step::class)]
+ private Collection $steps;
+ public function __construct()
+ {
+ $this->steps = new ArrayCollection();
+ }
+
public function getId(): ?int
{
return $this->id;
}
- public function setId(?int $id): self
- {
- $this->id = $id;
- return $this;
- }
-
public function getGameAssoc(): ?string
{
return $this->gameAssoc;
}
- public function setGameAssoc(?string $gameAssoc): self
+ public function setGameAssoc(?string $gameAssoc): void
{
$this->gameAssoc = $gameAssoc;
- return $this;
- }
-
- public function getRedPoints(): ?int
- {
- return $this->redPoints;
- }
-
- public function setRedPoints(?int $redPoints): self
- {
- $this->redPoints = $redPoints;
- return $this;
- }
-
- public function getBluePoints(): ?int
- {
- return $this->bluePoints;
- }
-
- public function setBluePoints(?int $bluePoints): self
- {
- $this->bluePoints = $bluePoints;
- return $this;
- }
-
- public function getRedExplodedBomb(): ?bool
- {
- return $this->redExplodedBomb;
- }
-
- public function setRedExplodedBomb(?bool $redExplodedBomb): self
- {
- $this->redExplodedBomb = $redExplodedBomb;
- return $this;
- }
-
- public function getBlueExplodedBomb(): ?bool
- {
- return $this->blueExplodedBomb;
- }
-
- public function setBlueExplodedBomb(?bool $blueExplodedBomb): self
- {
- $this->blueExplodedBomb = $blueExplodedBomb;
- return $this;
- }
-
- public function getResign(): ?string
- {
- return $this->resign;
- }
-
- public function setResign(?string $resign): self
- {
- $this->resign = $resign;
- return $this;
- }
-
- public function getCreated(): ?DateTime
- {
- return $this->created;
- }
-
- public function setCreated(?DateTime $created): self
- {
- $this->created = $created;
- return $this;
- }
-
- public function getUpdated(): ?DateTime
- {
- return $this->updated;
- }
-
- public function setUpdated(?DateTime $updated): self
- {
- $this->updated = $updated;
- return $this;
}
public function getGrid(): ?Grid
@@ -188,10 +112,9 @@ class PlayedGame
return $this->grid;
}
- public function setGrid(?Grid $grid): self
+ public function setGrid(?Grid $grid): void
{
$this->grid = $grid;
- return $this;
}
public function getRed(): ?User
@@ -199,10 +122,9 @@ class PlayedGame
return $this->red;
}
- public function setRed(?User $red): self
+ public function setRed(?User $red): void
{
$this->red = $red;
- return $this;
}
public function getRedAnon(): ?Gamer
@@ -210,10 +132,9 @@ class PlayedGame
return $this->redAnon;
}
- public function setRedAnon(?Gamer $redAnon): self
+ public function setRedAnon(?Gamer $redAnon): void
{
$this->redAnon = $redAnon;
- return $this;
}
public function getBlue(): ?User
@@ -221,10 +142,9 @@ class PlayedGame
return $this->blue;
}
- public function setBlue(?User $blue): self
+ public function setBlue(?User $blue): void
{
$this->blue = $blue;
- return $this;
}
public function getBlueAnon(): ?Gamer
@@ -232,20 +152,83 @@ class PlayedGame
return $this->blueAnon;
}
- public function setBlueAnon(?Gamer $blueAnon): self
+ public function setBlueAnon(?Gamer $blueAnon): void
{
$this->blueAnon = $blueAnon;
- return $this;
}
- public function getStep(): ?Step
+ public function getRedPoints(): ?int
{
- return $this->step;
+ return $this->redPoints;
}
- public function setStep(?Step $step): self
+ public function setRedPoints(?int $redPoints): void
{
- $this->step = $step;
- return $this;
+ $this->redPoints = $redPoints;
+ }
+
+ public function getBluePoints(): ?int
+ {
+ return $this->bluePoints;
+ }
+
+ public function setBluePoints(?int $bluePoints): void
+ {
+ $this->bluePoints = $bluePoints;
+ }
+
+ public function getRedExplodedBomb(): ?bool
+ {
+ return $this->redExplodedBomb;
+ }
+
+ public function setRedExplodedBomb(?bool $redExplodedBomb): void
+ {
+ $this->redExplodedBomb = $redExplodedBomb;
+ }
+
+ public function getBlueExplodedBomb(): ?bool
+ {
+ return $this->blueExplodedBomb;
+ }
+
+ public function setBlueExplodedBomb(?bool $blueExplodedBomb): void
+ {
+ $this->blueExplodedBomb = $blueExplodedBomb;
+ }
+
+ public function getResign(): ?string
+ {
+ return $this->resign;
+ }
+
+ public function setResign(?string $resign): void
+ {
+ $this->resign = $resign;
+ }
+
+ public function getCreated(): ?DateTime
+ {
+ return $this->created;
+ }
+
+ public function setCreated(?DateTime $created): void
+ {
+ $this->created = $created;
+ }
+
+ public function getUpdated(): ?DateTime
+ {
+ return $this->updated;
+ }
+
+ public function setUpdated(?DateTime $updated): void
+ {
+ $this->updated = $updated;
+ }
+
+ public function getSteps(): Collection
+ {
+ return $this->steps;
}
}
diff --git a/src/Entity/Step.php b/src/Entity/Step.php
index 82e554a..77430f0 100644
--- a/src/Entity/Step.php
+++ b/src/Entity/Step.php
@@ -44,7 +44,7 @@ class Step
#[Column(nullable: true)]
private ?bool $wBomb = null;
- #[ManyToOne(inversedBy: 'step')]
+ #[ManyToOne(inversedBy: 'steps')]
private ?PlayedGame $playedGame = null;
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
diff --git a/src/EventListener/MineseekerClientEventListener.php b/src/EventListener/MineseekerClientEventListener.php
deleted file mode 100644
index 722487a..0000000
--- a/src/EventListener/MineseekerClientEventListener.php
+++ /dev/null
@@ -1,60 +0,0 @@
-
- * @category Class
- * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
- * @link www.splendidbear.org
- * @since 2026. 04. 09.
- */
-class MineseekerClientEventListener
-{
- public function onClientConnect(ClientConnectedEvent $event): void
- {
- $conn = $event->getConnection();
- echo $conn->resourceId . ' connected' . PHP_EOL;
- }
-
- public function onClientDisconnect(ClientDisconnectedEvent $event): void
- {
- $conn = $event->getConnection();
- echo $conn->resourceId . ' disconnected' . PHP_EOL;
- }
-
- public function onClientError(ClientErrorEvent $event): void
- {
- $conn = $event->getConnection();
- $e = $event->getException();
- echo 'connection error occurred: ' . $e->getMessage() . PHP_EOL;
- }
-
- public function onServerStart(ServerLaunchedEvent $event): void
- {
- echo 'Server was successfully started !' . PHP_EOL;
- }
-
- public function onClientRejected(ClientRejectedEvent $event): void
- {
- $origin = $event->getOrigin();
- echo 'connection rejected from ' . $origin . PHP_EOL;
- }
-}
diff --git a/src/Interfaces/TopicManagerInterface.php b/src/Interfaces/TopicManagerInterface.php
index 2b68264..da8378e 100644
--- a/src/Interfaces/TopicManagerInterface.php
+++ b/src/Interfaces/TopicManagerInterface.php
@@ -10,8 +10,7 @@
namespace App\Interfaces;
-use Ratchet\ConnectionInterface;
-use Ratchet\Wamp\Topic;
+use Symfony\Component\Security\Core\User\UserInterface;
/**
* Interface TopicManagerInterface
@@ -25,9 +24,9 @@ use Ratchet\Wamp\Topic;
*/
interface TopicManagerInterface
{
- public function subscribe(Topic $topic, ConnectionInterface $connection): void;
+ public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user): void;
- public function unSubscribe(Topic $topic, ConnectionInterface $connection): void;
+ public function unSubscribe(string $gameAssoc, string $userName): void;
- public function publish(Topic $topic, ConnectionInterface $connection, $event): void;
+ public function publish(string $gameAssoc, string $userName, array $event): void;
}
diff --git a/src/Interfaces/WebsocketManagerInterface.php b/src/Interfaces/WebsocketManagerInterface.php
deleted file mode 100644
index d965b7c..0000000
--- a/src/Interfaces/WebsocketManagerInterface.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
- * @category Interface
- * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
- * @link www.splendidbear.org
- * @since 2026. 04. 09.
- */
-interface WebsocketManagerInterface
-{
- public function reConnect(EntityManagerInterface $entityManager): ?EntityManagerInterface;
-}
diff --git a/src/Migrations/.gitignore b/src/Migrations/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Migrations/2026/04/Version20260409194708.php b/src/Migrations/2026/04/Version20260409194708.php
new file mode 100644
index 0000000..2afca42
--- /dev/null
+++ b/src/Migrations/2026/04/Version20260409194708.php
@@ -0,0 +1,42 @@
+
+ * @category Class
+ * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
+ * @link www.splendidbear.org
+ * @since 2026. 04. 09.
+ */
+final class Version20260409194708 extends AbstractMigration
+{
+ public function getDescription(): string
+ {
+ return 'Refactor entities';
+ }
+
+ public function up(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE step RENAME COLUMN wbomb TO w_bomb');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE step RENAME COLUMN w_bomb TO wbomb');
+ }
+}
diff --git a/src/Periodic/MinePeriodic.php b/src/Periodic/MinePeriodic.php
deleted file mode 100644
index 0caed85..0000000
--- a/src/Periodic/MinePeriodic.php
+++ /dev/null
@@ -1,51 +0,0 @@
-
- * @category Class
- * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
- * @link www.splendidbear.org
- * @since 2026. 04. 09.
- *
- * @method MinePeriodic|null find($id, $lockMode = null, $lockVersion = null)
- * @method MinePeriodic|null findOneBy(array $criteria, array $orderBy = null)
- * @method MinePeriodic[] findAll()
- * @method MinePeriodic[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
- */
-class MinePeriodic implements PeriodicInterface
-{
- public function __construct(private PdoPeriodicPing $ping) { }
-
- /**
- * This function is executed every 5 seconds.
- *
- * For more advanced functionality, try injecting
- * a Topic Service to perform actions on your
- * connections every x seconds.
- */
- public function tick(): void
- {
- $this->ping->tick();
- }
-
- public function getTimeout(): int
- {
- return 300;
- }
-}
diff --git a/src/Rpc/MineseekerRpc.php b/src/Rpc/MineseekerRpc.php
deleted file mode 100644
index 1711a4f..0000000
--- a/src/Rpc/MineseekerRpc.php
+++ /dev/null
@@ -1,69 +0,0 @@
-
- * @category Class
- * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
- * @link www.splendidbear.org
- * @since 2026. 04. 09.
- */
-class MineseekerRpc implements RpcInterface
-{
- public function __construct(private RpcManager $manager) { }
-
- /**
- * Name of RPC, use for pubsub router (see step3)
- *
- * @return string
- */
- public function getName(): string
- {
- return 'mineseeker.rpc';
- }
-
- /**
- * It handles the game starting processes
- *
- * @param ConnectionInterface $connection
- * @param WampRequest $request
- * @param array $params
- *
- * @return boolean
- */
- public function startGame(ConnectionInterface $connection, WampRequest $request, array $params): bool
- {
- return $this->manager->saveGrid($params);
- }
-
- /**
- * It handles when somebody trying to connect to the party
- *
- * @param ConnectionInterface $connection
- * @param WampRequest $request
- * @param array $params
- *
- * @return string Json string for frontend w/ numbering consideration. (=> a number is not string)
- */
- public function connectGame(ConnectionInterface $connection, WampRequest $request, array $params): string
- {
- return $this->manager->getConnectInformation($params);
- }
-}
diff --git a/src/Topic/MineseekerTopic.php b/src/Topic/MineseekerTopic.php
deleted file mode 100644
index b575d9f..0000000
--- a/src/Topic/MineseekerTopic.php
+++ /dev/null
@@ -1,95 +0,0 @@
-
- * @category Class
- * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
- * @link www.splendidbear.org
- * @since 2026. 04. 09.
- */
-class MineseekerTopic implements TopicInterface
-{
- public function __construct(private TopicManager $manager) { }
-
- /**
- * Like RPC is will use to prefix the channel
- *
- * @return string
- */
- public function getName(): string
- {
- return 'mineseeker.topic';
- }
-
- /**
- * This will receive any Subscription requests for this topic.
- *
- * @param ConnectionInterface $connection
- * @param Topic $topic
- * @param WampRequest $request
- *
- * @return void
- */
- public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request): void
- {
- $this->manager->subscribe($topic, $connection);
- }
-
- /**
- * This will receive any UnSubscription requests for this topic.
- *
- * @param ConnectionInterface $connection
- * @param Topic $topic
- * @param WampRequest $request
- *
- * @return void
- */
- public function onUnSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request): void
- {
- $this->manager->unSubscribe($topic, $connection);
- }
-
- /**
- * This will receive any Publish requests for this topic.
- *
- * @param ConnectionInterface $connection
- * @param Topic $topic
- * @param WampRequest $request
- * @param $event
- * @param array $exclude
- * @param array $eligible
- *
- * @return mixed|void
- * @internal param Topic $Topic
- * @internal param array $eligibles
- */
- public function onPublish(
- ConnectionInterface $connection,
- Topic $topic,
- WampRequest $request,
- $event,
- array $exclude,
- array $eligible
- ) {
- $this->manager->publish($topic, $connection, $event);
- }
-}
diff --git a/src/Util/RpcManager.php b/src/Util/RpcManager.php
index 1c72ba1..49faf15 100644
--- a/src/Util/RpcManager.php
+++ b/src/Util/RpcManager.php
@@ -16,9 +16,10 @@ use App\Entity\PlayedGame;
use App\Interfaces\RpcManagerInterface;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\ORMException;
use Exception;
+use JsonException;
use Psr\Log\LoggerInterface;
+use RuntimeException;
/**
* Class RpcManager
@@ -30,13 +31,12 @@ use Psr\Log\LoggerInterface;
* @link www.splendidbear.org
* @since 2026. 04. 09.
*/
-class RpcManager extends WebsocketManager implements RpcManagerInterface
+class RpcManager implements RpcManagerInterface
{
public function __construct(
- private EntityManagerInterface $entityManager,
- private LoggerInterface $logger,
+ private readonly EntityManagerInterface $entityManager,
+ private readonly LoggerInterface $logger,
) {
- parent::__construct($logger);
}
public function getConnectInformation($params): string
@@ -45,17 +45,33 @@ class RpcManager extends WebsocketManager implements RpcManagerInterface
$grid = $this->getGrid($gameAssoc);
$users = null !== $grid ? $this->getUsers($gameAssoc) : null;
- return base64_encode(json_encode([
- 'grid' => $grid,
- 'users' => $users,
- ], JSON_THROW_ON_ERROR, 512));
+ try {
+ return base64_encode(json_encode([
+ 'grid' => $grid,
+ 'users' => $users,
+ ], JSON_THROW_ON_ERROR, 512));
+ } catch (JsonException $e) {
+ throw new RuntimeException($e->getMessage());
+ }
}
public function saveGrid($data): bool
{
+ $existingGame = $this->entityManager
+ ->getRepository(PlayedGame::class)
+ ->findOneByGameAssoc($data[1]);
+
+ if (null !== $existingGame) {
+ return true;
+ }
+
$playedGame = new PlayedGame();
$grid = new Grid();
- $rows = json_decode(base64_decode($data[0]), true, 512, JSON_THROW_ON_ERROR);
+ try {
+ $rows = json_decode(base64_decode($data[0]), true, 512, JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ throw new RuntimeException($e->getMessage());
+ }
try {
foreach ($rows as $row) {
@@ -66,7 +82,6 @@ class RpcManager extends WebsocketManager implements RpcManagerInterface
/** Save Row */
$gridRow->setGrid($grid);
$this->entityManager->persist($gridRow);
-
}
/** Save Grid */
@@ -81,8 +96,6 @@ class RpcManager extends WebsocketManager implements RpcManagerInterface
$this->entityManager->persist($playedGame);
$this->entityManager->flush();
- } catch (ORMException $e) {
- $this->logger->error($e->getMessage());
} catch (Exception $e) {
$this->logger->error($e->getMessage());
}
diff --git a/src/Util/TopicManager.php b/src/Util/TopicManager.php
index 57bb23f..07665cb 100644
--- a/src/Util/TopicManager.php
+++ b/src/Util/TopicManager.php
@@ -10,20 +10,20 @@
namespace App\Util;
-use App\Entity\User;
use App\Entity\Gamer;
use App\Entity\PlayedGame;
use App\Entity\Step;
+use App\Entity\User;
use App\Interfaces\TopicManagerInterface;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
-use Gos\Bundle\WebSocketBundle\Client\ClientManipulatorInterface;
+use RuntimeException;
+use JsonException;
use Psr\Log\LoggerInterface;
-use Ratchet\ConnectionInterface;
-use Ratchet\Wamp\Topic;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Mercure\HubInterface;
+use Symfony\Component\Mercure\Update;
+use Symfony\Component\Security\Core\User\UserInterface;
/**
* Class TopicManager
@@ -35,79 +35,110 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
* @link www.splendidbear.org
* @since 2026. 04. 09.
*/
-class TopicManager extends WebsocketManager implements TopicManagerInterface
+class TopicManager implements TopicManagerInterface
{
public function __construct(
- protected ClientManipulatorInterface $clientManipulator,
- protected EntityManagerInterface $entityManager,
- protected RequestStack $requestStack,
- protected LoggerInterface $logger
- )
- {
- parent::__construct($logger);
+ private readonly HubInterface $hub,
+ private readonly EntityManagerInterface $entityManager,
+ private readonly LoggerInterface $logger
+ ) {
}
- public function subscribe(Topic $topic, ConnectionInterface $connection): void
+ public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user): void
{
- /** this will broadcast the message to ALL subscribers of this topic. */
- $user = $this->clientManipulator->getClient($connection);
- $userName = $user->getUserIdentifier() ?: 'anon_' . $connection->resourceId;
+ $playedGame = $this->getPlayedGame($gameAssoc);
+ if (null === $playedGame) {
+ return;
+ }
- /** if more user wants to connect than 2 to one channel */
- if ($topic->count() > 2) {
- $topic->remove($connection);
- } else {
- $users = $this->controlUsers($topic, $userName, $user);
+ $users = $this->getUserCollection($playedGame);
+ $count = $this->getPlayerCount($users);
+ $isKnown = in_array($userName, array_filter(array_values($users)), true);
- $topic->broadcast([
- 'userTopicId' => $connection->resourceId,
- 'channel' => $topic->getId(),
- 'user' => $userName,
- 'userCnt' => $topic->count(),
- 'users' => $users
- ]);
+ /** Reject a third player who is not a reconnecting player */
+ if ($count >= 2 && !$isKnown) {
+ return;
+ }
+
+ /** Save the player to the database on a fresh join */
+ if (!$isKnown && $count < 2) {
+ $users = $this->saveUserToDb($gameAssoc, $userName, $user, $count + 1);
+ $count = $this->getPlayerCount($users);
+ }
+
+ $topic = 'mineseeker/channel/' . $gameAssoc;
+
+ try {
+ $this->hub->publish(new Update(
+ $topic,
+ json_encode([
+ 'userTopicId' => $userName,
+ 'channel' => $topic,
+ 'user' => $userName,
+ 'userCnt' => $count,
+ 'users' => $users,
+ ], JSON_THROW_ON_ERROR)
+ ));
+ } catch (JsonException $e) {
+ throw new RuntimeException($e->getMessage());
}
}
- public function unSubscribe(Topic $topic, ConnectionInterface $connection): void
+ public function unSubscribe(string $gameAssoc, string $userName): void
{
- /** This will broadcasts the message to ALL subscribers of this topic. */
- $topic->broadcast(['msg' => $connection->resourceId . ' has left ' . $topic->getId()]);
+ $topic = 'mineseeker/channel/' . $gameAssoc;
+
+ $this->hub->publish(new Update(
+ $topic,
+ json_encode(['msg' => $userName . ' has left ' . $topic])
+ ));
}
- public function publish(Topic $topic, ConnectionInterface $connection, $event): void
+ public function publish(string $gameAssoc, string $userName, array $event): void
{
- $user = $this->clientManipulator->getClient($connection);
- $userName = $user->getUserIdentifier();
-
- /** Save every step by user to db */
null === $event['resign']
- ? $this->saveStepToDb($topic, $event)
- : $this->saveResignToDb($topic, $event['resign']);
+ ? $this->saveStepToDb($gameAssoc, $event)
+ : $this->saveResignToDb($gameAssoc, $event['resign']);
- $topic->broadcast([
- 'userTopicId' => $connection->resourceId,
- 'channel' => $topic->getId(),
- 'user' => $userName,
- 'userCnt' => $topic->count(),
- 'data' => $event
- ]);
+ $playedGame = $this->getPlayedGame($gameAssoc);
+ $users = $this->getUserCollection($playedGame);
+ $count = $this->getPlayerCount($users);
+ $topic = 'mineseeker/channel/' . $gameAssoc;
+
+ try {
+ $this->hub->publish(new Update(
+ $topic,
+ json_encode([
+ 'userTopicId' => $userName,
+ 'channel' => $topic,
+ 'user' => $userName,
+ 'userCnt' => $count,
+ 'data' => $event,
+ ], JSON_THROW_ON_ERROR)
+ ));
+ } catch (JsonException $e) {
+ throw new RuntimeException($e->getMessage());
+ }
}
- /**
- * Save Resign event to database
- *
- * @param $topic
- * @param $color
- */
- private function saveResignToDb(Topic $topic, $color): void
+ private function getPlayedGame(string $gameAssoc): ?PlayedGame
{
- $gameAssoc = explode('/', $topic->getId())[2];
-
- /** @var PlayedGame $playedGame */
- $playedGame = $this->entityManager
+ return $this->entityManager
->getRepository(PlayedGame::class)
->findOneByGameAssoc($gameAssoc);
+ }
+
+ private function getPlayerCount(array $users): int
+ {
+ $red = '' !== $users['red'] || '' !== $users['redAnon'] ? 1 : 0;
+ $blue = '' !== $users['blue'] || '' !== $users['blueAnon'] ? 1 : 0;
+
+ return $red + $blue;
+ }
+
+ private function saveResignToDb(string $gameAssoc, string $color): void
+ {
+ $playedGame = $this->getPlayedGame($gameAssoc);
$playedGame->setResign($color);
@@ -115,30 +146,17 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
$this->entityManager->flush();
}
- /**
- * Save steps and point information to database
- *
- * @param $topic
- * @param $event
- */
- private function saveStepToDb(Topic $topic, $event): void
+ private function saveStepToDb(string $gameAssoc, array $event): void
{
try {
- $gameAssoc = explode('/', $topic->getId())[2];
-
- /** @var PlayedGame $playedGame */
- $playedGame = $this->entityManager
- ->getRepository(PlayedGame::class)
- ->findOneByGameAssoc($gameAssoc);
+ $playedGame = $this->getPlayedGame($gameAssoc);
$step = new Step();
-
$step->setRow($event['coords'][0]);
$step->setCol($event['coords'][1]);
$step->setWBomb($event['bomb']);
$step->setPlayedGame($playedGame);
$step->setCreated(new DateTime());
-
$this->entityManager->persist($step);
$playedGame->setBluePoints($event['bluePoints']);
@@ -146,7 +164,6 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
$playedGame->setBlueExplodedBomb($event['blueExplodedBomb'] ? true : null);
$playedGame->setRedExplodedBomb($event['redExplodedBomb'] ? true : null);
$playedGame->setUpdated(new DateTime());
-
$this->entityManager->persist($playedGame);
$this->entityManager->flush();
@@ -155,62 +172,11 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
}
}
- /**
- * Control all users in a channel
- *
- * @param Topic $topic
- * @param string $userName
- * @param $user
- *
- * @return array
- */
- private function controlUsers(Topic $topic, string $userName, TokenInterface $user): array
+ private function saveUserToDb(string $gameAssoc, string $userName, ?UserInterface $user, int $count): array
{
- $gameAssoc = explode('/', $topic->getId())[2];
+ $playedGame = $this->getPlayedGame($gameAssoc);
- /** @var PlayedGame $playedGame */
- $playedGame = $this->entityManager
- ->getRepository(PlayedGame::class)
- ->findOneByGameAssoc($gameAssoc);
-
- /** @var $users {array} */
- $users = $this->getUserCollection($playedGame);
-
- $red = '' !== $users['red'] || '' !== $users['redAnon'] ? 1 : 0;
- $blue = '' !== $users['blue'] || '' !== $users['blueAnon'] ? 1 : 0;
- $one = $topic->count() === 1;
- $two = $topic->count() === 2;
-
- /** This checks it is a reconnection */
- if (($one && ($red + $blue === 0)) || ($two && ($red + $blue === 1))) {
- /** @var $users {array} w/ save users to database */
- $users = $this->saveUserToDb($topic, $userName, $user, $topic->count());
- }
-
- return $users;
- }
-
- /**
- * Save user data to database
- *
- * @param $topic
- * @param $userName
- * @param $user
- * @param $count
- *
- * @return array
- */
- private function saveUserToDb(Topic $topic, string $userName, TokenInterface $user, $count)
- {
- $gameAssoc = explode('/', $topic->getId())[2];
-
- /** @var PlayedGame $playedGame */
- $playedGame = $this->entityManager
- ->getRepository(PlayedGame::class)
- ->findOneByGameAssoc($gameAssoc);
-
- /** when the user is not anonym */
- null !== $user->getUser()
+ null !== $user
? $this->saveRegisteredUser($userName, $count, $playedGame)
: $this->saveAnonUser($userName, $count, $playedGame);
@@ -220,23 +186,15 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
return $this->getUserCollection($playedGame);
}
- /**
- * Saves the registered user to the database
- *
- * @param string $userName
- * @param int $count
- * @param PlayedGame $playedGame
- */
- private function saveRegisteredUser(string $userName, int $count, PlayedGame $playedGame)
+ private function saveRegisteredUser(string $userName, int $count, PlayedGame $playedGame): void
{
- /** @var User $FOSUser */
+ /** @var User $user */
$user = $this->entityManager
->getRepository(User::class)
->findOneByUsername($userName);
try {
if ($count === 1) {
- /** @var $random {integer} Active player: red: 0, blue: 1 */
$random = random_int(0, 1);
!$random ? $playedGame->setRed($user) : $playedGame->setBlue($user);
} else {
@@ -249,17 +207,8 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
}
}
- /**
- * Save anonymous Gamer to database
- *
- * @param string $userName
- * @param int $count
- * @param PlayedGame $playedGame
- */
- private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame)
+ private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame): void
{
- // $request = $this->requestStack->getCurrentRequest(); // TODO nem megy...
-
try {
$anon = new Gamer();
$anon->setUsername($userName);
@@ -267,7 +216,6 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
$this->entityManager->persist($anon);
if ($count === 1) {
- /** @var $random {integer} Active player: red: 0, blue: 1 */
$random = random_int(0, 1);
!$random ? $playedGame->setRedAnon($anon) : $playedGame->setBlueAnon($anon);
} else {
@@ -280,20 +228,13 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
}
}
- /**
- * Get user collection from PlayedGame entity
- *
- * @param $playedGame
- *
- * @return array
- */
private function getUserCollection(PlayedGame $playedGame): array
{
return [
- 'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '',
- 'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '',
- 'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
- 'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : ''
+ 'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '',
+ 'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '',
+ 'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
+ 'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : '',
];
}
}
diff --git a/src/Util/WebsocketManager.php b/src/Util/WebsocketManager.php
deleted file mode 100644
index 35f41bf..0000000
--- a/src/Util/WebsocketManager.php
+++ /dev/null
@@ -1,47 +0,0 @@
-
- * @category Class
- * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
- * @link www.splendidbear.org
- * @since 2026. 04. 09.
- */
-class WebsocketManager implements WebsocketManagerInterface
-{
- public function __construct(private LoggerInterface $logger) { }
-
- public function reConnect(EntityManagerInterface $entityManager): ?EntityManagerInterface
- {
- try {
- $connection = $entityManager->getConnection();
-
- if (false === $connection->ping()) {
- $connection->close();
- $connection->connect();
- }
- } catch (RuntimeException $e) {
- $this->logger->error($e->getMessage());
- }
-
- return $entityManager;
- }
-}
diff --git a/symfony.lock b/symfony.lock
index fec0ec9..da7b284 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -1,7 +1,4 @@
{
- "cboden/ratchet": {
- "version": "v0.4.1"
- },
"cocur/slugify": {
"version": "v3.1"
},
@@ -74,27 +71,12 @@
"egulias/email-validator": {
"version": "2.1.4"
},
- "evenement/evenement": {
- "version": "v3.0.1"
- },
"friendsofsymfony/user-bundle": {
"version": "v2.1.2"
},
"gos/pnctl-event-loop-emitter": {
"version": "v0.1.7"
},
- "gos/pubsub-router-bundle": {
- "version": "v0.3.3"
- },
- "gos/web-socket-bundle": {
- "version": "v1.8.12"
- },
- "gos/websocket-client": {
- "version": "v0.1.2"
- },
- "guzzlehttp/psr7": {
- "version": "1.4.2"
- },
"jdorn/sql-formatter": {
"version": "v1.2.17"
},
@@ -119,42 +101,15 @@
"psr/container": {
"version": "1.0.0"
},
- "psr/http-message": {
- "version": "1.0.1"
- },
"psr/log": {
"version": "1.0.2"
},
"psr/simple-cache": {
"version": "1.0.1"
},
- "ralouphie/getallheaders": {
- "version": "3.0.3"
- },
- "ratchet/rfc6455": {
- "version": "0.2.4"
- },
- "react/cache": {
- "version": "v0.4.2"
- },
- "react/dns": {
- "version": "v0.4.13"
- },
- "react/event-loop": {
- "version": "v0.5.2"
- },
- "react/promise": {
- "version": "v2.5.1"
- },
"react/promise-timer": {
"version": "v1.3.0"
},
- "react/socket": {
- "version": "v0.8.11"
- },
- "react/stream": {
- "version": "v0.7.7"
- },
"roave/security-advisories": {
"version": "dev-master"
},
@@ -275,6 +230,18 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
+ "symfony/mercure-bundle": {
+ "version": "0.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "0.4",
+ "ref": "b141b8c8f13bc8c31d718a5488039b712c0d3592"
+ },
+ "files": [
+ "config/packages/mercure.yaml"
+ ]
+ },
"symfony/mime": {
"version": "v4.3.5"
},
@@ -295,9 +262,6 @@
"config/packages/test/monolog.yaml"
]
},
- "symfony/options-resolver": {
- "version": "v4.0.9"
- },
"symfony/orm-pack": {
"version": "v1.0.5"
},
diff --git a/templates/Game/play.html.twig b/templates/Game/play.html.twig
index f71ce0c..8a59943 100644
--- a/templates/Game/play.html.twig
+++ b/templates/Game/play.html.twig
@@ -9,8 +9,9 @@
+ data-game-id="{{ app.request.get('gameAssoc') }}"
+ data-mercure-hub-url="{{ mercure_hub_url }}"
+ data-mercure-subscriber-jwt="{{ mercure_subscriber_jwt }}">
{% endblock %}
@@ -27,7 +28,7 @@
{% block stylesheets %}
{{ encore_entry_link_tags('mineseekerStyle') }}
-