spits-online / laravel-options
Cached database options for Laravel
Requires
- php: ^8.3
- illuminate/cache: ^12.0||^13.0
- illuminate/console: ^12.0||^13.0
- illuminate/contracts: ^12.0||^13.0
- illuminate/database: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
- illuminate/view: ^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- nunomaduro/collision: ^8.6
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.0||^5.0
- pestphp/pest-plugin-arch: ^4.0||^5.0
- pestphp/pest-plugin-laravel: ^4.0||^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Cached database options for Laravel
Keep app-wide settings, like a site name, a maintenance banner or a feature
flag, in one options table and read them anywhere with option(). Every
option is loaded with a single query the first time you read one, then served
from memory and the cache, so calling option() twenty times in a request
costs one query at most.
use SpitsOnline\Options\Facades\Option; option(['site_name' => 'Spits', 'max_uploads' => 5]); option('site_name'); // 'Spits' option('footer_text', 'Made with care'); // 'Made with care' Option::integer('max_uploads'); // 5
A drop-in replacement for appstract/laravel-options: same table, same helpers and directives, and your existing options keep working. See Switching from appstract/laravel-options.
Requirements
- PHP 8.3 or higher
- Laravel 12 or 13
Installation
composer require spits-online/laravel-options php artisan migrate
That's it: the migration runs with your app's own, without publishing, and options are cached in your default cache store from the start. See Caching to change the store.
Usage
Reading an option
option('site_name');
Pass a default for when the option doesn't exist. A closure only runs when it's needed:
option('site_name', 'My app'); option('site_name', fn () => config('app.name'));
An option that exists with the value null returns null, not the default.
Reading an option with a guaranteed type
The typed getters return the value as that type, or throw
InvalidOptionType when the stored value is something else. Like Laravel's
Config::string(), they're the safe choice when the value goes into typed
code.
use SpitsOnline\Options\Facades\Option; Option::string('site_name'); Option::integer('max_uploads', 10); Option::float('vat_rate'); Option::boolean('maintenance_banner', false); Option::array('allowed_countries', []);
Setting options
Pass an array of key => value pairs. Several options are written in one
query.
option(['site_name' => 'Spits']); option([ 'max_uploads' => 5, 'allowed_countries' => ['NL', 'BE'], 'maintenance_banner' => false, ]);
Or through the facade:
Option::set('site_name', 'Spits');
Values are stored as JSON, so you read back what JSON gives you: strings,
numbers, booleans, null and arrays keep their type, and objects come back as
arrays.
Checking and removing options
option_exists('site_name'); // true option()->remove('site_name'); // true Option::remove('max_uploads', 'allowed_countries'); // true
remove() returns whether any of the options existed.
Reading all options
Option::all(); // ['site_name' => 'Spits', 'max_uploads' => 5, ...]
Using options in Blade
@option prints an option, escaped like {{ }}. Use {!! !!} only for HTML
you trust.
<title>@option('site_name', 'My app')</title> {!! option('footer_html') !!} @optionExists('maintenance_banner') <div class="banner">We're doing maintenance tonight.</div> @endif @optionExists('support_email') <a href="mailto:@option('support_email')">Contact us</a> @else <a href="/contact">Contact us</a> @endoptionExists
Managing options from the console
option:set stores the value as a string:
php artisan option:set site_name Spits
Pass --json to store a number, boolean or array:
php artisan option:set max_uploads 5 --json
php artisan option:set allowed_countries '["NL","BE"]' --json
Read, list and remove options:
php artisan option:get max_uploads php artisan option:list php artisan option:remove max_uploads allowed_countries
Querying the table directly
The Option model is there for anything the helpers don't cover. Saving or
deleting through it clears the cache too.
use SpitsOnline\Options\Models\Option; Option::query()->where('key', 'like', 'mail_%')->get();
Caching
Caching is on by default; there's nothing to set up. The first read in a request loads every option at once. After that:
- Within a request or queued job, options come from memory, so any number of reads costs no extra queries. Each new request or job, including under Octane and in long-running queue workers, loads them fresh.
- Across requests, options come from the cache, so a request often runs no
options query at all. Writes through
option(), the facade, the model or the commands clear the cache, and inside a database transaction they clear it again after the commit, so no request keeps reading the old values.
These optional env keys change the defaults:
OPTIONS_CACHE_STORE=redis OPTIONS_CACHE_TTL=3600 OPTIONS_CACHE=false
OPTIONS_CACHE_STOREis a store fromconfig/cache.php. Leave it out to use your default store.OPTIONS_CACHE_TTLis in seconds. Leave it out to keep the options cached until the next write.OPTIONS_CACHE=falseturns the cache off. Options are then still loaded once per request and kept in memory.
There's no config file to publish. To change a default, create
config/options.php with only the keys you change. It's merged over the
package defaults key by key:
<?php return [ 'cache' => [ 'store' => 'redis', ], ];
Two things to know:
- Running more than one server? Use a shared store such as
redis,memcachedordatabase, notfile. Otherwise a write on one server leaves the others with old values. With thedatabasestore (Laravel's default) the cache lookup is itself a query, but still only one per request. - Changed the
optionstable another way, such as in SQL, aDB::table('options')query or a seeder? Clear the cache:
php artisan option:clear-cache
Option::flush();
Error handling
Every exception extends SpitsOnline\Options\Exceptions\OptionsException, so
one catch covers them all.
| Exception | Thrown when |
|---|---|
InvalidOptionType |
A typed getter, such as Option::integer(), finds another type. |
InvalidOptionValue |
A value can't be stored as JSON (such as NAN), or a stored value isn't valid JSON. |
use SpitsOnline\Options\Exceptions\InvalidOptionType; use SpitsOnline\Options\Facades\Option; try { $limit = Option::integer('max_uploads'); } catch (InvalidOptionType $e) { report($e); $limit = 10; }
A corrupt row only breaks reads of that one option, never the others.
Testing your app
Options live in your database, so there is nothing to fake. Use
RefreshDatabase and set the options a test needs. The cache is skipped
inside a database transaction, so tests never see values from another test.
use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); it('shows the maintenance banner', function () { option(['maintenance_banner' => true]); $this->get('/')->assertSee('We\'re doing maintenance tonight.'); });
Switching from appstract/laravel-options
This package uses the same table, the same JSON values and the same migration
name, so your data stays as it is and php artisan migrate won't run the
migration again.
composer remove appstract/laravel-options composer require spits-online/laravel-options
Then search your app for Appstract\Options and replace it. Read
UPGRADE for the details, including the one change in behaviour:
@option now escapes its output.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
This package started from appstract/laravel-options by Gijs Jorissen and Appstract. Its idea, its API and its migration are theirs; thank you for building it and sharing it under the MIT license.
License
The MIT License (MIT). Please see License File for more information.