Laravel

🗃️ Re-useable Eloquent Model Filters

Re-useable and self-contained filters for Laravel's Eloquent Models.

Filters are a tricky thing to get right, and I haven't found the perfect solution yet, but reusable filter classes is close and proved to work very well for rapidly building filters for models.

😴

TL;DR

By creating small isolated filter classes you could improve the structure of your application. By keeping your filters reuseable you’ll enforce consistent naming of relationships or columns and also promote reuse of existing code, keeping your application simpler to maintain and easier to iterate. There are many ways to expand on this and it’s something I would encourage experimenting with.

I've been working on a large client project for the last few months. It has a custom admin area where users can manage their members, website content and more. There's a lot of different pages for managing the different types of content, with each needing a set of filters to provide users an easy and convenient way to narrow down the results. This project is also expected to have new features post-launch and so at an early stage I wanted to build a robust and scalable filtering system to improve iteration times and keep maintenance low.

To start, let's look at an example of how I've filtered models in the past.

How I Used To Do It

In the past I was fairly naive with filtering of records, to be honest I was naive with a lot of different core systems with applications. I'm sure everyone has used, or currently uses, this method. Effectively, just chaining on conditions when retrieving the model. Lets setup an environment that we can use throughout this article.

Our simple application has three models. A User, Article and Tag. These are fairly simple and have the following columns:

  • User

    • id

    • username

    • is_active

    • created_at

    • updated_at

  • Article

    • id

    • title

    • author_id

    • is_published

    • created_at

    • updated_at

  • Tag

    • id

    • title

    • author_id

    • is_active

    • created_at

    • updated_at

These will have the following relations defined, for the purposes of this article I'll leave out the structure of any pivot tables.

  • User can have many Article

  • Article can have many Tag

  • User can have many Tag

This is a fairly simple setup with some basic, common, relations between our models. Our, hypothetical, customer has asked for a way to search articles as they're finding it difficult to find ones they're looking for. Take the naive approach we might adjust our query as follows:

        
            Article::where('title', 'LIKE', '%{$request->get('search')}%')
	->get()
;
        
    

Perfect, we would then setup our frontend to show this filter, for our purposes this is a minimal application, so a simple input field will be suitable for us:

        
            <div>
  <form action="filter">
    <input type="text" name="search" />
  	<button>Filter</button>
  </form>
</div>
        
    

Our customer is now happy that they're able to easily filter their articles. Now, they're asking if they can have a way to filter the published status of the article. No problem we say, add the extra query, and HTML:

        
            Article::where('title', 'LIKE', '%{$request->get('search')}%')
    ->where('is_published', $request->boolean('is_published'));		
    ->get()
;
        
    
        
            <div>
  <form action="filter">
    <input type="text" name="search" />
    <input type="checkbox" name="is_published" />
    <button>Filter</button>
  </form>
</div>
        
    

Perfect. They're now happy with the articles for now, but would be great if we could search tags and filter by the active status. I'm sure you can see where we're going here...

        
            Tag::where('title', 'LIKE', '%{$request->get('search')}%')
	->where('is_active', $request->boolean('is_active'));		
	->get()
;
        
    
        
            <div>
  <form action="filter">
    <input type="text" name="search" />
    
    <input type="checkbox" name="is_active" />
    
  	<button>Filter</button>
  </form>
</div>
        
    

We now have four very similar filters that could be reduced down into just two. And this would infinitely scale with other models or filter types. We could also use this opportunity as a way to make our front end reusable too.

Now, let's look at the approach I've taken with my client project.

Reusing Filter Classes

I would like to start with this by saying that is by far not a perfect solution, and is something I'm wanting to refine over time for future projects. For example, you could make use on invokable classes rather than the array structure I used here. There's lots of ways this could be tweaked and improved, I would love to here yours! Tweet me!

Per-column classes

We're going to start by extracting our filter code into reusable classes for each column. This provides us with a reusable filter that we can reuse on different pages. For example, we might want the same filter to show on our reporting and listing pages.

To start let's scaffold a base Filter class that will contain the relevant logic we need.

        
            abstract class Filter {
	public string $view;
	public string $label;

	abstract function apply($request, $query);

	public function render()
    {
		return view($this->view, [
			'label' => $this->label,
			'name' => $this->name,
			'id' => $this->name,
		]);
    }
}
        
    

This will be our starting class, let's break this down:

  • We define two variables that every filter should have:

    • view which will be the path to a blade template that will render our filter's UI

    • label which will be the readable version of name for the input's label

  • We also have two methods:

    • apply will be implemented by each of our child classes and is responsible for taking a query builder, applying conditions to it, and then returning the query builder

    • render is responsible for building our blade view

For the purposes of this article I'm only going to use Article for this section, but the concepts here will also be usable for other models too.

Now let's create two filter classes for our Article, TitleFilter and IsPublishedFilter

        
            class TitleFilter extends Filter
{
	public string $view = 'filters.title';
	public string $label = 'Title';

	public function apply($request, $query)
	{
		return $query->where('title', $request->get($this->name));
	}
}

class IsPublishedFilter extends Filter
{
	public string $view = 'filters.published';
	public string $label = 'published';

	public function apply($request, $query)
    {
		return $query->where('is_published', $request->boolean($this->name));
    }
}
        
    

I'm not going to include the blade views here but we might assume TitleFilter would be a text input and then IsPublishedFilter might be a switch or a checkbox.

Now we need to "register" these with a model somehow. The way I did this in the project was by creating a Filterable trait that could then be used on any model I needed to add filters to, this trait looked something like:

        
            trait Filterable
{
	public static function getFilters()
	{
		return static::$filters ?? [];
	}

	public function scopeFiltered($query, $request)
	{
		foreach(self::getFilters() as $filter) {
			$query = resolve($filter)->apply($request, $query);
		}

		return $query;
	}
}
        
    

This trait defines two methods, getFilters which returns the filters defined against the model or an empty array, and scopeFiltered which uses Eloquent Scopes to provide a chainable method on the Eloquent builder for this method. scopeFiltered simply accepts the current query builder and request and then instances our defined filters and applies them to the builder, passing in the request.

Our model might look something like:

        
            class Article {
	use Filterable;

	public static $filters = [
		TitleFilter::class,
		IsPublishedFilter::class,
	];
}
        
    

We could then apply the filters that are in the request by calling it before any other methods we like:

        
            Article::filtered($request)->limit(20)->get();
        
    

If title or published is defined inside our request then it'll be applied as conditions within the query builder.

You would then be able to look over the model's static $filters array and call render on each filter within the blade template for your page. I'm not going to show that here but it would be very similar to the scopeFiltered function but by calling render.

This is great, but we've not really reduced the amount of code we have, but have extracted our filters for easier reusability. Let's look next at extracting these to more generic filter classes where we can pass options through.

Building generic filter classes

The next step is to start with as generic filters as possible. You'll be able to easily expand on them creating more custom filters as needed, but that should be the exception. Now, let's take our two filters and extract them down into something that's more reusable. Let's create a TextFilter and a BooleanFilter.

First, we'll need to update our Filter abstract class to allow us to define configurable columns, let's do that first.

        
            abstract class Filter {
	public string $view;
	public string $label;
	public string $column;
	public string $name;

	abstract function apply($request, $query);

	public function setOptions($options)
	{
		$this->label = $options['label'];
		$this->column = $options['column'];
		$this->name = $options['name'];

		return $this;
	}

	public function render()
    {
		return view($this->view, [
			'label' => $this->label,
			'name' => $this->name,
			'id' => $this->name,
		]);
    }
}
        
    

What we've done here is added two new variables name and column each to allow us to extract the request name for our filter and the column name we're applying it to. We've then added a new setOptions method which will be used to define these settings when we're configuring them and filtering.

Now let's amend our TitleFilter and IsPublishedFilter to our new TextFilter and BooleanFilter.

        
            class TitleFilter extends Filter {
	public string $view = 'filters.text';
	
	public function apply($request, $query)
	{
		return $query->where($this->column, $request->get($this->name));
	}
}
        
    
        
            class BooleanFilter extends Filter {
	public string $view = 'filters.switch';

	public function apply($request, $query)
	{
		return $query->where($this->column, $request->boolean($this->name));
	}
}
        
    

What we've done is refactored our two column based filters into two generic TextFilter and BooleanFilter classes. They're doing the same thing, but are using much more generic structures allowing us to easily reuse these.

You might be wondering, how are we going to use this within our Model? Well, let's start by showing the new $filters array configuration:

        
            // model
public static $filters = [
	TextFilter::class => [
		'column' => 'title',
		'name' => 'title',
		'label' => 'Search Title',
	],
	BooleanFilter::class => [
		'column' => 'is_published',
		'name' => 'published',
		'label' => 'Published?',
	],
];
        
    

With this setup our key is the class that is our filter, we then have an array of options for each entry. You can also change this to instead instantiate a class passing the config into the constructor.

Now, we'll want to change our Filterable trait to use this new structure:

        
            trait Filterable
{
	public static function getFilters()
	{
		return static::$filters ?? [];
	}

	public function scopeFiltered($query, $request)
	{
		foreach(self::getFilters() as $filter => $options) {
			$query = resolve($filter)->setOptions($options)->apply($request, $query);
		}

		return $query;
	}
}
        
    

That's it! We can now reuse these two filters in any of our models. For example, let's take this approach and apply some filters to our Tag model:

        
            // Tag model
public static $filters = [
	BooleanFilter::class => [
		'name' => 'active',
		'label' => 'Active',
		'column' => 'is_active',
	],
	TextFilter::class => [
		'name' => 'title',
		'label' => 'Title',
		'column' => 'title',
	],
];
        
    

Simple! If we needed a custom filter for Tag which has some odd logic or structure we could then extend one of these base filters and then implement our custom logic inside the apply method.

This can also be expanded to also support relationships via dot notation, however, this article has already gotten quite long so I might cover that in a follow up.

As I mentioned earlier though, this is likely not the best solution and there are packages out there that handle Eloquent based filtering, however, I wanted to try something like this and I think it worked out for the project. Let me know on Twitter how you filter Eloquent models!

Tagged Under: Eloquent, Filtering, Models, Laravel, Refactoring,