Browse documentationOpen

byrcsc/laravel-approval · 1.x

Notifications.

Enable optional notification delivery, silence or replace any message, and write a notification that declares its own recipients.

Notification delivery is opt-in and disabled by default, so installing the package cannot send external messages and an application's own notification rules stay in charge.

APPROVAL_NOTIFICATIONS_ENABLED=true

While it is false, no listeners are registered at all — an application that leaves notifications off pays for none of this.

This page covers which notifications are sent and to whom. To change what they say — rewording, translating, or restructuring the emails — see notification content. Publishing the translations or the mail views needs none of the class replacement described below.

The map

Enabling delivery activates approval.notifications.map:

EventNotificationRecipients
ApprovalSubmittedApprovalRequestedNotificationThe active stage's holders
StageCompletedApprovalRequestedNotificationThe newly active stage's holders
ApprovalApprovedApprovalApprovedNotificationThe requester
ApprovalRejectedApprovalRejectedNotificationThe requester
ApprovalReturnedApprovalReturnedNotificationThe requester
DraftConflictedApprovalConflictNotificationThe requester
ApprovalDelegatedApprovalAssignedNotificationThe delegate
ApproverReassignedApprovalAssignedNotificationThe new holder
ApproversResyncedApprovalAssignedNotificationThe holders a resync added
ApproversEscalatedApprovalAssignedNotificationThe holders escalation added
StageOverdueApprovalOverdueNotificationThe overdue stage's holders
StageOverdueReminderApprovalOverdueNotificationThe overdue stage's holders

Not every event is mapped. The mapped ones are the state changes that leave somebody waiting on an action; the rest are available to listen to.

Channels

'notifications' => [
    'channels' => ['mail', 'database'],
],

The default is ['mail']. The setting applies to every shipped notification; a replacement class may override via() to choose its own.

Silencing one event

Set its entry to null:

'map' => [
    Events\StageOverdueReminder::class => null,
    // ...
],

Replacing a message

Point the entry at your own class. It must extend ApprovalNotification:

use ByRcsc\LaravelApproval\Events\ApprovalEvent;
use ByRcsc\LaravelApproval\Notifications\ApprovalNotification;
use Illuminate\Notifications\Messages\MailMessage;

final class PurchaseOrderNeedsApproval extends ApprovalNotification
{
    public static function recipients(ApprovalEvent $event): iterable
    {
        return self::activeApprovers($event);
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Purchase order awaiting your approval')
            ->line($this->subject().' needs your sign-off.')
            ->action('Review', route('purchase-orders.approvals.show', $this->request()));
    }
}
'map' => [
    Events\ApprovalSubmitted::class => PurchaseOrderNeedsApproval::class,
],

Notifications declare their own recipients

This is unusual for Laravel and deliberate. Recipients are the part applications most often change, so keeping them on the class means one entry in the map replaces both the message and its audience — no listener to replace.

The base class provides the helpers the shipped notifications use:

self::activeApprovers($event);   // holders of the request's active stage
self::approversOf($stage);       // holders of a specific stage
self::requesterOf($event);       // the requester, as a list

An empty result is valid and sends nothing, which is what happens when a stage's approver records have all been deleted.

Helpers on the instance

$this->event;          // the ApprovalEvent
$this->request();      // the ApprovalRequest
$this->approvable();   // the record, or null if deleted since
$this->subject();      // a readable name for the record

subject() tries name, title, reference, and label on the record, falling back to ClassName #key, so a subject line always identifies something. A deleted record reads as "a deleted record".

Delivery is deferred to after commit

Every approval event fires inside a transaction, and a request that is rolled back a line later should not have already told six people it needs their approval. The shipped listener wraps the send in DB::afterCommit(). Outside a transaction the callback runs immediately, so nothing is delayed that does not need to be.

Configuration is validated at boot

A typo in the map is silent until the day somebody was supposed to be told something, so it is checked when the package boots:

ProblemException
The event class does not existInvalidConfigurationException::unknownNotificationEvent()
The notification class does not existInvalidConfigurationException::unknownNotification()
It does not extend ApprovalNotificationInvalidConfigurationException::notificationIsNotANotification()

A null entry is not an error — it is how an event is switched off.

Overdue notifications need the sweep

StageOverdue and StageOverdueReminder only fire when approval:escalate runs. The record_only escalation default sends nothing on its own; enabling notifications is what turns the recorded fact into a message. See SLAs and escalation.