Skip to content

Commit 5effd42

Browse files
committed
Refactor to DatastarEventStream
1 parent 131e9f4 commit 5effd42

File tree

6 files changed

+169
-107
lines changed

6 files changed

+169
-107
lines changed

README.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Executes JavaScript in the browser.
151151

152152
## Custom Controllers
153153

154-
If you prefer to send SSE events using a custom controller instead of a Blade view, you can do so by extending the `DatastarController` class and overriding the `stream` method.
154+
You can send SSE events using a custom controller instead of a Blade view using the `DatastarEventStream` trait. Pass a callable into the `getStreamedResponse()` method and return the response.
155155

156156
```php
157157
// routes/web.php
@@ -166,17 +166,23 @@ Route::resource('/custom-controller', CustomController::class);
166166

167167
namespace App\Http\Controllers;
168168

169-
use Putyourlightson\Datastar\Http\Controllers\DatastarController;
169+
use Illuminate\Routing\Controller;
170+
use Putyourlightson\Datastar\DatastarEventStream;
171+
use Symfony\Component\HttpFoundation\StreamedResponse;
170172

171-
class CustomController extends DatastarController
173+
class CustomController extends Controller
172174
{
173-
protected function stream(): void
175+
use DatastarEventStream;
176+
177+
public function index(): StreamedResponse
174178
{
175-
$signals = $this->sse->getSignals();
176-
$this->sse->mergeSignals(['count' => $signals->count + 1]);
177-
$this->sse->mergeFragments('
178-
<span id="button-text">Increment again</span>
179-
');
179+
return $this->getStreamedResponse(function() {
180+
$signals = $this->getSignals();
181+
$this->mergeSignals(['count' => $signals->count + 1]);
182+
$this->mergeFragments('
183+
<span id="button-text">Increment again</span>
184+
');
185+
});
180186
}
181187
}
182188
```

src/DatastarEventStream.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) PutYourLightsOn
4+
*/
5+
6+
namespace Putyourlightson\Datastar;
7+
8+
use Illuminate\Support\Facades\View;
9+
use Putyourlightson\Datastar\Models\Signals;
10+
use Putyourlightson\Datastar\Services\Sse;
11+
use starfederation\datastar\ServerSentEventGenerator;
12+
use Symfony\Component\HttpFoundation\StreamedResponse;
13+
use Throwable;
14+
15+
trait DatastarEventStream
16+
{
17+
/**
18+
* Returns a streamed response.
19+
*/
20+
protected function getStreamedResponse(callable $callable): StreamedResponse
21+
{
22+
$response = new StreamedResponse($callable);
23+
24+
foreach (ServerSentEventGenerator::headers() as $name => $value) {
25+
$response->headers->set($name, $value);
26+
}
27+
28+
return $response;
29+
}
30+
31+
/**
32+
* Returns a signals model populated with signals passed into the request.
33+
*/
34+
protected function getSignals(): Signals
35+
{
36+
return new Signals(ServerSentEventGenerator::readSignals());
37+
}
38+
39+
/**
40+
* Merges HTML fragments into the DOM.
41+
*/
42+
protected function mergeFragments(string $data, array $options = []): void
43+
{
44+
app(Sse::class)->mergeFragments($data, $options);
45+
}
46+
47+
/**
48+
* Removes HTML fragments from the DOM.
49+
*/
50+
protected function removeFragments(string $selector, array $options = []): void
51+
{
52+
app(Sse::class)->removeFragments($selector, $options);
53+
}
54+
55+
/**
56+
* Merges signals.
57+
*/
58+
protected function mergeSignals(array $signals, array $options = []): void
59+
{
60+
app(Sse::class)->mergeSignals($signals, $options);
61+
}
62+
63+
/**
64+
* Removes signal paths.
65+
*/
66+
protected function removeSignals(array $paths, array $options = []): void
67+
{
68+
app(Sse::class)->removeSignals($paths, $options);
69+
}
70+
71+
/**
72+
* Executes JavaScript in the browser.
73+
*/
74+
public function executeScript(string $script, array $options = []): void
75+
{
76+
app(Sse::class)->executeScript($script, $options);
77+
}
78+
79+
/**
80+
* Renders a view, catching exceptions.
81+
*/
82+
protected function renderView(string $view, array $variables): void
83+
{
84+
if (!View::exists($view)) {
85+
$this->throwException('View `' . $view . '` does not exist.');
86+
}
87+
88+
try {
89+
view($view, $variables)->render();
90+
} catch (Throwable $exception) {
91+
$this->throwException($exception);
92+
}
93+
}
94+
95+
/**
96+
* Throws an exception with the appropriate formats for easier debugging.
97+
*
98+
* @phpstan-return never
99+
*/
100+
protected function throwException(Throwable|string $exception): void
101+
{
102+
app(Sse::class)->throwException($exception);
103+
}
104+
}

src/DatastarServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ private function registerScript(): void
5858
}
5959

6060
/**
61+
* @uses Sse::mergeFragments()
62+
* @uses Sse::removeFragments()
63+
* @uses Sse::mergeSignals()
64+
* @uses Sse::removeSignals()
65+
* @uses Sse::executeScript()
6166
* @uses Sse::setSseInProcess
6267
*/
6368
private function registerDirectives(): void

src/Http/Controllers/DatastarController.php

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,40 @@
66
namespace Putyourlightson\Datastar\Http\Controllers;
77

88
use Illuminate\Routing\Controller;
9+
use Putyourlightson\Datastar\DatastarEventStream;
910
use Putyourlightson\Datastar\Models\Config;
10-
use Putyourlightson\Datastar\Services\Sse;
1111
use Symfony\Component\HttpFoundation\StreamedResponse;
12-
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1312

1413
class DatastarController extends Controller
1514
{
16-
public function __construct(
17-
protected readonly Sse $sse,
18-
) {
19-
}
15+
use DatastarEventStream;
2016

2117
/**
2218
* Default controller action.
2319
*/
2420
public function index(): StreamedResponse
2521
{
26-
$response = new StreamedResponse(function() {
27-
$this->stream();
22+
return $this->getStreamedResponse(function() {
23+
$hashedConfig = request()->input('config');
24+
$config = Config::fromHashed($hashedConfig);
25+
if ($config === null) {
26+
$this->throwException('Submitted data was tampered.');
27+
}
28+
29+
$view = $config->view;
30+
$signals = $this->getSignals();
31+
$variables = array_merge(
32+
[config('datastar.signalsVariableName', 'signals') => $signals],
33+
$config->variables,
34+
);
35+
36+
if (strtolower(request()->header('Content-Type')) === 'application/json') {
37+
// Clear out params to prevent them from being processed by controller actions.
38+
request()->query->replace();
39+
request()->request->replace();
40+
}
41+
42+
$this->renderView($view, $variables);
2843
});
29-
30-
$this->sse->prepareResponse($response);
31-
32-
return $response;
33-
}
34-
35-
/**
36-
* Streams the response.
37-
*/
38-
protected function stream(): void
39-
{
40-
$hashedConfig = request()->input('config');
41-
$config = Config::fromHashed($hashedConfig);
42-
if ($config === null) {
43-
throw new BadRequestHttpException('Submitted data was tampered.');
44-
}
45-
46-
$view = $config->view;
47-
$signals = $this->sse->getSignals();
48-
$variables = array_merge(
49-
[config('datastar.signalsVariableName', 'signals') => $signals],
50-
$config->variables,
51-
);
52-
53-
if (strtolower(request()->header('Content-Type')) === 'application/json') {
54-
// Clear out params to prevent them from being processed by controller actions.
55-
request()->query->replace();
56-
request()->request->replace();
57-
}
58-
59-
$this->sse->renderView($view, $variables);
6044
}
6145
}

src/Models/Signals.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ public function __call(string $name, array $arguments)
3232
}
3333

3434
/**
35-
* Returns the signal value.
35+
* Returns the signal value, falling back to a default value.
3636
*/
37-
public function get(string $name): mixed
37+
public function get(string $name, mixed $default = null): mixed
3838
{
39-
return $this->getNestedValue($name);
39+
return $this->getNestedValue($name, $default);
4040
}
4141

4242
/**
@@ -86,15 +86,15 @@ public function remove(string $name): static
8686
}
8787

8888
/**
89-
* Returns a nested value, or null if it does not exist.
89+
* Returns a nested value, falling back to a default value.
9090
*/
91-
private function getNestedValue(string $name): mixed
91+
private function getNestedValue(string $name, mixed $default = null): mixed
9292
{
9393
$parts = explode('.', $name);
9494
$current = &$this->values;
9595
foreach ($parts as $part) {
9696
if (!isset($current[$part])) {
97-
return null;
97+
return $default;
9898
}
9999
$current = &$current[$part];
100100
}

src/Services/Sse.php

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55

66
namespace Putyourlightson\Datastar\Services;
77

8-
use Illuminate\Support\Facades\View;
9-
use Putyourlightson\Datastar\Models\Signals;
108
use starfederation\datastar\ServerSentEventGenerator;
11-
use Symfony\Component\HttpFoundation\StreamedResponse;
129
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1310
use Throwable;
1411

@@ -29,40 +26,6 @@ class Sse
2926
*/
3027
private array|null $sseOptionsInProcess = [];
3128

32-
/**
33-
* Returns a signals model populated with signals passed into the request.
34-
*/
35-
public function getSignals(): Signals
36-
{
37-
return new Signals(ServerSentEventGenerator::readSignals());
38-
}
39-
40-
/**
41-
* Prepares the response for server sent events.
42-
*/
43-
public function prepareResponse(StreamedResponse $response): void
44-
{
45-
foreach (ServerSentEventGenerator::headers() as $name => $value) {
46-
$response->headers->set($name, $value);
47-
}
48-
}
49-
50-
/**
51-
* Renders a view, catching exceptions.
52-
*/
53-
public function renderView(string $view, array $variables): void
54-
{
55-
if (!View::exists($view)) {
56-
$this->throwException('View `' . $view . '` does not exist.');
57-
}
58-
59-
try {
60-
view($view, $variables)->render();
61-
} catch (Throwable $exception) {
62-
$this->throwException($exception);
63-
}
64-
}
65-
6629
/**
6730
* Merges HTML fragments into the DOM.
6831
*/
@@ -132,6 +95,22 @@ public function setSseInProcess(string $method, array $options = []): void
13295
$this->sseOptionsInProcess = $options;
13396
}
13497

98+
/**
99+
* Throws an exception with the appropriate formats for easier debugging.
100+
*
101+
* @phpstan-return never
102+
*/
103+
public function throwException(Throwable|string $exception): void
104+
{
105+
request()->headers->set('Accept', 'text/html');
106+
107+
if ($exception instanceof Throwable) {
108+
throw $exception;
109+
}
110+
111+
throw new BadRequestHttpException($exception);
112+
}
113+
135114
/**
136115
* Returns merged event options with null values removed.
137116
*/
@@ -189,20 +168,4 @@ private function sendSseEvent(string $method, ...$args): void
189168
// Start a new output buffer to capture any subsequent inline content.
190169
ob_start();
191170
}
192-
193-
/**
194-
* Throws an exception with the appropriate formats for easier debugging.
195-
*
196-
* @phpstan-return never
197-
*/
198-
private function throwException(Throwable|string $exception): void
199-
{
200-
request()->headers->set('Accept', 'text/html');
201-
202-
if ($exception instanceof Throwable) {
203-
throw $exception;
204-
}
205-
206-
throw new BadRequestHttpException($exception);
207-
}
208171
}

0 commit comments

Comments
 (0)