Thanks to Ratchet, it's easy to get the user info from the session your website's visitors create. As per the Ratchet documentation, not all session handlers are compatible with this system.
To use session sharing, the session handler must be a service defined in your application. The below example creates a service which uses PDO as the session handler and re-uses the same connection opened by Doctrine.
session.handler.pdo:
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
arguments:
- !service { class: PDO, factory: 'database_connection:getWrappedConnection' }
- { lock_mode: 0 }
If using the PDO session handler, ensure you have set up the database correctly as well.
First, you will need to ensure the FrameworkBundle is configured to use the session handler service you've defined. For Symfony Standard based applications, you should update the app/config/config.yml
file. For Symfony Flex applications, you should update config/packages/framework.yaml
.
framework:
session:
handler_id: 'session.handler.pdo'
Next, you will need to ensure the GosWebSocketBundle is configured to use the same session handler. For Symfony Standard based applications, you should update the app/config/config.yml
file. For Symfony Flex applications, you should update config/packages/gos_web_socket.yaml
.
gos_web_socket:
client:
firewall: main # Can be an array of firewalls
session_handler: 'session.handler.pdo'
Note: You must ensure your application's sessions are set up correctly to allow both the websocket server and your HTTP server to read the same session cookies.
Important: If you change session name in Symfony change parameter "'session.name" in php.ini
When a connection is opened to the websocket server, the user is authenticated against the firewall(s) you have configured the bundle to use from your application.
Similar to the getUser()
method in controllers, an anonymous user is represented as a string and an authenticated user is represented as a Symfony\Component\Security\Core\User\UserInterface
object (typically the User entity or data object you have configured).
The Symfony\Component\Security\Core\Authentication\Token\TokenInterface
that all user info is derived from is stored in a bundle specific persistence layer, by default this is an in-memory storage layer.
The storage layer can be customized using the gos_web_socket.client.storage
configuration key. For Symfony Standard based applications, you should update the app/config/config.yml
file. For Symfony Flex applications, you should update config/packages/gos_web_socket.yaml
.
gos_web_socket:
client:
firewall: main
session_handler: 'session.handler.pdo'
storage:
driver: 'app.websocket.client_storage.predis'
ttl: 28800 # Optional, time to live if you use a compatible cache driver
prefix: client # Optional, key prefix if you use a compatible cache driver, creates key as "client:1" instead of "1"
services:
app.websocket.client_storage.predis:
class: Gos\Bundle\WebSocketBundle\Client\Driver\PredisDriver
arguments:
- '@Predis\Client'
- '%web_socket_server.client_storage.prefix'
In this example, the storage has been changed to a service defined in your application to use a Predis Client as the storage driver.
A decorator is provided which allows for cache drivers from Doctrine's Cache Library to be used as the client storage driver.
The below example is used to create a Redis cache provider:
doctrine_cache:
providers:
redis_cache:
redis:
host: 127.0.0.1
port: 6379
database: 3
websocket_cache_client:
type: redis
alias: app.doctrine_cache.websocket
You can now use it as the driver for the client storage layer.
gos_web_socket:
client:
firewall: main
session_handler: 'session.handler.pdo'
storage:
driver: 'app.doctrine_cache.websocket' # The service which should be decorated
decorator: 'gos_web_socket.client.driver.doctrine_cache' # The decorator to apply to the driver
A decorator is provided which allows for cache drivers from Symfony's Cache Component to be used as the client storage driver.
The below example is used to create a Redis cache provider:
framework:
cache:
default_redis_provider: redis://localhost
You can now use it as the driver for the client storage layer.
gos_web_socket:
client:
firewall: main
session_handler: 'session.handler.pdo'
storage:
driver: 'cache.adapter.redis' # The service which should be decorated
decorator: 'gos_web_socket.client.driver.symfony_cache' # The decorator to apply to the driver
If need be, you can also create your own storage driver. All drivers must implement Gos\Bundle\WebSocketBundle\Client\Driver\DriverInterface
.
Whenever the Ratchet\ConnectionInterface
instance is available, you are able to retrieve the user account info using a Gos\Bundle\WebSocketBundle\Client\ClientManipulatorInterface
instance (by default, the @gos_web_socket.client.manipulator
service).
For example inside a RPC handler:
<?php
namespace App\Websocket\Rpc;
use Gos\Bundle\WebSocketBundle\Client\ClientManipulatorInterface;
use Gos\Bundle\WebSocketBundle\Router\WampRequest;
use Gos\Bundle\WebSocketBundle\RPC\RpcInterface;
use Ratchet\ConnectionInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class AcmeRpc implements RpcInterface
{
/**
* @var ClientManipulatorInterface
*/
private $clientManipulator;
/**
* @param ClientManipulatorInterface $clientManipulator
*/
public function __construct(ClientManipulatorInterface $clientManipulator)
{
$this->clientManipulator = $clientManipulator;
}
/**
* Adds the params together, if the user is authenticated
*
* @param ConnectionInterface $connection
* @param WampRequest $request
* @param array $params
*
* @return array
*/
public function sum(ConnectionInterface $connection, WampRequest $request, $params)
{
$user = $this->clientManipulator->getClient($connection);
if ($user instanceof UserInterface) {
return ['result' => array_sum($params)];
}
return ['error' => true, 'msg' => 'You must be authenticated to use this function.'];
}
/**
* Name of the RPC handler, used by the PubSub router.
*
* @return string
*/
public function getName()
{
return 'acme.rpc';
}
}
You can use the findAllByUsername
method of the client manipulator to find all active connections for the given username.
<?php
namespace App\Websocket\Topic;
use Gos\Bundle\WebSocketBundle\Router\WampRequest;
use Gos\Bundle\WebSocketBundle\Topic\TopicInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\Topic;
final class AcmeTopic implements TopicInterface
{
/**
* @var ClientManipulatorInterface
*/
private $clientManipulator;
/**
* @param ClientManipulatorInterface $clientManipulator
*/
public function __construct(ClientManipulatorInterface $clientManipulator)
{
$this->clientManipulator = $clientManipulator;
}
/**
* 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)
{
// This will broadcast the message to ALL subscribers of this topic.
$topic->broadcast(['msg' => $connection->resourceId.' has joined '.$topic->getId()]);
}
/**
* 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)
{
// This will broadcast the message to ALL subscribers of this topic.
$topic->broadcast(['msg' => $connection->resourceId.' has left '.$topic->getId()]);
}
/**
* This will receive any Publish requests for this topic.
*
* @param ConnectionInterface $connection
* @param Topic $topic
* @param WampRequest $request
* @param mixed $event
* @param array $exclude
* @param array $eligibles
*
* @return mixed
*/
public function onPublish(
ConnectionInterface $connection,
Topic $topic,
WampRequest $request,
$event,
array $exclude,
array $eligible
) {
if (!isset($event['username'])) {
// Broadcast an error back to the publisher
$topic->broadcast(
['error' => true, 'msg' => 'The username parameter is required.'],
[],
[$connection->WAMP->sessionId]
);
return;
}
$recipients = $this->clientManipulator->findAllByUsername($topic, $params['username']);
if (!empty($recipients)) {
$recipientIds = [];
foreach ($recipients as $recipient) {
$recipientIds[] = $userConnection['connection']->WAMP->sessionId;
}
$topic->broadcast('message', [], $recipientIds);
}
}
/**
* Like RPC the name is used to identify the channel
*
* @return string
*/
public function getName()
{
return 'acme.topic';
}
}
For information on sharing the config between server and client, read the Sharing Config Code Cookbook.
For info on bootstrapping the session with extra information, check out the Events