Refine will be launching soon 🎉 Sign up for the early access list to stay up to date.
Refine for Laravel
A package by Hammerstone

Deferred Option Condition

Hammerstone Refine has not launched yet. Sign up for the early access list to stay up to date!

The DeferredOptionCondition extends the OptionCondition, but there is one major difference: the DeferredOptionCondition receives its options via AJAX.

It does not send any options to the frontend, but rather sends an endpoint where the frontend can search for options.

This is very useful when you have a huge set of options and don't want to send them all down as JSON, or when you don't want to expose every option to the frontend.

Basic Usage

You construct the DeferredOptionCondition like every other condition, passing in an ID and display.

1DeferredOptionCondition::make('user_id', 'User')
2 ->deferredOptionProvider(function($search, $find) {
3 // @TODO Find your options.
4 });

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

Unlike the OptionCondition, we call ->deferredOptionProvider instead of ->options like you normally would.

Deferred Option Provider

The deferred option provider is the method by which you determine what options should be presented to the user. You configure it by passing a callback to deferredOptionProvider.

1DeferredOptionCondition::make('user_id', 'User')
2 ->deferredOptionProvider(function($search, $find) {
3 // @TODO Find your options.
4 });
Code highlighting powered by torchlight.dev, a Hammerstone product.

Your callback should accept two parameters: $search and $find.

Searching for Options

When your end-user is searching for an option, your deferredOptionProvider callback will be passed the value that they have typed in as the $search parameter. The $find parameter will be null.

For example, if your user has typed Sean into the frontend component, your provider will be called with Sean as the first parameter. You could then search based on that criteria:

1DeferredOptionCondition::make('user_id', 'User')
2 ->deferredOptionProvider(function($search, $find) {
3 $query = User::query();
4 
5 if ($search) {
6 $query->where('name', 'like', "%{$search}%")->limit(20);
7 }
8 
9 // @TODO return results.
10 });

You're not limited in any way whatsoever to how you search, so you could utilize Laravel Scout, search via some external API, or read data from a disk.

1// Laravel Scout...
2DeferredOptionCondition::make('user_id', 'User')
3 ->deferredOptionProvider(function($search, $find) {
4 $query = User::query();
5 
6 if ($search) {
7 // Using Scout to do a more robust search.
8 $query->search($search)->limit(20);
9 }
10 });
1// External API Client...
2DeferredOptionCondition::make('user_id', 'User')
3 ->deferredOptionProvider(function($search, $find) {
4 if ($search) {
5 // Some external API Client.
6 $query = Client::search($search);
7 }
8 });
1// Reading off of disk and filtering...
2DeferredOptionCondition::make('country_id', 'Country')
3 ->deferredOptionProvider(function($search, $find) {
4 $records = Storage::get('countries.json');
5 $records = json_decode($records, JSON_OBJECT_AS_ARRAY);
6 $records = collect($records);
7 
8 if ($search) {
9 // Reading off of the disk and filter.
10 $records = $records->filter(function($record) use ($search) {
11 return Str::contains($record['name'], $search);
12 });
13 }
14 });

Finding Options

The find parameter is used when the user has already selected one or more options, submitted them, and now the backend needs to validate those options are allowed.

So instead of searching for a bit of text, we are finding based on IDs.

To take our original example of using Eloquent to search for values, we would now amend it to handle finding as well:

1DeferredOptionCondition::make('user_id', 'User')
2 ->deferredOptionProvider(function($search, $find) {
3 $query = User::query();
4 
5 if ($search) {
6 $query->where('name', 'like', "%{$search}%")->limit(20);
7 } else {
8 $query->whereIn('id', $find);
9 }
10 
11 // @TODO return results.
12 });

The lifecycle of this event is as follows:

  • The user entered "Sean" as a search term
  • The deferredOptionProvider sent back a set of options for that term
  • The user picked one or more
  • The frontend sends the IDs of those options to the backend for querying
  • Refine uses the deferredOptionProvider to find those options by ID, proving they are valid.

This prevents users from sneaking IDs into the payload that they aren't allowed to query on.

Imagine your provider restricted querying to only users that were in the same department as the authenticated user:

1DeferredOptionCondition::make('user_id', 'User')
2 ->deferredOptionProvider(function($search, $find) {
3 // Restrict searching via an Eloquent scope.
4 $query = User::sameDepartmentAs(Auth::id());
5 
6 if ($search) {
7 // Searching for users...
8 $query->where('name', 'like', "%{$search}%")->limit(20);
9 } else {
10 // Verifying the IDs from the frontend...
11 $query->whereIn('id', $find);
12 }
13 });
Code highlighting powered by torchlight.dev, a Hammerstone product.

Even if the end-user knew the IDs of some employees in a different department, they wouldn't be able to stuff them in the payload because Refine will verify those IDs are valid before applying them to the query.

Return Values

For both searching and finding, the value that you return from your deferredOptionProvider needs to be consistent with the basic Option Condition.

Specifically, your provider needs to return either an array of options that have both id and display keys, or an associative array where keys are the id and values are the displays.

One easy way to do this is with Eloquent's pluck method:

1DeferredOptionCondition::make('user_id', 'User')
2 ->deferredOptionProvider(function($search, $find) {
3 $query = User::query();
4 
5 if ($search) {
6 $query->where('name', 'like', "%{$search}%")->limit(20);
7 } else {
8 $query->whereIn('id', $find);
9 }
10 
11 // Using Eloquent's `pluck` method to generate an associative
12 // array with ID as the key, and name as the value.
13 return $query->pluck('name', 'id');
14 });

For more information on what you can return from your provider, check out the defining options section in the Option Condition docs.

Setting up Routing

In order for the frontend to query your backend for options, you'll need to set up routing. Refine exposes a single route for all deferred options, which you can set up by calling Route::refine() in your web.php or elsewhere.

1Route::refine();
2 
3// All your other routes.

The DeferredOptionController will handle finding the right filter, passing the user input into the right condition, and return the options for the frontend to consume.

If you'd like to restrict this route, you're welcome to do so in the standard Laravel way.

1// Protect the Refine routes behind the auth middleware.
2Route::middleware(['auth'])->group(function () {
3 Route::refine();
4});

Customizing the Route

If you'd prefer to not use the standard Refine route, you're welcome to set up your own route and customize the condition by passing a new route name to route().

1Route::post('/some/url/you/prefer', [DeferredOptionController::class])
2 ->name('deferred-options');
1DeferredOptionCondition::make('user_id', 'User')
2 ->route('deferred-options')
3 ->deferredOptionProvider(function($search, $find) {
4 // ...
5 });

Clauses

The Deferred Option exposes all the same clauses as the Option Condition.