feat: Phase 2 - Laravel Reverb + real-time notifications

- Install laravel/reverb + laravel-echo + pusher-js
- Configure broadcasting to reverb in .env.example + .env
- Add Echo + Pusher client in resources/js/app.js
- Update all 14 notifications to implement ShouldBroadcast + broadcastOn()
- Update NotificationBell component to listen for real-time events
- Add Livewire.emit in notification-bell blade for JS-to-Livewire bridge

Tests: 101 passing (319 assertions)
This commit is contained in:
Javier Braña
2026-08-31 12:16:56 +02:00
parent 78d1dbf45a
commit 9222eefdf9
26 changed files with 1192 additions and 44 deletions
+8 -1
View File
@@ -33,7 +33,7 @@ SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
@@ -47,6 +47,13 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REVERB_APP_ID=1
REVERB_APP_KEY=local
REVERB_APP_SECRET=secret
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
+8
View File
@@ -3,6 +3,7 @@
namespace App\Livewire\Common;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\On;
use Livewire\Component;
class NotificationBell extends Component
@@ -37,6 +38,13 @@ class NotificationBell extends Component
$this->loadNotifications();
}
#[On('notification-received')]
public function handleNotification($message = 'Nueva notificación')
{
$this->loadNotifications();
$this->dispatch('notify', $message);
}
public function render()
{
return view('livewire.common.notification-bell');
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Feature;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class FeatureCompletedNotification extends Notification
class FeatureCompletedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class FeatureCompletedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,14 @@ class FeatureCompletedNotification extends Notification
'message' => "Elemento '{$this->feature->name}' marcado como completado",
];
}
}
public function broadcastOn(): array
{
$projectId = $this->feature->layer?->phase?->project_id;
if (! $projectId) {
return [];
}
$userIds = \App\Models\Project::find($projectId)?->users->pluck('id')->toArray() ?? [];
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Inspection;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class InspectionCompletedNotification extends Notification
class InspectionCompletedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class InspectionCompletedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -29,4 +30,10 @@ class InspectionCompletedNotification extends Notification
'message' => "Inspección completada en '{$this->inspection->feature?->name}': ".($this->inspection->result ?? 'sin resultado'),
];
}
}
public function broadcastOn(): array
{
$userIds = $this->inspection->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Inspection;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class InspectionDeletedNotification extends Notification
class InspectionDeletedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class InspectionDeletedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -29,4 +30,10 @@ class InspectionDeletedNotification extends Notification
'message' => "Inspección eliminada en '{$this->inspection->feature?->name}' por {$this->deletedBy}",
];
}
}
public function broadcastOn(): array
{
$userIds = $this->inspection->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Inspection;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class InspectionUpdatedNotification extends Notification
class InspectionUpdatedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class InspectionUpdatedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -30,4 +31,10 @@ class InspectionUpdatedNotification extends Notification
'message' => "Inspección actualizada en '{$this->inspection->feature?->name}'",
];
}
}
public function broadcastOn(): array
{
$userIds = $this->inspection->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueAssignedNotification extends Notification
class IssueAssignedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueAssignedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -27,4 +28,9 @@ class IssueAssignedNotification extends Notification
'message' => "Se te ha asignado la incidencia '{$this->issue->title}'",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->issue->assigned_to)];
}
}
@@ -4,10 +4,11 @@ namespace App\Notifications;
use App\Models\IssueComment;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
class IssueCommentedNotification extends Notification
class IssueCommentedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -15,7 +16,7 @@ class IssueCommentedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -30,4 +31,13 @@ class IssueCommentedNotification extends Notification
'message' => "{$this->comment->user?->name} comentó en '{$issue?->title}': ".Str::limit($this->comment->body, 60),
];
}
}
public function broadcastOn(): array
{
$issue = $this->comment->issue;
$userIds = [];
if ($issue->assigned_to) $userIds[] = $issue->assigned_to;
if ($issue->reported_by) $userIds[] = $issue->reported_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueReportedNotification extends Notification
class IssueReportedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueReportedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,10 @@ class IssueReportedNotification extends Notification
'message' => "Nuevo issue '{$this->issue->title}' (prioridad: {$this->issue->priority})",
];
}
}
public function broadcastOn(): array
{
$userIds = $this->issue->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueStatusChangedNotification extends Notification
class IssueStatusChangedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueStatusChangedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -34,4 +35,12 @@ class IssueStatusChangedNotification extends Notification
'message' => "La incidencia '{$this->issue->title}' ha sido {$label}",
];
}
}
public function broadcastOn(): array
{
$userIds = [];
if ($this->issue->assigned_to) $userIds[] = $this->issue->assigned_to;
if ($this->issue->reported_by) $userIds[] = $this->issue->reported_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\IssueTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueTaskAssignedNotification extends Notification
class IssueTaskAssignedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueTaskAssignedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,9 @@ class IssueTaskAssignedNotification extends Notification
'message' => "Se te ha asignado la tarea '{$this->task->title}'",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\IssueTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueTaskOverdueNotification extends Notification
class IssueTaskOverdueNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueTaskOverdueNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,9 @@ class IssueTaskOverdueNotification extends Notification
'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskAssignedNotification extends Notification
class TaskAssignedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class TaskAssignedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -27,4 +28,9 @@ class TaskAssignedNotification extends Notification
'message' => "Se te ha asignado la tarea '{$this->task->title}'",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
+12 -3
View File
@@ -5,9 +5,10 @@ namespace App\Notifications;
use App\Models\Comment;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskCommentNotification extends Notification
class TaskCommentNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -15,7 +16,7 @@ class TaskCommentNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,12 @@ class TaskCommentNotification extends Notification
'message' => "Nuevo comentario en '{$this->task->title}' por {$this->comment->user->name}: ".substr($this->comment->body, 0, 100),
];
}
}
public function broadcastOn(): array
{
$userIds = [];
if ($this->task->assigned_to) $userIds[] = $this->task->assigned_to;
if ($this->task->created_by) $userIds[] = $this->task->created_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskOverdueNotification extends Notification
class TaskOverdueNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class TaskOverdueNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -27,4 +28,9 @@ class TaskOverdueNotification extends Notification
'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskStatusChangedNotification extends Notification
class TaskStatusChangedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class TaskStatusChangedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -42,4 +43,12 @@ class TaskStatusChangedNotification extends Notification
'message' => "La tarea '{$this->task->title}' cambió de {$oldLabel} a {$label}",
];
}
}
public function broadcastOn(): array
{
$userIds = [];
if ($this->task->assigned_to) $userIds[] = $this->task->assigned_to;
if ($this->task->created_by) $userIds[] = $this->task->created_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
+1
View File
@@ -17,6 +17,7 @@ return Application::configure(basePath: dirname(__DIR__))
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
+1
View File
@@ -10,6 +10,7 @@
"blade-ui-kit/blade-heroicons": "^2.7",
"gasparesganga/php-shapefile": "^3.4",
"laravel/framework": "^12.0",
"laravel/reverb": "^1.11",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1",
"league/geotools": "^1.3",
Generated
+764 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "59f27ca027f68b2cd10cb3d30bae901b",
"content-hash": "043b5e79681c66887e3615a82073d335",
"packages": [
{
"name": "blade-ui-kit/blade-heroicons",
@@ -285,6 +285,136 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "clue/redis-protocol",
"version": "v0.3.2",
"source": {
"type": "git",
"url": "https://github.com/clue/redis-protocol.git",
"reference": "6f565332f5531b7722d1e9c445314b91862f6d6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/redis-protocol/zipball/6f565332f5531b7722d1e9c445314b91862f6d6c",
"reference": "6f565332f5531b7722d1e9c445314b91862f6d6c",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\Redis\\Protocol\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.",
"homepage": "https://github.com/clue/redis-protocol",
"keywords": [
"parser",
"protocol",
"redis",
"resp",
"serializer",
"streaming"
],
"support": {
"issues": "https://github.com/clue/redis-protocol/issues",
"source": "https://github.com/clue/redis-protocol/tree/v0.3.2"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2024-08-07T11:06:28+00:00"
},
{
"name": "clue/redis-react",
"version": "v2.8.0",
"source": {
"type": "git",
"url": "https://github.com/clue/reactphp-redis.git",
"reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca",
"reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca",
"shasum": ""
},
"require": {
"clue/redis-protocol": "^0.3.2",
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.0 || ^1.1",
"react/promise-timer": "^1.11",
"react/socket": "^1.16"
},
"require-dev": {
"clue/block-react": "^1.5",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\React\\Redis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"description": "Async Redis client implementation, built on top of ReactPHP.",
"homepage": "https://github.com/clue/reactphp-redis",
"keywords": [
"async",
"client",
"database",
"reactphp",
"redis"
],
"support": {
"issues": "https://github.com/clue/reactphp-redis/issues",
"source": "https://github.com/clue/reactphp-redis/tree/v2.8.0"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2025-01-03T16:18:33+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
@@ -814,6 +944,53 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "evenement/evenement",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/igorw/evenement.git",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^9 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Evenement\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
}
],
"description": "Événement is a very simple event dispatching library for PHP",
"keywords": [
"event-dispatcher",
"event-emitter"
],
"support": {
"issues": "https://github.com/igorw/evenement/issues",
"source": "https://github.com/igorw/evenement/tree/v3.0.2"
},
"time": "2023-08-08T05:53:35+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
@@ -1753,6 +1930,85 @@
},
"time": "2026-04-20T16:07:33+00:00"
},
{
"name": "laravel/reverb",
"version": "v1.11.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/reverb.git",
"reference": "52ce5fd88cd1d7eacfdcf6b91cea4704cc546f27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/reverb/zipball/52ce5fd88cd1d7eacfdcf6b91cea4704cc546f27",
"reference": "52ce5fd88cd1d7eacfdcf6b91cea4704cc546f27",
"shasum": ""
},
"require": {
"clue/redis-react": "^2.6",
"guzzlehttp/psr7": "^2.6",
"illuminate/console": "^10.47|^11.0|^12.0|^13.0",
"illuminate/contracts": "^10.47|^11.0|^12.0|^13.0",
"illuminate/http": "^10.47|^11.0|^12.0|^13.0",
"illuminate/support": "^10.47|^11.0|^12.0|^13.0",
"laravel/prompts": "^0.1.15|^0.2.0|^0.3.0",
"php": "^8.2",
"pusher/pusher-php-server": "^7.2",
"ratchet/rfc6455": "^0.4",
"react/promise-timer": "^1.10",
"react/socket": "^1.14",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/http-foundation": "^6.3|^7.0|^8.0"
},
"require-dev": {
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
"pestphp/pest": "^2.0|^3.0|^4.0",
"phpstan/phpstan": "^1.10",
"ratchet/pawl": "^0.4.1",
"react/async": "^4.2",
"react/http": "^1.9"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Reverb\\ApplicationManagerServiceProvider",
"Laravel\\Reverb\\ReverbServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Reverb\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Joe Dixon",
"email": "joe@laravel.com"
}
],
"description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.",
"keywords": [
"WebSockets",
"laravel",
"real-time",
"websocket"
],
"support": {
"issues": "https://github.com/laravel/reverb/issues",
"source": "https://github.com/laravel/reverb/tree/v1.11.1"
},
"time": "2026-08-06T14:48:58+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.3.2",
@@ -4476,6 +4732,69 @@
},
"time": "2026-03-22T23:03:24+00:00"
},
{
"name": "pusher/pusher-php-server",
"version": "7.3.0",
"source": {
"type": "git",
"url": "https://github.com/pusher/pusher-http-php.git",
"reference": "058d8464246118110a341fc2e6e70c7c8b6a7f2c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/058d8464246118110a341fc2e6e70c7c8b6a7f2c",
"reference": "058d8464246118110a341fc2e6e70c7c8b6a7f2c",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.8.2 || ^8.0",
"guzzlehttp/promises": "^2.0.3 || ^3.0",
"guzzlehttp/psr7": "^2.6.3 || ^3.0",
"php": "^7.3|^8.0",
"psr/http-client": "^1.0",
"psr/log": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"overtrue/phplint": "^2.3",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Pusher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Library for interacting with the Pusher REST API",
"keywords": [
"events",
"messaging",
"php-pusher-server",
"publish",
"push",
"pusher",
"real time",
"real-time",
"realtime",
"rest",
"trigger"
],
"support": {
"issues": "https://github.com/pusher/pusher-http-php/issues",
"source": "https://github.com/pusher/pusher-http-php/tree/7.3.0"
},
"time": "2026-08-07T12:47:11+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@@ -4755,6 +5074,213 @@
],
"time": "2025-05-03T02:24:46+00:00"
},
{
"name": "ratchet/rfc6455",
"version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/ratchetphp/RFC6455.git",
"reference": "9b05f371219cbaf9748b505f139617dd0715592b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/9b05f371219cbaf9748b505f139617dd0715592b",
"reference": "9b05f371219cbaf9748b505f139617dd0715592b",
"shasum": ""
},
"require": {
"php": ">=7.4",
"psr/http-factory-implementation": "^1.0",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"guzzlehttp/psr7": "^2.7",
"phpunit/phpunit": "^9.5",
"react/socket": "^1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Ratchet\\RFC6455\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"role": "Developer"
},
{
"name": "Matt Bonneau",
"role": "Developer"
}
],
"description": "RFC6455 WebSocket protocol handler",
"homepage": "http://socketo.me",
"keywords": [
"WebSockets",
"rfc6455",
"websocket"
],
"support": {
"chat": "https://gitter.im/reactphp/reactphp",
"issues": "https://github.com/ratchetphp/RFC6455/issues",
"source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.1"
},
"time": "2026-06-06T14:34:23+00:00"
},
{
"name": "react/cache",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/cache.git",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/promise": "^3.0 || ^2.0 || ^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, Promise-based cache interface for ReactPHP",
"keywords": [
"cache",
"caching",
"promise",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/cache/issues",
"source": "https://github.com/reactphp/cache/tree/v1.2.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2022-11-30T15:59:55+00:00"
},
{
"name": "react/dns",
"version": "v1.14.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/dns.git",
"reference": "7562c05391f42701c1fccf189c8225fece1cd7c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3",
"reference": "7562c05391f42701c1fccf189c8225fece1cd7c3",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async DNS resolver for ReactPHP",
"keywords": [
"async",
"dns",
"dns-resolver",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/dns/issues",
"source": "https://github.com/reactphp/dns/tree/v1.14.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-18T19:34:28+00:00"
},
{
"name": "react/event-loop",
"version": "v1.6.0",
@@ -4900,6 +5426,243 @@
],
"time": "2025-08-19T18:57:03+00:00"
},
{
"name": "react/promise-timer",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise-timer.git",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7.0 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\Timer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
"homepage": "https://github.com/reactphp/promise-timer",
"keywords": [
"async",
"event-loop",
"promise",
"reactphp",
"timeout",
"timer"
],
"support": {
"issues": "https://github.com/reactphp/promise-timer/issues",
"source": "https://github.com/reactphp/promise-timer/tree/v1.11.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-04T14:27:45+00:00"
},
{
"name": "react/socket",
"version": "v1.17.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/socket.git",
"reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08",
"reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.0",
"react/dns": "^1.13",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.6 || ^1.2.1",
"react/stream": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3.3 || ^2",
"react/promise-stream": "^1.4",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Socket\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
"keywords": [
"Connection",
"Socket",
"async",
"reactphp",
"stream"
],
"support": {
"issues": "https://github.com/reactphp/socket/issues",
"source": "https://github.com/reactphp/socket/tree/v1.17.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-19T20:47:34+00:00"
},
{
"name": "react/stream",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/stream.git",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.8",
"react/event-loop": "^1.2"
},
"require-dev": {
"clue/stream-filter": "~1.2",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
"keywords": [
"event-driven",
"io",
"non-blocking",
"pipe",
"reactphp",
"readable",
"stream",
"writable"
],
"support": {
"issues": "https://github.com/reactphp/stream/issues",
"source": "https://github.com/reactphp/stream/tree/v1.4.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-11T12:45:25+00:00"
},
{
"name": "spatie/laravel-permission",
"version": "6.25.0",
+82
View File
@@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];
+102
View File
@@ -0,0 +1,102 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'),
'rate_limiting' => [
'enabled' => env('REVERB_APP_RATE_LIMITING_ENABLED', false),
'max_attempts' => env('REVERB_APP_RATE_LIMIT_MAX_ATTEMPTS', 60),
'decay_seconds' => env('REVERB_APP_RATE_LIMIT_DECAY_SECONDS', 60),
'terminate_on_limit' => env('REVERB_APP_RATE_LIMIT_TERMINATE', false),
],
],
],
],
];
+41
View File
@@ -17,8 +17,10 @@
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"daisyui": "^5.5.19",
"laravel-echo": "^2.4.0",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.5.12",
"pusher-js": "^8.6.0",
"tailwindcss": "^3.4.19",
"vite": "^7.0.7"
}
@@ -2156,6 +2158,28 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/laravel-echo": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.4.0.tgz",
"integrity": "sha512-8w0fAGSNt6THfbNyqdKc29bhfeNpJg13CGx2fcLgoX0/f0mTJm/AIkYTTakmcr9pc42ZB68cSoE00j4/xNaFGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*"
},
"peerDependenciesMeta": {
"pusher-js": {
"optional": true
},
"socket.io-client": {
"optional": true
}
}
},
"node_modules/laravel-vite-plugin": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz",
@@ -2832,6 +2856,16 @@
"node": ">=10"
}
},
"node_modules/pusher-js": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.6.0.tgz",
"integrity": "sha512-wShJPfCS/kYkCBVzVW67wa9cnQIgHTszEK2XHNrFkOgGruuGw081aERAxfRjfdFU+WcIt8x6dvbwkTW4iZuQ8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3266,6 +3300,13 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true,
"license": "Unlicense"
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+2
View File
@@ -13,8 +13,10 @@
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"daisyui": "^5.5.19",
"laravel-echo": "^2.4.0",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.5.12",
"pusher-js": "^8.6.0",
"tailwindcss": "^3.4.19",
"vite": "^7.0.7"
},
+14
View File
@@ -1,5 +1,19 @@
import './bootstrap';
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
import localforage from 'localforage';
// Generic function to queue any offline action
@@ -84,4 +84,18 @@
@endif
</div>
</div>
@push('scripts')
<script>
document.addEventListener('livewire:load', () => {
const userId = {{ auth()->id() ?? 'null' }};
if (userId && window.Echo) {
window.Echo.private(`user.${userId}`)
.notification((notification) => {
Livewire.emit('notification-received', notification.data.message ?? 'Nueva notificación');
});
}
});
</script>
@endpush
</div>
+7
View File
@@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});