Create filters in Laravel with OOP best practices
Tags: php, laravel
Published: 2020-03-31 11:58:36

In this post I'll show you how can you create filters in Laravel following good Object-oriented programming principles. 

Before we begin, keep in mind that you need to have a good understanding of Laravel, OOP and SOLID principles, also you'll need to have Laravel installed on your computer.

### Create Laravel project
From the command line go to your apps folder (I keep mine in `~/Documents/apps`) and create new Laravel project and cd into it.
`laravel new filters`
`cd filters`

### Set up the environment
For this kind of applications I like to use `sqlite` as my database, because it's so easy to set it up, but you can use some other database provider if you want to.

To set up `sqlite`, edit the `DB_CONNECTION` in the `.env` file to `sqlite` (you can delete all other `DB` related fields like `DB_HOST`, `DB_PORT`, etc.) and make `database.sqlite` file in the `app/database/` folder.

To create this file, run 
`touch database/database.sqlite`

**NOTE**: By default `sqlite` connection in Laravel is looking for this file, but if you want to change the name of your database, you can override the default by setting `DB_DATABASE` in the `.env` file.

### Set up the application
Next step is to set up the application. For simplicity this app will only have posts, categories and tags.

**TIP**: When creating a model in laravel, if you pass `-a` as argument at the end, it will generate a migration, seeder, factory, and resource controller for the model. Also there are various parameters that you can pass, if you want to see them just run `php artisan make:model --help`.

To create a model for post and all the other files, run 
`php artisan make:model Post -a`

We will do the same for the categories and for the tags.
`php artisan make:model Category -a`
`php artisan make:model Tag -a`

Because one post can have many tags, and one tag can belong to many posts, we are going to need a pivot table between those two models.
`php artisan make:migration create_post_tag_table`

#### Migrations

Ok, now we have everything we need. We can now modify the migrations to suit our needs.

In the posts table we'll have `title`, `body`, `views`, `user_id` so that we can set up a relationship between `Post` and a `User`, and `category_id` because post belongs to a `Category`.
```php
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->integer('views');
    $table->bigInteger('user_id')->unsigned();
    $table->bigInteger('category_id')->unsigned();
    $table->timestamps();

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');

    $table->foreign('category_id')
        ->references('id')
        ->on('categories')
        ->onDelete('cascade');
});
```

`categories` table will only have a name.
```php
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});
```

The `tags` table will also have only a name.
```php
Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});
```

And our `post_tag` pivot table will look like this
```php
Schema::create('post_tag', function (Blueprint $table) {
    $table->primary(['post_id', 'tag_id']);
    $table->bigInteger('post_id')->unsigned();
    $table->bigInteger('tag_id')->unsigned();
    $table->timestamps();

    $table->foreign('post_id')
        ->references('id')
        ->on('posts')
        ->onDelete('cascade');

    $table->foreign('tag_id')
        ->references('id')
        ->on('tags')
        ->onDelete('cascade');
});
```

We can now migrate our tables
`php artisan migrate`

#### Models

Next step is to define the relaions in our models.

Our `Post` model belongs to a `User` and a `Category`, and it have many to many relationship with `Tag`. Let's write that
```php
// ... [namespace and imports]

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}
```

The `User` can have many `Post`
```php
// ... [namespace and imports]

class User extends Authenticatable
{
    use Notifiable;

    // ... [class properties]

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
```

`Tag` belongs to many `Post`
```php
// ... [namespace and imports]

class Tag extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}
```

`Category` can have many `Post`
```php
// ... [namespace and imports]

class Category extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}
```

Now our relationships are all set up.

#### Factories

In order to quickly generate some dummy data, we will make use of Laravel factories that we've created when we made our models. We can find them in the `database/factories` folder. We can see there that we have factory for each model. If you don't know what factories are, you can read more in the [Laravel documentation](https://laravel.com/docs/7.x/seeding#using-model-factories).

In the `categories` table we have only a name column, so the `CategoryFactory` will look like this
```php
// ... [namespace and imports]

$factory->define(Category::class, function (Faker $faker) {
    return [
        'name' => $faker->word,
    ];
});
```

**NOTE**: Laravel factories are using [`$faker`](https://github.com/fzaninotto/Faker) to generate fake data.

The `posts` table must have `user_id` and `category_id` (it belongs to a `User` and it belongs to a `Category`), so we must represent that in the factory. So for each `Post` that we're going to make, we are also going to make a `User` and a `Category` and assign their id to the `posts` table. This is the `PostFactory`
```php
// ... [namespace and imports]

$factory->define(Post::class, function (Faker $faker) {
    return [
        'title' => $faker->word,
        'body' => $faker->text(),
        'views' => $faker->numberBetween(1000, 9999),
        'user_id' => factory(\App\User::class), // make User and assign the id
        'category_id' => factory(\App\Category::class), // make Category and assign the id
    ];
});
```

The `TagFactory`
```php
// ... [namespace and imports]

$factory->define(Tag::class, function (Faker $faker) {
    return [
        'name' => $faker->word,
    ];
});
```

The `UserFactory` is already provided by Laravel, so we don't have to change anything there.

We've made our factories. Now if we want to create a 50 post records, all we need to do is call our post factory
```php
factory(\App\Post::class, 50)->create();
```

**NOTE**: if we want to override some value in the factory, we can do that in the `create()` method, we can pass array of key and values. For example:
```php
factory(\App\Post::class, 2)->create(['title' => 'Some title']);
```
This will create 2 posts that will have Some title as title.

#### Seeders

To seed our database with data, we'll use database seeders. We've created them when we created our model. We can find them in `database/seeds` folder. We have a seeder for each of our models (except for `User` because we didn't create that model, it comes with Laravel buy default). All we need to do is to set the factory we want to run for each seeder. We are only going to make seeder for `Post` and `Tag` models, because in the `PostFactory` we've set the factory so that it'll generate a `User` and a `Category` for each `Post` that we make.

In the `PostSeeder`
```php
// ... [namespace and imports]

class PostSeeder extends Seeder
{
    public function run()
    {
        factory(\App\Post::class, 20)->create();
    }
}
```

In the `TagSeeder`
```php
// ... [namespace and imports]

class TagSeeder extends Seeder
{
    public function run()
    {
        factory(\App\Tag::class, 20)->create();
    }
}
```

And we have to instruct Laravel to use this seeders when seeding the default data. In the `DatabaseSeeder` put this
```php
// ... [namespace and imports]

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // $this->call(UserSeeder::class);
        $this->call(PostSeeder::class);
        $this->call(TagSeeder::class);
    }
}
```

We can now seed our database with data
`php artisan db:seed`

This will create 20 posts, 20 users, 20 categories and 20 tags.

### Making filters

Ok, so now we've come to the most important part of this article, making the filters.

What we want to do here is to filter the posts, so let's begin with that. In my `Post` model I'll add new method `filterBy`, and because we know that we're going to use Laravel query builder, we can make that method local scope. If you don't know what local scope is you can read the documentation about [local scopes](https://laravel.com/docs/7.x/eloquent#local-scopes). 

In a nutshell, it's just a way to add a constraint to the existing query. To set a method as a scope, all we need to do is to prefix it with `scope`. 

So our `filterBy` method will now be `scopeFilterBy` and the first argument of this function is the `$query`, then we can pass whatever we want as an argument. In our case we want to pass all the filters, so our second argument will be `$filters`.

Now in that method I will write how I want my API to look like. Keep in mind that none of this that I'm going to write doesn't exist yet. It's just a way for me to know how I want to interact with my API. And I know that I want to get my filters from the request.

I will have something like this `http://some.url/posts?title=something`, so the filters will be passed as array `[title => something]`
```php
class Post extends Model
{
    public function scopeFilterBy($query, $filters)
    {
        $namespace = 'App\Utilities\PostFilters';
        $filter = new FilterBuilder($query, $filters, $namespace);

        return $filter->apply();
    }
}
```

Basically I want to have a class with a name `FilterBuilder` that will accept `$query`, `$filters` and `$namespace` (this will be the folder in which I will put all my filters) and in that class I'll have a method called `apply()` where I will do the filtering and I'll return the query builder.

Ok, now we know how our API should look like, let's build it.

In the `app` directory create a new folder, I'll name that folder `Utilities`, but you can name it whatever you want, it doesn't matter, in the end it all comes down to preference.

In that folder create `FilterBuilder.php`
```php
namespace App\Utilities;

class FilterBuilder
{
    protected $query;
    protected $filters;
    protected $namespace;

    public function __construct($query, $filters, $namespace)
    {
        $this->query = $query;
        $this->filters = $filters;
        $this->namespace = $namespace;
    }

    public function apply()
    {
        
    }
}
```

Let's think how do we want our filter to work, but we must keep in mind that we want our filter to be made according to SOLID principles. Ultimately I want to make this class, and make sure to never touch it again. We can do that if make the abstraction good enough. At the end what we want this class to do, is to just loop through various classes that represent our filters. That way if we want to add some filter, all we need to do is to make new class. That's it. Let's make that.
```php
// ... [other code in the class]

public function apply()
{
    foreach ($this->filters as $name => $value) {
        $normailizedName = ucfirst($name);
        $class = $this->namespace . "\\{$normailizedName}";

        if (! class_exists($class)) {
            continue;
        }

        if (strlen($value)) {
            (new $class($this->query))->handle($value);
        } else {
            (new $class($this->query))->handle();
        }
    }

    return $this->query;
}
```

Let me explain what's going on in that method:
1. Loop through each filters that we passed (we are going to get the filters from the request as array `[title => something]`)
2. Capitalize the first letter of the name of the filter (`title` -> `Title`) and append it to the provided namespace (`App\Utilities\PostFilters\Title`)
3. Check if that class exist, if not, continue
4. If the class exist, check if `$value` is provided (`?title=something`), 
  + if it is, instantiate the class with the query, and call `handle()` method with the `$value` as parameter
  + if not, instantiate the class and call `handle()` without any parameter (this is for sorting, for example if we want to sort them by popularity `http://some.url/posts?title=something&popular`)
5. Return the query

We have defined how this class should filter. Now we need to make the filters and make them adhere to the same contract.

In the `app/Utilities` make new file that will be our interface `FilterContract.php`
```php
namespace App\Utilities;

interface FilterContract
{
    public function handle($value): void;
}
```

Make new folder in `app/Utilities` called `PostFilters`, that's the namespace that we passed in the `FilterBuilder`, `App\Utilities\PostFilters`, remember?!

In there we can now add our filters, each as separate class. We want to filter our posts by title, ok, we just have to create new file in `app/Utilities/PostFilters` called `Title.php` that will implement `FilterContract` and put our filtering logic in there.
```php
namespace App\Utilities\PostFilters;

use App\Utilities\FilterContract;

class Title implements FilterContract
{
    protected $query;

    public function __construct($query)
    {
        $this->query = $query;
    }

    public function handle($value): void
    {
        $this->query->where('title', $value);
    }
}
```

That's all we have to do, we have to accept the query in the constructor, and make a `handle` method that will filter the posts by some logic. Very simple. And the best part is that if we want to add another filter, all we have to do is to add another class. We can use our filter like this
```php
App\Post::filterBy(request()->all())->get();
```

The url should look something like this `http://some.url/post?title=something`

What if we want to filter posts by category or by tag? No big deal, just make new file `Tag.php` and put your filtering logic there.
```php
namespace App\Utilities\PostFilters;

use App\Utilities\FilterContract;

class Tag implements FilterContract
{
    protected $query;

    public function __construct($query)
    {
        $this->query = $query;
    }

    public function handle($value): void
    {
        $this->query->whereHas('tags', function ($q) use ($value) {
            return $q->where('name', $value);
        });
    }
}
```

Now we can filter posts by tag, just eager load them, and call the `filterBy()`
```php
App\Post::with('tags')->filterBy(request()->all())->get();
```

The url should look something like this `http://some.url/post?tag=some-tag`

That's it, we're done.

Last thing we could do is to extract that repeating logic in the filter classes into abstract class.

Make `QueryFilter.php` file in `app/Utilities` and add only the constructor
```php
namespace App\Utilities;

abstract class QueryFilter
{
    protected $query;

    public function __construct($query)
    {
        $this->query = $query;
    }
}
```

Now `Title` and `Tag` class won't have a consturctor on their own, but they will extend the `QueryFilter` class.

`Title` class
```php
namespace App\Utilities\PostFilters;

use App\Utilities\QueryFilter;
use App\Utilities\FilterContract;

class Title extends QueryFilter implements FilterContract
{
    public function handle($value): void
    {
        $this->query->where('title', $value);
    }
}
```

`Tag` class
```php
namespace App\Utilities\PostFilters;

use App\Utilities\QueryFilter;
use App\Utilities\FilterContract;

class Tag extends QueryFilter implements FilterContract
{
    public function handle($value): void
    {
        $this->query->whereHas('tags', function ($q) use ($value) {
            return $q->where('name', $value);
        });
    }
}
```

And there we go, we now have a working filter. 

If you want to add filter to some other model, just create `app/Utilities/[Model]Filters` folder, and add your filters there. Then make `filterBy` method in your model, and done.

All the code is available on github on this [link](https://github.com/StefanT123/laravel-filter)

Thank you for reading and if you have any questions or if you think that something can be improved, please comment below.