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

Preferences Module Laravel Package

anomaly/preferences-module

Anomaly Preferences Module adds a flexible user preferences system to Laravel/Streams Platform applications, letting you define, store, and retrieve per-user settings via a simple API, with control panel integration for easy management.

View on GitHub
Deep Wiki
Context7

Getting Started

anomaly/preferences-module is not a standalone Laravel package — it is a Streams Platform module. Nothing in it will boot without the Streams core, so treat the install as a three-step chain:

composer require anomaly/streams-platform
composer require anomaly/preferences-module
php artisan module:install anomaly/preferences-module   # or add to config/streams.php modules
php artisan migrate

Where to look first, in this order:

  1. src/PreferencesModuleServiceProvider.php — bindings, route registration, and the preferences repository singleton.
  2. src/Preference/PreferenceRepository.php — the public API (get, set, save, section, all). This is what you will use 90% of the time.
  3. src/Preference/PreferenceModel.php — the Eloquent model and the actual table schema (user_id, key, value, section).
  4. src/Preference/PreferenceFormBuilder.php / PreferenceTableBuilder.php — only relevant if you are customizing the control panel UI.
  5. resources/config/ and resources/lang/ — defaults and section labels.

First real use case — store a value a developer would otherwise hardcode, and read it back:

use Anomaly\PreferencesModule\Preference\Contract\PreferenceRepositoryInterface;

$preferences = app(PreferenceRepositoryInterface::class);

$preferences->set('integrations.mailchimp.key', 'abc123', 'integrations');
$preferences->save();

$key = $preferences->get('integrations.mailchimp.key'); // 'abc123'

Once that round-trips, the module is working. Everything else is scoping and UI.

Implementation Patterns

Define your preferences at boot, not in controllers. Register defaults from your own module's service provider so a fresh install is usable without visiting the control panel:

public function boot(PreferenceRepositoryInterface $preferences)
{
    $defaults = [
        'integrations.mailchimp.key' => null,
        'mail.digest'                => 'daily',
        'mail.enabled'               => true,
    ];

    foreach ($defaults as $key => $value) {
        if ($preferences->get($key, null) === null) {
            $preferences->set($key, $value, 'mail');
        }
    }

    $preferences->save();
}

Namespace keys by section. The section argument is your only real grouping mechanism in the repo, and it doubles as the tab in the admin form. Always pass it — leaving it blank scatters preferences into the default section and makes the UI unusable.

Scope to a user when the value is personal. The model carries a user_id; the repository resolves the current user by default. Use this for timezone, locale, notification opt-ins, and anything that shouldn't be shared across a tenant. For global app settings, prefer this module's global scope only if you don't already have a settings table — see gotchas.

Read through a small wrapper, not the repository directly. Injecting PreferenceRepositoryInterface into every controller is noisy and couples your business logic to the module. Wrap it:

class AppPreferences
{
    public function __construct(private PreferenceRepositoryInterface $prefs) {}

    public function mailDigestEnabled(): bool
    {
        return (bool) ($this->prefs->get('mail.digest.enabled') ?? false);
    }
}

Now swapping the storage backend later (or forking the package) is a one-file change.

Extend the admin form for custom fields. If your preference needs a non-string UI (toggle, select, encrypted key), add the field to PreferenceFormBuilder via a service provider:

$builder->on('ready', function ($builder) {
    $builder->add('integrations_mailchimp_key', 'anomaly.field_type.encrypted');
});

Use the repository as a cache-friendly read path. The module reads from an in-memory collection after the first query, so calling get() in loops is cheap. Call save() once at the end of a batch, not per key.

Gotchas and Tips

Streams is a hard dependency and a version minefield. The package has zero releases of note and zero dependents, which means it is not exercised by a wide matrix of Laravel versions. Pin both anomaly/streams-platform and this package, and test on the exact Laravel version your app targets before committing. If you're on Laravel 11+/12, fork it — the ecosystem is effectively dormant.

Preferences are not Streams settings. PyroCMS/Streams ships setting() for global, migration-backed config and preferences for user-editable, database-backed values. Mixing them (writing app config via preferences, or secrets via settings) causes confusing cache behavior. Rule of thumb: secrets and infra → .env; global product config → settings; per-user → preferences.

Cache invalidation is the #1 bug source. After save(), other processes won't necessarily see the new value until they re-read. In long-running workers and queued jobs, explicitly reload the collection before reading, or clear the cache in your deploy step:

php artisan cache:clear
php artisan streams:compile

Key collisions across modules. There is no automatic namespacing. If two of your own modules use api.key, the second write silently overwrites the first. Adopt a vendor.module.feature convention from day one.

Missing keys return null, not your default. get('foo') and get('foo', null) are the same thing when the row doesn't exist. If you need a distinguishable "unset" vs "explicitly null", store a sentinel or check all()/section() membership. This bites when a user clears a field and your ?? 'fallback' kicks back in.

Migrations assume Streams ordering. The preferences table references the users table and the Streams core tables. If you run migrate:fresh in a test suite, make sure Streams' own migrations run first, or the FK constraints will fail. Add the module to your RefreshDatabase seed path in phpunit.xml, not separately.

Admin UI needs the Streams admin theme and permissions. If preferences render blank in the CP, it's usually a missing access_preferences-style permission on the role, not a code bug. Grant the permission in the Users module and the form will populate.

Debugging tips. Watch storage/logs/laravel.log for Streams' own error handler output — it wraps exceptions in StreamsException, which hides the original. Run php artisan streams:compile to rebuild the compiled view/addon manifests after any change to a service provider; a stale manifest is the most common cause of "my binding isn't taking effect."

Extension points worth knowing. The contract PreferenceRepositoryInterface, the PreferenceModel, and the form/table builders are all bindable and overridable from your module's service provider. There's no formal event bus in the package, so if you need "on preference saved" hooks, wrap save() rather than listening for an event that doesn't fire.

Given the maintenance profile — 0 stars, 0 dependents, MIT — treat this as a vendored dependency, not a supported one. Fork it into your monorepo, keep your wrapper (AppPreferences) thin, and be prepared to replace it with a 50-line custom settings table if the Streams core stops receiving updates. Do not build critical multi-tenant features directly against PreferenceRepository without an abstraction layer.

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.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json