Query manipulation with open-close principle

solid

Just a week ago I sat down in front of my computer and decided to try to create a common solution for injecting easy pagination. It’s an ordinary problem and I wouldn’t waste my time for doing it every time when I need to display more than 10 or 50 elements on the screen. So, as usually I started with installing Symfony2 and KnpPaginatorBundle, but after a couple of minutes I realized that the implementation can be more general than I assumed at the beginning. I could use the same design also for filtering or manipulating queries…

So, I started coding. It’s really amazing how the code evolve during every hour – I made a draft with injecting `Paginator` service and `RequestStack` into my example `PostRepository` (yes, I know that was a bad idea), then tried to define some manager or data provider to call the repository and pass the result to the paginator, then it really made sense to add filters. I had to add more than one service to my class what couldn’t be done by the constructor, of course if you wouldn’t like to finish with the code like this:

1
2
3
4
5
6
7
8
public function __construct(/** ... **/)
{
    if (func_get_args() != array_filter(func_get_args(), function($element) { return $element instanceof SearchFieldInterface; } )) {
            throw new InvalidArgumentException('One of argument passed to SimpleFieldFilter is not instance of SearchFieldInterface');
    }
 
    $this->searchFields = func_get_args();
}

And then a fellow sitting next to me asked why I don’t use tags and Compiler Passes. Well, that was right to the point and these last change closed the architecture.

The result

I will start from the results, because as I believe it’s the most interesting part. I have published a bundle with proposed solution for manipulation queries in Doctrine and Symfony2 (https://github.com/piotrpasich/symfony-search-solution).

So, the purpose of architecture is to create a really simple method to add services which should be able to manipulate queries. To add a new filter to the query you only need to define a new tagged service in `services.yml` file like:

1
2
3
4
5
    pp_acme.post.search.field.content:
        class: %pp_acme.post.search.like_field.class%
        arguments: ["p", "content"]
        tags:
            - { name: pp_acme.post.filter.field }

The most important part in the configuration is the tag with name: pp_acme.post.filter.field . The Compiler Pass will recognize if the service should be included as a query filter by this tag and add this object to the query listener. I have created an interface for simple classes called `SearchFieldInterface` which can be added to `PostFilterListenerInterface` by `addSearchField` method.

The heart

The heart of this solution is `PostProvider` class. It builds the query from PostRepository and pass to injected `EventDispatcher`.

There is one more injected interface in `PostProvider` – QueryResultInterface. I have prepared two implementations – first which returns results directly from Doctrine (BasicQueryResult) and second which returns paginated results (PaginationQueryResult).

The following code presents an example method providing all data from database but eventually filtered by `pp.post.preQuery` listeners.

1
2
3
4
5
6
7
8
9
public function findAllBy(Post $searchData)
{
    $query = $this->postRepository->createQueryBuilder('p');

    $event = new PostEvent($query, $searchData);
    $this->eventDispatcher->dispatch('pp.post.preQuery', $event);

    return $this->queryResult->getResult($query);
}

Filters in listeners

Open/close principle says that the class should be open for extension, but closed for modification and putting filters into event listeners keeps this perfectly. So, lets take a look at the one of filter classes. Of course we can define some other kind of query modifier, not only the filter if we want to.

The class should be open for extension, but closed for modification

As I wrote at the beginning I have used the `CompilerPass` to incject tagged filtering classes to a proper listener – `SimpleFieldFilter`. This class has only two methods – first to add new search fields objects, and the second to run them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SimpleFieldFilter implements PostFilterListenerInterface
{
    /**
     * @var SearchField\SearchFieldInterface[]
     */

    protected $searchFields = array();

    public function addSearchField(SearchFieldInterface $searchField)
    {
        $this->searchFields[] = $searchField;
    }

    public function filter(PostEvent $postEvent)
    {
        $searchData = $postEvent->getSearchData();

        foreach ($this->searchFields as $searchField) {
            $searchDataMethodName = "get{$searchField->getFieldName()}";

            if ($searchData->$searchDataMethodName()) {
                $searchField->addConditions($postEvent->getQuery(), $searchData->$searchDataMethodName());
            }
        }
    }
}

I think the `filter` method might be refactorized, but it catch the idea (I’ll show how you can change it in next post).

1
2
3
4
5
6
7
8
9
10
11
12
class LikeSearchField extends SearchFieldAbstract
{

    public function addConditions(QueryBuilder $queryBuilder, $value)
    {
        $queryBuilder->andWhere("{$this->getTableAlias()}.{$this->getFieldName()} LIKE :{$this->getFieldName()}")
                     ->setParameter($this->getFieldName(), '%' . $value . '%');

        return $queryBuilder;
    }

}

Conclusion

So, now I’m able to add, modify and paginate queries really quickly. Moreover, I can extend this solution by adding and tagging new services what is really comfortable, because I don’t need to modify any of my classes and this code could be stored in a new bundle (except the Event and adding it to the Dispatcher).

Of course, every solution has own weakness, but the most important thing is to discuss this with others, explore, compare and develop the code, so share your solution.

1 Comments

Leave a Comment.