GraphQL by night — Implementing GraphQL Subscription with Laravel/Lumen

GraphQL Logo

In the first article of this GraphQL article series, I explained the basics of GraphQL Schema & Queries. In the second article, I implemented the GraphQL server with nuwave/lighthouse. In this article, I’ll implement the subscription feature of GraphQL in the Laravel/Lumen based application with the lighthouse.

If you haven’t gone through my previous articles, find the links below.

Prerequisite

Before we proceed further, we need the SubscriptionServiceProvider class to be registered in our application. If you’re using Laravel, in the config/app.php ’s $providers array, add the following line.

\Nuwave\Lighthouse\Subscriptions\SubscriptionServiceProvider::class,

And, if you’re using Lumen, then add the following line in your bootstrap/app.php file.

$app->register(
\Nuwave\Lighthouse\Subscriptions\SubscriptionServiceProvider::class
);

Understanding GraphQL Subscription

GraphQL subscription is an implementation of a real-time notification service. You subscribe for an event and when that event occurs in the server, it then sends a notification to the subscriber via Socket or SSE.

To define the subscription in our schema, we’ll add the following.

type Subscription {
userCreated (name: String!): User
}

We have passed the argument name for an example. You may or may not pass any argument based on your scenario. Now, you need to create a subscription resolver using the following command.

php artisan lighthouse:subscription UserCreated

By default, the lighthouse looks for the studly case of your field name in the registered subscription directory that is defined in your lighthouse’s config file. If you want to create a resolver with a different name other than the convention, then you will need to provide the resolver using the @subscription(class: “Namespace\\Of\\UnconventionalClass”) directive. Your subscription class must be an instance of the GraphQLSubscription type.

Explaining the GraphQLSubscription instance class

In our UserCreated class we have several methods.

  • can — Firstly, the can method should return a truthy value if we want to let a subscriber subscribe to the event. In our schema, we expected an argument name to be received. We can access those arguments using the $subscriber->args array and determine if the user can subscribe to this event or not. You can also access the $subscriber->context->user() to access currently logged-in user. This is the first layer of protection that stops the subscriber from being subscribed. If the user has access, with the request’s response, he’ll get a channel name under the extensions value. We’ll see that later.
  • authorize — Secondly, this method is used when the subscriber wants to connect to the channel found due to the can method’s truthy value. It’s used to validate over an HTTP request when trying to connect that channel. If you’re using laravel-echo to connect to channels (will discuss later), the graphql/subscriptions/auth endpoint will be used to authorize access to that channel. You’ll get the subscriber instance who is trying to connect to and the current request variable. If the implementation of this method returns a false value, the subscriber will not be able to connect to that channel. If it returns a truthy value, then the subscriber will be able to receive the subscription messages.
  • filter — Thirdly, this method can be used to filter out who should receive the message for the event that occurred. If your implemented method returns a truthy value, then the subscriber will receive the message. The filter method has the access to the Subscriber instance and the event’s payload (returned by the resolver or the value passed when you triggered manually).
  • resolve — This method can be used if you want to modify or append something to the data received by the resolver or passed when triggered manually.

So, our class looks like this. I have only implemented the abstract methods. You can implement the other described methods as well. I’ve authorized all the users and will broadcast to all the connected users. You can modify the methods the way you want to.

<?php

namespace App\GraphQL\Subscriptions;

use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription;
use Nuwave\Lighthouse\Subscriptions\Subscriber;

class UserCreated extends GraphQLSubscription
{
public function authorize (Subscriber $subscriber, Request $request): bool {
return true;
}
public function filter (Subscriber $subscriber, $root) : bool {
return true;
}
}

Sidenote: If you want to know how Laravel Broadcasting works in-depth, you may want to read this article.

Broadcasting an event/subscription

Lighthouse provides two ways of broadcasting an event. You can use the @broadcast directive. Otherwise, you can trigger manually by calling the Nuwave\Lighthouse\Execution\Utils\Subscription::broadcast(‘event’,$data) method.

  • Using the directive
type Mutation {
createUser(name: String): User!
@field(resolver: "App\\GraphQL\\Mutations\\CreateUser")
@broadcast(subscription: "userCreated")
}

So, when the mutation is resolved, before returning the value to the frontend, it’ll broadcast the value returned by the resolver to the event userCreated that we defined earlier in the Subscription schema.

  • Triggering manually

From your code, you can manually broadcast the event. To do that, from anywhere in your code (mainly from Laravel Event Listener or resolvers), you can do the following.

<?phpnamespace App\GraphQL\Mutations;use Nuwave\Lighthouse\Execution\Utils\Subscription;class CreateUser 
{
public function __invoke ($_, array $args) {
// ... code logic here
Subscription::broadcast('userCreated', $user);
}
}

As we have our subscription schema ready, as well as broadcasting the event from our codebase. We are just two steps away.

  • Choosing the medium/driver to broadcast the event.
  • Receiving the broadcast for that subscription.

Available drivers for GraphQL Subscription with Lighthouse

There are several ways to integrate the subscription with Laravel/Lumen using the lighthouse. It is shipped with two drivers out of the box. one is echo and another one is pusher.

Laravel echo server is an open-source project by the community. You need to deploy and maintain your own if you want to use the echo driver to use the subscription.

On the other hand, you can use the pusher driver to send your subscription data. But there are two scenarios as well. If you use the pusher service, then you’ll have to buy a subscription plan to use in the production. I guess your user base will be higher than the free plan. Otherwise, you can use an alternative of the pusher that uses the pusher protocol, the laravel-websockets. In this case, you’ll also have to deploy your own service to keep the laravel-websockets up & running. I’ll try to explain both the implementation of Laravel echo & WebSockets with Lighthouse.

Lighthouse to implement Subscription with Pusher

If you’re using the Pusher service rather than the Laravel-Websockets, then you’ll first need to install the package for the pusher.

composer require pusher/pusher-php-server

If you’re like me that you don’t know the frontend, I’d suggest you go through the pusher implementation in this repository.

Go through the project’s readme file for the installation process. Change the environment variables. Check the comment in the .env.example file written for the pusher related variables.

Now, if using pusher service, in the project’s resources/js/pusher.link.js read the lines between 10 to 14. Build the javascript files if you’ve changed them, build JS files with yarn run watch or use npm if you want to.

Now, if you open the application in your browser, you’ll see a page like this.

Homepage of the application

In the inspect element’s network tab, you’ll see that a WebSocket connection has been established if the ports are configured properly.

On the other hand, if you want to use the Laravel WebSockets package, change the environment file, rebuild the docker containers. Again, comment and uncomment the resources/js/pusher.link.js file’s variables based on your scenario. Rebuild the JS files. And reload your application in the browser. You should see the image as above. Your key value in the JS file should match the value PUSHER_APP_KEY as your .env file. Keep it as it is for now. It works with the default mentioned dummy values.

Lighthouse to implement Subscription with Echo

To implement the echo driver as your subscription broadcaster, you’ll need to install the illuminate/broadcasting package if you’re using Lumen. It already comes with Laravel. To install the broadcasting package, you need to run the following command.

composer require illuminate/broadcasting

You’ll also have to register the provider in your application. Uncomment in the config/app.php's $proviers array if using Laravel. Or in Lumen, add the following line in the bootstrap/app.php

$app->register(
\Illuminate\Broadcasting\BroadcastServiceProvider::class
);

To implement the echo driver as a subscription broadcaster, I’d suggest you go through the following repository.

As I don’t know the frontend, I have implemented the frontend in the above repository, where the frontend connects to the second repository. Go through the repository’s readme file to know how to install the project.

Now, to use the echo driver, you’ll have to edit the resources/js/apollo.js. Comment the pusher related variable and import. Uncomment the echo related variable and import.

Then in the resources/js/echo.link.js change the host pointing the immediately above repository’s application URL. You should keep both the projects up & running to use the echo driver for the subscription.

Even though the environment variables are set in the .env.example but keep in mind that you’ll need these variables set like this.

BROADCAST_DRIVER=redis # For laravel/lumen to send payload
LIGHTHOUSE_BROADCASTER=echo # For lighthouse to choose the driver

Next, build the project with yarn run watch and load the application in your browser. If the set up was successful, in the inspect element, you’ll see that the page has successfully connected to the WebSocket.

How does it work?

I don’t know JS and frontend. I “may” not know it perfectly. But my guess is, when the homepage loads, it first tries to establish a socket connection. Then tries to subscribe to the listed subscriptions and sends a request to the GraphQL server for the userCreated event. And with the response, it gets a channel name to connect for that subscription.

Getting the channel name

Then in both the pusher.link.js or echo.link.js scenarios, we extract the channel name from the payload we received using the _getChannel method.

If we could extract the channel name, we then try to subscribe to that channel for both the echo and pusher driver.

Next, it sends the request to the auth endpoint to validate the user’s authorization for the channel. This is where our Subscription resolver’s authorize method gets called.

Checking the user’s authorization for the channel.

If it gets a positive response, then it connects to the channel.

Subscribing to the channel

If everything is like this for you. No exception is thrown in the console and your WebSocket is ready to receive the message. You’ll then be able to receive the event message.

Now, if I click the above button, it’ll then show the even message.

Result gets broadcasted to the frontend
Proof that this user was created when the button was clicked

Notes

  • When getting the channel from the GraphQL server, it by default sends a private-* channel. Even though the data are pushed to the presence channel. The team has already been notified. They’re working on it.
  • If you’re thinking of using the Laravel Websockets, you should look at the following codes which were actually added by the repository’s owner. Firstly, AppServiceProvider and then Websocket directory.

You may find this article a bit messy and clumsy and scattered but if you go through the codebases I shared, I guess it’ll be clear as crystal.

Happy coding. ❤

procrastinator | programmer | !polyglot | What else 🙄

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store