DEV Community

StefanT123
StefanT123

Posted on

Create filters in Laravel with OOP best practices

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.

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.

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});

The tags table will also have only a name.

Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});

And our post_tag pivot table will look like this

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

// ... [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

// ... [namespace and imports]

class User extends Authenticatable
{
    use Notifiable;

    // ... [class properties]

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

Tag belongs to many Post

// ... [namespace and imports]

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

Category can have many Post

// ... [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.

In the categories table we have only a name column, so the CategoryFactory will look like this

// ... [namespace and imports]

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

NOTE: Laravel factories are using $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

// ... [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

// ... [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

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:

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

// ... [namespace and imports]

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

In the TagSeeder

// ... [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

// ... [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.

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]

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

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.

// ... [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

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.

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

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.

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()

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

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

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

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

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

Top comments (1)

Collapse
 
dals profile image
Dalhatu Njidda

This was super useful. Thanks!