jwderoos/configurable
A Laravel package that adds configurable options to Eloquent models. Define options and values, then build variants with their own attributes and relations—ideal for configurable products, plans, or any model needing selectable configurations. Ships with traits, migrations, and relationships.
Install the package and publish its migration/config:
composer require jwderoos/configurable
php artisan vendor:publish --tag="configurable-config"
php artisan vendor:publish --tag="configurable-migrations"
php artisan migrate
The first place to look is the Configurable trait (or InteractsWithConfiguration) applied to the model you want to make configurable. In 1.3.0 the recommended path is to declare configuration keys via PHP attributes on the model class:
use Jwderoos\Configurable\Attributes\Configurable;
use Jwderoos\Configurable\Attributes\Config;
#[Configurable]
class Team extends Model
{
use \Jwderoos\Configurable\Concerns\Configurable;
#[Config(key: 'display_name', default: 'My Team', cast: 'string')]
#[Config(key: 'max_users', default: 5, cast: 'int')]
#[Config(key: 'features', default: [], cast: 'array')]
protected array $configurable = [];
}
First use case: read and write config values on the model instance.
$team->config('max_users'); // 5
$team->setConfig('max_users', 25); // persisted
$team->config('features'); // cast to array
Run the published migration before touching config, otherwise reads fall through to defaults and writes throw a "table not found" error.
Attribute-driven schema (new in 1.3.0). Attributes let you define keys, defaults, casts, and validation alongside the model instead of scattering them across a service provider or config array. Keep attribute declarations in one place per model — the package resolves them via reflection at boot, so they behave like a declarative schema.
Fallback chain. Config resolution typically goes: stored DB value → attribute default → package config default. Use defaults in the attribute for sensible bootstrap values and avoid null checks in callers.
Casting. cast: reuses Laravel's cast conventions (int, bool, array, json, datetime, custom casters). This keeps your model clean — no manual json_decode on $model->config('features').
Admin / settings screens. Because keys are declared, you can reflect over them to build a settings form dynamically:
$schema = Configurable::getSchema(Team::class);
// [['key' => 'max_users', 'default' => 5, 'cast' => 'int'], ...]
This is the biggest day-to-day win: new config options only require an attribute, not a new column or config file edit.
Environment overrides. Prefer keeping truly environment-specific values in config/configurable.php and only persisting user-editable settings in the DB. Mixing the two leads to confusion about which wins.
Testing. In tests, use the package's fresh-migration setup and reset cached config between tests (see gotchas). Assert via the public accessor rather than querying the config table directly — the storage layout is an implementation detail.
setConfig() in a loop or job, call the package's refresh/reset method (usually $model->refreshConfig() or $model->unsetConfigCache()) if you read the value again in the same request. Stale reads after a write are the #1 source of "my setting didn't save" bug reports.composer dump-autoload) and that the #[Configurable] class-level attribute is present — some versions only scan classes marked configurable.migrate in an existing project.cast in an attribute doesn't rewrite stored values. If you switch string → array, backfill or you'll get cast exceptions on read.config('configurable') to hold model settings.CastsAttributes) and a custom repository/storage driver are the intended hooks. Overriding the config table name is supported via the published config; overriding the storage backend is possible but brittle across minor versions.php artisan vendor:publish --tag="configurable-config" --force only after diffing — forced publish will overwrite table-name customizations.How can I help you explore Laravel packages today?