Refine for Laravel
A package by Hammerstone

Custom Conditions

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

We have done our best to provide you with as many conditions as is possible, but there are going to be times where you may need a different kind of condition that we don't supply out of the box.

For our example, we're going to pretend that you need to filter by a distance from an address. Having this type of condition out of the box is beyond the scope of this package, but may be useful to your specific app.

Let's take a look at how we would build that out.

Creating the Class

The very first thing you need to do is create a new class that extends the base Condition class provided to you by the package. This will give you several helpful methods right off the bat.

Your editor will most likely show you one method that is declared abstract in the Condition class, left up to you to implement.

If you stub it out, you should now have the following:

class DistanceFromAddressCondition extends Condition
{
public function applyCondition($query, $input)
{
// TODO: Implement applyCondition() method.
}
}

Attributes and Clauses

Most of the time, you will be applying your logic against an attribute on a model which is backed by a column in a database table. Because this is an extremely common use case, we have provided a UsesAttributes trait that brings along a few helpful methods.

Another common use case is to have "clauses" for your condition. Clauses are phrases like "is less than", "is greater than", "contains", "does not contain", etc. Most conditions have clauses. To that end, we also provide a HasClauses trait to make that simpler.

In our example here, we are not going to apply our logic against an attribute, but we are going to have clauses, so we'll add that trait and stub out the methods it requires.

class DistanceFromAddressCondition extends Condition
{
use HasClauses;
 
protected function clauses()
{
// TODO: Implement clauses() method.
}
 
public function applyCondition($query, $input)
{
// TODO: Implement applyCondition() method.
}
}
Code highlighting powered by torchlight.dev, a Hammerstone product.

To keep the example simple, we'll let the user choose between "Is Near" and "Is Not Near" for their clauses.

class DistanceFromAddressCondition extends Condition
{
use HasClauses;
 
const CLAUSE_NEAR = 'near';
const CLAUSE_NOT_NEAR = 'not';
 
protected function clauses()
{
return [[
'id' => static::CLAUSE_NEAR,
'display' => 'Is Near'
], [
'id' => static::CLAUSE_NOT_NEAR,
'display' => 'Is Not Near'
]];
}
 
public function applyCondition($query, $input)
{
// TODO: Implement applyCondition() method.
}
}

Because we're measuring distance, we'll need to allow the developer to pass in the latitude and longitude columns. We'll add methods for that.

class DistanceFromAddressCondition extends Condition
{
use HasClauses;
 
const CLAUSE_NEAR = 'near';
const CLAUSE_NOT_NEAR = 'not';
 
public $latitude;
public $longitude;
 
protected function clauses()
{
return [[
'id' => static::CLAUSE_NEAR,
'display' => 'Is Near'
], [
'id' => static::CLAUSE_NOT_NEAR,
'display' => 'Is Not Near'
]];
}
 
public function latlon($lat, $lon)
{
$this->latitude = $lat;
$this->longitude = $lon;
 
return $this;
}
 
public function applyCondition($query, $input)
{
// TODO: Implement applyCondition() method.
}
}

Ensurance

To ensure that you always configure everything correctly, you can and some "ensurance" in the boot method:

class DistanceFromAddressCondition extends Condition
{
public function boot()
{
$this->addEnsurance([$this, 'ensureLatLonConfigured']);
}
 
// [Omitted methods]
 
protected function ensureLatLonConfigured()
{
if (!$this->latitude || !$this->longitude) {
throw new ConfigurationException(
'Latitude and longitude attributes must both be configured.'
);
}
}
}

Add ensurance to ensure the developer has set things up correctly.

Add validation to validate that the user has given you good data.

Several of the standard conditions ensure certain aspects are configured correctly. Adding ensurance provides reasonable errors to the developer early, instead of failing in mysterious ways later.

Validating

Adding ensurance ensures that the developer has configured the condition properly, but you'll also want to validate the user's input. You can add rules in the boot method as well.

We're going to add a validation rule to test that the user has entered a valid, geocode-able address:

class DistanceFromAddressCondition extends Condition
{
public function boot()
{
$this->addEnsurance([$this, 'ensureLatLonConfigured']);
 
$this->addRules([
'value' => [
'required',
// Any custom Laravel rule (outside the scope of this example)
new AddressCanBeFoundRule
]
]);
}
 
// [Omitted methods]
}

Applying the User's Input

Now for the fun part: applying the user's input to the query.

Once you reach the applyCondition method you know that you're working with valid data based on the validation rules, and a valid configuration based on the ensurance.

Two parameters are passed to the applyCondition method, where we do all of our work. The query parameter is the Eloquent (or Base) query, against which you apply your wheres, whereNots, or anything else your condition may require.

The input variable will contain everything that the user has chosen. Because we're reusing the frontend from the Text Condition, we know that input will contain a clause and a value key.

class DistanceFromAddressCondition extends Condition
{
// [Omitted methods]
 
public function applyCondition($query, $input)
{
// If the user has chose "near", then we're looking for
// records less than a certain distance away, so we'll
// use the `<` operand.
$operand = $input['clause'] === static::CLAUSE_NEAR ? '<' : '>';
 
// Geocode the address (outside the scope of the example).
$address = Geocoding::code($input['value']);
 
$sql = <<<SQL
(
ST_Distance_Sphere(
point({$this->longitude}, {$this->latitude}),
point(?,?)
) * .000621371192) $operand ?
SQL;
 
// Apply to the query.
$query->whereRaw($sql, [
$address->longitude,
$address->latitude,
$miles = 5
]);
}
}

This leaves us with the final class:

class DistanceFromAddressCondition extends Condition
{
use HasClauses;
 
const CLAUSE_NEAR = 'near';
const CLAUSE_NOT_NEAR = 'not';
 
public $latitude;
 
public $longitude;
 
public function boot()
{
$this->addEnsurance([$this, 'ensureLatLonConfigured']);
 
$this->addRules([
'value' => [
'required',
new AddressCanBeFoundRule
]
]);
}
 
protected function clauses()
{
return [[
'id' => static::CLAUSE_NEAR,
'display' => 'Is Near'
], [
'id' => static::CLAUSE_NOT_NEAR,
'display' => 'Is Not Near'
]];
}
 
public function latlon($lat, $lon)
{
$this->latitude = $lat;
$this->longitude = $lon;
 
return $this;
}
 
public function applyCondition($query, $input)
{
// If the user has chose "near", then we're looking for
// records less than a certain distance away, so we'll
// use the `<` operand.
$operand = $input['clause'] === static::CLAUSE_NEAR ? '<' : '>';
 
// Geocode the address (outside the scope of the example).
$address = Geocoding::code($input['value']);
 
// https://tighten.co/blog/a-mysql-distance-function-you-should-know-about/
$sql = <<<SQL
(
ST_Distance_Sphere(
point({$this->longitude}, {$this->latitude}),
point(?,?)
) * .000621371192) $operand ?
SQL;
 
// Apply to the query.
$query->whereRaw($sql, [
$address->longitude,
$address->latitude,
$miles = 5
]);
}
 
protected function ensureLatLonConfigured()
{
if (!$this->latitude || !$this->longitude) {
throw new ConfigurationException(
'Latitude and longitude attributes must both be configured.'
);
}
}
}
Code highlighting powered by torchlight.dev, a Hammerstone product.

Using the Condition

Now that you've built your custom condition you can use it in any filter you want:

public function conditions() {
return [
DistanceFromAddressCondition::make('branch_location')
->latlon('store_lat', 'store_lon')
];
}

and your user will be presented with: [Branch Location] [Is Near] [User entered address].

Hopefully this example shows you how powerful and flexible custom conditions can be to meet any specific needs you might have.