Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filagora Notifications Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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());
    }
    
  2. 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.

Implementation Patterns

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.

Gotchas and Tips

  • 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.
  • The per-person read cache is request-scoped, forgotten per queue job. In a long-running worker or Octane-style process, don't assume a preference changed elsewhere will be visible mid-request. In a queued job you get a fresh read per job — which is usually what you want, but it means a fan-out job that reads once and loops is safer than one that reads per iteration.
  • 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.
  • Routes are outside the 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.
  • Bridges silently don't bind if you've already bound the sibling contract yourself, or if a sibling provider registers later than expected. If push/forum/message preferences "stop working", check your own container bindings before blaming config.
  • No audit trail, by design. Changes leave no log entry; the upstream site's audit behaviour is a future package. If compliance needs it, listen to PreferenceChanged and write your own.
  • No retention rule in the privacy contributor — stored preferences live indefinitely alongside the user. Pair it with your own cleanup if you have data-minimisation obligations.
  • Digest frequency drift is the most likely production bug: the preference row is the source of truth, and any user column you keep must be fed from PreferenceChanged, including changes made via the unsubscribe flow.
  • Extension points worth knowing: 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core