Skip to content

Creating a notification

Take a look at the following notification:

php
<?php declare(strict_types=1);

namespace App\Notifications\Company\CallToAction;

use App\Modules\Member\Models\Member;
use App\Modules\Member\Services\UnsafeDirectLoginLinkGenerator;
use App\Modules\Notification\Attributes\TriggerMode;
use App\Modules\Notification\Attributes\TriggerCondition;
use App\Modules\Notification\Attributes\TriggerModeEnum;
use App\Modules\Team\Models\Team;
use App\Notifications\Senders\RikkeMemberSupport;
use App\Notifications\Support\HasFrequencyLimit;
use App\Notifications\Support\Mail\MailMessage;
use App\Notifications\Support\Mail\MailNotification;
use App\Notifications\Support\ShouldLimitFrequency;

/**
 * Sent when company has passed 10 members threshold in the company membership.
 * It asks company administrators to add more administrators to manage the company membership
 * even better.
 */
#[TriggerMode(TriggerModeEnum::React)]
#[TriggerCondition('WHEN', 'One or more members are being added to a company membership')]
#[TriggerCondition('IF', 'The number of membership seats filled becomes at least 11.')]
#[TriggerCondition('AND IF', 'There is only one company administrator in the company membership')]
#[TriggerCondition('SEND THIS EMAIL TO', 'The company administrator.')]
final class CompanyHasMoreThanXMembers extends MailNotification implements ShouldLimitFrequency
{
    use HasFrequencyLimit;

    private readonly Team $team;

    public function __construct(Team $team)
    {
        $this->team = $team;

        $this->setFrequencyLimit(ShouldLimitFrequency::ONCE, $team);
    }

    public function build(Member $administrator): MailMessage
    {
        $prefix = $administrator->isNameSetAndMeaningful()
            ? "{$administrator->getFirstName()}: "
            : '';

        $salutation = $administrator->isNameSetAndMeaningful()
            ? "Dear {$administrator->getFirstName()},"
            : 'Hi,';

        return (new MailMessage())
            ->fromSender(new RikkeMemberSupport())
            ->subject("{$prefix}Manage your Company Membership even better with this tip!")
            ->htmlAndText(
                'notifications.email.company.callToAction.companyHasMoreThanXMembers--html',
                'notifications.email.company.callToAction.companyHasMoreThanXMembers--text',
                [
                    'salutation' => $salutation,
                    'companyName' => $this->team->name,
                    'membershipCount' => $this->team->getActiveMembersCount(),
                    'directLoginLinkToCompanyProfile' => UnsafeDirectLoginLinkGenerator::withRedirectToCompanyProfile($administrator),
                ]
            );
    }
}

First of all, take a look at the class attributes

php
/**
 * Sent when company has passed 10 members threshold in the company membership.
 * It asks company administrators to add more administrators to manage the company membership
 * even better.
 */
#[TriggerMode(TriggerModeEnum::React)]
#[TriggerCondition('WHEN', 'One or more members are being added to a company membership')]
#[TriggerCondition('IF', 'The number of membership seats filled becomes at least 11.')]
#[TriggerCondition('AND IF', 'There is only one company administrator in the company membership.')]
#[TriggerCondition('SEND THIS EMAIL TO', 'The company administrator.')]

We use PHP 8 Attributes to define notification metadata. These attributes are extracted using PHP's reflection and shown on the notification system control panel. The available attributes are:

  • #[TriggerMode]: Accepts a TriggerModeEnum value:
    • TriggerModeEnum::React for notifications sent based on a notifiable's action
    • TriggerModeEnum::Cron for notifications sent via cron jobs
  • #[CronCommand]: Required when TriggerMode is Cron. Specifies the command that triggers the notification.
  • #[TriggerCondition]: Defines conditions that must be met for the notification to be sent. Multiple conditions can be specified using multiple attributes.
  • #[Disabled]: Optionally marks a notification as disabled with a reason.

Class declaration

php
class CompanyHasMoreThanXMembers extends MailNotification implements ShouldLimitFrequency
  • A mail-notification class extends the base MailNotification class. This base class contains a couple of handful methods and also enables the notifications to be sent using Laravel's notification system.
  • There are three other types of notifications: SMS notifications (extending SmsNotification), postcard notifications (extending Postcard), and in-app notifications (extending InAppNotification).
  • A notification class may implement the interfaces ShouldLimitFrequency, ShouldCheckConditionsBeforeSendingOut, and ShouldCheckNotificationOptOut, and ShouldDelay to be extended with additional checks before being sent-out.

Building messages

php
public function build(): \App\Notifications\Support\Mail\MailMessage

Building mail messages is a straightforward task. Just take a look at the API and you'll get a good sense of what you need to do to make the notification work.

Note that the build method receives an instance of Recipient, which is the recipient of the notification.

Sending a notification to a notifiable

A notifiable is a model class that the system can send a notification to. A notifiable model class should implement Recipient contract. We use a unified trait Notifiable to send out notifications.

php
$member->notify($notification);

We use Redis + horizon to send notifications.

Events

  1. \App\Modules\Notification\Events\NotificationQueued Called right after we try to queue the notification. This event is used to create the notification log.
  2. \Illuminate\Notifications\Events\NotificationSending Is dispatched by Laravel before trying to send the notification from queue. This event is mainly used to determine whether to send or to cancel sending the notification. We should not use this event directly.
  3. \Illuminate\Notifications\Events\NotificationSent Dispatched by Laravel after sending the actual notification. We use this event to update the notification log (mark the notification as sent) & cache its content to be displayed on the notification control panel.
  4. \Illuminate\Notifications\Events\NotificationFailed Dispatched by laravel when the notification was failed. The failure can occur due to multiple reasons, including exceptions. We use this event to update the log and mark the notification as failed.
  5. \App\Modules\Notification\Events\NotificationCanceled We use this event to mark the notification as failed (it's a temp solution). This event is dispatched when the pre-conditions to send a specific notification is not satisfied.

Creating an In-App Notification

In-app notifications are displayed within the platform UI in the user's notification center. Here's a complete example:

php
<?php declare(strict_types=1);

namespace App\Notifications\Course;

use App\Modules\Course\Models\Course;
use App\Modules\Member\Models\Member;
use App\Modules\Notification\Attributes\TriggerMode;
use App\Modules\Notification\Attributes\TriggerCondition;
use App\Modules\Notification\Attributes\TriggerModeEnum;
use App\Notifications\Support\InApp\InAppMessage;
use App\Notifications\Support\InApp\InAppNotification;
use App\Notifications\Support\InApp\Priority;

/**
 * Sent when a member completes a course to celebrate their achievement.
 */
#[TriggerMode(TriggerModeEnum::React)]
#[TriggerCondition('WHEN', 'A member completes all lessons in a course')]
#[TriggerCondition('SEND THIS NOTIFICATION TO', 'The member who completed the course')]
final class CourseCompletedCelebration extends InAppNotification
{
    public function __construct(private readonly Course $course)
    {
    }

    public function build(Member $member): InAppMessage
    {
        return (new InAppMessage(
            title: 'Congratulations! 🎉',
            body: "You've completed {$this->course->title}! Well done on finishing the course.",
            tags: ['course', 'achievement']
        ))
            ->cta('View Certificate', route('course.certificate', $this->course))
            ->priority(Priority::High)
            ->expiresAt(now()->addDays(30));
    }
}

InAppMessage API

The InAppMessage class provides a fluent API for building in-app notifications:

Basic Structure

php
// Minimum required: title OR body
new InAppMessage(title: 'Title', body: 'Body text')
new InAppMessage(title: 'Title only')
new InAppMessage(body: 'Body only')

Adding Call-to-Action

php
$message = (new InAppMessage(title: 'New Feature Available'))
    ->cta('Try It Now', 'https://example.com/new-feature');

Both CTA text and URL are required together. URLs are validated for security (only http:// and https:// schemes allowed).

Priority Levels

php
use App\Notifications\Support\InApp\Priority;

$message->priority(Priority::Low);      // Low priority
$message->priority(Priority::Normal);   // Default
$message->priority(Priority::High);     // Important
$message->priority(Priority::Urgent);   // Critical/urgent

Tags for Categorization

php
// Set multiple tags
$message->tags(['course', 'achievement', 'certificate']);

// Add single tag
$message->addTag('milestone');

Expiration

php
// Expire after 7 days
$message->expiresAt(now()->addDays(7));

// Expire after 1 month
$message->expiresAt(now()->addMonth());

Method Chaining

InAppMessage is immutable - each method returns a new instance:

php
$message = (new InAppMessage(title: 'Welcome!'))
    ->body('Start your learning journey today.')
    ->cta('Get Started', route('courses.index'))
    ->tags(['welcome', 'onboarding'])
    ->priority(Priority::High)
    ->expiresAt(now()->addWeek());

Validation Rules

  • Title: Maximum 255 characters
  • Body: Maximum 1000 characters
  • CTA Text: Maximum 100 characters
  • CTA URL: Maximum 500 characters, must be valid http:// or https:// URL
  • Priority: Must be one of: Low, Normal, High, Urgent

Sending In-App Notifications

php
// Send to a single member
$member->notify(new CourseCompletedCelebration($course));

// Send synchronously (without queueing)
$member->notifyNow(new CourseCompletedCelebration($course));

Querying In-App Notifications

The InAppNotification model provides convenient scopes:

php
use App\Modules\Notification\Models\InAppNotification;

// Get unread notifications for a member
$unread = InAppNotification::query()
    ->forMember($member)
    ->unread()
    ->notExpired()
    ->get();

// Get notifications by tag
$courseNotifications = InAppNotification::query()
    ->forMember($member)
    ->withTag('course')
    ->get();

// Mark as read
$notification->markAsRead();

// Mark as clicked (also marks as read)
$notification->markAsClicked();