Open nateajohnson opened 5 years ago
For implementing this, I think we need to implement the handler from laravel websockets, so we can close the subscriptions. https://docs.beyondco.de/laravel-websockets/1.0/advanced-usage/custom-websocket-handlers.html
I get it working, I will try to make a repo (with lighthouse v3, since there are issues with subscriptions in v4), but so far, let me copy-paste the code:
In config/websockets.php
, replace:
'channel_manager' => \App\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
App\WebSockets\Channels\ChannelManagers\ArrayChannelManager.php
:
<?php
namespace App\WebSockets\Channels\ChannelManagers;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions as Storage;
use Ratchet\ConnectionInterface;
class ArrayChannelManager extends \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager
{
public function removeFromAllChannels(ConnectionInterface $connection)
{
$storage = app(Storage::class);
collect(Arr::get($this->channels, $connection->app->id, []))
->each(function (Channel $channel, string $channelName) use ($storage) {
$storage->deleteSubscriber($channelName);
});
parent::removeFromAllChannels($connection);
}
}
In AppServiceProvider
, register your own Router:
public function register()
{
$this->app->singleton('websockets.router', function () {
return new \App\WebSockets\Server\Router();
});
}
that Router should be:
<?php
namespace App\WebSockets\Server;
use App\WebSockets\WebSocketHandler;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelController;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelsController;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchUsersController;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\TriggerEventController;
class Router extends \BeyondCode\LaravelWebSockets\Server\Router
{
public function echo()
{
$this->get('/app/{appKey}', WebSocketHandler::class);
$this->post('/apps/{appId}/events', TriggerEventController::class);
$this->get('/apps/{appId}/channels', FetchChannelsController::class);
$this->get('/apps/{appId}/channels/{channelName}', FetchChannelController::class);
$this->get('/apps/{appId}/channels/{channelName}/users', FetchUsersController::class);
}
}
The App\WebSockets\WebsocketHandler
should be:
<?php
namespace App\WebSockets;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions as Storage;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;
class WebSocketHandler extends \BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler
{
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
{
if ($message->getPayload()) {
$payload = json_decode($message->getPayload(), true);
$eventName = Str::camel(Str::after(Arr::get($payload, 'event'), ':'));
if ($eventName === 'unsubscribe') {
$storage = app(Storage::class);
$storage->deleteSubscriber(
Arr::get($payload, 'data.channel')
);
}
}
parent::onMessage($connection, $message);
}
}
It works fine for me, but I'm having this issue: https://github.com/beyondcode/laravel-websockets/issues/163 , but maybe it is from laravel-websockets
itself.
Hello @enzonotario thanks for your effort but can you please explain the steps in details? how can we make websocket which listen to subscription like ws:\link:port\subscription
@gehad17 firstly, try to get it working with Pusher, folllowing the docs. After that, you will be able to get it working with laravel-websockets
. Try with lighthouse v3, since v4 have some issues related to subscriptions.
Hey @enzonotario , I tried using your implementation as is but I keep getting an error when I try connecting to the graphql endpoint. Using the debugger, I find TypeError: Return value of Nuwave\Lighthouse\Execution\BaseRequest::query() must be of the type string, null returned. Have you encountered such error ?
@lukadriel7 That error seems to be when you reach the endpoint without a query. I mean, if I enter via a browser to localhost:8000/graphql (without passing the query as a query param), I get that error. So, how are you testing this? have you setup the Apollo Link?
@enzonotario I am working on the back end of my application for now, so I am using the graphql playground to try the subscriptions.
I have never use the playground, and from this comment it seems that won't work (https://github.com/nuwave/lighthouse/issues/750#issuecomment-485798128). So if you really want to test it, you have to setup the client implementation.
Thanks, I will try it as soon as possible and let you know.
Hi folks! I could create an example: https://github.com/enzonotario/lighthouse-laravel-websockets . Sorry for the additional boilerplate (Inertia), but so far is just the only way I know to use Vue (and for me it's just a copy-paste from other projects). The EchoLink is just a copy from one I had in an Angular project (based in Lighthouse's docs), so maybe it has to be improved for Vue.
Thank you very much. I will check it
Does anyone know offhand, is there still issues in v4 with subscriptions?
If people aren't using beyondcode/laravel-websockets
with this package, then can anyone reccomend a package/how to do websockets?
If people aren't using
beyondcode/laravel-websockets
with this package, then can anyone reccomend a package/how to do websockets?
I think the easiest way is to use pusher, since the subscriptions were implemented with pusher in mind. Not sure if It will change soon. You could read the code in this repository and try to adapt your application.
Hi! I followed exactly the steps in @enzonotario 's tutorial, but i get an error message. Does anyone have any idea what am i missing here?
Are you sending your request to graphql/subscriptions/auth
? The response body isn't revealing much except that your request seems to be missing Lighthouse's router and controller. That should be returning JSON with a token like this: {"auth":"171ab1b4008b4b48743d:7af46076a36531cb0f68127c636d30cfec8e0f18f07886f258631588fb93df50"}
@DonovanChan yes, of courese. Here is the constructor from EchoLink
constructor() {
super();
const token = AUTH_TOKEN();
window.Pusher = require('pusher-js');
Pusher.logToConsole = true;
this.subscriptions = [];
this.echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.PUSHER_APP_CLUSTER,
authEndpoint: 'graphql/subscription/auth',
wsHost: window.location.hostname,
wsPort: 6001,
wssPort: 6001,
disableStats: true,
enabledTransports: ['ws', 'wss'],
auth: {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
},
});
}
It looks like you have a typo in your config: authEndpoint
should have "subscriptions" with an "s". To verify, try copying your existing endpoint and comparing it to your routes:
php artisan route:list | grep "graphql/subscription/auth"
It should give you a result like this when it's correct:
| | POST | graphql/subscriptions/auth | lighthouse.subscriptions.auth | Nuwave\Lighthouse\Support\Http\Controllers\SubscriptionController@authorize |
Ah ok, i didn't see that :-) I fixed it, but unfortunately i still get the same error.
Your response to the route:list
command should show you the controller. That's a good place to start.
@DonovanChan Thank you for your help! :-) i figured it out... Don't ask me why, but it wasn't enough to say that the authEndpoint is graphql/subscriptions/auth I tried authEndpoint: http: //127.0.0.1:8000/graphql/subscriptions/auth, and now it works :-)
So I have been working hard to get subscriptions on a level I am happy with in my application and have come up with the following solution I'd like to get you feedback on if possible.
Note: This code assumes that laravel-websockets
is installed in the same application codebase as lighthouse
, however you can still run php artisan websockets:serve
on another server.
Note: This code assumes you are using a shared caching solution like Redis for your subscriptions storage, a slower cache solution (like file
) might result in your websockets server to perform badly and or cause race conditions updating the subscriptions storage.
Note: This code was written with PHP 7.4 in mind, this will work fine on most PHP versions, you'll just need to remove some type hints in some places.
You'll need the following classes:
<?php
namespace App\Support\Websockets\Server\Channels;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager;
class LighthouseArrayChannelManager extends ArrayChannelManager
{
protected function determineChannelClass(string $channelName): string
{
if (starts_with($channelName, 'private-lighthouse-')) {
return PrivateLighthouseChannel::class;
}
return parent::determineChannelClass($channelName);
}
}
And also:
<?php
namespace App\Support\Websockets\Server\Channels;
use Ratchet\ConnectionInterface;
use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PrivateChannel;
class PrivateLighthouseChannel extends PrivateChannel
{
public function unsubscribe(ConnectionInterface $connection): void
{
parent::unsubscribe($connection);
if (starts_with($this->channelName, 'private-lighthouse-') && !$this->hasConnections()) {
static::lighthouseSubscriptionsStorage()->deleteSubscriber($this->channelName);
}
}
private static function lighthouseSubscriptionsStorage(): StoresSubscriptions
{
return app(StoresSubscriptions::class);
}
}
To "activate" this code you'll need to modify your config/websockets.php
file and set the channel_manager
option to App\Support\Websockets\Server\Channels\ArrayChannelManager::class
.
When deployed you will need to restart you socket server to apply the new channel manager.
This will allow subscriptions to work with laravel-websockets
using the pusher
driver in lighthouse
, you can leave out the webhook configuration since that is what the above code should provide (cleaning up subscribers and topics).
As a bonus I didn't like to have multiple auth endpoints for the websockets so I created a channel authorizer and disabled the Lighthouse pusher routes by removing this line from config/lighthouse.php
:
'pusher' => [
'driver' => 'pusher',
- 'routes' => \Nuwave\Lighthouse\Subscriptions\SubscriptionRouter::class.'@pusher',
'connection' => 'pusher',
],
I created this channel class:
<?php
namespace App\Broadcasting\Channels;
use App\User;
use Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions;
class LighthouseSubscriptionChannel
{
private AuthorizesSubscriptions $subscriptionAuthorizer;
public function __construct(AuthorizesSubscriptions $subscriptionAuthorizer)
{
$this->subscriptionAuthorizer = $subscriptionAuthorizer;
}
public function join(User $user): bool
{
return $this->subscriptionAuthorizer->authorize(request());
}
}
And in my BroadcastServiceProvider
added the following:
Broadcast::channel('lighthouse-{id}-{time}', \App\Broadcasting\Channels\LighthouseSubscriptionChannel::class);
This will "proxy" the authentication request for a private lighthouse channel through your "normal" broadcasting authentication endpoint so you can use the same Pusher config for both GraphQL subscriptions & normal Pusher channels which you might use in your application.
Possibly some for of this might make it into Ligthouse and/or spawn a package to supply this code, but for now this is working great for me and I have not found a downside to this approach except that I cannot use this if I use a seperate application for just the websockets server (that will require changes to the laravel-websockets
package to achieve that).
If you'd like to chat, come find my on the Lighthouse Slack as @stayallive
.
@stayallive @GregPeden @thekonz I am open for including support for this in Lighthouse. At least a dozen people seem interested, judging by the likes on https://github.com/nuwave/lighthouse/issues/847#issuecomment-618254044. Is one of you willing to champion this issue? I think we would be able to come up with a robust solution that benefits everyone.
I'll be having a child soon so I think I won't have the time in the coming weeks 😅
One wish I have for this feature is that leaving a subscription channel should result in the proper deletion of the subscription. That's one thing i know is a possible downside of the current subscription implementation. The list of subscribers on a topic just gets bigger and bigger if noone cleans up, so listening to the pusher channelVacated
webhook is key currently. The echo
implementation sadly does not use PresenceChannel
anymore so we don't have a way to listen to redis and delete subscribers properly (another thing I don't have the time for currently ;)).
So maybe Alex or Greg?
Congratulations 🎉
I came back to this issue because of https://github.com/nuwave/lighthouse/issues/1796. The collective debugging effort in there could be channeled towards a community driven solution.
I beat you to it... had a kid 7 weeks ago. ;)
But I am currently using laravel-websockets myself. After I meet internal company deliverable goals (on which I am waaaay behind) I'll consider packaging it for inclusion here.
Hello
I've been trying to get
beyondcode/laravel-websockets
to work with lighthouse but keep running into this error.Error during WebSocket handshake: Unexpected response code: 426
Then I stumbled across this comment that says it is incompatible with lighthouse. Any thoughts on this? I'd love to be able to use both. I saw there was a lighthouse websockets project published last year, but it doesn't seem to have much action at the moment, which is why I looked into using the beyondcode server.
https://github.com/beyondcode/laravel-websockets/issues/111#issuecomment-505349459
Thanks, Nate