cult3

Sending data via Pusher in a Laravel Application

Nov 30, 2015

Table of contents:

  1. Listening for webhooks
  2. Events
  3. Sending data to Pusher
  4. Conclusion

Over the last couple of weeks we’ve been adding Pusher to a Laravel application.

First, we set up the Client for making requests to Pusher and the Storage to store the currently active users in Setting up Pusher in a Laravel Application.

Last week we looked at authenticating users using both private and presence channels in Authenticating with Pusher in a Laravel application.

In this final instalment we will add the the functionality to subscribe and unsubscribe users from channels, as well as look at how we can push data to the client.

Listening for webhooks

When a user successfully authenticates (as we saw last week), Pusher will send us a webhook so we know we can send data to that user.

So the first thing we can do is to add a route to accept Pusher’s webhooks:

$router->post("webhooks/pusher/{webhook}", [
    "as" => "pusher.webhooks",
    "uses" => "PusherController@webhooks",
]);

When we receive a request from Pusher, there will be no authentication. To prevent unauthorised requests, Pusher will include a signature that we can check against to make sure the request is valid.

To check to make sure the signature is valid, we can encapsulate this process in a class:

class Signature
{
    /**
     * @var string
     */
    private $secret;

    /**
     * @var string
     */
    private $signature;

    /**
     * @var array
     */
    private $body;

    /**
     * @param string $secret
     * @param string $signature
     * @param array $body
     * @return void
     */
    public function __construct($secret, $signature, array $body)
    {
        $this->secret = $secret;
        $this->signature = $signature;
        $this->body = $body;
    }

    /**
     * Check to see if the signature is valid
     *
     * @return bool
     */
    public function isValid()
    {
        $expected = hash_hmac(
            "sha256",
            json_encode($this->body),
            $this->secret,
            false
        );

        return $this->signature == $expected;
    }
}

In this class we are simply generating the hash and asserting that it matches the signature that was provided.

The tests for this class look like this:

class SignatureTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_return_false_when_signature_is_invalid()
    {
        $signature = new Signature("secret", "signature", ["hello" => "world"]);

        $this->assertFalse($signature->isValid());
    }

    /** @test */
    public function should_return_true_when_signature_is_valid()
    {
        $signature = new Signature(
            "secret",
            "2677ad3e7c090b2fa2c0fb13020d66d5420879b8316eb356a2d60fb9073bc778",
            ["hello" => "world"]
        );

        $this->assertTrue($signature->isValid());
    }
}

Next we can check the signature in a Middleware class like this:

class PusherWebhook
{
    /**
     * Handle an incoming request
     *
     * @param Request $request
     * @param Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $secret = config("pusher.secret");
        $signature = $request->header("X_PUSHER_SIGNATURE");

        $signature = new Signature($secret, $signature, $request->all());

        if (!$signature->isValid()) {
            throw new PreconditionFailedException("invalid_webhook_signature");
        }

        return $next($request);
    }
}

In this example I’m grabbing the X_PUSHER_SIGNATURE header and creating a new instance of the Signature class.

If the signature is not valid we can throw a PreconditionFailedException Exception that will bubble up to the surface and return the appropriate HTTP response as we saw in Dealing with Exceptions in a Laravel API application.

Finally we can define the webhooks() method on the PusherController ready for the next section:

/**
 * Accept a Webhook event
 *
 * @param string $webhook
 * @return response
 */
public function webhooks($webhook)
{

}

Events

Now that a user has authenticated correctly we need to listen for events from Pusher. The two events we are interested in are MemberAdded and MemberRemoved which will be sent via webhooks. This will tell us to add or remove the members from the Redis storage that we created in Setting up Pusher in a Laravel Application.

So the first thing I’m going to do is to create an abstract Event class:

abstract class Event
{
    /**
     * Check to see if the Webhook is valid
     *
     * @return bool
     */
    abstract public function isValid();

    /**
     * Handle the Event
     *
     * @return void
     */
    abstract public function handle();

    /**
     * Get the UUID from the channel name
     *
     * @param string $channel
     * @return string
     */
    protected function getUuidFromChannelName($channel)
    {
        $position = strpos($channel, "-", strpos($channel, "-") + 2);

        return substr($channel, $position + 1);
    }
}

First I will require that classes that extend this class should implement a isValid() method and a handle() method.

The isValid() will check against our business rules to ensure the request is valid, and the handle() method will deal with actually handling the event.

Finally I will also include a getUuidFromChannelName() method. The account id will be in the channel name, and so this is simply a method to parse the uuid from the channel string.

Next I will create the MemberAdded class:

class MemberAdded extends Event
{
    /**
     * @var string
     */
    private $channel;

    /**
     * @var string
     */
    private $account_id;

    /**
     * @var string
     */
    private $user_id;

    /**
     * @var Storage
     */
    private $storage;

    /**
     * @param string $channel
     * @param string $user_id
     * @param Storage $storage
     * @return void
     */
    public function __construct($channel, $user_id, Storage $storage)
    {
        $this->channel = $channel;
        $this->account_id = $this->getUuidFromChannelName($channel);
        $this->user_id = $user_id;
        $this->storage = $storage;
    }
}

Here I’m injecting the $channel and the $user_id which I will be receiving from the webhook. I’m also parsing the account id from the channel name. I’m also injecting an object that implements the Storage interface from Setting up Pusher in a Laravel Application

Next I can define the isValid() method:

/**
 * Check to see if the Webhook is valid
 *
 * @return bool
 */
public function isValid()
{
// Check against business rules
}

This is where you would check against your business rules to ensure that the request is valid. This is going to be dependant on your application, but I would recommend using Guard classes as we looked at in Implementing Business Rules as Guards.

Finally we can handle the event. In this case we need to add the user id to the Storage:

/**
 * Handle the Event
 *
 * @return void
 */
public function handle()
{
    $users = $this->storage->get($this->account_id);

    if (in_array($this->user_id, $users)) return;

    $users[] = $this->user_id;

    $this->storage->set($this->account_id, $users);
}

The MemberRemoved event is basically exactly the same but instead of adding to the storage, we are removing from the storage:

class MemberRemoved extends Event
{
    /**
     * @var string
     */
    private $channel;

    /**
     * @var string
     */
    private $account_id;

    /**
     * @var string
     */
    private $user_id;

    /**
     * @var Storage
     */
    private $storage;

    /**
     * @param string $channel
     * @param string $user_id
     * @param Storage $storage
     * @return void
     */
    public function __construct($channel, $user_id, Storage $storage)
    {
        $this->channel = $channel;
        $this->account_id = $this->getUuidFromChannelName($channel);
        $this->user_id = $user_id;
        $this->storage = $storage;
    }

    /**
     * Check to see if the Webhook is valid
     *
     * @return bool
     */
    public function isValid()
    {
        // Check against business rules
    }

    /**
     * Handle the Event
     *
     * @return void
     */
    public function handle()
    {
        $users = $this->storage->get($this->account_id);

        if (($key = array_search($this->user_id, $users)) !== false) {
            unset($users[$key]);
        }

        if (count($users) > 0) {
            return $this->storage->set($this->account_id, $users);
        }

        $this->storage->remove($this->account_id);
    }
}

The tests for these two classes are fairly simple. You basically just need to test that your business rules are working correctly and that the storage is incremented or decremented.

In my case, if a business rule is violated a specific Exception will be thrown which I can assert against.

Remember, in Setting up Pusher in a Laravel Application we defined an ArrayStorage implementation. You can use that implementation in these tests to make life easier.

I won’t show tests for these classes because its dependant on the specific application business rules, but you should definitely have tests!

Finally we can add to the Manager service class from last week to make accepting webhooks and handling the events easier.

First we add an array of events as a class property:

/**
 * @var array
 */
private $events = [
    'member_added' => MemberAdded::class,
    'member_removed' => MemberRemoved::class
];

Next we can add a method to accept the webhook and return the event:

/**
 * Create a new Event
 *
 * @param string $channel
 * @param string $user_id
 * @param string $name
 * @return void
 */
public function event($channel, $user_id, $name)
{
    foreach ($this->$events as $key => $class) {
        if ($key == $name) {
            return new $class($channel, $user_id, $this->storage);
        }
    }

    throw new InvalidWebhookEvent('invalid_webhook_event', [$name]);
}

Finally we can finish off the webhooks() method from earlier:

/**
 * Accept a Webhook event
 *
 * @param Request $request
 * @return response
 */
public function webhooks(Request $request)
{
    foreach ($request()->input('events') as $event) {
        $channel = $event['channel'];
        $user_id = $event['user_id'];
        $name = $event['name'];

        $event = Pusher::event($channel, $user_id, $name);

        if ($event->isValid()) $event->handle();
    }

    return response([], 200);
}

Sending data to Pusher

Now that we have the entire Pusher infrastructure set up, we can start sending data to the client whenever something of interest occurs in the API.

For example, imagine we are building a project management application and we want to send data to the client whenever a new project is created. The process for adding this functionality to your application would be as follows.

Firstly, whenever a new project is created we would want to fire an event:

event(new ProjectWasCreated($project));

Next we can define a listener that should be added to the queue:

class PushNewProject implements ShouldQueue
{
    /**
     * @var string
     */
    private $event = "project_was_created";
}

We can inject objects that implement the Storage and Client interfaces from Setting up Pusher in a Laravel Application:

/**
 * @var Storage
 */
private $storage;

/**
 * @var Client
 */
private $client;

/**
 * @param Storage $storage
 * @param Client $client
 * @return void
 */
public function __construct(Storage $storage, Client $client)
{
    $this->storage = $storage;
    $this->client = $client;
}

Next we can define the handle() method of the event:

/**
 * Handle the event
 *
 * @param ProjectWasCreated $event
 * @return void
 */
public function handle(ProjectWasCreated $event)
{

}

First we get the account of the project from the event:

$account = $event->project->account;

Next we get the active users of that account from the storage:

$users = $this->storage->get($account->uuid);

Next we build the payload to send to the user:

$data = $event->project->toArray();

Next we iterate through the users and send the Pusher message to each one:

foreach ($users as $uuid) {
    $user = User::where("uuid", $uuid)->first();

    $channel = sprintf("private-user-%s", $uuid);

    $this->client->send($channel, $this->event, $data);
}

To test this class we can write the following test:

class PushNewProjectTest extends \TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function should_send_data_to_selected_users()
    {

    }

First we create a mock of the Client and set the expectation that the send() method will be called 3 times.

$client = m::mock(Client::class);
$client->shouldReceive("send")->times(3);

Next we create 3 users and one account:

$users = factory(User::class, 3)->create();
$account = factory(Account::class)->create();

Next I will create an instance of ArrayStorage and I will add the users to the account:

$storage = new ArrayStorage();
$users->each(function ($user) use ($storage, $account) {
    $storage->set($account->uuid, $user->uuid);
});

Next I will create the Project and the ProjectWasCreated event:

$project = factory(Project::class)->create(["account_id" => $account->id]);
$event = new ProjectWasCreated($project);

Finally I will instantiate a new instance of the PushNewProject listener with the Storage and the Client and then handle the event:

$listener = new PushNewProject($storage, $client);
$listener->handle($event);

The test should pass if the send() method on the Client mock is called three times.

I’ve totally missed out any business logic checking. For example checking to make sure the user should get the message or not based upon their privileges in the account. However, that is entirely dependant on the situation of the event, but it is something to bare in mind!

Conclusion

So there you have it, a complete step-by-step guide for setting up Pusher in your Laravel application.

I think the scenario that we have built for is a pretty standard setup for modern SaaS businesses, so hopefully you will be able to use it as a starting point for adding Pusher to your application.

I’m a big fan of Pusher because it makes setting up WebSockets functionality in your application a breeze. Your users are really going to be impressed when they see your application updating in real time!

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.