Laravel & Vue: Real-Time Notifications with Reverb, Redis, and TDD - 02
José Rafael Gutierrez
1 month ago
Chapter 2: Creating Notification Channels with Reverb in Laravel
Objective
Implement secure and customized notification channels in Laravel using Reverb, starting with failing tests and following the TDD cycle of red → green → refactor. This includes:
1. Creating channels to transmit notifications to individual users or administrators.
2. Securing the channels with permissions and restrictions.
3. Validating functionality through error-guided solutions and tests.
Introduction
This chapter uses TDD to implement custom notification channels in Laravel with Reverb. We will first write tests that will fail, identifying what the application lacks, and based on those errors, we will develop the necessary code.
We will focus on two types of channels:
- Private channels for users.
- A global channel for administrators.
For now, we will use the User
model with the is_admin
attribute to simplify role differentiation.
TDD Cycle for User Channels
Write Tests for Private User Channels
Create a test file:
sail artisan make:test UserNotificationChannelTest
Modify tests/Feature/UserNotificationChannelTest.php
:
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserNotificationChannelTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_access_own_notification_channel()
{
// Given
$user = User::factory()->create();
// When
$response = $this->actingAs($user)->post('/broadcasting/auth', [
'channel_name' => 'private-user-notifications.' . $user->id,
'socket_id' => '12345.67890', // valid socket_id format
], ['X-CSRF-TOKEN' => csrf_token()]);
// Then
$response->assertStatus(200);
}
public function test_user_cannot_access_other_users_notification_channel()
{
// Given
$user = User::factory()->create();
$otherUser = User::factory()->create();
// When
$response = $this->actingAs($user)->post('/broadcasting/auth', [
'channel_name' => 'private-user-notifications.' . $otherUser->id,
'socket_id' => '12345.67890', // valid socket_id format
], ['X-CSRF-TOKEN' => csrf_token()]);
// Then
$response->assertStatus(403);
}
}
Note: The
socket_id
is a unique identifier generated by the client (typically during a websocket connection) and must follow a specific format expected by Pusher. A validsocket_id
should be in thedigits.dot.digits
format, like1234.5678
. When using Pusher during tests, providing a validsocket_id
is essential to avoid errors and allow proper channel authorization.
Run Initial Tests (Red)
Run the tests:
sail artisan test --filter=UserNotificationChannelTest
Expected Errors:
-
404 Error: The
/broadcasting/auth
route is not defined.
Error Analysis:
The 404 error indicates that the route for channel authentication is missing. This error guides us to implement the basic broadcasting configuration.
Configure Channel Authentication
1. Create the Broadcast Service Provider
sail artisan make:provider BroadcastServiceProvider
2. Enable the BroadcastServiceProvider
:
Ensure the service is registered in bootstrap/providers.php
:
<?php
return [
App\Providers\AppServiceProvider::class,
//...
App\Providers\BroadcastServiceProvider::class,
];
3. Define the Broadcasting Route:
Modify the boot
method of BroadcastServiceProvider
:
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Foundation\Support\Providers\BroadcastServiceProvider as ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
public function boot()
{
Broadcast::routes(); // Register the '/broadcasting/auth' route
require base_path('routes/channels.php'); // Define the channels
}
}
4. Clear the Cache:
After modifying configuration files, it is important to clear Laravel’s cache to apply the changes:
sail artisan config:clear
5. Start the Reverb Server:
Start the server:
sail artisan reverb:start --host="0.0.0.0" --port=8080 --hostname="localhost"
Run Tests for Private User Channels (Red)
Run the tests again. They will now fail with a 403 error, as the channels are not yet defined:
sail artisan test --filter=UserNotificationChannelTest
Implement Private Channels (Green)
Define the channels in routes/channels.php
:
Broadcast::channel('user-notifications.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Run the tests again. Both should pass:
sail artisan test --filter=UserNotificationChannelTest
Refactor
Review the organization of the routes and channel code. If more channels are added in the future, consider creating a dedicated file for them.
Repeat the TDD Cycle for Administrator Channels
Write Tests for the Administrator Channel
Create a test file:
sail artisan make:test AdminNotificationChannelTest
Modify tests/Feature/AdminNotificationChannelTest.php
:
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminNotificationChannelTest extends TestCase
{
use RefreshDatabase;
public function test_admin_can_access_admin_notification_channel()
{
// Given
$admin = User::factory()->state(['is_admin' => true])->create();
// When
$response = $this->actingAs($admin)->post('/broadcasting/auth', [
'channel_name' => 'admin-notifications',
'socket_id' => '12345.67890', // valid socket_id format
], ['X-CSRF-TOKEN' => csrf_token()]);
// Then
$response->assertStatus(200);
}
public function test_regular_user_cannot_access_admin_notification_channel()
{
// Given
$user = User::factory()->state(['is_admin' => false])->create();
// When
$response = $this->actingAs($user)->post('/broadcasting/auth', [
'channel_name' => 'admin-notifications',
'socket_id' => '12345.67890', // valid socket_id format
], ['X-CSRF-TOKEN' => csrf_token()]);
// Then
$response->assertStatus(403);
}
}
Run Tests for Administrators (Red)
Run the tests. Both will fail, as the administrator channel is not defined:
sail artisan test --filter=AdminNotificationChannelTest
Implement the Administrator Channel
Add the channel in routes/channels.php
:
Broadcast::channel('admin-notifications', function ($user) {
return $user->is_admin;
});
Run Tests for Administrators (Red)
Run the tests. Both will fail, as the is_admin
attribute does not exist in the User
model:
sail artisan test --filter=AdminNotificationChannelTest
Add the missing is_admin Attribute to the User Model
1. Create the Migration
Run the following command to create a new migration:
sail artisan make:migration add_is_admin_to_users_table --table=users
2. Modify the Migration
Open the created migration in database/migrations/
and add the is_admin
field:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};
3. Update the User Model
Update the User
model (app/Models/User.php
) to cast is_admin
as a boolean:
class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_admin' => 'boolean',
];
}
4. Run the Migration
Run the migration to apply the changes:
sail artisan migrate
Run Tests for Administrators (Green)
sail artisan test --filter=AdminNotificationChannelTest
Now run all tests to verify they all pass:
sail artisan test
Refactor (If Necessary)
Review the routes/channels.php
file. If more channels are added, consider grouping them into a dedicated file to improve organization.
Conclusion
In this chapter, we implemented secure channels for users and administrators by following the TDD cycle. The 404 and 403 errors guided us in developing the routes and permissions needed for each channel.
In the next chapter, we will configure Redis to store and manage notifications, optimizing performance and handling unread messages. 🚀