Привет, Хабр! Хочу немного вклиниться в цикл статей и показать как можно простым путём сделать обновление списка комментариев в ленте в режиме реального времени. Как это происходит, например, на github


Для реализации этой задачи будем использовать сервис Post Hawk. Код доступен в соответствующей ветке.

Часть 1. Устанавливаем зависимости (обязательная)


Если за основу у вас взята ветка 5, то необходимо добавить в composer.json зависимость:
composer require post-hawk/hawk-api-bundle ~1.0

а если 5.1, то достаточно просто
composer update

и дождаться установки (обновления) пакета(-ов).

Далее устанавливаем erlang. Билды под разные системы находятся здесь, или, если есть желание, можно собрать из исходников.
При установке в windows, не забываем ставить галочку добавить путь в PATH.

Переходим в любое место файловой системы, какое вам больше нравится (можно рядом с основным проектом) и клонируем сервер и клиент
git clone https://github.com/postHawk/hawk_client.git
git clone https://github.com/postHawk/hawk_server

Собираем rebar:
$ git clone git://github.com/rebar/rebar.git
$ cd rebar
$ ./bootstrap

получившийся файлик (rebar) копируем в оба репозитория. Он нам необходим для сборки проекта.
Проект использует 2 версию rebar. Под rebar3 пока запустить не удаётся

Переходим в папку с сервером, собираем его и запускаем:
cd hawk_server
nano src/hawk_server.app.src
#заполняем данные о пользователе

{env, [
            {statistic, [{use, false}]},
            user, #{
               <<"login">> => <<"symblog">>,
               <<"domain">> => [ %список доменов с которых будут приниматься подключения
                   <<"127.0.0.1:8000">>
               ],
               <<"key">> => <<"very secret key">> %api ключ. Должен совпадать на сервере и клиенте
            }}
]}

mv .erlang .erlang_
rebar get-deps compile
mv .erlang_ .erlang

erl -name 'hawk_server@127.0.0.1' -boot start_sasl  -setcookie test -kernel inet_dist_listen_min 9000  inet_dist_listen_max 9005


Аналогично собираем и запускаем клиент:
cd hawk_client
nano src/hawk_client.app.src
#заполните название server_node, например, 'test_hawk_server@127.0.0.1' и api_key (должен совпадать с серверным). Сохраните файл

{env, [
         {api_key, <<"very secret key">>},
         {server_node, 'hawk_server@127.0.0.1'}
     ]}

mv .erlang .erlang_
./rebar get-deps compile
mv .erlang_ .erlang
erl -name 'hawk_client@127.0.0.1' -boot start_sasl  -setcookie test -kernel inet_dist_listen_min 9000  inet_dist_listen_max 9005

Если нужно запустить процесс в фоне, просто добавьте к набору параметров опцию -detached
Для пользователей windows утилиту запуска стоит изменить с erl на werl

Часть 2. Дорабатываем блог (обязательная для ветки 5)


Кофигурация бандла:

//app/AppKernel.php
$bundles = array(
    ...
    new Hawk\ApiBundle\HawkApiBundle(),
    new FOS\JsRoutingBundle\FOSJsRoutingBundle(),
);

#app/config/config.yml
hawk_api:
    client:
        host: '%hawk_api.client.host%' #ip или домен
        port: '%hawk_api.client.port%' #порт, который слушает клиент
        key: '%hawk_api.client.key%'

#app/config/parameters.yml
parameters:
    hawk_api.client.host: 127.0.0.1
    hawk_api.client.port: 7777
    hawk_api.client.key: 'very secret key'

#app/config/routing.yml
hawk:
    resource: '@HawkApiBundle/Controller/'
    prefix:   /hawk

fos_js_routing:
    resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml"

Ставим assets:
php app/console assets:install --symlink web

Дорабатываем контроллер:


...
use Blogger\BlogBundle\Entity\Blog;
use Hawk\ApiBundle\Event\GroupMessage;

В функцию добавления комментария помещаем отправку уведомлений:
...
$em->persist($comment);
$em->flush();

$this->sendNotification($comment, $blog);

return $this->redirect($this->generateUrl('BloggerBlogBundle_blog_show', array(
...

ну и добавляем саму функцию:

Код
/**
 * Отправка уведомления о новом комментарии
 * @param Comment $comment комментарий
 * @param Blog $blog блог
 */
private function sendNotification(Comment $comment, Blog $blog)
{
    //формируем тело комментария
    $comment_text = $this->renderView('BloggerBlogBundle:Comment:index.html.twig', [
        'comments'  => [$comment]
    ]);

    //формируем сообщение
    $gMessage = new GroupMessage();
    $gMessage
        ->setFrom('comment_demon')
        ->setGroups(['blog_' . $blog->getId()])
        ->setText(['comment' => $comment_text])
        ->setEvent('new_comment') //будет сгенерирован на клиенте
    ;

    //отсылаем
    $api = $this
        ->container
        ->get('event_dispatcher')
        ->dispatch(GroupMessage::NEW_MESSAGE, $gMessage)
        ->getResult() //HawkApi
    ;

    //если возникли ошибки пишем их в лог
    if($api->hasErrors()){
        $logger = $this->get('logger');
        $logger->error('Error sending message: ' . print_r($api->getErrors(), 1));
    }
}


Немного остановлюсь на том, что здесь происходит. Отправка сообщений через сервис возможна двумя способами. Первый это как на примере выше, через систему событий symfony. Создаётся объект сообщения (простого или группового) и посылается с определённым типом события. Второй вариант — это отправка непосредственно с помощью апи, которое можно получить из контейнера:
$api = $this->get('hawk_api.api')->getApi();
$api
    ->registerUser($id)
    ->sendMessage($from, $to, $text, $event)
    ->execute()
    ->getResult('sendMessage')
;


Дорабатываем шаблоны:


В src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig меняем строку
    <article class="blog">

на
    <article class="blog" data-blog-id="{{ blog.id }}">

, так как нам необходим будет id блога для подписки пользователя.

Подключаем скрипты:
{#src/Blogger/BlogBundle/Resources/views/layout.html.twig#}
{% block javascripts %}
    {% javascripts
        '@BloggerBlogBundle/Resources/public/js/*'
        output='js/plugins,js'
        filter='?yui_js'
        %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    {% javascripts
        '../vendor/post-hawk/hawk-api/Resources/public/js/hawk_api.min.js'
    %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
    <script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
{% endblock %}


Ну и пишем немного js


код
$(document).ready(function () {
    connectToHawk();
});

function connectToHawk()
{
    //отправляем запрос за токеном
    $.post(Routing.generate('hawk_token', {
        useSessionId: 1
    }), {}, function (data) {
        if(data.errors === false){
            //инициализируем подключение
            HAWK_API.init({
                user_id: data.result.id,
                token: data.result.token,
                url: data.result.ws,
                debug: true
            });

            //подписываемся на новые комментарии
            HAWK_API.bind_handler('new_comment', function(e, msg){
                //игнорируем служебные сообщения
                if(msg.from === 'hawk_client')
                    return;
                //находим список комментариев и последний из них
                //создаём объект нового
                var $comments = $('.previous-comments'),
                    $last = $comments.find('.comment:last'),
                    cls = 'odd',
                    $comment = $(msg.text.comment)
                    ;

                //определяемся с классом комментария
                if($last.size()){
                    cls = $last.hasClass('odd') ? 'even' : 'odd';
                }

                $comment
                    .removeClass('odd')
                    .addClass(cls)
                    .hide()
                ;
                //показываем
                $comments.append($comment);
                $comment.show('normal')
            });

            //подписываемся на новые комментарии
            HAWK_API.bind_handler('open', function(e, msg){
                var id = $('.blog').data('blogId');
                //добавляем пользователя в группу блога
                //если её нет, то она будет создана с публичным доступом
                HAWK_API.add_user_to_group(['blog_' + id]);
             });
        } else {
            if(data.errors !== 'no_user') {
                console.error(data);
            }
        }
    });
}


Первое, что мы делаем, это отправляем запрос за токеном для подключения к серверу сообщений. Так как пользователь у нас не аторизованный, то говорим контроллеру использовать в качестве id пользователя, id его сессии.
Если всё хорошо, то мы получим в ответе: id пользователя, токен подключения и адрес сервера.
Далее, инициализируем подключение и подписываемся на события. В данном случае, нас интересуют два из них. Первое — open, возникает после успешного подключения к сокету. Второе — new_comment (его мы передаём при отправке сообщения), непосредственно новый комментарий.

Вот собственно и всё. Благодарю за внимание. Код этой части доступен на github в соответствующей ветке.
Поделиться с друзьями
-->

Комментарии (2)


  1. BoShurik
    17.06.2016 17:36
    +2

    Путь, несомненно, тёмный, но я так и не нашёл подходящего варианта, как заставить симфони подключить файл из библиотеки, которая не является бандлом. Буду благодарен за подсказку.


    Давно уже не пользовался ассетиком, но вроде бы он умеет так (gulp так точно может):
    {% javascripts
        '../vendor/post-hawk/hawk-api/Resources/public/js/hawk_api.min.js'
    %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}


    1. Slavenin999
      17.06.2016 17:45

      Да, действительно умеет, спасибо! Подправил статью.