Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

devhoussam123's avatar

Can't get Notification broadcasting with user observer on updated

What I want to achieve:

It integrates the UserObserver to listen for user status updates and broadcasts the UserStatusUpdated event. Additionally, it notifies the super admin about the user status update using Filament's Notification::make().

Pusher Log:

Screenshot

My Codes:

2014_10_12_000000_create_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('avatar_url', 2048)->nullable();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->string('status')->default('active');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

User.php

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens;
    use HasFactory;
    use Notifiable;
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'avatar_url',
        'name',
        'email',
        'password',
        'status',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}

UserStatusUpdated.php

<?php

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;

class UserStatusUpdated implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public User $user,
    ) {
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('user.' . $this->user->id),
        ];
    }

    /**
     * Get the data to broadcast.
     *
     * @return array<string, mixed>
     */
    public function broadcastWith(): array
    {
        return [
            'id' => $this->user->id,
            'status' => $this->user->status,
        ];
    }
}

UserObserver.php

<?php

namespace App\Observers;

use App\Enums\UserRoles;
use App\Events\UserStatusUpdated;
use App\Models\User;
use App\Notifications\UserDeleted;
use App\Notifications\UserForceDeleted;
use App\Notifications\Welcome;
use Filament\Notifications\Notification;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Spatie\Permission\Models\Role;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        //
    }

    /**
     * Handle the User "updated" event.
     */
    public function updated(User $user): void
    {
        // Check if the user's status has been updated.
        if ($user->wasChanged('status')) {
            // Broadcast event for user status update.
            broadcast(new UserStatusUpdated($user));

            // Notify 'super_admin' when a user status is updated.
            $superAdminRole = Role::where('name', UserRoles::SuperAdmin)->first();

            if ($superAdminRole) {
                Notification::make()
                    ->icon('bi-person')
                    ->iconColor('primary')
                    ->title(__('User Status Updated'))
                    ->body($user->name . ' - ' . $user->email . __(' status updated to ') . $user->status)
                    ->broadcast($superAdminRole->users);
            }

        }
    }

    /**
     * Handle the User "deleted" event.
     */
    public function deleted(User $user): void
    {
        //
    }

    /**
     * Handle the User "restored" event.
     */
    public function restored(User $user): void
    {
        //
    }

    /**
     * Handle the User "force deleted" event.
     */
    public function forceDeleted(User $user): void
    {
        //
    }
}

channels.php

<?php

use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Broadcast::routes();

Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id;
}, ['middleware' => ['role:super_admin']]);

php artisan route:list

GET|POST|HEAD broadcasting/auth ........... Illuminate\Broadcasting › BroadcastController@authenticate

config/app.php

uncomment App\Providers\BroadcastServiceProvider provider in the providers array of your config/app.php configuration file.

config/filament.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Broadcasting
    |--------------------------------------------------------------------------
    |
    | By uncommenting the Laravel Echo configuration, you may connect Filament
    | to any Pusher-compatible websockets server.
    |
    | This will allow your users to receive real-time notifications.
    |
    */

    'broadcasting' => [

        'echo' => [
            'broadcaster' => 'pusher',
            'key' => env('VITE_PUSHER_APP_KEY'),
            'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
            'wsHost' => env('VITE_PUSHER_HOST'),
            'wsPort' => env('VITE_PUSHER_PORT'),
            'wssPort' => env('VITE_PUSHER_PORT'),
            // 'authEndpoint' => '/api/v1/broadcasting/auth',
            'disableStats' => true,
            'encrypted' => true,
        ],

    ],

    ...

];

resources/js/bootstrap.js

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

import axios from "axios";
window.axios = axios;

window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

import Echo from "laravel-echo";

import Pusher from "pusher-js";
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: "pusher",
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? "mt1",
    wsHost: import.meta.env.VITE_PUSHER_HOST
        ? import.meta.env.VITE_PUSHER_HOST
        : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
});

var channel = Echo.private(`App.Models.User.${userId}`);
channel.listen("UserStatusUpdated", function (e) {
    console.log(e.user.name);
});

.env

PUSHER_APP_ID=your-pusher-app-id
PUSHER_APP_KEY=your-pusher-key
PUSHER_APP_SECRET=your-pusher-secret
PUSHER_APP_CLUSTER=mt1

BROADCAST_DRIVER=pusher
0 likes
8 replies
gych's avatar

You have the listener in your bootstrap.js file but where is userId coming from? I can't see it declared in your file, it seems like you've added this in bootstrap.js for testing purposes? In which part of your project are you planning to use this listener?

Also try and update the code like my example below but make sure that the userId is properly declared before trying this.

const channel = window.Echo.private(`App.Models.User.${userId}`);
channel.listen(".UserStatusUpdated", function (e) {
    console.log(e.user.name);
});
devhoussam123's avatar

@gych bydoway I'm using filament php, I update my code but I still can't get real-time broadcasting notification when a regular user update it's status (active, inactive)

bootstrap.js

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

import axios from "axios";
window.axios = axios;

window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

import Echo from "laravel-echo";

import Pusher from "pusher-js";
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: "pusher",
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? "mt1",
    wsHost: import.meta.env.VITE_PUSHER_HOST
        ? import.meta.env.VITE_PUSHER_HOST
        : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
});

const channel = window.Echo.private(`App.Models.User.${this.user.id}`);
channel.listen(".UserStatusUpdated", function (e) {
    console.log(e.user.name);
});

channels.php

<?php

use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Broadcast::routes();

Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id;
}, ['middleware' => ['role:super_admin']]);

UserStatusUpdated.php

<?php

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;

class UserStatusUpdated implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    /**
     * The user instance.
     *
     * @var \App\Models\User
     */
    public $user;

    /**
     * Create a new event instance.
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('user.' . $this->user->id),
        ];
    }

    /**
     * Get the data to broadcast.
     *
     * @return array<string, mixed>
     */
    public function broadcastWith(): array
    {
        return [
            'id' => $this->user->id,
            'status' => $this->user->status,
        ];
    }
}

UserObserver.php

<?php

namespace App\Observers;

use App\Enums\UserRoles;
use App\Events\UserStatusUpdated;
use App\Models\User;
use App\Notifications\UserDeleted;
use App\Notifications\UserForceDeleted;
use App\Notifications\Welcome;
use Filament\Notifications\Notification;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Spatie\Permission\Models\Role;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        //
    }

    /**
     * Handle the User "updated" event.
     */
    public function updated(User $user): void
    {
        // Check if the user's status has been updated.
        if ($user->wasChanged('status')) {
            // Notify 'super_admin' when a user status is updated.
            $superAdminRole = Role::where('name', UserRoles::SuperAdmin)->first();

            if ($superAdminRole) {
                Notification::make()
                    ->icon('bi-person')
                    ->iconColor('primary')
                    ->title(__('User Status Updated'))
                    ->body($user->name . ' - ' . $user->email . __(' status updated to ') . $user->status)
                    ->broadcast($superAdminRole->users);

                // Broadcast event for user status update.
                broadcast(new UserStatusUpdated($user));
            }
        }
    }

    /**
     * Handle the User "deleted" event.
     */
    public function deleted(User $user): void
    {
        //
    }

    /**
     * Handle the User "restored" event.
     */
    public function restored(User $user): void
    {
        //
    }

    /**
     * Handle the User "force deleted" event.
     */
    public function forceDeleted(User $user): void
    {
        //
    }
}
gych's avatar

@devhoussam123 Ok I didn't notice it before but I see in the pusher logs that you've already managed to make a successfull connection to the channel.

In the development process you can always enable pusher's console logging. You can do this by adding this to the bootstrap.js file

Pusher.logToConsole = true;

This will give you more info in the console. Let me know what the log results are after enabling this.

gych's avatar

@devhoussam123 Did you already add this to your bootstrap.js file?

Pusher.logToConsole = true;

It should give you much more detailed info about the Pusher process, not only errors.

devhoussam123's avatar

@gych errors outside the filament panel you should know I'm trying to use it inside filament panel Screenshot

Inside Filament Panel:

Updated:

AppServiceProvider.php

<?php

namespace App\Providers;

use Filament\Support\Assets\Js;
use Illuminate\Support\ServiceProvider;
use Filament\Support\Facades\FilamentAsset;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        /**
         * Registering JavaScript files.
         * https://filamentphp.com/docs/3.x/support/assets#registering-javascript-files
         */
        FilamentAsset::register([
            Js::make('script', __DIR__ . '/../../resources/assets/js/bootstrap.js')->module(),
        ]);
    }
}

bootstrap.js

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

import axios from "axios";
window.axios = axios;

window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

import Echo from "laravel-echo";

import Pusher from "pusher-js";
window.Pusher = Pusher;

Pusher.logToConsole = true;

window.Echo = new Echo({
    broadcaster: "pusher",
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? "mt1",
    wsHost: import.meta.env.VITE_PUSHER_HOST
        ? import.meta.env.VITE_PUSHER_HOST
        : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
});

const channel = window.Echo.private(`App.Models.User.${this.user.id}`);
channel.listen(".UserStatusUpdated", function (data) {
    alert(JSON.stringify(data.user.name));
});

Screenshot

gych's avatar

@devhoussam123 Sadly I've no experience in working with Filament But from where is this.user.id coming in your bootstrap.js file, I can't see it defined anywhere?

Please or to participate in this conversation.