Refine for Laravel
A package by Hammerstone

Option Condition

Refine is a paid package. To purchase, head to hammerstone.dev.

The option condition is used when you want to allow your users to pick between a set of finite options, usually from a select element.

Basic Usage

OptionCondition::make('branch')
->attribute('branch_id')
->options([
10 => 'Dallas',
14 => 'Chicago',
21 => 'Atlanta',
]);

This condition has an id of branch, is applied against branch_id column, and has a display value to the end user of "Branch".

This will allow your users to pick the employees branch from a pre-defined list of options.

Defining Options

When using the OptionCondition, you must supply it with a list of valid options by calling the ->options(...) method. There are a few different values you can pass in.

Key Value Pairs

The first is a simple key => value array as we did earlier. This example uses IDs:

OptionCondition::make('branch')
->attribute('branch_id')
->options([
10 => 'Dallas',
14 => 'Chicago',
21 => 'Atlanta',
]);
Code highlighting powered by torchlight.dev, a Hammerstone product.

Here's an example using constants as enums:

OptionCondition::make('status')
->options([
Invoice::STATUS_SENT => 'Sent',
Invoice::STATUS_PAID => 'Paid',
Invoice::STATUS_OVERDUE => 'Overdue',
]);

Arrays

The associative array above is really just a shorthand for the format below, where each option is an array with an id and a display key.

OptionCondition::make('branch')
->attribute('branch_id')
->options([[
'id' => 10,
'display' => 'Dallas',
], [
'id' => 14,
'display' => 'Chicago',
], [
'id' => 21,
'display' => 'Atlanta',
]]);

This more verbose format is preferable when you want to attach some extra information for displaying on your frontend. If you wanted to pass e.g. a "branch code" out to your frontend, you could add a key to each option:

OptionCondition::make('branch')
->attribute('branch_id')
->options([[
'id' => 10,
'display' => 'Dallas',
'code' => 'DAL-10'
], [
'id' => 14,
'display' => 'Chicago',
'code' => 'CHI-14'
], [
'id' => 21,
'display' => 'Atlanta',
'code' => 'ATL-21'
]]);

You can add any additional information you want, as long as the id and display remain.

Callables

Finally, you can pass any callable into the options method if you'd prefer:

OptionCondition::make('branch')
->attribute('branch_id')
// Using a closure
->options(function() {
// Get all the branch names keyed by id.
return Branch::query()->pluck('name', 'id');
});

This could be a good way to add profile pictures to an employee dropdown:

public function conditions()
{
return [
OptionCondition::make('manager')
->attribute('manager.id')
// Using the array callable syntax
->options([$this, 'managers'])
];
}
 
public function managers()
{
return Employee::manager()->get()
->map(function ($employee) {
return [
'id' => $employee->id,
'display' => $employee->name,
'avatar' => $employee->avatar
];
});
}

Loading via AJAX

If you'd prefer to load your options via AJAX based on your users' choices, take a look at the Deferred Option Condition.

Extending

If you wanted to encapsulate some of that logic remember that the OptionCondition can be extended, as it is just a normal class.

class BranchCondition extends OptionCondition
{
public $id = 'branch';
 
public $attribute = 'branch_id';
 
public function boot()
{
parent::boot();
 
$this->options = Branch::query()->pluck('name', 'id');
}
}
Code highlighting powered by torchlight.dev, a Hammerstone product.

Application to the Query

In the example above, you may be wondering where the 10, 14, and 21 came from. Those are the IDs of the particular branches in this example. So when your user chooses

[Branch] [is equal to] [Dallas]

the resulting SQL will be equivalent to

[branch_id] [=] [10].

By default we use the option's ID as the value we bind into the query, in this case 10. This works very well in most cases when you're wanting your users to pick between IDs or enums that you don't mind sending to the frontend.

There are times, however, when you may want not want to send the IDs to the frontend, or the value you're binding to the query is a calculated value that may change over time.

Your IDs need to be unique and unchanging, so that the frontend and backend can always tie together, and any stabilized filters continue to work.

In that case, you may add a _value key to your option array:

OptionCondition::make('branch')
->attribute('branch_id')
->options([[
'id' => 1,
'display' => 'Dallas',
'_value' => 10
], [
'id' => 2,
'display' => 'Chicago',
'_value' => 14
], [
'id' => 3,
'display' => 'Atlanta',
'_value' => 21
]]);

By doing this, you have now disassociated the id from the _value that gets bound to the query. You are now free to change the values in the future without breaking any stored filters.

It's also important to remember that the _value key does not get sent to the frontend, which might be important for your application. Any keys that a prefixed with an underscore are not sent to the frontend.

The _value can also accept a callback, opening the door for some powerful applications. Imagine a scenario where you wanted be able to filter to employees that are in the same branch as the user running the report.

OptionCondition::make('branch')
->attribute('branch_id')
->options([[
'id' => 10,
'display' => 'Dallas',
], [
'id' => 14,
'display' => 'Chicago',
], [
'id' => 21,
'display' => 'Atlanta',
],[
'id' => 'me',
'display' => 'My Branch',
'_value' => function() {
return Auth::user()->branch->id;
}
]]);

Now when a user who is in the Dallas branch runs this filter, it will show Dallas people. And when the exact same filter is run by a Chicago employee, it will show Chicago employees.

The Null Option

The OptionCondition ships with several useful clauses (see below), one of which is CLAUSE_NOT_SET, which looks for nulls.

However, in our branch example from above, you can easily envision a user trying to find employees that are "in the Dallas branch or aren't in a branch at all". This would technically be possible by using a combination of CLAUSE_NOT_SET and CLAUSE_EQUALS, but going that route is much more complicated than it otherwise should be.

To solve for this common use-case, we've added a nullOption method for you. By passing an ID into the nullOption method, you're telling the OptionCondition that whenever the user selects that option, we need to look for nulls.

Let's continue our example by adding a "No Branch" option:

OptionCondition::make('branch')
->attribute('branch_id')
->nullOption('none')
->options([
'none' => 'No Branch',
10 => 'Dallas',
14 => 'Chicago',
21 => 'Atlanta',
]);

Now when your user chooses 'none', the OptionCondition knows that that is the null option, and will intelligently query for nulls.

Your user can now build out their desired criteria from earlier very easily:

[Branch] [is one of] [No Branch, Dallas]

You can set your null option ID to anything you want, as long as it doesn't conflict with another option's id. We recommend against setting it to a literal null, as that can get coerced and type juggled in weird ways between PHP, HTML, and Javascript.

Validation

The OptionCondition will automatically validate that the user's input is correct. Because it uses the HasClauses trait, the clause will be validated to ensure it is one that you've allowed.

Additionally, it will look for the presence of a selected key in your user's data unless they've chosen the CLAUSE_SET or CLAUSE_NOT_SET clauses. If it's required to be there, it will be checked to ensure it's an array full of IDs that match the option IDs you've defined.

Even if your user selects one of the clauses that only requires a single value (CLAUSE_EQUALS or CLAUSE_DOESNT_EQUAL), the selected key should still be an array (albeit with a single item).

Ignoring Invalid Selections

Sometimes you may not want to throw validation errors when a user selects an invalid option. This is especially true when your options are data-driven and may change over time. If one of your users has a saved filter with a selected option that no longer exists, you may prefer to ignore that selection instead of throwing errors.

For example, imagine a product filter that contains categories:

class ProductFilter extends Filter
{
public function initialQuery()
{
return Product::query();
}
 
public function conditions()
{
return [
OptionCondition::make('category')
->attribute('category.id')
->options(function() {
return Category::pluck('display', 'id');
})
];
}
}

In this scenario, if a category is removed from the database then it's possible it will cause a user's previously valid filter to become invalid. In reality, you probably just want to ignore the invalid selection.

You can do so by calling ignoreInvalidSelections on the Option Condition.

OptionCondition::make('category')
->attribute('category.id')
->ignoreInvalidSelections()
->options(function() {
return Category::pluck('display', 'id');
});

This will instruct Refine to silently ignore any invalid selections.

Note that telling Refine to ignore the invalid selections will still filter them out, the user will just not be notified. Invalid selections never make it to the database regardless of how you set your condition up. This does not expose any sort of data injection vulnerability.

Clauses

Below you'll see all of the clauses available on the OptionCondition.

To read more general information about clauses, head over to the clauses page.

CLAUSE_EQUALS

The attribute is equal to the value the user chose.

If the user chose the nullOption, the attribute is null.

CLAUSE_DOESNT_EQUAL

The attribute is not equal to the value the user chose or is null.

If the user chose the nullOption, the attribute is not null.

CLAUSE_IN

The attribute is equal to one of the options that the user chose.

If the user included the nullOption in their selection, then the attribute could also be null.

CLAUSE_NOT_IN

The attribute is not equal to one of the options that the user chose or it is null.

If the user included the nullOption in their selection, then the attribute is not equal to one of the options that the user chose and it is not null.

CLAUSE_SET

The attribute is not null.

CLAUSE_NOT_SET

The attribute is null.