Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] More Convenient Model Broadcasting #37491

Merged
merged 13 commits into from
Jun 1, 2021
Merged

[8.x] More Convenient Model Broadcasting #37491

merged 13 commits into from
Jun 1, 2021

Conversation

taylorotwell
Copy link
Member

@taylorotwell taylorotwell commented May 26, 2021

This PR adds some convenience features around broadcasting model state changes - inspired by @tonysm's work on his Turbo + Laravel integration.

Broadcasting Model Events

There is a new trait which can be added to models to allow for the easy broadcasting of model events (created, updated, trashed, restored, deleted). Before adding this feature I previously used to create event classes for these model events, such as a DeploymentUpdated event in Laravel Vapor. These events would then be marked as ShouldBroadcast. If you are using these events for other purposes in your application that approach works great but if you only need the event so that it can be broadcast to your frontend then the new Eloquent BroadcastsEvents trait can save you some boilerplate code.

Usage of the trait looks like this:

<?php

namespace App\Models;

use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use BroadcastsEvents, HasFactory;

    /**
     * Get the user that the post belongs to.
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Get the channels that model events should broadcast on.
     *
     * @param  string  $event
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn($event)
    {
        return [$this];
    }
}

Note that the broadcastOn method receives the event type (string of created, updated, etc.) and returns the channels that the event should be broadcast on. Also note that you may simply return Eloquent instances as channels. Of course, you can still return full Channel or PrivateChannel instances. This brings me to the next point...

Eloquent model channel conventions... If you provide an Eloquent model instance in place a channel, the framework will automatically derive the channel name by calling the broadcastChannel method on the instance and create a PrivateChannel instance for that channel string. All Eloquent models now implement the Illuminate\Broadcasting\HasBroadcastChannel interface which dictates they implement this method. The base implementation will return a string channel name of the given format: App.Models.ModelName.{id} where, of course, the actual model name and ID value are replaced in the string with their actual values.

Often, you will want to broadcast an event on the model's own channel as well as its parent channel. For example, when a deployment is updated in Vapor, we broadcast that on the deployment's own channel as well as the environment's channel so that the list of deployments on the environment page can be live updated. With this new feature, you may simply return the model itself as well as the related model from the broadcastOn method:

/**
 * Get the channels that model events should broadcast on.
 *
 * @param  string  $event
 * @return \Illuminate\Broadcasting\Channel|array
 */
public function broadcastOn($event)
{
    return [$this, $this->user];
}

Of course, using the new PHP 8 match construct, it is easy to disable broadcasting for certain events:

/**
 * Get the channels that model events should broadcast on.
 *
 * @param  string  $event
 * @return \Illuminate\Broadcasting\Channel|array
 */
public function broadcastOn($event)
{
    return match($event) {
        'updated' => [],
        default => [$this],
    };
}

Broadcast Routes

To leverage the automatic determination of broadcast channel routes for models when actually registering your channel authentication callbacks, you may now pass an Eloquent model instance or class name into the Broadcast::channel method:

Broadcast::channel(User::class, function ($user, $id) {
    return (int) $user->id === (int) $id;
});

Listening For Model Events

Since there is not an actual event class that corresponds to these events that are automatically broadcasted, you need to listen to them slightly differently in Echo's client (I may improve this a bit in a separate PR). By convention, the events will be broadcast using the following convention: ModelNameCreated, ModelNameUpdated, etc. Since these events are broadcast outside of any namespace, you must prefix them with a . in Echo (I would like to improve this UX). The events will have a public model property where you can access the corresponding model attributes in your client side JavaScript application:

So, listening to the events looks like the following:

Echo.private('App.Models.User.1')
    .listen('.UserUpdated', (e) => {
        console.log(e.model)
    })
    .listen('.PostCreated', (e) => {
        console.log(e.model);
    });

@GrahamCampbell GrahamCampbell changed the title More Convenient Model Broadcasting [8.x] More Convenient Model Broadcasting May 26, 2021
@dinhquochan
Copy link
Contributor

Awesome, It's clean!

* @param string $event
* @return \Illuminate\Broadcasting\PendingBroadcast|null
*/
protected function broadcastIfBroadcastChannelsExistForEvent($instance, $event)
Copy link
Contributor

@joelbutcher joelbutcher May 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@taylorotwell rather than having this (relatively) long method name, what would you think to having a method shouldBroadcastEvent($event) and then using this in bootBroadcastsEvents? E.g.

    /**
  * Boot the event broadcasting trait.
  *
  * @return void
  */
    public static function bootBroadcastsEvents()
    {
        // code…

        static::created(function ($model) {
            if ($this->shouldBroadcastEvent(‘created’)) {
                $this->broadcastCreated();
            }
        });

        // more code…
    }

@tpetry
Copy link
Contributor

tpetry commented May 26, 2021

I am currently working on a similar concept. The biggest problem is getting events for models in some form of relationship with another model. there are so many edge cases that a simple approach like yours currently means some models have to publish up to 20 events, and some models would have to do extensive database lookups to get all related models. To be honest, i still don‘t have an easy solution.

@tonysm
Copy link
Contributor

tonysm commented May 27, 2021

I am currently working on a similar concept. The biggest problem is getting events for models in some form of relationship with another model. there are so many edge cases that a simple approach like yours currently means some models have to publish up to 20 events, and some models would have to do extensive database lookups to get all related models. To be honest, i still don‘t have an easy solution.

@tpetry not sure if this would solve your issue, but sounds like a use case for $model->broadcastUpdatedTo($anotherModel), which could be called outside the model (independently of the model events) and would send the updated event to that passed model/channel. This way, the models wouldn't even have to be related.

@tpetry
Copy link
Contributor

tpetry commented May 27, 2021

@tpetry not sure if this would solve your issue, but sounds like a use case for $model->broadcastUpdatedTo($anotherModel), which could be called outside the model (independently of the model events) and would send the updated event to that passed model/channel. This way, the models wouldn't even have to be related.

Yeah, kind of. This is in concept the same solution as Taylor's approach:

public function broadcastOn($event)
{
    return [$this, $this->user];
}

And as i said this concept is working, and i have a very similar solution live on an application. So @taylorotwell your approach is good and has been proven to work for us. But it's quite messy if you have models which are in a relationship with many models. Take for example updated user information, the user may be in a relationship with many models. I don't know of any solution solving this problem efficiently. There are databases like RethinkDB with a really good approach but all these approaches do not fit laravel's architecture. I just wanted to add one of my learnings i had when doing this in the hope someone has a brilliant idea to solve it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants