Full example of websocket application working with a RestApi
Example of a fully working rest api working together with a websocket server.
Note: there is a Docker image for this example. Check zareba_pawel/php-websockets-sandstone.
Use case
We want a rest api endpoint to post articles, and a multichannel chat where we can talk, and be notified when an article is published.
How to do it with Sandstone:
Rest Api stack:
A rest api will allow to POST articles
by calling POST rest-api.php/api/articles
,
then the rest api will respond with a http status CREATED.
Websocket stack:
In a same way, a multichannel chat server in mounted
by running php websocket-server.php
,
we can enter and publish message to channels (i.e channel/general
).
Push server:
When someone will publish an article using rest api,
an event will be sent to websocket server thread using Push server,
and this event will be publish to all chat channels
to notice chat users that an article has just been published.
Installation
Create your composer.json:
composer.json
{
"require": {
"eole/sandstone": "2.0.x"
},
"autoload": {
"psr-4": {
"": "src/"
}
}
}
Then run composer update
.
Project structure
Silex\Application
Eole\Sandstone\Application
src/MyApp.php
class MyApp extends Eole\Sandstone\Application
{
public function __construct()
{
parent::__construct([
'debug' => true,
]);
// Sandstone requires JMS serializer
$this->register(new Eole\Sandstone\Serializer\ServiceProvider());
// Register and configure your websocket server
$this->register(new Eole\Sandstone\Websocket\ServiceProvider(), [
'sandstone.websocket.server' => [
'bind' => '0.0.0.0',
'port' => '25569',
],
]);
// Register Push Server and ZMQ bridge extension
$this->register(new \Eole\Sandstone\Push\ServiceProvider());
$this->register(new \Eole\Sandstone\Push\Bridge\ZMQ\ServiceProvider(), [
'sandstone.push.server' => [
'bind' => '127.0.0.1',
'host' => '127.0.0.1',
'port' => 5555,
],
]);
// Register serializer metadata
$this['serializer.builder']->addMetadataDir(
__DIR__,
''
);
}
}
rest-api.php
require_once 'vendor/autoload.php';
use Symfony\Component\HttpFoundation\JsonResponse;
$app = new MyApp();
// Creating an api endpoint at POST api/articles
$app->post('api/articles', function () use ($app) {
// Persisting article...
$articleId = 42;
$event = new ArticleEvent();
$event->id = $articleId;
$event->title = 'Unicorns spotted in Alaska';
$event->url = 'http://unicorn.com/articles/unicorns-spotted-alaska';
// Dispatch an event on article creation
$app['dispatcher']->dispatch('article.created', $event);
return new JsonResponse($articleId, 201);
});
// Send all 'article.created' events to push server
$app->forwardEventToPushServer('article.created');
$app->run();
websocket-server.php
require_once 'vendor/autoload.php';
$app = new MyApp();
// Add a route `chat/{channel}` and its Topic factory (works same as mounting API endpoints)
$app->topic('chat/{channel}', function ($topicPattern) {
return new ChatTopic($topicPattern);
});
// Encapsulate your application and start websocket server
$websocketServer = new Eole\Sandstone\Websocket\Server($app);
$websocketServer->run();
Implement ChatTopic class
In this example, we used a ChatTopic
class.
In our case, we want:
-
ChatTopic
just broadcast all incoming chat messages to every subscribers, - broadcast a message when an article has been posted.
So we need to listen to article.created
event:
The topic class has to implement Symfony\Component\EventDispatcher\EventSubscriberInterface
to automatically listen to events (to be used with forwardEventToPushServer
).
So let's implement ChatTopic
class:
src/ChatTopic.php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ChatTopic extends Eole\Sandstone\Websocket\Topic implements EventSubscriberInterface
{
/**
* Broadcast message to each subscribing client.
*
* {@InheritDoc}
*/
public function onPublish(Ratchet\Wamp\WampConnection $conn, $topic, $event)
{
$this->broadcast([
'type' => 'message',
'message' => $event,
]);
}
/**
* Subscribe to article.created event.
*
* {@InheritDoc}
*/
public static function getSubscribedEvents()
{
return [
'article.created' => 'onArticleCreated',
];
}
/**
* Article created listener.
*
* @param ArticleEvent $event
*/
public function onArticleCreated(ArticleEvent $event)
{
$this->broadcast([
'type' => 'article_created',
'message' => 'An article has just been published: '.$event->title.', read it here: '.$event->url,
]);
}
}
About serialization
When an object is sent to websocket server from rest api stack, or is sent by rest api response to http client, Sandstone uses JMS serializer to serialize it at one side, and deserialize at the other side.
In this example, an instance of ArticleEvent
is sent to websocket server.
So this is how the class and its serializer metadata look like:
src/ArticleEvent.php
use Symfony\Component\EventDispatcher\Event;
class ArticleEvent extends Event
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $title;
/**
* @var string
*/
public $url;
}
src/ArticleEvent.yml
ArticleEvent:
exclusion_policy: NONE
properties:
id:
type: integer
title:
type: string
url:
type: string
That's why we needed to add in MyApp
:
src/MyApp.php
// Register serializer metadata
$this['serializer.builder']->addMetadataDir(
__DIR__,
''
);
Now, let's run the thing
The rest api endpoint will be hit with:
POST rest-api.php/api/articles
HTTP/1.1 201 Created
Content-Length: 2
Content-Type: application/json
42
Run the websocket server and push server with:
php websocket-server.php
# Websocket server is starting
[info] Initialization... []
[info] Bind websocket server {"bind":"0.0.0.0","port":"25569"}
[info] Bind push server {"bind":"127.0.0.1","host":"127.0.0.1","port":5555}
# Someone connects to websocket server
[info] Connection event {"event":"open"}
[info] Authentication... []
[info] Anonymous connection []
# the connected guy subscribes to a chat topic
[info] Topic event {"event":"subscribe","topic":"chat/general"}
# Websocket server received an event from rest api
[info] Push message event {"event":"article.created"}