Implementing Server Sent Events with Slim 4

Hi everyone,

I’m having some trouble getting Server Sent Events working using Slim 4 and the built-in PHP dev server. I’ve attempted to piece together a working implementation using the code and discussion in this PR about NonBufferedBody since it was linked to from this issue which in turn was linked to from this issue about SSE and Slim 3.

I’m not entirely sure what I’m doing wrong, but I’m assuming the issue is that since I’m returning a response the connection is closed and what I end with is polling instead of SSE (in the network tab I can see it just continuously hits the endpoint instead of opening just one connection). I’ve tested SSE using the example in the MDN link above and I can see that it opens only one connection and continuously sends data as expected so I’ve ruled out that its a problem with the frontend or PHP dev server.

Here is the route I’m using:

<?php
...
$app->get('/featured', function (Request $request, Response $response, $args) {
    $data = time();
    $nonBufferedResponse = $response
        ->withBody(new NonBufferedBody)
        ->withHeader('Content-Type', 'text/event-stream')
        ->withHeader('Cache-Control', 'no-cache');

    $body = $nonBufferedResponse->getBody();
    $body->write("data: <time>$data</time>\n\n");

    return $nonBufferedResponse;
});

Can anyone point me in the right direction? Thank you

1 Like

As far as I can see it still looks like an issue for Slim. On the other hand, you don’t write anything about the purpose of your project. Why do you avoid nodejs for serving events? If not, you can take a look at my extremely easy and simple hybrid solution for websocket integration with slim4:

Slim 4 Websocket integration

@tj_gumis Its just a toy project, I started it to play around with the htmx library on the front-end, noticed it had SSE integration and wanted to try it so I decided to use Slim 4. If its an issue with Slim can you elaborate at all? I’m curious what the issue could be.

htms looks interesting but its docs are discouraging :frowning: .

Speaking about Slim.
Looks like the issue is a lack of possibility to disable output buffering, that among all should theoretically:

allow to give real-time feedback at every iteration of a long-running loop.

Slim 3 has an output buffering setting whose legal values are false, ‘append’ and ‘prepend’ (default ‘append’). However, false seems to simply instruct the framework to neither prepend nor append stray echo’s to the PSR7 response.

Following the discussion, there were two proposition of specialized implementations of the stream interface and even PR submitted but it is unclear for me what finally happened with it. It might be finally merged or rejected. I think Odan might know something more about it.

Still, if it is just a toy project, as I told you before, you can play with ws integration I linked in my first response.

In Slim 4 you can still use the NonBufferedBody to send unbuffered event streams.
You just have to add a whitespace at the end of the line.

Here is an example (that works on my machine).

HTML

<html>
<head>
    <meta charset="UTF-8">
    <title>Server-sent events demo</title>
</head>
<body>
<button>Close the connection</button>

<ul>
</ul>

<script>
    const button = document.querySelector('button');
    const evtSource = new EventSource('events');
    console.log(evtSource.withCredentials);
    console.log(evtSource.readyState);
    console.log(evtSource.url);
    const eventList = document.querySelector('ul');

    evtSource.onopen = function() {
        console.log("Connection to server opened.");
    };

    evtSource.onmessage = function(e) {
        const newElement = document.createElement("li");

        newElement.textContent = "message: " + e.data;
        eventList.appendChild(newElement);
    };

    evtSource.onerror = function() {
        console.log("EventSource failed.");
    };

    button.onclick = function() {
        console.log('Connection closed');
        evtSource.close();
    };

    // evtSource.addEventListener("ping", function(e) {
    //   var newElement = document.createElement("li");
    //
    //   var obj = JSON.parse(e.data);
    //   newElement.innerHTML = "ping at " + obj.time;
    //   eventList.appendChild(newElement);
    // }, false);
</script>
</body>
</html>

The Slim 4 route to handle (and send) server-side events to the client / browser:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\NonBufferedBody;
// ...

$app->get('/events', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response = $response
        ->withBody(new NonBufferedBody())
        ->withHeader('Content-Type', 'text/event-stream')
        ->withHeader('Cache-Control', 'no-cache');

    $body = $response->getBody();

    // 1 is always true, so repeat the while loop forever (aka event-loop)
    while (1) {
        // Send named event
        $now = date('Y-m-d H:i:s');
        $event = sprintf("event: %s\ndata: %s\n\n", 'ping', json_encode(['time' => $now]));

        // Add a whitespace to the end
        $body->write($event . ' ');

        // break the loop if the client aborted the connection (closed the page)
        if (connection_aborted()) {
            break;
        }

        // sleep for 1 second before running the loop again
        sleep(1);
    }

    return $response;
});

The result:

image

2 Likes

Told ya :smile:
He’s the one of the very few existing examples of an extinct species.

2 Likes

Thank you @odan! That worked, it works for me without adding the whitespace as well which seemed to cause an issue with the way htmx handles SSE (but not when creating the event source manually as you showed). I appreciate all the help guys.

1 Like

Hi there,
in my project setup I have to add ->withHeader('X-Accel-Buffering', 'no') to the response headers to get SSE working:

$response = $response
            ->withBody(new NonBufferedBody())
            ->withHeader('Content-Type','text/event-stream')
            ->withHeader('Cache-Control','no-cache')
            ->withHeader('X-Accel-Buffering', 'no');

I’m using nginx + php-fpm.

One more hint on this topic:
One should keep in mind that connection_aborted() only works, if something is send (write to the body) to the client. Otherwise the server side will not recognize, that the client dropped the connection and the loop will run ‘forever’.