Sandstone Edition Cookbook
Once you installed a fresh edition of Sandstone, you can start to build your real-time RestApi.
That means creating API endpoints, websocket topics…
Creating an API endpoint
As Sandstone extends Silex, just create a controller class and a method, then mount it with Silex.
Also, this edition allows to use annotations for routing.
src/App/Controller/HelloController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use DDesrosiers\SilexAnnotations\Annotations as SLX;
use Alcalyn\SerializableApiResponse\ApiResponse;
/**
* @SLX\Controller(prefix="/api")
*/
class HelloController
{
/**
* Test endpoint which returns a hello world.
*
* @SLX\Route(
* @SLX\Request(method="GET", uri="hello/{name}"),
* @SLX\Value(variable="name", default="world")
* )
*
* @param string $name
*
* @return ApiResponse
*/
public function getHello($name)
{
$result = [
'hello' => $name,
];
return new ApiResponse($result, Response::HTTP_OK);
}
}
Note: Using
ApiResponseallows to make your controllers return a non-yet-serialized object (see alcalyn/serializable-api-response).Sandstone transforms the
ApiResponseto a SymfonyResponseonly at the very end, after serialization.
Related documentation:
Creating a websocket topic
A websocket topic is like a “category”, or a “channel” of communication. It allows to listen to messages from a same “channel”, without receiving all others messages from the websocket server.
Technically, each topic has its own Topic class,
which contains its own logic.
Under Sandstone, a topic has a name (i.e chat/general) and can be declared like a route.
Creating the topic class
src/App/Topic/ChatTopic.php
namespace App\Topic;
use Ratchet\Wamp\WampConnection;
use Eole\Sandstone\Websocket\Topic;
class ChatTopic extends Topic
{
/**
* Broadcast message to each subscribing client.
*
* {@InheritDoc}
*/
public function onPublish(WampConnection $conn, $topic, $event)
{
$this->broadcast([
'message' => $event,
]);
}
}
Register the topic
src/App/AppWebsocketProvider.php
use App\Topic\ChatTopic;
public function register(Container $app)
{
$app->topic('chat/{channel}', function ($topicPattern) {
return new ChatTopic($topicPattern);
});
}
Note: Don’t forget to restart websocket server when editing topics. You can run
make restart_websocket_serverif you use Docker.
Then you can now subscribe to chat/general, chat/private, chat/whatever, …
Sandstone Topic class extends Ratchet\Wamp\Topic,
which is based on Wamp protocol.
Note that you can use all Silex route configuration like:
$this
->topic('chat/{channel}', function ($topicPattern) {
return new ChatTopic($topicPattern);
})
->value('channel', 'general') // Set a default channel name in case someone subscribes to `chat`
->assert('channel', '[a-z]') // Add constraint on channel name, only lowercases
->convert('channel', function () { /* ... */ }) // Add a converter on channel name
;
Note: You can’t use
->method('get')or->requireHttps()for a topic route ;)
Retrieve route arguments from topic name
In case your topic name is something like chat/{channel}
and you need to pass the {channel} argument to your Topic class:
$this->topic('chat/{channel}', function ($topicPattern, $arguments) {
$channelName = $arguments['channel'];
return new ChatTopic($topicPattern, $channelName);
});
Related documentation:
- Silex routing: http://silex.sensiolabs.org/doc/2.0/usage.html.
- Wamp protocol implementation on RatchetPHP: http://socketo.me/docs/wamp.
- Wamp protocol: http://wamp-proto.org/.
Send a Push event from RestApi to a websocket topic
Sometimes you’ll want to notify websocket clients when the RestApi state changes (a new resource has been PUT or POSTed, or a resource has been PATCHed…).
Then this part is for you.
The logic here is to dispatch an event from the controller behind i.e postArticle,
then this event will be forwarded (i.e redisptached) over the WebsocketApplication.
Then just listen this event from a topic, and do something like broadcast a message…
1. Dispatch event from controller
src/App/Controller/HelloController.php
public function getHello($name)
{
$this->container['dispatcher']->dispatch(HelloEvent::HELLO, new HelloEvent($name));
}
Note:
$this->containeris passed to your controllers constructors if you use the@SLX\Controllerannotation.
2. Mark the event to be forwarded
app/RestApiApplication.php
use App\Event\HelloEvent;
private function registerUserProviders()
{
$app->forwardEventToPushServer(HelloEvent::HELLO);
}
Note: This must be done only in RestApi stack. If it’s done in websocket stack, the event will be redispatched infinitely to itself!
3. Listen the event from a topic
It will listen and receive the event that has been serialized/deserialized through the Push server, from the RestApi thread to the websocket server thread.
src/App/Topic/ChatTopic.php
namespace App\Topic;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Ratchet\Wamp\WampConnection;
use Eole\Sandstone\Websocket\Topic;
use App\Event\HelloEvent;
class ChatTopic extends Topic implements EventSubscriberInterface
{
/**
* Subscribe to article.created event.
*
* {@InheritDoc}
*/
public static function getSubscribedEvents()
{
return [
HelloEvent::HELLO => 'onHello',
];
}
/**
* Article created listener.
*
* @param HelloEvent $event
*/
public function onHello(HelloEvent $event)
{
$this->broadcast([
'message' => 'Someone called api/hello. Hello '.$event->getName(),
]);
}
}
Note: Sandstone automatically subscribes topics (to the EventDispatcher) that implement the
Symfony\Component\EventDispatcher\EventSubscriberInterface.
Up to you to create a HelloEvent class and create serialization metadata.
Note: You need to create serialization metadata for objects that are forwarded. It need to be serialized and deserialized around the Push server.
Related documentation:
Doctrine
Sandstone edition integrates Doctrine:
- Doctrine DBAL and ORM are installed,
- you can use entities with annotations or yaml mapping, the
orm:schema-tool… - Doctrine commands are available under
php bin/console - Entities serialization is well handled (fixes relations infinite loops). See serializer-doctrine-proxies.
Creating an entity
Here using annotations.
src/App/Entity/Article.php
namespace App\Entity;
use Doctrine\ORM\Mapping\Entity;
/**
* @Entity
*/
class Article
{
/**
* @var int
*
* @Id
* @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @Column(type="string")
*/
private $title;
/**
* @var \DateTime
*
* @Column(type="datetime")
*/
private $dateCreated;
// getters and setters...
}
Serialization metadata
If your entity is meant to be serialized, which happens in any of these cases:
- rendered in json (or xml, yml…) to the RestApi user
- sent to the websocket server from the rest api (forwarded)
src/App/Resources/serializer/App.Entity.Article.yml
App\Entity\Article:
exclusion_policy: NONE
properties:
id:
type: integer
title:
type: string
dateCreated:
type: DateTime
Updating the database
Use the Doctrine command:
php bin/console orm:schema-tool:update --force
Retrieve Repository from container
$app['orm.em']->getRepository('App\\Entity\\Article');
Related documentation:
- Serializer available Types (
string,integer, …) - Serializer metadata Yaml reference
- Doctrine available Types
- Doctrine commands
Debugging with Symfony web profiler
Silex web profiler is already integrated in Sandstone edition.
It is available under /index-dev.php/_profiler/.
It allows you to debug RestApi requests: exceptions, Doctrine queries, called listeners…
Sandstone also provides a Push message debugger (since version 1.1)
to check which messages has been sent to the websocket stack.
Cross origin
If your front-end application is not hosted under the same domain name
(i.e http://localhost for the front-end and http://localhost:8480 for the RestApi),
then you probably get cross origin errors when trying to query your RestApi using Ajax.
This is a server-side security against XSS attacks.
To fix this issue, you have to configure your RestApi server to let him send responses to a precise domain name.
The edition integrates jdesrosiers/silex-cors-provider, so you just have to configure it:
config/environment.php
return [
'cors' => [
// only serve `localhost` (if your front-end application is on `localhost`)
'cors.allowOrigin' => 'http://localhost',
// or to serve **All clients**
'cors.allowOrigin' => '*',
],
];
About Makefile
The Makefile only works for a Docker installation.
make: Used most of the time, install and run the project. Makes containers started.
make run: Start containers.
make bash: Open a bash session into php container.
make update: Use it to update composer dependencies, rebuild and recreate docker containers.
make logs: display container logs.
make restart_websocket_server: Should be used after the websocket source code changed,
in example when you develop a websocket topic.
make optimize_autoloader: Optimize composer autoloader and reduce autoloader execution time by ~80%.
Only use it in prod. Use make to remove optimization.
make book: Display help (make commands, urls to api, PHPMyAdmin…)
make book
#
# Default urls:
# http://0.0.0.0:8480/hello/world.html Diagnostic page.
# http://0.0.0.0:8480/index-dev.php/api/hello *hello world* route in **dev** mode.
# http://0.0.0.0:8480/api/hello *hello world* route in **prod** mode.
# http://0.0.0.0:8480/index-dev.php/_profiler/ Symfony web profiler (only dev mode).
# http://0.0.0.0:8481 PHPMyAdmin (login: `root` / `root`).
# ws://0.0.0.0:8482 Websocket server.
#
# Make commands:
# make Install application and run it
# make run Run application
# make bash Enter in php container
# make logs Display containers logs and errors
# make update rebuild containers, update composer dependencies...
# make restart_websocket_server Reload websocket stack, i.e when code is updated
# make book Display this help
#
# See Sandstone edition cookbook:
# https://github.com/eole-io/sandstone-edition
#
# See Sandstone documentation:
# https://eole-io.github.io/sandstone
#