Refine for Laravel
A package by Hammerstone

Refinements

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

Allowing your user to build up the perfect filter row by row is a fundamental aspect of Refine.

There are times, however, when filtering on a single attribute at a time isn't quite enough.

Imagine that your user wants to find contacts that have: performed a specific event at least 4 times in the past year.

It's very straightforward to give your users a dropdown of event types to choose from:

OptionCondition::make('events.type')
->options(function() {
return EventTypes::query()->pluck('name', 'type');
});

With this single condition, your users can find contacts that have performed an event. But what they can't do is find users that have performed that event in the past year.

If you were to add a date condition that let them filter on the time the event happened, it would lose the type specificity. Your users could find contacts that had performed an event in the last year, but not an event of a specific type in the last year.

To overcome this limitation, Refine has built-in "refinements" for every type of condition, which allow you to filter down to contacts that have performed an event of a certain type in the last year.

Refining by Date

To refine a condition by date, all you need to do is call refineByDate on your condition.

OptionCondition::make('events.type')
->options(/* ... */)
->refineByDate();
Code highlighting powered by torchlight.dev, a Hammerstone product.

That's it! Now your user is given the full power of an Option Condition and a Date Condition, on the same line, operating against the same data.

Refinements only work on attributes that are attached to relationships! Read more below.

@TODO Screenshot

Everything available on the Date Condition is available as a refinement, because the date refinement is using the Date Condition under the hood.

Every clause, every customization, every method is available to you. Let's take a look at how you'd customize the date refinement.

Changing the Date Refinement Attribute

If you do nothing at all, the date refinement will assume that your date attribute is called created_at, and will generate SQL similar to the following:

select * from contacts
where
contacts.id in (
select events.contact_id from events
where
-- Set by the date refinement
created_at between '2020-01-01 00:00:00' and '2020-12-31 23:59:59'
-- Set by the option condition
and (type = 'click')
)

If you want to change created_at to a different column, you can just pass a string into the refineByDate method:

OptionCondition::make('events.type')
->options(/* ... */)
->refineByDate('event_happened_at');

Now the query will look at the event_happend_at column instead of the created_at column.

Customizing the Date Refinement by Closure

If you want to further customize the refinement, you can pass a closure in and do anything you want.

OptionCondition::make('events.type')
->options(/* ... */)
->refineByDate(function(DateCondition $refinement) {
$refinement
->attribute('event_happened_at')
->withoutClauses([
DateCondition::CLAUSE_GREATER_THAN_OR_EQUAL,
DateCondition::CLAUSE_LESS_THAN_OR_EQUAL,
]);
});

You have full power to restrict the clauses, change the attribute type, set timezones, etc.

Hinting a Custom Date Condition

If you have a standard Date Condition that you use in your app, you may typehint that class and Refine will give that to you instead of Refine's Date Condition.

OptionCondition::make('events.type')
->options(/* ... */)
->refineByDate(function(AcmeDateCondition $refinement) {
// $refinement instanceof AcmeDateCondition === true
});

Your hinted Date Condition will need to extend the base Date Condition, because we are relying on many of those methods.

If you hint nothing you'll get Refine's Date Condition, and if you hint something that doesn't extend Refine's Date Condition, you'll get a configuration exception.

Refining by Count

Imagine that your user wants to find contacts that have: performed a specific event at least 4 times in the past year.

We've covered how to handle the in the past year part (refining by date), but what about the at least 4 times part?

That's where refining by count comes in. To refine by count, you only need to call refineByCount:

OptionCondition::make('events.type')
->options(/* ... */)
->refineByCount();

Now your user is given the full power of an Option Condition and a Numeric Condition, on the same line, operating against the same data.

@TODO Screenshot

Refine will now intelligently add grouping to your query and produce SQL similar to the following:

select * from contacts
where
contacts.id in (
select events.contact_id from events
where
-- Added by the Option Condition
type = 'link'
-- Intelligent grouping by the Count Refinement
group by
events.contact_id
having
-- Added by the Count Refinement
count(*) >= 4
)
Code highlighting powered by torchlight.dev, a Hammerstone product.

Everything available on the Numeric Condition is available as a refinement, because the count refinement is just using the Numeric Condition under the hood.

Every clause, (almost!) every customization, every method is available to you. Let's take a look at how you'd customize the date refinement.

Customizing the Count Refinement By Closure

If you want to customize your count refinement, you can pass a closure to the refineByCount method. This method will receive a NumericCondition by default as its first argument.

OptionCondition::make('events.type')
->options(/* ... */)
->refineByCount(function(NumericCondition $refinement) {
$refinement->withoutClauses([
NumericCondition::CLAUSE_GREATER_THAN_OR_EQUAL,
NumericCondition::CLAUSE_LESS_THAN_OR_EQUAL,
]);
});

Hinting a Custom Numeric Condition

If you have a standard Numeric Condition that you use in your app, you may typehint that class and Refine will give that to you instead of Refine's Numeric Condition.

OptionCondition::make('events.type')
->options(/* ... */)
->refineByCount(function(AcmeNumericCondition $refinement) {
// $refinement instanceof AcmeNumericCondition === true
});

Your hinted Numeric Condition will need to extend the base Numeric Condition, because we are relying on many of those methods.

If you hint nothing you'll get Refine's Date Condition, and if you hint something that doesn't extend Refine's Numeric Condition, you'll get a configuration exception.

Things You Cannot Customize

There are a few things you cannot change about the count refinement.

The first thing is the attribute against which it is applied. The count refinement will always be applied against count(*). Refine will add the grouping to make sure this is possible.

Additionally, you can't change the fact that allowFloats will always be set to false in the count refinement. Since you can't have a partial record, we force this to be false.

Using Both Types of Refinement

You are free to use no refinements, one refinement, or both refinements on your conditions.

If your application lines up well with our default values, then adding both types of refinement is as simple as adding two lines to your condition.

OptionCondition::make('events.type')
->options(/* ... */)
->refineByDate()
->refineByCount();

Of course if you need to customize one or the other type of refinement, you're welcome to do so.

When You Can Use Refinements

Refinements are available for every type of condition, but they do require certain types of attributes to work.

For instance, you wouldn't ever try to find Contacts where name equals 'Aaron' at least 1 time. The attribute name is just a single attribute on a model, there is no way to have the name equal Aaron at least once.

For refinements to work, the attribute you're querying on must be a relationship attribute on one of Laravel's *Many relationships.

Any of the following relationship types will work:

  • HasMany
  • HasManyThrough
  • MorphToMany
  • BelongsToMany

Consider a Contact that has many Events:

Contact.php

class User extends Model
{
public function events()
{
return $this->hasMany(Event::class);
}
}

In your ContactFilter, you could allow your users to filter on the event type, and refine by date or count:

ContactFilter.php

class ContactFilter extends Filter
{
public function initialQuery()
{
return Contact::query();
}
 
public function conditions()
{
return [
OptionCondition::make('event.type')
->options(function() {
return EventTypes::query()->pluck('name', 'type');
})
->refineByDate()
->refineByCount()
];
}
}