Статьи

Еще один пример WebSockets, Socket.io и AngularJs, работающих с Silex Backend

Помните мой  последний пост о WebSockets  и AngularJs? Сегодня мы будем играть с чем-то похожим. Я хочу создать интерфейс ключ-значение для игры с веб-сокетами. Позвольте мне объяснить это немного.

Сначала мы увидим бэкэнд. Одно приложение Silex с двумя маршрутами: получить один и отправить один:

<?php
 
include __DIR__ . '/../../vendor/autoload.php';
include __DIR__ . '/SqlLiteStorage.php';
 
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Silex\Provider\DoctrineServiceProvider;
 
$app = new Application([
    'debug'      => true,
    'ioServer'   => 'http://localhost:3000',
    'httpServer' => 'http://localhost:3001',
]);
 
$app->after(function (Request $request, Response $response) {
    $response->headers->set('Access-Control-Allow-Origin', '*');
});
 
$app->register(new G\Io\EmitterServiceProvider($app['httpServer']));
$app->register(new DoctrineServiceProvider(), [
    'db.options' => [
        'driver' => 'pdo_sqlite',
        'path'   => __DIR__ . '/../../db/app.db.sqlite',
    ],
]);
$app->register(new G\Io\Storage\Provider(new SqlLiteStorage($app['db'])));
 
$app->get('conf', function (Application $app, Request $request) {
    $chanel = $request->get('token');
    return $app->json([
        'ioServer' => $app['ioServer'],
        'chanel'   => $chanel
    ]);
});
 
$app->get('/{key}', function (Application $app, $key) {
    return $app->json($app['gdb.get']($key));
});
 
$app->post('/{key}', function (Application $app, Request $request, $key) {
    $content = json_decode($request->getContent(), true);
 
    $chanel = $content['token'];
    $app->json($app['gdb.post']($key, $content['value']));
 
    $app['io.emit']($chanel, [
        'key'   => $key,
        'value' => $content['value']
    ]);
 
    return $app->json(true);
});
 
$app->run();

Как мы видим, мы регистрируем одного поставщика услуг:

$app->register(new G\Io\Storage\Provider(new SqlLiteStorage($app['db'])));

Этому провайдеру нужен экземпляр StorageIface

namespace G\Io\Storage;
 
interface StorageIface
{
    public function get($key);
 
    public function post($key, $value);
}

Наша реализация использует SqlLite, но довольно просто перейти на другое хранилище базы данных или даже базу данных NoSql.

use Doctrine\DBAL\Connection;
use G\Io\Storage\StorageIface;
 
class SqlLiteStorage implements StorageIface
{
    private $db;
 
    public function __construct(Connection $db)
    {
        $this->db = $db;
    }
 
    public function get($key)
    {
        $statement = $this->db->executeQuery('select value from storage where key = :KEY', ['KEY' => $key]);
        $data      = $statement->fetchAll();
 
        return isset($data[0]['value']) ? $data[0]['value'] : null;
    }
 
    public function post($key, $value)
    {
        $this->db->beginTransaction();
 
        $statement = $this->db->executeQuery('select value from storage where key = :KEY', ['KEY' =>; $key]);
        $data      = $statement->fetchAll();
 
        if (count($data) > 0) {
            $this->db->update('storage', ['value' => $value], ['key' => $key]);
        } else {
            $this->db->insert('storage', ['key' => $key, 'value' => $value]);
        }
 
        $this->db->commit();
 
        return $value;
    }
}

Мы также регистрируем другого поставщика услуг:

$app->register(new G\Io\EmitterServiceProvider($app['httpServer']));

Ответственность этого поставщика заключается в том, чтобы уведомлять сервер веб-сокета о любых изменениях в хранилище:

namespace G\Io;
 
use Pimple\Container;
use Pimple\ServiceProviderInterface;
 
class EmitterServiceProvider implements ServiceProviderInterface
{
    private $server;
 
    public function __construct($url)
    {
        $this->server = $url;
    }
 
    public function register(Container $app)
    {
        $app['io.emit'] = $app->protect(function ($chanel, $params) use ($app) {
            $s = curl_init();
            curl_setopt($s, CURLOPT_URL, '{$this->server}/emit/?' . http_build_query($params) . '&_chanel=' . $chanel);
            curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
            $content = curl_exec($s);
            $status  = curl_getinfo($s, CURLINFO_HTTP_CODE);
            curl_close($s);
 
            if ($status != 200) throw new \Exception();
 
            return $content;
        });
    }
}

Сервер Websocket — это простой   сервер socket.io, а также   сервер Express для обработки триггеров бэкэнда.

var
    express = require('express'),
    expressApp = express(),
    server = require('http').Server(expressApp),
    io = require('socket.io')(server, {origins: 'localhost:*'})
    ;
 
expressApp.get('/emit', function (req, res) {
    io.sockets.emit(req.query._chanel, req.query);
    res.json('OK');
});
 
expressApp.listen(3001);
 
server.listen(3000);

Наше клиентское приложение — это приложение AngularJs:

<!doctype html>
<html ng-app="app">
<head>
    <script src="//localhost:3000/socket.io/socket.io.js"></script>
    <script src="assets/angularjs/angular.js"></script>
    <script src="js/app.js"></script>
    <script src="js/gdb.js"></script>
</head>
<body>
  
<div ng-controller="MainController">
    <input type="text" ng-model="key">
    <button ng-click="change()">change</button>
</div>
  
</body>
</html>
angular.module('app', ['Gdb'])
 
    .run(function (Gdb) {
        Gdb.init({
            server: 'http://localhost:8080/gdb',
            token: '4b96716bcb3d42fc01ff421ea2cfd757'
        });
    })
 
    .controller('MainController', function ($scope, Gdb) {
        $scope.change = function () {
            Gdb.set('key', $scope.key).then(function() {
                console.log("Value set");
            });
        };
 
        Gdb.get('key').then(function (data) {
            $scope.key = data;
        });
 
        Gdb.watch('key', function (value) {
            console.log("Value updated");
            $scope.key = value;
        });
    })
;

Как мы видим, приложение AngularJs использует одну небольшую библиотеку под названием Gdb для обработки связи с бэкендом и WebSockets:

angular.module('Gdb', [])
    .factory('Gdb', function ($http, $q, $rootScope) {
 
        var socket,
            gdbServer,
            token,
            watches = {};
 
        var Gdb = {
            init: function (conf) {
                gdbServer = conf.server;
                token = conf.token;
 
                $http.get(gdbServer + '/conf', {params: {token: token}}).success(function (data) {
                    socket = io.connect(data.ioServer);
                    socket.on(data.chanel, function (data) {
                        watches.hasOwnProperty(data.key) ? watches[data.key](data.value) : null;
                        $rootScope.$apply();
                    });
                });
            },
 
            set: function (key, value) {
                var deferred = $q.defer();
 
                $http.post(gdbServer + '/' + key, {value: value, token: token}).success(function (data) {
                    deferred.resolve(data);
                });
 
                return deferred.promise;
            },
 
            get: function (key) {
                var deferred = $q.defer();
 
                $http.get(gdbServer + '/' + key, {params: {token: token}}).success(function (data) {
                    deferred.resolve(JSON.parse(data));
                });
 
                return deferred.promise;
            },
 
            watch: function (key, closure) {
                watches[key] = closure;
            }
        };
 
        return Gdb;
    });

И это все. Вы можете увидеть весь проект на  GitHub .