Aug 11, 2014
Table of contents:
A common requirement you will hear from a business owner or a client is the ability to “filter” results in some sort of way. For example, you probably never want to show spam comments on blogs posts, or you might want to mark certain users as “deleted” without actually having to delete them from the database.
Another really common requirement is the ability to host multiple clients within the same application. This is called Multi-Tenancy. I’ve already covered Multi-Tenancy in Laravel 4, but there are many ways to achieve this functionality.
In today’s tutorial I want to look at a feature of Doctrine 2 that allows you to filter queries regardless of where the query originated in your application. This means you don’t have to have the same logic in multiple places around your codebase.
A filter is basically a way to add a clause to a query whenever that query is run. This means that the results of the query will automatically be filtered by that clause without you having to specify it.
For example, if you were running a Software as a Service project management application, you might want to always append the following clause to your database queries to ensure that only the current client’s data was returned from the database.
WHERE client_id = :client_id;
Doctrine allows you to add this filter at the SQL level. This means results that do not match your query clause will never be hydrated out of the database, and you don’t have to worry about maintaining the clause in multiple repositories or places around your project.
I think the main benefit of using filters is the ability to define a business rule once, rather than in multiple places around your application.
Taking care of satisfying these kinds of business rules at the right level of abstraction will ensure that inconsistencies do not crop up over time.
It’s very easy to pick up an old project or a new project for the first time and not be aware of what business rules should be in place.
By ensuring filters are placed within the ORM, you don’t have to worry about them being forgotten about in your code.
Arguably the benefit of using filters could also be a drawback.
Business rules should be articulated within the domain of your code, and not your ORM. The ORM is essentially “replaceable”, and so it would probably be a bad practice to encode business critical rules at that level of abstraction.
If that is the case for your project, you might be better off using filters as a way of filtering out certain “bad” data results or deleted entities. Basically as a way of filtering data, but not as a solution to implementing your business rules.
A Doctrine Filter is simply a PHP class that extends Doctrine’s SQLFilter
class.
The class should implement a addFilterConstraint()
method that returns the clause that should be appended to the query.
An excellent example of a Doctrine 2 query can be found in Mitchell van Wijngaarden‘s Laravel-Doctrine package.
<?php namespace Mitch\LaravelDoctrine\Filters;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class TrashedFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $metadata, $table)
{
if ($this->isSoftDeletable($metadata->rootEntityName)) {
return "{$table}.deleted_at IS NULL || NOW() < {$table}.deleted_at";
}
return "";
}
private function isSoftDeletable($entity)
{
return array_key_exists(
"Mitch\LaravelDoctrine\Traits\SoftDeletes",
class_uses($entity)
);
}
}
This Filter allows you to add soft delete functionality to your project. This means you can delete entities in your application, but they won’t really be deleted from the database. This filter will only return entities that have not been “deleted”.
As you can see from the code above, the addFilterConstraint()
method will return a string that will be appended to the database query.
You will also notice that Mitchell is checking to see if the current entity “is soft deletable”:
private function isSoftDeletable($entity)
{
return array_key_exists('Mitch\LaravelDoctrine\Traits\SoftDeletes', class_uses($entity));
}
By default, this filter will be added to all database queries. The method above is checking to see the the SoftDeletes
trait is implemented on the entity.
I think having the Doctrine filter applied to all of your queries will probably make or break the decision as to whether you implement them or not.
If you want do want to filter all queries, or many of your entities, applying a trait as above is a great solution.
However, if you need to filter only a single table, I’m sure there are probably better solutions. As with Doctrine Events, having Doctrine run the filter on all entities is going to be either really important to the functionality or a pretty big headache that you don’t need to introduce to your application.
To add a filter you need to register it with Doctrine’s configuration:
$metadata->addFilter("trashed", "Mitch\LaravelDoctrine\Filters\TrashedFilter");
As you can see, you need to specify a key and the full namespace of the your filter class.
The Soft Deletes example above shows how you can add a filter with a simple clause.
However it is often the case that you need to filter by a specific value such as the current client’s id.
To set a parameter on a filter you can simply get an instance of it from the Entity Manager and use the setParameter()
method:
$filter = $em->getFilters()->getFilter("tenant");
$filter->setParameter("tenant_id", $id);
You can also enable or disable filters. This would be handy if you wanted to create an overall dashboard that collates date from all tenants within your application:
$filter = $em->getFilters()->disable("tenant");
Doctrine filters are a fantastic bit of added functionality, but I don’t think it would make or break your decision to use Doctrine. I think Doctrine filters can be very useful in certain situations, but a bit of a headache in others.
As I mentioned above, I don’t think you should have your business rules encoded in your ORM layer. The business rules of your application are important to your domain, whereas the database is merely an implementation detail.
Use Doctrine filters in situations where you want to add a clause to the majority of your entities. Doctrine filters will be run for every entity, so it doesn’t really make sense to use them for a single entity.
This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.