Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Gripcontrol Laravel Package

fanout/gripcontrol

A PHP library for building GRIP-enabled realtime applications with Fanout. It helps parse and generate GRIP headers, hold HTTP responses, publish messages to channels, and handle WebSocket-over-HTTP, simplifying PubSub and realtime API endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

fanout/gripcontrol is the PHP implementation of GRIP (Generic Realtime Intermediary Protocol). You use it when your Laravel app sits behind a GRIP proxy (Pushpin, Fanout Cloud/Fastly Fanout) and needs to do one of three things:

  1. Tell the proxy to hold a request open and subscribe the client to channels (Grip-Hold / Grip-Channel headers).
  2. Handle WebSocket-over-HTTP events proxied back to your app.
  3. Publish messages to channels out-of-band via the proxy's /publish/ endpoint.

Install and pin it (the package is archived, so lock the version):

composer require fanout/gripcontrol:^1.0

The classes live in the Grip\ namespace. Wire a singleton in a service provider, mirroring the Pushpin-style config array the library expects:

// config/grip.php
return [
    'clients' => [
        [
            'control_uri' => env('GRIP_CONTROL_URI', 'http://127.0.0.1:5561/'),
            'control_iss' => env('GRIP_CONTROL_ISS'),
            'key'         => env('GRIP_KEY'),
        ],
    ],
    'sig_key' => env('GRIP_SIG_KEY'),
];
// app/Providers/GripServiceProvider.php
use Grip\GripPubControl;

public function register(): void
{
    $this->app->singleton(GripPubControl::class, fn () =>
        new GripPubControl(config('grip.clients'))
    );
}

First use case: a broadcast endpoint. A controller publishes a WebSocket message to a channel, and any client subscribed through Pushpin receives it:

use Grip\GripPubControl;
use Grip\WebSocketMessageFormat;

public function __invoke(GripPubControl $pub, string $room)
{
    $pub->publish("room-{$room}", new WebSocketMessageFormat(
        json_encode(['type' => 'ping', 'at' => now()->toIso8601String()])
    ));

    return response()->noContent();
}

Look first at the library's README and the Pushpin docs on the three headers — everything else in the package is plumbing around those.

Implementation Patterns

Subscribe-on-request via middleware. The core pattern is: your normal Laravel response, plus GRIP headers that instruct the proxy to hold it. Do this in a route middleware or a dedicated controller rather than scattering headers in every action:

public function handle($request, Closure $next)
{
    $response = $next($request);

    if ($request->attributes->get('grip.subscribe')) {
        $response->headers->set('Grip-Hold', 'stream');
        $response->headers->set('Grip-Channel', implode(',', $request->attributes->get('grip.channels')));
    }

    return $response;
}

If your release ships the instruct helpers (GripInstruct and friends), build the header set there and dump it onto the response — it saves you from hand-formatting the comma-separated channel list.

WebSocket-over-HTTP endpoint. When Pushpin proxies WS traffic, it POSTs application/websocket-events bodies with a Grip-Sig header. Parse the context, loop the events, and return the response the library assembles:

use Grip\WebSocketContext;

public function ws(Request $request)
{
    $ctx = WebSocketContext::from_env($request); // older builds: from_env()
    // ... validate_sig($request->header('Grip-Sig'), config('grip.sig_key'))
    while ($event = $ctx->next_event()) {
        if ($event->type === 'TEXT') {
            $ctx->send($event->content);          // echo back
        }
    }
    return response($ctx->get_body(), 200, $ctx->get_headers());
}

Treat that route as withoutMiddleware(['web', 'csrf']) — the proxy, not a browser cookie, is the caller.

Offload publishing to the queue. Publishing is an HTTP call to the proxy. Put it in a job or listener so a slow or down proxy doesn't stall a user request:

class BroadcastToRoom implements ShouldQueue
{
    public function handle(GripPubControl $pub): void
    {
        $pub->publish("room-{$this->room}", new WebSocketMessageFormat($this->payload));
    }
}

Fire it from a domain event listener, keeping the "publish" concern out of controllers.

Choose the format per transport. WebSocketMessageFormat for WS frames, HttpStreamFormat for chunked streaming/SSE-ish clients, HttpResponseFormat for long-poll bodies. Publish several at once with publish_formats($channel, [...]) when one channel serves mixed clients.

Testing. Point GRIP_CONTROL_URI at a local stub (or Http::fake() around a thin wrapper you own) and assert on the job/format objects rather than on real pushes.

Gotchas and Tips

  • Archived, zero dependents. No PHP 8.3+ guarantees, minimal typing, no upstream fixes. Read the whole source (it's small) before relying on it, and consider a thin adapter class so you can swap in a hand-rolled client later.
  • Headers are a no-op without the proxy. If Grip-Hold reaches the browser instead of Pushpin, requests hang or behave bizarrely. Verify the proxy is actually in front in every environment, including local Docker.
  • Validate Grip-Sig. The proxy signs requests; if you skip validate_sig, anyone can post a forged WebSocket-events body or instruct your app. Use the same key Pushpin holds.
  • Multiple Grip-Channel values. Symfony's HeaderBag collapses array sets with commas; don't call headers->set() twice and expect two header lines. Set one comma-separated string, or use headers->set($name, $values, false) where you need raw control.
  • Don't let Laravel add Content-Length on held/streamed responses, and set Cache-Control: no-cache — otherwise intermediaries buffer and the hold never appears to the client.
  • Long-poll needs a body even when empty. Returning 204 or an empty 200 without a Grip-Channel header silently unsubscribes the client.
  • Reading the request body twice is fatal. Pushpin's WS-events body is a stream; getContent() is cached by Laravel, but any earlier middleware that consumed it will break parsing.
  • Publish failures are easy to lose. PubControlClient returns a boolean rather than throwing; wrap it and log. Combined with a queued job, set retries/backoff so a proxy restart doesn't drop messages.
  • Extend via your own wrapper. Write App\Realtime\Grip that composes GripPubControl and the format classes — that's your seam for mocking in tests and for the eventual migration off this package.
  • Config file alternative: GripPubControl::from_config_file() expects a JSON array in Pushpin's config shape. Prefer the constructor + Laravel config so php artisan config:cache works.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums