Laravel debounce
by zackaj
Laravel-debounce allows you to accumulate / debounce a job,notification or command to avoid spamming your users and your app’s queue.
It also tracks and registers every request occurrence and gives you a nice report tracking with information like ip address
and authenticated user
per request.
Table of Contents
Introduction
This laravel package uses UniqueJobs (atomic locks) and caching to run only one instance of a task in a debounced interval of x seconds delay.
Everytime a new activity is recoreded (occurrence), the execution is delayed with x seconds.
Features
- Debounce Notifications, Jobs and Artisan Commands Basic usage & Advanced usage
- Report Tracking
- Bonus CLI Debounce
Demo
A debounced notification to bulk notify users about new uploaded files.
See Code
FileUploaded.php
<?php
namespace App\Notifications;
use App\Models\File;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class FileUploaded extends Notification
{
use Queueable;
public function __construct(public File $file) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'files' => $this->file->user->files()
->where('created_at', '>=', $this->file->created_at)
->get(),
];
}
}
DemoController.php
<?php
namespace App\Http\Controllers;
use App\Models\File;
use App\Models\User;
use App\Notifications\FileUploaded;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Zackaj\LaravelDebounce\Facades\Debounce;
class DemoController extends Controller
{
public function normalNotification(Request $request)
{
$user = $request->user();
$file = File::factory()->create(['user_id' => $user->id]);
$otherUsers = User::query()->whereNot('id', $user->id)->get();
Notification::send($otherUsers, new FileUploaded($file));
return back();
}
public function debounceNotification(Request $request)
{
$user = $request->user();
$file = File::factory()->create(['user_id' => $user->id]);
$otherUsers = User::query()->whereNot('id', $user->id)->get();
Debounce::notification(
notifiables: $otherUsers,
notification:new FileUploaded($file),
delay: 5,
uniqueKey:$user->id,
);
return back();
}
}
Installation
Prerequisites
- Laravel application (11.x , 10.x should be fine)
- Up and running cache system that supports atomic locks
- Up and running queue worker
Composer
composer require zackaj/laravel-debounce
Usage
Basic usage
You can debounce existing jobs, notifications and commands with zero setup.
Warning you can’t access report or track the requests without extending the package’s classes, see Advanced usage
use Zackaj\LaravelDebounce\Facades\Debounce;
//job
Debounce::job(
job:new Job(),//replace
delay:5,//delay in seconds
uniqueKey:auth()->user()->id,//debounce per Job class name + uniqueKey
sync:false, //optional, job will be fired to the queue
);
//notification
Debounce::notification(
notifiables: auth()->user(),
notification: new Notification(),//replace
delay: 5,
uniqueKey: auth()->user()->id,
sendNow: false,
);
//command
Debounce::command(
command: new Command(),//replace
delay: 5,
uniqueKey: $request->ip(),
parameters: ['name' => 'zackaj'],//see Artisan::call() signature
toQueue: false,//optional, send command to the queue when executed
outputBuffer: null,//optional, //see Artisan::call() signature
);
Advanced usage
In order to use:
your existing jobs, notificaitons and commands must extend:
use Zackaj\LaravelDebounce\DebounceJob;
use Zackaj\LaravelDebounce\DebounceNotification;
use Zackaj\LaravelDebounce\DebounceCommand;
or just generate new ones using the available make commands.
Make commands
- Notification
php artisan make:debounce-notification TestNotification
- Job
php artisan make:debounce-job TestJob
- Command
php artisan make:debounce-command TestCommand
No facade usage
Alternatively, now you can debounce from the job, notification and command instances directly without using the Debounce facade
used in Basic usage
(new Job())->debounce(...);
(new Notification())->debounce(...);
(new Command())->debounce(...);
Report Tracking
Laravel-debounce uses the cache to store every request occurrence, use getReport()
method within your debounceables to access the report chain that has a collection of occurrences.
Every report will have one occurrence minimum.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Zackaj\LaravelDebounce\DebounceJob;
class Jobless extends DebounceJob implements ShouldQueue
{
use Dispatchable;
public function handle(): void
{
$this->getReport()->occurrences;//collection of occurrences
$this->getReport()->occurrences->count();
$this->getReport()->occurrences->first()->happenedAt;
$this->getReport()->occurrences->first()->ip;
$this->getReport()->occurrences->first()->ips;
$this->getReport()->occurrences->first()->requestHeaders;//HeaderBag
$this->getReport()->occurrences->first()->user;//authenticated user | null
}
}
Before After Hooks
If you wish to run some code before and/or after firing the debounceables
you can use the available hooks.
Important: after()
hook could run before your debounceable
is handled if it’s sent to the queue
when:
sendNow==false
and your notificationimplements ShouldQueue
sync==false
and your jobimplements ShouldQueue
toQueue==true
(command)
see: Basic usage
Debounce job
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
public function before(): void
{
//run before dispatching the job
}
public function after(): void
{
//run after dispatching the job
}
}
Debounce notification
You get the $notifiables
injected into the hooks.
<?php
...
class FileUploaded extends DebounceNotification
{
...
public function before($notifiables): void
{
//run before sending the notification
}
public function after($notifiables): void
{
//run after sending the notification
}
}
Debounce command
Due to limitations, the hook methods must be static
.
<?php
...
class Test extends DebounceCommand
{
...
public static function before(): void
{
//run before executing the command
}
public static function after(): void
{
//run after executing the command
}
}
Override Timestamp
By default laravel-debounce debounces from the last occurrence happenedAt
timestamp
public function getLastActivityTimestamp(): ?Carbon
{
return $this->getReport()->occurrences->last()->happenedAt;
}
You can override this method in your debounceables
in order to debounce from a custom timestamp of choice, if null returned the debouncer will fallback to the default implementation above.
Debounce job
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
public function getLastActivityTimestamp(): ?Carbon
{
return Message::latest()->first()?->seen_at;
}
}
Debounce notification
You get the $notifiables
injected into the method.
<?php
...
class FileUploaded extends DebounceNotification
{
...
public function getLastActivityTimestamp(mixed $notifiables): ?Carbon
{
return $this->file->user->files->latest()->first()?->created_at;
}
}
Debounce command
Due to limitations, the method must be static
.
<?php
...
class Test extends DebounceCommand
{
...
public static function getLastActivityTimestamp(): ?Carbon
{
return User::latest()->first()?->created_at;
}
}
Bonus CLI Debounce
For fun, you can actually debounce commands from the CLI using debounce:command
Artisan command.
php artisan debounce:command 5 uniqueKey app:test
here’s the signature for the command:
php artisan debounce:command {delay} {uniqueKey} {signature*}
Debugging And Monitoring
I recommend using Laravel telescope to see the debouncer live in queues tab and to debug any failures.
Known Issues
1- Unique lock gets stuck sometimes when jobs fail github issue
-
cause: this happens when deleted models are unserialized causing the job to fail without clearing the lock.
-
solution: If you’re using
database
cache driver delete the entry from job_locks table.
Contributing
Contributions, issues and suggestions are always welcome! See contributing.md
for ways to get started.