filagora/filagora-notifications
Adds a ready-to-use notification layer to Filagora, with configurable channels, database and mail delivery, and clean integration into Laravel's notification system so marketplace users stay informed about activity.
Install the package, run its migration, and let filagora:adopt snapshot the user_notification_preferences table if you are retrofitting an existing site:
composer require filagora/notifications
php artisan migrate
php artisan filagora:adopt # schema snapshot, only for existing installs
Then, before writing any feature code, do two things:
Register your categories. The category registry is the spine of the package — nothing works until a category id exists. The seven standard ids are registered for you; register_standard in the config switches that off if you are replacing them wholesale. Register your own in a service provider's boot():
use Filagora\Notifications\NotificationCategories;
use Filagora\Notifications\Category;
public function boot(NotificationCategories $categories): void
{
$categories->register(Category::make('orders.shipped')
->label('Order shipped')
->channels(['mail', 'push'])
->unsubscribable());
}
Add the trait to your user model (HasNotificationPreferences) and look at the config file first — gates, bridges.*, routes.enabled, unsubscribe.signed_ttl, and the configurable user attribute names for suppression (receives_email, email_status) and private-message contactability (allow_private_messages) are all there.
Your first real use case is almost always "should I send this?" — either in a mailable/notification:
if (! Preferences::canDeliver($user, 'orders.shipped', NotificationChannel::Mail)) {
return;
}
…or as a matrix on a preferences screen:
$rows = Preferences::matrix($user); // category × channel grid for the UI
Build the preferences page yourself or point unsubscribe.manage_url at one you already have; the package deliberately does not ship a full preferences UI, only the read-only relation manager for the admin user screen.
Category registration is a registry, not an enum — treat order as meaningful. The first registration of an id wins. That means a host's own register() call in a provider that boots after a package is a silent no-op if the package already claimed the id. Register host categories as early as you can, or accept that package-provided definitions (label, channels, unsubscribable flag) are the canonical ones.
Channels drive the UI, not hard-coded lists. Pushes and the bell derive from which channels a category supports. If a category only supports mail, asking about push throws — so iterate $category->channels() rather than a fixed array when building your matrix view.
Reading vs. writing. Reads are cheap and cached per person, and that cache is dropped at every queue job boundary. Writes go through set() (switch channels) and setMode() (enums with more than two states — digest frequency being the obvious one). Every stored-value change fires PreferenceChanged, which is your hook for keeping denormalised columns in sync:
Event::listen(PreferenceChanged::class, function ($event) {
if ($event->category === 'digest' && $event->channel === NotificationChannel::Mail) {
$event->user->update(['digest_frequency' => $event->mode]);
}
});
This matters because the package reads digest frequency only from the preference row; a legacy digest_frequency column on users is the host's problem to keep aligned.
Email suppression is a contract you should expect to replace. The default AttributeSuppression reads receives_email and email_status and — importantly — never suppresses a model that lacks those attributes. If your site has extra rules (e.g. entitlement revocation for inactive subscribers), bind your own implementation:
$this->app->bind(EmailSuppression::class, TenantSuppression::class);
Then always gate mail on canDeliver() rather than allows() alone, otherwise you'll deliver to bounced addresses.
Unsubscribe links: decorate, don't hand-roll. In a mailable:
$mail->withSymfonyMessage(
fn ($message) => Unsubscribe::decorate($message, $user, 'orders.shipped')
);
oneClickUrl() produces the RFC 8058 POST target, url() the human-confirming page. Both are signed. Enable the routes (routes.enabled) only if you want the package to serve them; they are off by default, sit outside the web middleware group, and reuse the paths an extracted site has already mailed so old links keep working. Swap the confirming page via unsubscribe.view, and set unsubscribe.signed_ttl if you want links to expire.
Bridges are all-or-nothing glue. PushBridge, ForumsBridge and MessagesBridge bind after all providers boot, and only while the sibling package's default binding is still in place — your own binding always wins. Disable individually with bridges.push, bridges.forums, bridges.messages. If you're not installing filagora-push, filagora-forums or filagora-messages, you can ignore this section entirely; nothing breaks.
Admin surface. The UserNotificationPreferencesRelationManager is read-only and gated on filagora.notifications.view. Grant that ability sparingly — it exposes what people have opted out of. The privacy contributor id is notifications: exports list stored choices, erasure removes them while preserving entries that a (now erased) staff member changed on someone else's behalf.
InvalidPreference is thrown, not swallowed. An unregistered category, a channel the category doesn't support, or a switch queried as a mode (or vice versa) all throw. The upstream site answered "on" for unknown ids; this package does not. If you have dynamic category ids coming from user input, validate against the registry first.register_standard off is a big hammer. Turning it off unregisters the seven standard ids, and any stored rows referencing them become unreadable (and will throw). Only do this at greenfield time.web group. No session, no CSRF. That's deliberate — the one-click POST must be reachable by mail clients — but it also means your own middleware (locale, tenant resolution, auth) won't run there. If you need tenant awareness on the unsubscribe page, hook the view or replace the route.PreferenceChanged and write your own.PreferenceChanged, including changes made via the unsubscribe flow.EmailSuppression (bind), unsubscribe.view (replace page), unsubscribe.manage_url (link to your own UI), gates config (ability name), and the four attribute-name config keys for suppression and private messaging. Almost everything else — categories, channels, links — is a call, not a config value.How can I help you explore Laravel packages today?