Laravel 5: используйте события, отправленные сервером, для отправки сообщений в браузер

Я просмотрел Этот вопрос SO и Эта статья посвящена реализации событий, отправленных сервером, в Laravel 5. Хотя между ними я не могу понять, как отправлять обновления клиенту на основе события. В этом случае событие представляет собой выбрасываемое ClientException. ClientException в моем приложении вызван ошибкой, сделанной пользователем. Когда один из них выбрасывается, я хочу отправить клиенту обновление, которое заполняет универсальную панель ошибок.

Вот что у меня в лезвии:

<script>
    var source = new EventSource("{{ route('globalmessages') }}");
    source.addEventListener("message", function(e)
    {
        $("#errors").html(e);
    }, false);
</script>

<div id="errors">
</div>

EventSource успешно выполняет действие контроллера:

public function pushErrors()
{
    $response = new StreamedResponse(function()
    {
        $errors = ???; // How do I populate this
        if (!empty($error))
        {
            echo $error;
            ob_flush();
            flush();
        }
        else
        {
            \Log::info("No errors to push");
        }
    });

    $response->headers->set('Content-Type', 'text/event-stream');
    return $response;
}

А обработка ошибок происходит в Handler.php@render:

if ($e instanceof ClientException)
    {
        $message = $e->getMessage(); // Need to send this to client
        return \Response::json([]);
    }
    else
    {
        return parent::render($request, $e);
    }

Итак, что мне нужно сделать, так это каким-то образом передать ошибки маршруту контроллера. Я пытался использовать синглтон, но не смог заставить его работать. Я пытался дать контроллеру поле для его хранения, но оно всегда было пустым.

Кроме того, эта текущая реализация, кажется, запускается каждые 5 секунд. Я хотел бы иметь возможность вызывать функцию, когда выдается ClientException, чтобы отправить обновление.

Спасибо за любую помощь.

РЕДАКТИРОВАТЬ:

Еще немного информации:

Вот действие контроллера, которое отображает мою страницу:

class HomeController
{
    public function getHome()
    {
        return view('home');
    }
}

Мой контроллер, использующий бесконечный цикл:

class MainController extends RouteController
{
    public function pushErrors()
    {
        $response = new StreamedResponse();
        $response->headers->set('Content-Type', 'text/event-stream');
        $response->headers->set('Cach-Control', 'no-cache');

        $response->setCallback(
            function()
            {
                while (true)
                {
                    $error = MessageQueue::dequeue();
                    if (!empty($error))
                    {
                        echo 'data: ' . $error. "\n\n";
                        ob_flush();
                        flush();
                    }
                    sleep(1);
                }
            });

        return $response;
    }
}

Если я убираю цикл while, этот код работает, поскольку сообщение отправляется, соединение разрывается, затем восстанавливается и снова нажимается. С циклом while страница просто зависает.

Мои маршруты:

+--------+----------+----------------+----------------+--------------------------------------------------+------------+
| Domain | Method   | URI            | Name           | Action                                           | Middleware |
+--------+----------+----------------+----------------+--------------------------------------------------+------------+
|        | GET|HEAD | globalmessages | globalmessages | App\Http\Controllers\MainController@pushErrors   |            |
|        | GET|HEAD | /              | /              | App\Http\Controllers\HomeController@getHome      |            |
|        | POST     | login          | login          | App\Http\Controllers\HomeController@postLogin    |            |
+--------+----------+----------------+----------------+--------------------------------------------------+------------+

HomeController@postLogin — это маршрут, который я вызываю, который генерирует ClientException.


person Troncoso    schedule 09.04.2015    source источник
comment
Можете ли вы обновить свой HTML, чтобы показать маршрут, к которому вы подключаете объект EventSource? errors не указан как маршрут в вашем дампе. У вас также есть маршрут globalmessages, указывающий на MainController@pushErrors, действие в вашем контроллере — pushMessages(), что это?   -  person Vigs    schedule 10.04.2015
comment
Извиняюсь. Я исправил опечатки.   -  person Troncoso    schedule 10.04.2015


Ответы (1)


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

Как бы я реализовал это, чтобы создать таблицу, содержащую все ваши ServerSentEvents, затем в моем запросе контроллера из этой таблицы и получить строки, которые еще не были отправлены.

В этом примере мы проверим, есть ли данные в нашем массиве $data, и отправим их клиенту, если они есть.

DB::table('ServerSentEvents')->where('sent', 0)->get();

Возвращает пустой массив, если из запроса не возвращаются строки.

public function pushErrors() {

$response = new Symfony\Component\HttpFoundation\StreamedResponse(function() {

    $data = $this->getData();

    while (true) {
        if (!empty($data)) {
            echo 'data: ' . json_encode($data) . "\n\n";
            ob_flush();
            flush();

            /* update the table rows as sent */
            $ids = [];
            foreach($data as $event){
                $ids[] = $event->id;
            }
            DB::table('ServerSentEvents')->whereIn('id', $ids)->update('sent', 1);              
        }

        //sleep for 3 seconds
        sleep(3);

        //requery for new events that need to be sent
        $data = $this->getData();
    }

});

    $response->headers->set('Content-Type', 'text/event-stream');
    return $response;
}

public function getData(){
    $data = DB::table('ServerSentEvents')->where('sent', 0)->get();
    return $data;
}

Вам также необходимо вставлять новые строки в базу данных при возникновении ClientException:

if ($e instanceof ClientException)
{
    DB::table('ServerSentEvents')->insert(['message' => $e->getMessage(), 'sent' => 0]);        
}
else
{
    return parent::render($request, $e);
}

В зависимости от того, какую информацию вы хотите получить на основе ваших событий, вам может потребоваться добавить или удалить дополнительные столбцы в таблице ServerSentEvents.

Таблица ServerSentEvents — это таблица, которую вам нужно будет создать, я не привожу здесь структуру, потому что в строках может быть больше информации, которую вы хотите сохранить, а общая концепция извлечения данных из базы данных — это главное.

person Vigs    schedule 09.04.2015
comment
Спасибо за ответ. Однако есть две проблемы: 1. Я хотел избежать зацикливания. Я бы предпочел просто иметь возможность вызывать функцию в момент обнаружения исключения, которая будет отправлять данные клиенту. 2. Я пытался не хранить их в базе данных. К этим ошибкам относятся такие вещи, как «Вы не можете использовать это значение» или «У вас нет на это прав». Вещи, которые вы обычно просто получаете от вызова ajax и через экран. Хранение их в БД казалось ненужным. Кроме того, моя функция обратного вызова запускается каждые 5 секунд без использования цикла. - person Troncoso; 09.04.2015
comment
С php браузер делает HTTP-запрос на ваш сервер, ваш веб-сервер (возможно, apache) выполнит php-код без цикла, код выполняется нормально, и ответ отправляется в браузер. Вот почему вам нужна петля. Цикл поддерживает соединение между клиентом и сервером открытым, в противном случае клиент делает запрос, а затем получает обычный ответ http. Цикл — это фундаментальная концепция длительного опроса. - person Vigs; 09.04.2015
comment
Вы не можете просто отправить данные клиенту, если между клиентом и сервером еще не установлено соединение. У HTTP есть жизненный цикл, когда браузер делает запрос, а затем ответ от сервера отправляется в браузер. Сервер не может инициировать соединение между сервером и клиентским браузером, поэтому для сохранения соединения используется длительный опрос, который имитирует общение в реальном времени. По крайней мере, это верно для PHP при использовании метода на основе HTTP, с скомпилированными языками это может быть по-другому. - person Vigs; 09.04.2015
comment
Что касается отсутствия сохранения данных в базе данных, вам нужно иметь возможность где-то сохранять данные, если вы не хотите, вы можете записать их в файл JSON, а затем прочитать из файла в цикле. В PHP нет постоянного состояния, так как при каждом запросе код интерпретируется, а затем выполняется. При каждом запуске кода все переменные являются новыми и не будут содержать значения из предыдущих вызовов/выполнений без загрузки этих значений из файла, базы данных или другого постоянного хранилища. - person Vigs; 09.04.2015
comment
Единственной альтернативой сохранению в базе данных или файле было бы использование чего-то вроде Redis или Memcache. Laravel поддерживает и то, и другое через свои драйверы кеша. laravel.com/docs/4.2/cache . Надеюсь, что это даст вам больше информации/помощи! - person Vigs; 09.04.2015
comment
Ради интереса я использовал настройку файлового ввода-вывода. Когда я не использую цикл, событие срабатывает каждые 5 секунд, и все работает нормально. Когда я добавляю цикл для запуска каждую секунду, моя страница никогда не загружается. Из-за этого сервер не отвечает, пока я его не убью. - person Troncoso; 10.04.2015
comment
Событие срабатывает каждые 5 секунд, потому что клиент javascript определяет, что он потерял соединение и ему необходимо повторно подключиться. Поскольку петли нет, соединение всегда закрывается. Для уточнения нужно 2 маршрута. Первый маршрут будет отображать ваш клиентский html и создавать клиентское соединение js, второй маршрут указывает на ваш контроллер и отправляет сообщения вашему клиенту. Если вы посещаете тот же маршрут, к которому подключается клиент отправки событий, он никогда не загрузится из-за бесконечного цикла. В этом весь смысл цикла. - person Vigs; 10.04.2015
comment
Маршрут, который отображает страницу, называется getHome. Доступ к маршруту pushErrors возможен только через источник событий Javascript. - person Troncoso; 10.04.2015
comment
То есть с бесконечным циклом маршрут getHome никогда не загружается? Странный. Можете ли вы также обновить свой вопрос о маршрутах и ​​​​коде контроллера? - person Vigs; 10.04.2015
comment
Я добавил больше информации. - person Troncoso; 10.04.2015