Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Filtering / Searching

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Designing how to Filter

Paginated a big collection is a must. But you might also want a client to be able to search or filter that collection. Ok, so how do we search on the web? Well usually, you fill in a box, hit submit, and that makes a GET request with your search term as a query parameter like ?q=. The server reads that and returns the results.

I have an idea! Let's do the exact same thing! First, we will of course add a test. Add a new programmer at the top of the pagination test with $this->createProgrammer(). I want to do a search that will not return this new programmer, but still will return the original 25. To do that, give it a totally different nickname, like 'nickname' => 'willnotmatch'. Keep the avatar number as 3... because we don't really care:

... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
... lines 8 - 71
public function testGETProgrammersCollectionPaginated()
{
$this->createProgrammer(array(
'nickname' => 'willnotmatch',
'avatarNumber' => 5,
));
... lines 78 - 120
}
... lines 122 - 226
}

For the query parameter, use whatever name you want: how about ?filter=programmer:

... lines 1 - 85
// page 1
$response = $this->client->get('/api/programmers?filter=programmer');
... lines 88 - 228

If you're feeling fancy, you could have multiple query parameters for different fields, or some cool search syntax like on GitHub. That's all up to you - the API will still work exactly the same.

Filtering the Collection

Great news: it turns out that this is going to be pretty easy. First, get the filter value: $filter = $request->query->get('filter');. Pass that to the "query builder" function as an argument. Let's update that to handle a filter string:

... lines 1 - 18
class ProgrammerController extends BaseController
{
... lines 21 - 76
public function listAction(Request $request)
{
$filter = $request->query->get('filter');
$qb = $this->getDoctrine()
->getRepository('AppBundle:Programmer')
->findAllQueryBuilder($filter);
... lines 84 - 89
}
... lines 91 - 189
}

In ProgrammerRepository, add a $filter argument, but make it optional:

... lines 1 - 8
class ProgrammerRepository extends EntityRepository
{
... lines 11 - 28
public function findAllQueryBuilder($filter = '')
{
... lines 31 - 38
}
}

Below, set the old return value to a new $qb variable. Then, if ($filter) has some value, add a where clause: andWhere('programmer.nickname LIKE :filter OR programmer.tagLine LIKE filter'). Then use setParameter('filter' , '%'.$filter.'%'). Finish things by returning $qb at the bottom:

... lines 1 - 8
class ProgrammerRepository extends EntityRepository
{
... lines 11 - 28
public function findAllQueryBuilder($filter = '')
{
$qb = $this->createQueryBuilder('programmer');
if ($filter) {
$qb->andWhere('programmer.nickname LIKE :filter OR programmer.tagLine LIKE :filter')
->setParameter('filter', '%'.$filter.'%');
}
return $qb;
}
}

If you were using something like Elastic Search, then you wouldn't be making this query through Doctrine: you'd be doing it through elastic search itself. But the idea is the same: prepare some search for Elastic, then use an Elastic Search adapter with Pagerfanta.

And that's all there is to it! Re-run the test:

./bin/phpunit -c app --filter filterGETProgrammersCollectionPaginated

Oooh a 500 error: let's see what we're doing wrong:

Parse error, unexpected '.' on ProgrammerRepository line 38.

Ah yes, it looks like I tripped over my keyboard. Delete that extra period and run this again:

./bin/phpunit -c app --filter filterGETProgrammersCollectionPaginated

Hmm, it's still failing: this time when it goes to page 2. To debug, let's see what happens if we comment out the filter logic and try again:

./bin/phpunit -c app --filter filterGETProgrammersCollectionPaginated

Now it fails on page 1: that extra willnotmatch programmer is returned and that makes index 5 Programmer4 instead of Programmer5. When we put the filter logic back, it has that exact same problem on page 2. Can you guess what's going on here? Yeas! We're losing our filter query parameter when we paginate through the results. womp womp.

Don't Lose the Filter Parameter!

In the test, the URL ends in ?page=2 with no filter on it. We need to maintain the filter query parameter through our pagination. Since we have everything centralized in, PaginationFactory that's going to be easy. Add $routeParams = array_merge() and merge $routeParams with all of the current query parameters, which is $request->query->all(). That should take care of it:

... lines 1 - 10
class PaginationFactory
{
... lines 13 - 19
public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array())
{
... lines 22 - 33
$paginatedCollection = new PaginatedCollection($programmers, $pagerfanta->getNbResults());
// make sure query parameters are included in pagination links
$routeParams = array_merge($routeParams, $request->query->all());
... lines 38 - 56
}
}

Run the tests one last time:

./bin/phpunit -c app --filter filterGETProgrammersCollectionPaginated

And we're green for filtering!

Leave a comment!

3
Login or Register to join the conversation
Nizar Avatar

Hello,
it is possible to do a course on elasticsearch symfony
have a good day

Reply
Claire Avatar

Hi, I was wondering for a use case were there will be 10+ filters a user will be able to search by. The url could be something
myurl.com?name=test&capacit...
Instead of writing out 10+ andWhere() statements i was wondering it there a way to pass a variable into the andWhere() statement. Something like andWhere('$filters LIKE :filter') however i have trouble as the single quotes are needed as part of the query syntax and ive tried concatenating the varible to the query but that didn't work either.
Wondering if you have any suggestions ?

Cheers
Claire

Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 4 years ago

So it is ok to have logic in repositories? Like if ($filter) { add some filtering } I was thinking about it long time and did not know the answer. From the testability perspective - we do not unit test repositories, and so less logic would be better. But we can still test them with the tests like you write - which call whole api endpoint.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST and serialization are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*", // 0.13.0
        "white-october/pagerfanta-bundle": "^1.0" // v1.2.4
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}
userVoice