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.
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 recorded (occurrence), the execution is delayed by x seconds.
- Debounce Notifications, Jobs and Artisan Commands Basic usage & Advanced usage
- Report Tracking
- Bonus CLI Debounce
A debounced notification to bulk notify users about new uploaded files.
debounce_compressed.mp4
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();
}
}
- Laravel application (11.x , 10.x should be fine)
- Up and running cache system that supports atomic locks
- Up and running queue worker
composer require zackaj/laravel-debounce
You can debounce existing jobs, notifications and commands with zero setup.
Warning you can't access report tracking 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
);
In order to use:
your existing jobs, notifications 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.
- Notification
php artisan make:debounce-notification TestNotification
- Job
php artisan make:debounce-job TestJob
- Command
php artisan make:debounce-command TestCommand
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(...);
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
}
}
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
<?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
}
}
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
}
}
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
}
}
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 your choice. If null
is returned the debouncer will fallback to the default implementation above.
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
public function getLastActivityTimestamp(): ?Carbon
{
return Message::latest()->first()?->seen_at;
}
}
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;
}
}
Due to limitations, the method must be static
.
<?php
...
class Test extends DebounceCommand
{
...
public static function getLastActivityTimestamp(): ?Carbon
{
return User::latest()->first()?->created_at;
}
}
For fun, you can actually debounce commands from the CLI using the 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*}
I recommend using Laravel telescope to see the debouncer live in the queues tab and to debug any failures.
- Unique lock gets stuck sometimes when jobs fail github issue, I made a fix to the laravel core framework about this give it a reaction: PR (merged)
- cause: this happens when deleted models are unserialized causing the job to fail without clearing the lock.
- solution: don't use
SerializesModels
trait on Notifications/Jobs. (old temporary solution, now the bug is fixed)
Contributions, issues and suggestions are always welcome! See contributing.md
for ways to get started.