marshmallow/boolean-group
A Laravel package for organizing boolean flags into named groups and evaluating them as a unit. Define reusable condition sets, combine checks with a fluent API, and keep feature-flag or eligibility logic readable and centralized.
marshmallow/boolean-group is a Laravel Nova field package that renders a group of boolean toggles (checkboxes/switches) backed by a single JSON column, so you can manage several related flags in one form field instead of one column per toggle. As of v1.2.0 the package supports Nova 5; earlier releases (v1.1.0 and below) target Nova 4. Beyond the version matrix, still confirm the exact field API exposed in the README and what it adds over Nova's built-in Laravel\Nova\Fields\BooleanGroup.
Minimal install
# Nova 5 app
composer require "marshmallow/boolean-group:^1.2"
# Nova 4 app
composer require "marshmallow/boolean-group:^1.1"
php artisan nova:publish # only if the README says assets/config need publishing
Where to look first
composer.json — require constraints for laravel/nova and PHP version; confirm the constraint actually matches your Nova major (a ^1.2 requirement paired with a Nova 4 app may still resolve but ship the wrong JS build).src/ — the field class (make(), options(), resolve()), plus the service provider: a package supporting Nova 4 and 5 often branches on Nova::version() when registering assets.resources/js/ — the Vue component; confirms whether options are static, reactive, or index-visible, and whether the build targets the same Vue version as your Nova.README.md — the authoritative API surface; treat any method below as illustrative until confirmed.First use case
A model with a JSON column for feature/notification flags:
php artisan make:migration add_settings_to_users_table
Schema::table('users', function (Blueprint $table) {
$table->json('settings')->nullable();
});
// app/Models/User.php
protected $casts = [
'settings' => 'array',
];
// app/Nova/User.php
use Marshmallow\BooleanGroup\BooleanGroup;
BooleanGroup::make('Settings')
->options([
'email_notifications' => 'Email notifications',
'sms_notifications' => 'SMS notifications',
'beta_features' => 'Beta features',
])
->help('Toggle the features available to this user.'),
That single field now writes an object like {"email_notifications":true,"sms_notifications":false,"beta_features":true} to the JSON column.
Defaults on create. Avoid null on new rows by seeding a default array, either in the migration, the model, or the field:
protected $attributes = [
'settings' => '{"email_notifications":true,"sms_notifications":false,"beta_features":false}',
];
Validation should constrain keys, not just the shape. Since the value is a JSON object, array rules aren't enough — reject unexpected keys and non-boolean values:
BooleanGroup::make('Settings')
->options($this->optionList())
->rules(['array'])
->rules(function () {
return collect(array_keys($this->optionList()))
->map(fn ($k) => "settings.$k")
->push('settings')
->all();
})
Better still, use a FormRequest that validates the allowed keys explicitly, because a malicious payload can otherwise inject arbitrary JSON into the column.
Centralize the option list. Don't duplicate the label map in the Nova resource, the Blade/Inertia views, and tests. Put it on the model or a config file:
// app/Models/User.php
public const SETTINGS = [
'email_notifications' => 'Email notifications',
'sms_notifications' => 'SMS notifications',
'beta_features' => 'Beta features',
];
BooleanGroup::make('Settings')->options(User::SETTINGS)
Reading flags elsewhere. Treat the JSON column as a bag of booleans and expose typed helpers rather than dereferencing arrays throughout the app:
public function setting(string $key): bool
{
return (bool) ($this->settings[$key] ?? false);
}
Settings pages and pseudo-resources. A common pattern is a singleton-style resource (Settings) that edits one row. Combine the boolean group with ->hideFromIndex() or ->onlyOnForms() so the index doesn't render dozens of checkmarks:
BooleanGroup::make('Notifications')
->options(NotificationFlags::labels())
->onlyOnForms()
Permissions matrices. A boolean group works for a small, fixed set of coarse permissions (publish, archive, export) on a single record. It does not scale to per-entity or per-role permissions — that belongs in a pivot table or a dedicated package (Spatie permissions, Gate/Policy). Use this field only when the set of toggles is small, global, and doesn't need auditing per key.
Querying the stored flags. Because the column is JSON, filter with JSON helpers:
User::whereJsonContains('settings->beta_features', true)->get();
User::where('settings->email_notifications', true)->get();
Keep these queries behind a scope so a future switch to real columns doesn't ripple through controllers:
public function scopeWithSetting(Builder $q, string $key): Builder
{
return $q->where("settings->{$key}", true);
}
Composing inside layouts. The field slots into Panel, Tab, and Field::make(...)->fields([...]) structures like any other Nova field. It's often paired with ->help() or a Section header to explain what the group controls.
Testing. Nova fields are cheap to test via the resource:
Nova::actingAs($admin);
$this->putJson("/nova-api/users/{$user->id}", [
'settings' => ['email_notifications' => true, 'beta_features' => false],
])->assertOk();
$this->assertTrue($user->fresh()->setting('email_notifications'));
Add an assertion that unexpected keys are rejected, since JSON blobs are easy to over-post.
Nova already ships a BooleanGroup field. Laravel\Nova\Fields\BooleanGroup covers the common case (JSON object of toggles with labels). Before adding a dependency, diff this package's features against the built-in. The calculus has improved with v1.2.0 — the package now tracks Nova 5 rather than being a single-release artifact — but it is still a low-traction package, so you can often reproduce the field locally in one file with ->options() and a matching cast.
Pin to the right major for your Nova version. v1.2.0 is the Nova 5 line; v1.1.0 and below are Nova 4. Constrain accordingly (^1.2 vs ^1.1) rather than a loose ^1.0 or dev-main, and check the release notes before bumping within the 1.x series in case a future patch changes the asset build. Review the source once and check the license — you are still the only watcher this package has.
Upgrading Nova 4 → 5 means re-publishing assets. Nova 5 support was added in PR #3, which almost certainly touched the compiled JS/CS components. After upgrading both Nova and this package: composer update, php artisan nova:publish, then php artisan view:clear and a hard browser refresh. Stale published assets are the most likely cause of a field that "disappears" or renders unstyled post-upgrade — check public/vendor/ and the browser console before blaming the package.
Mixed-version fleets. If you maintain several Nova apps on different majors, you cannot share a single lockfile assumption: a Nova 4 app pinned to ^1.2 may install a component built against Nova 5's frontend. Verify per-application, and tag your own internal notes with which line each app uses.
Key drift between stored JSON and options. When you rename or remove an option key, old rows keep the stale key. Either migrate the data (UPDATE ... SET settings = JSON_REMOVE(...)) or normalize on read. Decide explicitly whether removed keys should be preserved, dropped, or ignored — Nova will typically only submit keys the field renders.
Null vs empty object. A nullable JSON column can produce null on new records, which some field components render awkwardly. Default to {} (or your seeded defaults) at the database or model level, and confirm the field's resolve() handles null gracefully.
Database JSON support varies. whereJsonContains is well supported on MySQL 5.7+/8 and Postgres via ->, but SQLite only supports it with recent versions and JSON1 enabled. If you test on SQLite and deploy to MySQL, write the query tests against the deploy-time engine.
Casting is mandatory. With 'settings' => 'array' (or json/AsArrayObject), the field value round-trips cleanly. Without a cast, Nova will write a JSON string and reads will return strings, causing "all toggles truthy" surprises.
Index/detail rendering. Check whether the field renders on index and how many labels it prints. Long option lists can bloat a Nova index table; prefer ->hideFromIndex() and surface a summary badge or computed accessor instead.
Validation gaps are the main security risk. JSON columns accept anything the field submits. A user who can edit the form can usually POST extra keys or non-boolean values unless you validate explicitly. Lock this down in a FormRequest rather than relying on the Vue component's behavior — the Nova 5 port changed the frontend, so never let client-side behavior be your only guard.
Reactive/dependent fields. If one toggle should reveal another field, use Nova's dependsOn or a custom computed pattern in the resource rather than the boolean group itself — field-to-field reactivity here is typically outside the package's API.
Asset publishing. If the package ships compiled JS, run php artisan nova:publish after composer require/update, or the component may fail to register. If it fails silently, check nova-components/public/vendor and the browser console first.
Extension points. If you need extra behavior (custom colors, grouped sub-sections, read-only modes based on a Policy), subclass the field rather than patching vendor code, and override resolve/fill plus the Vue component. Keep that subclass in app/Nova/Fields so upgrades — including the Nova 4 → 5 jump — stay tractable.
How can I help you explore Laravel packages today?