Overview
Within any project, finding yourself implementing the same pattern of logic over and over again with little variation is typically a strong signal for refactoring the logic into a reusable mechanism.
In my case, this signal to refactor came up when implementing logic to send a success or failure Filament notification to alert my users of the result of their action.
This article is a deep dive into a simple trait for a Filament project, which made it quicker and easier to maintain a consistent notification feedback pattern throughout the application.
Use case
This trait is useful for performing operations that may fail and providing the user feedback via Filament Notification regarding if the operation was successful.
Examples of when you may find this trait useful include operations that interact with a database, cache, filesystem, or third-party system and are processed synchronously; one triggered by a user's interaction with the web application.
In a Filament application this would typically be in a Filament action or a Livewire event listener.
Assumptions
You have a working Laravel application with Filament 4 installed and configured.
Example invocations
Sending a result notification after an operation completes with:
- reporting any exception or error
- without a database transaction
- default notification titles
- no notification bodies
- default notification icons
1static::resultNotificationOperation(2 callback: function () {3 // your operation here4 },5);
Sending a result notification after an operation completes with:
- no reporting of any exception or error
- within a database transaction
- custom notification titles
- custom notification bodies
- custom notification icons
1static::resultNotificationOperation( 2 callback: function () { 3 // your operation here 4 }, 5 report: false, 6 transact: true, 7 successTitle: 'Save successful', 8 successBody: 'Your changes have been saved.', 9 successIcon: \Filament\Support\Icons\Heroicon::Bell,10 failureTitle: 'Save failed',11 failureBody: 'An error occurred while saving your changes.',12 failureIcon: \Filament\Support\Icons\Heroicon::ExclamationCircle,13);
Sending a result notification after an operation completes with:
- success notification title that depends on a value returned by the callback function
1static::resultNotificationOperation(2 callback: fn () => User::where('email', 'like', '%@gmail.com')->update(['is_gmail' => true]),3 successTitle: fn (int $count) => "Updated $count " . Str::plural('user', $count) . '.',4);
successTitle, successBody, and successIcon have the return value of the callback function passed as the first argument.
HasResultNotificationOperations trait
Below is the trait HasResultNotificationOperations that adds a protected static function named resultNotificationOperation to any class that uses it. Later
in this article, we'll examine how the function works in depth.
I created this trait in app/Concerns/Filament/Pages/, but use any directory in your application that makes sense for
your application.
1<?php 2 3namespace App\Concerns\Filament\Pages; 4 5use BackedEnum; 6use Closure; 7use Filament\Notifications\Notification; 8use Filament\Support\Icons\Heroicon; 9use Illuminate\Support\Facades\DB;10use Throwable;11 12trait HasResultNotificationOperations13{14 protected static function resultNotificationOperation(15 Closure $callback,16 bool $report = true,17 bool $transact = false,18 string|Closure|null $successTitle = 'Operation Successful',19 string|Closure|null $successBody = null,20 BackedEnum|Closure $successIcon = Heroicon::CheckCircle,21 string|Closure|null $failureTitle = 'Whoops! Something went wrong.',22 string|Closure|null $failureBody = null,23 BackedEnum|Closure $failureIcon = Heroicon::XCircle,24 ): void {25 try {26 $data = $transact ? DB::transaction(fn () => $callback()) : $callback();27 28 static::sendNotification(29 isSuccess: true,30 title: static::callOrDefault($successTitle, $data),31 body: static::callOrDefault($successBody, $data),32 icon: static::callOrDefault($successIcon, $data),33 );34 } catch (Throwable $th) {35 if ($report) {36 report($th);37 }38 39 static::sendNotification(40 isSuccess: false,41 title: static::callOrDefault($failureTitle, $th),42 body: static::callOrDefault($failureBody, $th),43 icon: static::callOrDefault($failureIcon, $th),44 );45 }46 }47 48 private static function callOrDefault(string|BackedEnum|Closure|null $callbackOrDefault, mixed $dataOrThrowable = null): mixed49 {50 return match (true) {51 $dataOrThrowable !== null && $callbackOrDefault instanceof Closure => $callbackOrDefault($dataOrThrowable),52 $callbackOrDefault instanceof Closure => $callbackOrDefault(),53 default => $callbackOrDefault,54 };55 }56 57 private static function sendNotification(bool $isSuccess, ?string $title, ?string $body, ?BackedEnum $icon): void58 {59 if (! $title) {60 return;61 }62 63 Notification::make()64 ->when(65 value: $isSuccess,66 callback: fn (Notification $notification) => $notification->success(),67 default: fn (Notification $notification) => $notification->danger(),68 )69 ->title($title)70 ->body($body)71 ->icon($icon)72 ->send();73 }74}
How the trait works
To examine how the protected static function in this trait works, let's work backwards from bottom-to-top of the file.
sendNotification private function
1private static function sendNotification(bool $isSuccess, ?string $title, ?string $body, ?BackedEnum $icon): void 2{ 3 if (! $title) { 4 return; 5 } 6 7 Notification::make() 8 ->when( 9 value: $isSuccess,10 callback: fn (Notification $notification) => $notification->success(),11 default: fn (Notification $notification) => $notification->danger(),12 )13 ->title($title)14 ->body($body)15 ->icon($icon)16 ->send();17}
The sendNotification function first checks if the $title argument is falsy. If it is, the function does nothing else
and returns early.
Otherwise, the function sends a Filament notification. The make static method on Filament\Notifications\Notification
creates a new instance of the Notification class.
Notification instance above returns $this which allows for chaining methods.
The when function is made available to the Notification class via the trait Illuminate\Support\Traits\Conditionable.
This function will:
- Check if the provided
valueis truthy - If it is, the Closure provided to the
callbackparameter is executed with the instance of the class as the first argument - If it is not, the Closure provided to the
defaultparameter is executed with the instance of the class as the first argument
In the sendNotification function, we are evaluating if the $isSuccess variable is true. If it is, we call success()
on the Notification instance. Otherwise, we call danger() on the Notification instance.
The title, body, and icon functions all set the corresponding property on the Notification instance to the provided
value.
Finally, the send function will push the notification into the current session as an array of data.
callOrDefault private function
1private static function callOrDefault(string|BackedEnum|Closure|null $callbackOrDefault, mixed $dataOrThrowable = null): mixed2{3 return match (true) {4 $dataOrThrowable !== null && $callbackOrDefault instanceof Closure => $callbackOrDefault($data),5 $callbackOrDefault instanceof Closure => $callbackOrDefault(),6 default => $callbackOrDefault,7 };8}
The callOrDefault private function is a function which will:
- if
$dataOrThrowableis not null and$callbackOrDefaultis a Closure- return the value returned by calling the
$callbackOrDefaultClosure, with$dataOrThrowableas the first argument
- return the value returned by calling the
- if
$dataOrThrowableis null and$callbackOrDefaultis a Closure- return the value returned by calling the
$callbackOrDefaultClosure, with no arguments
- return the value returned by calling the
- otherwise
- return the value
$callbackOrDefaultas provided
- return the value
null should never be returned by the callback function if the value needs to be passed to a Closure for a success notification detail. Use a scalar value instead (or throw an exception in case of failure).
resultNotificationOperation protected function
1protected static function resultNotificationOperation( 2 Closure $callback, 3 bool $report = true, 4 bool $transact = false, 5 string|Closure|null $successTitle = 'Operation Successful', 6 string|Closure|null $successBody = null, 7 BackedEnum|Closure $successIcon = Heroicon::CheckCircle, 8 string|Closure|null $failureTitle = 'Whoops! Something went wrong.', 9 string|Closure|null $failureBody = null,10 BackedEnum|Closure $failureIcon = Heroicon::XCircle,11): void {12 try {13 $data = $transact ? DB::transaction(fn () => $callback()) : $callback();14 15 static::sendNotification(16 isSuccess: true,17 title: static::callOrDefault($successTitle, $data),18 body: static::callOrDefault($successBody, $data),19 icon: static::callOrDefault($successIcon, $data),20 );21 } catch (Throwable $th) {22 if ($report) {23 report($th);24 }25 26 static::sendNotification(27 isSuccess: false,28 title: static::callOrDefault($failureTitle, $th),29 body: static::callOrDefault($failureBody, $th),30 icon: static::callOrDefault($failureIcon, $th),31 );32 }33}
Now for the protected static function resultNotificationOperation, which uses the two previously described functions.
This function is wrapped in try and catch blocks. The Throwable hint is used in the catch block because it will
catch both Exceptions and Errors.
Within the try block, the function will first set the value of the $data variable. If the $transact argument provided
is true, the provided $callback argument will be invoked within a database transaction.
Otherwise, the $callback will be invoked without being wrapped in a database transaction.
Next, the function will send the success notification (because the operation contained by $callback must not have
thrown any Exceptions or Errors for the function to reach this point).
trueis provided to theisSuccessparameter- the provided
$successTitlein combination with the$datavariable are used to determine the value provided to thetitleparameter - same for
$successBody - same for
$successIcon
In the event an Exception or Error is thrown when executing the $callback function, the catch block will:
- if
$reportistrue- use the report helper method to report the Exception or Error
- send the failure notification
falseis provided to theisSuccessparameter- the provided
$failureTitlein combination with the caught$th(Throwable) are used to determine the value provided to thetitleparameter - same for
$failureBody - same for
$failureIcon
$callback function or the caught Exception or Error, depending on if the operation was successful.
Conclusion
With this trait, I've been able to maintain consistency of providing feedback to the user throughout my Filament application. Both my experiences as a developer and the experiences of my users have benefited.