diff --git a/src/Illuminate/Broadcasting/BroadcastEvent.php b/src/Illuminate/Broadcasting/BroadcastEvent.php index e9c0897a553b..372212f35f17 100644 --- a/src/Illuminate/Broadcasting/BroadcastEvent.php +++ b/src/Illuminate/Broadcasting/BroadcastEvent.php @@ -60,8 +60,14 @@ public function handle(Broadcaster $broadcaster) $name = method_exists($this->event, 'broadcastAs') ? $this->event->broadcastAs() : get_class($this->event); + $channels = Arr::wrap($this->event->broadcastOn()); + + if (empty($channels)) { + return; + } + $broadcaster->broadcast( - Arr::wrap($this->event->broadcastOn()), $name, + $channels, $name, $this->getPayloadFromEvent($this->event) ); } diff --git a/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php index e48b15195741..a25b2ff2f678 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Container\Container; use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; +use Illuminate\Contracts\Broadcasting\HasBroadcastChannel; use Illuminate\Contracts\Routing\BindingRegistrar; use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Support\Arr; @@ -40,13 +41,19 @@ abstract class Broadcaster implements BroadcasterContract /** * Register a channel authenticator. * - * @param string $channel + * @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel * @param callable|string $callback * @param array $options * @return $this */ public function channel($channel, $callback, $options = []) { + if ($channel instanceof HasBroadcastChannel) { + $channel = $channel->broadcastChannelRoute(); + } elseif (is_string($channel) && class_exists($channel) && is_a($channel, HasBroadcastChannel::class, true)) { + $channel = (new $channel)->broadcastChannelRoute(); + } + $this->channels[$channel] = $callback; $this->channelOptions[$channel] = $options; diff --git a/src/Illuminate/Broadcasting/Channel.php b/src/Illuminate/Broadcasting/Channel.php index 798d6026a5f9..02b1a5caaa9a 100644 --- a/src/Illuminate/Broadcasting/Channel.php +++ b/src/Illuminate/Broadcasting/Channel.php @@ -2,6 +2,8 @@ namespace Illuminate\Broadcasting; +use Illuminate\Contracts\Broadcasting\HasBroadcastChannel; + class Channel { /** @@ -14,12 +16,12 @@ class Channel /** * Create a new channel instance. * - * @param string $name + * @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $name * @return void */ public function __construct($name) { - $this->name = $name; + $this->name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name; } /** diff --git a/src/Illuminate/Broadcasting/PrivateChannel.php b/src/Illuminate/Broadcasting/PrivateChannel.php index 045e630b178f..e53094b25c3f 100644 --- a/src/Illuminate/Broadcasting/PrivateChannel.php +++ b/src/Illuminate/Broadcasting/PrivateChannel.php @@ -2,16 +2,20 @@ namespace Illuminate\Broadcasting; +use Illuminate\Contracts\Broadcasting\HasBroadcastChannel; + class PrivateChannel extends Channel { /** * Create a new channel instance. * - * @param string $name + * @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $name * @return void */ public function __construct($name) { + $name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name; + parent::__construct('private-'.$name); } } diff --git a/src/Illuminate/Contracts/Broadcasting/HasBroadcastChannel.php b/src/Illuminate/Contracts/Broadcasting/HasBroadcastChannel.php new file mode 100644 index 000000000000..3b2c4018e389 --- /dev/null +++ b/src/Illuminate/Contracts/Broadcasting/HasBroadcastChannel.php @@ -0,0 +1,20 @@ +model = $model; + $this->event = $event; + } + + /** + * The channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn() + { + $channels = empty($this->channels) + ? ($this->model->broadcastOn($this->event) ?: []) + : $this->channels; + + return collect($channels)->map(function ($channel) { + return $channel instanceof Model ? new PrivateChannel($channel) : $channel; + })->all(); + } + + /** + * The name the event should broadcast as. + * + * @return string + */ + public function broadcastAs() + { + return class_basename($this->model).ucfirst($this->event); + } + + /** + * Manually specify the channels the event should broadcast on. + * + * @param array $channels + * @return $this + */ + public function onChannels(array $channels) + { + $this->channels = $channels; + + return $this; + } +} diff --git a/src/Illuminate/Database/Eloquent/BroadcastsEvents.php b/src/Illuminate/Database/Eloquent/BroadcastsEvents.php new file mode 100644 index 000000000000..969298cfe240 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/BroadcastsEvents.php @@ -0,0 +1,182 @@ +broadcastCreated(); + }); + + static::updated(function ($model) { + $model->broadcastUpdated(); + }); + + if (method_exists(static::class, 'bootSoftDeletes')) { + static::trashed(function ($model) { + $model->broadcastTrashed(); + }); + + static::restored(function ($model) { + $model->broadcastRestored(); + }); + } + + static::deleted(function ($model) { + $model->broadcastDeleted(); + }); + } + + /** + * Broadcast that the model was created. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastCreated($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('created'), 'created', $channels + ); + } + + /** + * Broadcast that the model was updated. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastUpdated($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('updated'), 'updated', $channels + ); + } + + /** + * Broadcast that the model was trashed. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastTrashed($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('trashed'), 'trashed', $channels + ); + } + + /** + * Broadcast that the model was restored. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastRestored($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('restored'), 'restored', $channels + ); + } + + /** + * Broadcast that the model was deleted. + * + * @param \Illuminate\Broadcasting\Channel|\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|array|null $channels + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + public function broadcastDeleted($channels = null) + { + return $this->broadcastIfBroadcastChannelsExistForEvent( + $this->newBroadcastableModelEvent('deleted'), 'deleted', $channels + ); + } + + /** + * Broadcast the given event instance if channels are configured for the model event. + * + * @param mixed $instance + * @param string $event + * @param mixed $channels + * @return \Illuminate\Broadcasting\PendingBroadcast|null + */ + protected function broadcastIfBroadcastChannelsExistForEvent($instance, $event, $channels = null) + { + if (! empty($this->broadcastOn($event)) || ! empty($channels)) { + return broadcast($instance->onChannels(Arr::wrap($channels))); + } + } + + /** + * Create a new broadcastable model event event. + * + * @param string $event + * @return mixed + */ + public function newBroadcastableModelEvent($event) + { + return tap(new BroadcastableModelEventOccurred($this, $event), function ($event) { + $event->connection = property_exists($this, 'broadcastConnection') + ? $this->broadcastConnection + : $this->broadcastConnection(); + + $event->queue = property_exists($this, 'broadcastQueue') + ? $this->broadcastQueue + : $this->broadcastQueue(); + + $event->afterCommit = property_exists($this, 'broadcastAfterCommit') + ? $this->broadcastAfterCommit + : $this->broadcastAfterCommit(); + }); + } + + /** + * Get the channels that model events should broadcast on. + * + * @param string $event + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn($event) + { + return [$this]; + } + + /** + * Get the queue connection that should be used to broadcast model events. + * + * @return string|null + */ + public function broadcastConnection() + { + // + } + + /** + * Get the queue that should be used to broadcast model events. + * + * @return string|null + */ + public function broadcastQueue() + { + // + } + + /** + * Determine if the model event broadcast queued job should be dispatched after all transactions are committed. + * + * @return bool + */ + public function broadcastAfterCommit() + { + return false; + } +} diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index fdcceedac783..67423f437996 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent; use ArrayAccess; +use Illuminate\Contracts\Broadcasting\HasBroadcastChannel; use Illuminate\Contracts\Queue\QueueableCollection; use Illuminate\Contracts\Queue\QueueableEntity; use Illuminate\Contracts\Routing\UrlRoutable; @@ -21,7 +22,7 @@ use JsonSerializable; use LogicException; -abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable +abstract class Model implements Arrayable, ArrayAccess, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable { use Concerns\HasAttributes, Concerns\HasEvents, @@ -1860,6 +1861,26 @@ public static function preventsLazyLoading() return static::$modelsShouldPreventLazyLoading; } + /** + * Get the broadcast channel route definition that is associated with the given entity. + * + * @return string + */ + public function broadcastChannelRoute() + { + return str_replace('\\', '.', get_class($this)).'.{'.Str::camel(class_basename($this)).'}'; + } + + /** + * Get the broadcast channel name that is associated with the given entity. + * + * @return string + */ + public function broadcastChannel() + { + return str_replace('\\', '.', get_class($this)).'.'.$this->getKey(); + } + /** * Dynamically retrieve attributes on the model. * diff --git a/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php b/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php new file mode 100644 index 000000000000..5b49f8c4bcbd --- /dev/null +++ b/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php @@ -0,0 +1,56 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + public function testBasicBroadcasting() + { + Event::fake([BroadcastableModelEventOccurred::class]); + + $model = new TestEloquentBroadcastUser; + $model->name = 'Taylor'; + $model->save(); + + Event::assertDispatched(function (BroadcastableModelEventOccurred $event) { + return $event->model instanceof TestEloquentBroadcastUser && + count($event->broadcastOn()) === 1 && + $event->broadcastOn()[0]->name == 'private-Illuminate.Tests.Integration.Database.TestEloquentBroadcastUser.'.$event->model->id; + }); + } + + public function testChannelRouteFormatting() + { + $model = new TestEloquentBroadcastUser; + + $this->assertEquals('Illuminate.Tests.Integration.Database.TestEloquentBroadcastUser.{testEloquentBroadcastUser}', $model->broadcastChannelRoute()); + } +} + +class TestEloquentBroadcastUser extends Model +{ + use BroadcastsEvents; + + protected $table = 'test_eloquent_broadcasting_users'; +}