Skip to content

Commit

Permalink
Refactor to DatastarEventStream
Browse files Browse the repository at this point in the history
  • Loading branch information
bencroker committed Jan 10, 2025
1 parent 131e9f4 commit 5effd42
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 107 deletions.
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Executes JavaScript in the browser.

## Custom Controllers

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.
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.

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

namespace App\Http\Controllers;

use Putyourlightson\Datastar\Http\Controllers\DatastarController;
use Illuminate\Routing\Controller;
use Putyourlightson\Datastar\DatastarEventStream;
use Symfony\Component\HttpFoundation\StreamedResponse;

class CustomController extends DatastarController
class CustomController extends Controller
{
protected function stream(): void
use DatastarEventStream;

public function index(): StreamedResponse
{
$signals = $this->sse->getSignals();
$this->sse->mergeSignals(['count' => $signals->count + 1]);
$this->sse->mergeFragments('
<span id="button-text">Increment again</span>
');
return $this->getStreamedResponse(function() {
$signals = $this->getSignals();
$this->mergeSignals(['count' => $signals->count + 1]);
$this->mergeFragments('
<span id="button-text">Increment again</span>
');
});
}
}
```
Expand Down
104 changes: 104 additions & 0 deletions src/DatastarEventStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* @copyright Copyright (c) PutYourLightsOn
*/

namespace Putyourlightson\Datastar;

use Illuminate\Support\Facades\View;
use Putyourlightson\Datastar\Models\Signals;
use Putyourlightson\Datastar\Services\Sse;
use starfederation\datastar\ServerSentEventGenerator;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Throwable;

trait DatastarEventStream
{
/**
* Returns a streamed response.
*/
protected function getStreamedResponse(callable $callable): StreamedResponse
{
$response = new StreamedResponse($callable);

foreach (ServerSentEventGenerator::headers() as $name => $value) {
$response->headers->set($name, $value);
}

return $response;
}

/**
* Returns a signals model populated with signals passed into the request.
*/
protected function getSignals(): Signals
{
return new Signals(ServerSentEventGenerator::readSignals());
}

/**
* Merges HTML fragments into the DOM.
*/
protected function mergeFragments(string $data, array $options = []): void
{
app(Sse::class)->mergeFragments($data, $options);
}

/**
* Removes HTML fragments from the DOM.
*/
protected function removeFragments(string $selector, array $options = []): void
{
app(Sse::class)->removeFragments($selector, $options);
}

/**
* Merges signals.
*/
protected function mergeSignals(array $signals, array $options = []): void
{
app(Sse::class)->mergeSignals($signals, $options);
}

/**
* Removes signal paths.
*/
protected function removeSignals(array $paths, array $options = []): void
{
app(Sse::class)->removeSignals($paths, $options);
}

/**
* Executes JavaScript in the browser.
*/
public function executeScript(string $script, array $options = []): void
{
app(Sse::class)->executeScript($script, $options);
}

/**
* Renders a view, catching exceptions.
*/
protected function renderView(string $view, array $variables): void
{
if (!View::exists($view)) {
$this->throwException('View `' . $view . '` does not exist.');
}

try {
view($view, $variables)->render();
} catch (Throwable $exception) {
$this->throwException($exception);
}
}

/**
* Throws an exception with the appropriate formats for easier debugging.
*
* @phpstan-return never
*/
protected function throwException(Throwable|string $exception): void
{
app(Sse::class)->throwException($exception);
}
}
5 changes: 5 additions & 0 deletions src/DatastarServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ private function registerScript(): void
}

/**
* @uses Sse::mergeFragments()
* @uses Sse::removeFragments()
* @uses Sse::mergeSignals()
* @uses Sse::removeSignals()
* @uses Sse::executeScript()
* @uses Sse::setSseInProcess
*/
private function registerDirectives(): void
Expand Down
62 changes: 23 additions & 39 deletions src/Http/Controllers/DatastarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,40 @@
namespace Putyourlightson\Datastar\Http\Controllers;

use Illuminate\Routing\Controller;
use Putyourlightson\Datastar\DatastarEventStream;
use Putyourlightson\Datastar\Models\Config;
use Putyourlightson\Datastar\Services\Sse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class DatastarController extends Controller
{
public function __construct(
protected readonly Sse $sse,
) {
}
use DatastarEventStream;

/**
* Default controller action.
*/
public function index(): StreamedResponse
{
$response = new StreamedResponse(function() {
$this->stream();
return $this->getStreamedResponse(function() {
$hashedConfig = request()->input('config');
$config = Config::fromHashed($hashedConfig);
if ($config === null) {
$this->throwException('Submitted data was tampered.');
}

$view = $config->view;
$signals = $this->getSignals();
$variables = array_merge(
[config('datastar.signalsVariableName', 'signals') => $signals],
$config->variables,
);

if (strtolower(request()->header('Content-Type')) === 'application/json') {
// Clear out params to prevent them from being processed by controller actions.
request()->query->replace();
request()->request->replace();
}

$this->renderView($view, $variables);
});

$this->sse->prepareResponse($response);

return $response;
}

/**
* Streams the response.
*/
protected function stream(): void
{
$hashedConfig = request()->input('config');
$config = Config::fromHashed($hashedConfig);
if ($config === null) {
throw new BadRequestHttpException('Submitted data was tampered.');
}

$view = $config->view;
$signals = $this->sse->getSignals();
$variables = array_merge(
[config('datastar.signalsVariableName', 'signals') => $signals],
$config->variables,
);

if (strtolower(request()->header('Content-Type')) === 'application/json') {
// Clear out params to prevent them from being processed by controller actions.
request()->query->replace();
request()->request->replace();
}

$this->sse->renderView($view, $variables);
}
}
12 changes: 6 additions & 6 deletions src/Models/Signals.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public function __call(string $name, array $arguments)
}

/**
* Returns the signal value.
* Returns the signal value, falling back to a default value.
*/
public function get(string $name): mixed
public function get(string $name, mixed $default = null): mixed
{
return $this->getNestedValue($name);
return $this->getNestedValue($name, $default);
}

/**
Expand Down Expand Up @@ -86,15 +86,15 @@ public function remove(string $name): static
}

/**
* Returns a nested value, or null if it does not exist.
* Returns a nested value, falling back to a default value.
*/
private function getNestedValue(string $name): mixed
private function getNestedValue(string $name, mixed $default = null): mixed
{
$parts = explode('.', $name);
$current = &$this->values;
foreach ($parts as $part) {
if (!isset($current[$part])) {
return null;
return $default;
}
$current = &$current[$part];
}
Expand Down
69 changes: 16 additions & 53 deletions src/Services/Sse.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@

namespace Putyourlightson\Datastar\Services;

use Illuminate\Support\Facades\View;
use Putyourlightson\Datastar\Models\Signals;
use starfederation\datastar\ServerSentEventGenerator;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Throwable;

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

/**
* Returns a signals model populated with signals passed into the request.
*/
public function getSignals(): Signals
{
return new Signals(ServerSentEventGenerator::readSignals());
}

/**
* Prepares the response for server sent events.
*/
public function prepareResponse(StreamedResponse $response): void
{
foreach (ServerSentEventGenerator::headers() as $name => $value) {
$response->headers->set($name, $value);
}
}

/**
* Renders a view, catching exceptions.
*/
public function renderView(string $view, array $variables): void
{
if (!View::exists($view)) {
$this->throwException('View `' . $view . '` does not exist.');
}

try {
view($view, $variables)->render();
} catch (Throwable $exception) {
$this->throwException($exception);
}
}

/**
* Merges HTML fragments into the DOM.
*/
Expand Down Expand Up @@ -132,6 +95,22 @@ public function setSseInProcess(string $method, array $options = []): void
$this->sseOptionsInProcess = $options;
}

/**
* Throws an exception with the appropriate formats for easier debugging.
*
* @phpstan-return never
*/
public function throwException(Throwable|string $exception): void
{
request()->headers->set('Accept', 'text/html');

if ($exception instanceof Throwable) {
throw $exception;
}

throw new BadRequestHttpException($exception);
}

/**
* Returns merged event options with null values removed.
*/
Expand Down Expand Up @@ -189,20 +168,4 @@ private function sendSseEvent(string $method, ...$args): void
// Start a new output buffer to capture any subsequent inline content.
ob_start();
}

/**
* Throws an exception with the appropriate formats for easier debugging.
*
* @phpstan-return never
*/
private function throwException(Throwable|string $exception): void
{
request()->headers->set('Accept', 'text/html');

if ($exception instanceof Throwable) {
throw $exception;
}

throw new BadRequestHttpException($exception);
}
}

0 comments on commit 5effd42

Please sign in to comment.