---
title: "Preferences — Notifications for Laravel | Roundly"
description: "Per-user channel, category and type preferences, mandatory types, the settings matrix, all-or-nothing update(), storage through options and locking."
url: https://roundly-consulting.com/open-source/docs/notifications-for-laravel/preferences
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Notifications for Laravel](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)

# Preferences

Each user decides per type, per category and per channel. By default muting stops the email only — the inbox keeps the item — and mandatory types cannot be muted at all:

```php
use RoundlyConsulting\Notifications\Enums\Channel;

$prefs = Notifications::preferences($user);        // or $user->notificationPreferences()

$prefs->mute('comment.posted');                    // mail only by default — the inbox keeps the item
$prefs->mute('comment.posted', Channel::Inbox);    // one channel (honoured only with inbox muting on)
$prefs->unmute('comment.posted');                  // back to the category and type default
$prefs->set('request.awaiting_approval', Channel::Mail, false);
$prefs->muteCategory('comments');
$prefs->unmuteCategory('comments');
$prefs->reset();                                    // every type and category override forgotten
$prefs->mute('security.alert');                     // MandatoryNotificationException
$prefs->muteCategory('comment');                    // UnknownNotificationCategoryException — no type uses it
```

## The effective channel

For type T, channel C and user U: C not in T.channels() → off; T.mandatory() → on; C is the inbox and notifications.preferences.inbox\_mutable is off → on; else the type override → the category override → T.enabledByDefault(C). Only overrides are stored, so a changed type default reaches every user who never touched it.

- A category mute does not silence a mandatory type in it; a stored mute of a type that later became mandatory is ignored.
- A category no registered type uses is refused on every write with UnknownNotificationCategoryException — a typo never mutes nothing silently.
- Every write drops stored overrides of unregistered types and unused categories.

## Reading for a settings screen

```php
$prefs->matrix();                                   // list<PreferenceRow{type: TypeDefinition, channels: list<ChannelPreference>}>
$prefs->enabled('comment.posted', Channel::Mail);   // bool
$prefs->muted('comment.posted');                    // mail off?
$prefs->overview();                                 // PreferencesOverview: rows, digest, quietHours, timezone
$prefs->digestPreference();                         // DigestPreferenceData
$prefs->quietHoursPreference();                     // ?QuietHours
$prefs->resolvedTimezone();                         // 'Europe/Bratislava'
```

ChannelPreference carries channel, enabled, default and locked — locked for mandatory types, and for the inbox unless inbox muting is on.

## Several changes at once

update() applies a whole settings screen all or nothing — what PATCH /preferences uses. Only what the update names changes:

```php
use RoundlyConsulting\Notifications\DataTransferObjects\DigestPreferenceData;
use RoundlyConsulting\Notifications\DataTransferObjects\PreferencesUpdate;
use RoundlyConsulting\Notifications\DataTransferObjects\QuietHours;
use RoundlyConsulting\Notifications\Enums\DigestFrequency;

Notifications::preferences($user)->update(new PreferencesUpdate(
    channels: ['comment.posted' => ['mail' => false]],      // type → channel → on/off
    categories: ['contracts' => ['mail' => false]],         // false mutes, true clears the mute
    digest: new DigestPreferenceData(DigestFrequency::Daily, '08:00'),
    setsQuietHours: true, quietHours: new QuietHours('22:00', '07:00'),   // setsQuietHours + null clears
    setsTimezone: true, timezone: 'Europe/Bratislava',                    // setsTimezone + null clears
));
```

Everything is validated first — an unregistered type, a channel other than inbox or mail, a mandatory type switched off, an unused category, a bad time or timezone throws before anything is written. Then every write runs under one preference lock in one transaction, so a failure half way rolls all of them back.

## Storage

Four options-for-laravel options, registered by the package, each with its own stored key. The owner is the user, so options.key\_type must equal notifications.key\_type. Reads and writes bypass options’ authorization — the package API is the boundary, and the HTTP layer only ever acts on the authenticated user:

| Option | Key | Value |
| --- | --- | --- |
| `Options\ChannelPreferences` | `notifications.channels` | {"types": {"<key>": {"mail": false}}, "categories": {"<cat>": {"mail": false}}} |
| `Options\DigestPreference` | `notifications.digest` | {"frequency": "daily", "time": "07:30"} |
| `Options\QuietHoursPreference` | `notifications.quiet_hours` | {"start": "22:00", "end": "07:00"} |
| `Options\TimezonePreference` | `notifications.timezone` | an IANA id |

## Concurrency and events

A write is a read-modify-write of the JSON map. It takes a cache lock (notifications.preferences.lock\_store, held lock\_seconds) around the whole transaction and releases it after the commit — inside a host transaction, after the host’s commit — so two concurrent mutes of different types both survive. The lock store must be shared across servers; the doctor warns about array and file. Every write fires NotificationPreferenceChanged after the commit, and digest, quiet-hours and timezone changes re-time the user’s held and parked mail in the same transaction.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)
