Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eb84dfe

Browse filesBrowse files
committed
Streamlining server event streaming
1 parent 0a9eb28 commit eb84dfe
Copy full SHA for eb84dfe

File tree

4 files changed

+361
-0
lines changed
Filter options

4 files changed

+361
-0
lines changed

‎src/Symfony/Component/HttpFoundation/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/CHANGELOG.md
+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for iterable of string in `StreamedResponse`
8+
* Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming
89

910
7.2
1011
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* Represents a streaming HTTP response for sending server events
16+
* as part of the Server-Sent Events (SSE) streaming technique.
17+
*
18+
* @see ServerEvent
19+
*
20+
* @author Yonel Ceruto <open@yceruto.dev>
21+
*
22+
* Example usage:
23+
*
24+
* return new EventStreamResponse(function () {
25+
* while (true) {
26+
* yield new ServerEvent(time(), type: 'ping');
27+
*
28+
* if (connection_aborted()) {
29+
* break;
30+
* }
31+
*
32+
* sleep(1);
33+
* }
34+
* });
35+
*/
36+
class EventStreamResponse extends StreamedResponse
37+
{
38+
/**
39+
* @param int $retry The event reconnection time in milliseconds
40+
*/
41+
public function __construct(?callable $callback = null, int $status = 200, array $headers = [], private int $retry = 0)
42+
{
43+
$headers += [
44+
'Content-Type' => 'text/event-stream',
45+
'Cache-Control' => 'no-cache',
46+
'Connection' => 'keep-alive',
47+
];
48+
49+
parent::__construct($callback, $status, $headers);
50+
}
51+
52+
public function setCallback(callable $callback): static
53+
{
54+
if ($this->callback) {
55+
return parent::setCallback($callback);
56+
}
57+
58+
$this->callback = function () use ($callback) {
59+
if (is_iterable($events = $callback($this))) {
60+
foreach ($events as $event) {
61+
$this->sendEvent($event);
62+
}
63+
}
64+
};
65+
66+
return $this;
67+
}
68+
69+
/**
70+
* @param bool $flush Whether output buffers should be flushed
71+
*
72+
* @return $this
73+
*/
74+
public function sendEvent(ServerEvent $event, bool $flush = true): static
75+
{
76+
if ($this->retry > 0 && 0 === $event->getRetry()) {
77+
$event->setRetry($this->retry);
78+
}
79+
80+
echo $event;
81+
82+
if ($flush && !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
83+
static::closeOutputBuffers(0, true);
84+
flush();
85+
}
86+
87+
return $this;
88+
}
89+
90+
public function getRetry(): int
91+
{
92+
return $this->retry;
93+
}
94+
95+
public function setRetry(int $retry): void
96+
{
97+
$this->retry = $retry;
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* A server event to send as part of the SSE streaming technique.
16+
*
17+
* @author Yonel Ceruto <open@yceruto.dev>
18+
*/
19+
class ServerEvent
20+
{
21+
/**
22+
* @param string|iterable<string> $data The event data field for the message
23+
* @param string|null $type The event type
24+
* @param int $retry The event reconnection time in milliseconds
25+
* @param string|null $id The event ID to set the EventSource object's last event ID value
26+
* @param string|null $comment The event comment
27+
*/
28+
public function __construct(
29+
private string|iterable $data,
30+
private ?string $type = null,
31+
private int $retry = 0,
32+
private ?string $id = null,
33+
private ?string $comment = null,
34+
) {
35+
}
36+
37+
public function getData(): iterable|string
38+
{
39+
return $this->data;
40+
}
41+
42+
/**
43+
* @return $this
44+
*/
45+
public function setData(iterable|string $data): static
46+
{
47+
$this->data = $data;
48+
49+
return $this;
50+
}
51+
52+
public function getType(): ?string
53+
{
54+
return $this->type;
55+
}
56+
57+
/**
58+
* @return $this
59+
*/
60+
public function setType(string $type): static
61+
{
62+
$this->type = $type;
63+
64+
return $this;
65+
}
66+
67+
public function getRetry(): int
68+
{
69+
return $this->retry;
70+
}
71+
72+
/**
73+
* @return $this
74+
*/
75+
public function setRetry(int $retry): static
76+
{
77+
$this->retry = $retry;
78+
79+
return $this;
80+
}
81+
82+
public function getId(): ?string
83+
{
84+
return $this->id;
85+
}
86+
87+
/**
88+
* @return $this
89+
*/
90+
public function setId(string $id): static
91+
{
92+
$this->id = $id;
93+
94+
return $this;
95+
}
96+
97+
public function getComment(): ?string
98+
{
99+
return $this->comment;
100+
}
101+
102+
public function setComment(string $comment): static
103+
{
104+
$this->comment = $comment;
105+
106+
return $this;
107+
}
108+
109+
public function __toString(): string
110+
{
111+
$event = [];
112+
113+
if ($this->comment) {
114+
$event[] = \sprintf(': %s', $this->comment);
115+
}
116+
if ($this->id) {
117+
$event[] = \sprintf('id: %s', $this->id);
118+
}
119+
if ($this->retry > 0) {
120+
$event[] = \sprintf('retry: %s', $this->retry);
121+
}
122+
if ($this->type) {
123+
$event[] = \sprintf('event: %s', $this->type);
124+
}
125+
if ($this->data) {
126+
if (is_iterable($this->data)) {
127+
foreach ($this->data as $data) {
128+
$event[] = \sprintf('data: %s', $data);
129+
}
130+
} else {
131+
$event[] = \sprintf('data: %s', $this->data);
132+
}
133+
}
134+
135+
return implode("\n", $event)."\n\n";
136+
}
137+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\EventStreamResponse;
16+
use Symfony\Component\HttpFoundation\ServerEvent;
17+
18+
class EventStreamResponseTest extends TestCase
19+
{
20+
public function testInitializationWithDefaultValues()
21+
{
22+
$response = new EventStreamResponse();
23+
24+
$this->assertSame('text/event-stream', $response->headers->get('content-type'));
25+
$this->assertSame('no-cache, private', $response->headers->get('cache-control'));
26+
$this->assertSame('keep-alive', $response->headers->get('connection'));
27+
28+
$this->assertSame(200, $response->getStatusCode());
29+
$this->assertSame(0, $response->getRetry());
30+
}
31+
32+
public function testStreamSingleEvent()
33+
{
34+
$response = new EventStreamResponse(function () {
35+
yield new ServerEvent(
36+
data: 'foo',
37+
type: 'bar',
38+
retry: 100,
39+
id: '1',
40+
comment: 'bla bla',
41+
);
42+
});
43+
44+
$expected = <<<STR
45+
: bla bla
46+
id: 1
47+
retry: 100
48+
event: bar
49+
data: foo
50+
51+
52+
STR;
53+
54+
$this->assertSameResponseContent($expected, $response);
55+
}
56+
57+
public function testStreamEventsAndData()
58+
{
59+
$data = static function (): iterable {
60+
yield 'first line';
61+
yield 'second line';
62+
yield 'third line';
63+
};
64+
65+
$response = new EventStreamResponse(function () use ($data) {
66+
yield new ServerEvent('single line');
67+
yield new ServerEvent(['first line', 'second line']);
68+
yield new ServerEvent($data());
69+
});
70+
71+
$expected = <<<STR
72+
data: single line
73+
74+
data: first line
75+
data: second line
76+
77+
data: first line
78+
data: second line
79+
data: third line
80+
81+
82+
STR;
83+
84+
$this->assertSameResponseContent($expected, $response);
85+
}
86+
87+
public function testStreamEventsWithRetryFallback()
88+
{
89+
$response = new EventStreamResponse(function () {
90+
yield new ServerEvent('foo');
91+
yield new ServerEvent('bar');
92+
}, retry: 1500);
93+
94+
$expected = <<<STR
95+
retry: 1500
96+
data: foo
97+
98+
retry: 1500
99+
data: bar
100+
101+
102+
STR;
103+
104+
$this->assertSameResponseContent($expected, $response);
105+
}
106+
107+
public function testStreamEventWithSendMethod()
108+
{
109+
$response = new EventStreamResponse(function (EventStreamResponse $response) {
110+
$response->sendEvent(new ServerEvent('foo'));
111+
});
112+
113+
$this->assertSameResponseContent("data: foo\n\n", $response);
114+
}
115+
116+
private function assertSameResponseContent(string $expected, EventStreamResponse $response): void
117+
{
118+
ob_start();
119+
$response->send();
120+
$actual = ob_get_clean();
121+
122+
$this->assertSame($expected, $actual);
123+
}
124+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.