Group multiple boolean attributes in Laravel Nova
Published on by Doeke Norg
Laravel Nova comes jam-packed with an amazing list of fields. These fields are pretty smart by default, and they are suited for almost every situation. But what if you have a situation in which the UI matches your needs, but the fields handles data differently. Are you forced to create a custom field? Well, maybe not.
Customize your field
Laravel Nova's fields have a bunch of methods that lets you customize their behavior. As you can read in the docs, you can customize the way the corresponding model is hydrated by attaching a fillUsing()
callback. And you can customize the way the field is resolved by attaching a resolveUsing()
callback. So let's use these functions, and create our own "custom" field.
BooleanGroup
Grouping multiple booleans into a Imagine you have a Message
model that represents a message that can be shown in multiple scopes, like: website
, app
and rss
. You want to query those messages easily by their scope, so you've added 3 boolean fields: scope_website
, scope_app
and scope_rss
.
Yes, you could have made categories and created a pivot table; but you only had these scopes, and then you remembered this was an example..
<?php namespace App\Nova\Resources; use App\Nova\Resource;use Illuminate\Http\Request;use Laravel\Nova\Fields\BooleanGroup; class MessageResource extends Resource{ // ... public function fields(Request $request): array { return [ BooleanGroup::make('Scopes')->options($options = [ 'scope_website' => 'Website', 'scope_app' => 'Application', 'scope_rss' => 'RSS Feed', ]) ]; }}
Hydrating the model attributes
Instead of adding 3 different fields to the Nova Resource
we want to add a BooleanGroup
of these scopes. By default, the BooleanGroup
field will store its options as a JSON blob on a single attribute. To change this behavior we are going to customize the hydration of our field by calling the fillUsing
method with this callback function:
use App\Model\Message;use Laravel\Nova\Http\Requests\NovaRequest; // this function goes inside ->fillUsing(...)function (NovaRequest $request, Message $model, string $attribute, string $requestAttribute) { // Make sure the `scopes` value exists on the request. if (!$request->exists($requestAttribute)) { return; } // Decode the values because it is send as a JSON blob. $values = json_decode($request[$requestAttribute], true); // Hydrate the model. foreach ($values as $key => $value) { $model->{$key} = $value; }}
The callback function for fillUsing()
receives 4 parameters:
-
NovaReqeust $request
The request object that has thePOST
values -
Message $model
The model we are going to hydrate -
string $attribute
The name of the attribute on the model (we are not using this in our example) -
string $requestAttribute
The name of the attribute inside the$request
object that has ourPOST
value
After making sure we posted the values, we can map al those options to their model attribute. Effectively calling something like:
$model->scope_website = 1; // or 0
Resolving the field
Now that we actually store the booleans on the correct attributes, it's time to take a look at the resolving of the field in Nova. While the model may have scopes set, all checkboxes will be turned "off". This is because the field still tries to retrieve their values from $model->scopes
; which doesn't exist. So lets fix that by adding a resolveUsing
callback.
You might have noticed we defined our field options inside a
$options
variable. This was intentional, because we need the keys for those options. As of this time, theresolveUsing
callback doesn't have access to the field itself to retrieve those options. This is our workaround.
// this function goes inside ->resolveUsing(...)function ($value, Message $model, string $attribute) use ($options) { $keys = array_keys($options); $values = array_map(function($value, $key) use ($model) { return $model->{$key}; }, $options, $keys); return array_combine($keys, $values);}
The callback function for resolveUsing()
receives 3 parameters:
-
mixed $value
The value that Laravel Nova tried to retrieve -
Message $model
The model that provided the value -
string $attribute
The name of the attribute on the model (again, we won't be needing this)
The only thing our callback needs to do is retrieve the values for every boolean from the model, and return those values as an array. This code is a bit of a mess. It's not easy to see what is going on, and we need to use
values within multiple function scopes. Let's clean this up a bit by using shorthand functions and some laravel collect
magic:
fn($value, Message $model) => collect($options)->map(fn($value, $key) => $model->{$key})
There you go, a nice one-liner that resolves the field from the correct model attributes. When you refresh your Nova page, the checkboxes should correctly indicate their status.
required
Bonus: make this field Just for fun, let's assume you need to select at least one scope. Your first instinct might be to just set ->required()
on the field, but that doesn't actually work, although it will give a nice asterisk *
on the form. Luckily we can also add a custom validation rule by calling the rules()
method on the field.
$field->rules('required', function (string $attribute, $value, callable $fail) { if (!array_filter(json_decode($value, true) ?? [])) { return $fail(sprintf('The "%s" field must have at least one option selected.', $attribute)); }})
Setting the required
rule will also add the asterisk *
to the form. The callback does a quick check to see if any value was returned as true
. If not; we call the $fail
callable and provide the reason for failing the validation.
And that's it; a custom field, without actually building a custom field.
PHP developer from Groningen, the Netherlands. Works mostly with Symfony, Laravel and a sprinkle of WordPress plugins. Testing enthusiast. Recently started blogger.