Deferred Option Condition
Refine is a paid package. To purchase, head to hammerstone.dev.
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.
DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { // @TODO Find your options. });
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
.
DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { // @TODO Find your options. });
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:
DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { $query = User::query(); if ($search) { $query->where('name', 'like', "%{$search}%")->limit(20); } // @TODO return results. });
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.
// Laravel Scout...DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { $query = User::query(); if ($search) { // Using Scout to do a more robust search. $query->search($search)->limit(20); } });
// External API Client...DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { if ($search) { // Some external API Client. $query = Client::search($search); } });
// Reading off of disk and filtering...DeferredOptionCondition::make('country_id', 'Country') ->deferredOptionProvider(function($search, $find) { $records = Storage::get('countries.json'); $records = json_decode($records, JSON_OBJECT_AS_ARRAY); $records = collect($records); if ($search) { // Reading off of the disk and filter. $records = $records->filter(function($record) use ($search) { return Str::contains($record['name'], $search); }); } });
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:
DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { $query = User::query(); if ($search) { $query->where('name', 'like', "%{$search}%")->limit(20); } else { $query->whereIn('id', $find); } // @TODO return results. });
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:
DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { // Restrict searching via an Eloquent scope. $query = User::sameDepartmentAs(Auth::id()); if ($search) { // Searching for users... $query->where('name', 'like', "%{$search}%")->limit(20); } else { // Verifying the IDs from the frontend... $query->whereIn('id', $find); } });
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:
DeferredOptionCondition::make('user_id', 'User') ->deferredOptionProvider(function($search, $find) { $query = User::query(); if ($search) { $query->where('name', 'like', "%{$search}%")->limit(20); } else { $query->whereIn('id', $find); } // Using Eloquent's `pluck` method to generate an associative // array with ID as the key, and name as the value. return $query->pluck('name', 'id'); });
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.
Route::refine(); // 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.
// Protect the Refine routes behind the auth middleware.Route::middleware(['auth'])->group(function () { Route::refine();});
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()
.
Route::post('/some/url/you/prefer', [DeferredOptionController::class]) ->name('deferred-options');
DeferredOptionCondition::make('user_id', 'User') ->route('deferred-options') ->deferredOptionProvider(function($search, $find) { // ... });
Clauses
The Deferred Option exposes all the same clauses as the Option Condition.