StefanT123
Posted on March 31, 2020
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:
- Loop through each filters that we passed (we are going to get the filters from the request as array
[title => something]
) - Capitalize the first letter of the name of the filter (
title
->Title
) and append it to the provided namespace (App\Utilities\PostFilters\Title
) - Check if that class exist, if not, continue
- 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 popularityhttp://some.url/posts?title=something&popular
)
- if it is, instantiate the class with the query, and call
- 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.
Posted on March 31, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024
November 21, 2024