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

Filters

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 $6.00

I setup my fixtures so that about half of my FortuneCookies have a "discontinued" value of true. For our startup fortune cookie company, that means we don't make them anymore. But we're not showing this information anywhere on the frontend yet.

But what if we wanted to only show fortune cookies on the site that we're still making? In other words, where discontinued is false. Yes yes, I know. This is easy. We could just go into CategoryRepository and add some andWhere() calls in here for fc.discontinued = true.

But what if we wanted this WHERE clause to be added automatically, and everywhere across the site? That's possible, and it's called a Doctrine Filter.

Creating the Filter Class

Let's start by creating the filter class itself. Create a new directory called Doctrine. There's no real reason for that, just keeping organized. In there, create a new class called DiscontinuedFilter, and make sure we put it in the right namespace:

<?php
namespace AppBundle\Doctrine;
class DiscontinuedFilter
{
}

That's a nice blank class. To find out what goes inside, Google for "Doctrine Filters" to get into their documentation. These filter classes are simple: just extend the SQLFilter class, and that'll force us to have one method. So let's do that - extends SQLFilter. My IDE is angry because SQLFilter has an abstract method we need to add. I'll use PHPStorm's Code->Generate shortcut and choose "Implement Methods". It does the work of adding that addFilterConstraint method for me. And for some reason, it's extra generous and gives me an extra ClassMetadata use statement, so I'll take that out.

... lines 1 - 2
namespace AppBundle\Doctrine;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class DiscontinuedFilter extends SQLFilter
{
/**
* Gets the SQL query part to add to a query.
*
* @param ClassMetaData $targetEntity
* @param string $targetTableAlias
*
* @return string The constraint SQL if there is available, empty string otherwise.
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
// ...
}
}

Ok, here's how this works. If this filter is enabled - and we'll talk about that - the addFilterConstraint() method will be called on every query. And this is our chance to add a WHERE clause to it. The $targetEntity argument is information about which entity we're querying for. Let's dump that to test that the method is called, and to see what that looks like:

... lines 1 - 7
class DiscontinuedFilter extends SQLFilter
{
... lines 10 - 17
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
var_dump($targetEntity);die;
}
}

Adding the Filter

Next, Doctrine somehow has to know about this class. If you're using Doctrine outside of Symfony, you'll use its Configuration object and call addFilter on it:

// from http://doctrine-orm.readthedocs.org/en/latest/reference/filters.html#configuration
$config->addFilter('locale', '\Doctrine\Tests\ORM\Functional\MyLocaleFilter');

You pass it the class name and some "key" - locale in their example. This becomes its nickname, and we'll refer to the filter later by this key.

In Symfony, we need the same, but it's done with configuration. Open up app/config/config.yml and find the doctrine spot, and under orm, add filters:. On the next line, go out four spaces, make up a key for the filter - I'll say fortune_cookie_discontinued and set that to the class name: AppBundle\Doctrine\DiscontinuedFilter:

... lines 1 - 46
doctrine:
... lines 48 - 62
orm:
... lines 64 - 65
filters:
fortune_cookie_discontinued: AppBundle\Doctrine\DiscontinuedFilter
... lines 68 - 76

Awesome - now Doctrine knows about our filter.

Enabling a Filter

But if you refresh the homepage, nothing! We do not hit our die statement. Ok, so adding a filter to Doctrine is 2 steps. First, you say "Hey Doctrine, this filter exists!" We just did that. Second, you need to enable the filter. That ends up being nice, because it means you can enable or disable a filter on different parts of your site.

Open up FortuneController. Let's enable the filter on our homepage. Yes yes, we are going to enable this filter globally for the site later. Just stay tuned.

To enable it here, first, get the EntityManager. And I'm going to add a comment, which will help with auto-completion on the next steps:

... lines 1 - 10
class FortuneController extends Controller
{
... lines 13 - 15
public function homepageAction(Request $request)
{
/** @var EntityManager $em */
$em = $this->getDoctrine()->getManager();
... lines 20 - 36
}
... lines 38 - 67
}

Once you have the entity manager, call getFilters() on it, then enable(). The argument to enable() needs to be whatever nickname you gave the filter before. Actually, I have a typo in mine - I'll fix that now. Copy the fortune_cookie_discontinued string and pass it to enable():

... lines 1 - 15
public function homepageAction(Request $request)
{
/** @var EntityManager $em */
$em = $this->getDoctrine()->getManager();
$em->getFilters()
->enable('fortune_cookie_discontinued');
... lines 22 - 36
}
... lines 38 - 69

Filter class, check! Filter register, check! Filter enabled, check. Moment of truth. Refresh! And there's our dumped ClassMetadata.

Adding the Filter Logic

We haven't put anything in DiscontinuedFilter yet, but most of the work is done. That ClassMetadata argument is your best friend: this is the Doctrine object that knows everything about the entity we're querying for. You can read your annotation mapping config, get details on associations, find out about the primary key and anything else your heart desires.

Now, this method will be called for every query. But we only want to add our filtering logic if the query is for a FortuneCookie. To do that, add: if, $targetEntity->getReflectionClass() - that's the PHP ReflectionClass object, ->name() != AppBundle\Entity\FortuneCookie, then we're going to return an empty string:

... lines 1 - 7
class DiscontinuedFilter extends SQLFilter
{
... lines 10 - 17
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->getReflectionClass()->name != 'AppBundle\Entity\FortuneCookie') {
return '';
}
... lines 23 - 24
}
}

It's gotta be an empty string. That tells Doctrine: hey, I don't want to add any WHERE clauses here - so just leave it alone. If you return null, it adds the WHERE but doesn't put anything in it.

Below this, it's our time to shine. We're going to return what you want in the WHERE clause. So we'll use sprintf, then %s. This will be the table alias - I'll show you in a second. Then, .discontinued = false. This is the string part of what we normally put in an andWhere() with the query builder. To fill in the %s, pass in $targetTableAlias:

... lines 1 - 17
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->getReflectionClass()->name != 'AppBundle\Entity\FortuneCookie') {
return '';
}
return sprintf('%s.discontinued = false', $targetTableAlias);
}
... lines 26 - 27

Remember how every entity in a query has an alias? We usually call createQueryBuilder() and pass it something like fc. That's the alias. In this case, Doctrine is telling us what the alias is so we can use it.

Alright. Refresh! Um ok, no errors. But it's also not obvious if this is working. So look at the number of fortune cookies in each category: 1, 2, 3, 3, 3, 4. Go back to FortuneController and delete the enable() call. Refresh again. Ah hah! All the numbers went up a little. Our filter is working.

Put the enable() call back and refresh again. Click the database icon on the web debug toolbar. You can see in the query that when we LEFT JOIN to fortune_cookie, it added this f1_.discontinued = false.

Woh woh woh. This is more amazing than I've been promising. Even though our query is for Category's, it was smart enough to apply the filter when it joined over to FortuneCookie. Because of this, when we call Category::getFortuneCookies(), that's only going to have the ones that are not discontinued. The filter is applied if the fortune cookie shows up anywhere in our query.

Passing Values to/Configuring a Filter

Sometimes, like in an admin area, we might want to show only discontinued fortune cookies. So can we control the value we're passing in the filter? To do this, remove false and add another %s. Add another argument to sprintf: $this->getParameter('discontinued'):

... lines 1 - 17
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
... lines 20 - 23
return sprintf('%s.discontinued = %s', $targetTableAlias, $this->getParameter('discontinued'));
}
... lines 26 - 27

This is kind of like the parameters we use in the query builder, except instead of using :discontinued, we concatenate it into the string. But wait! Won't this make SQL injection attacks possible! I hope you were yelling that :). But with filters, it's ok because getParameter() automatically adds the escaping. So, it's no worry.

If we just did this and refreshed, we've got a great error!

Parameter 'discontinued' does not exist.

This new approach means that when we enable the filter, we need to pass this value to it. In FortuneController, the enable() method actually returns an instance of our DiscontinuedFilter. And now we can call setParameter(), with the parameter name as the first argument and the value we want to set it to as the second:

... lines 1 - 15
public function homepageAction(Request $request)
{
/** @var EntityManager $em */
$em = $this->getDoctrine()->getManager();
$filters = $em->getFilters()
->enable('fortune_cookie_discontinued');
$filters->setParameter('discontinued', false);
... lines 23 - 68
}

Refresh! We see the slightly-lower cookie numbers. Change that to true and we should see really low numbers. We do!

Enabling a Filter Globally

Through all of this, you might be asking: "What good is a filter if I need to enable it all the time." Well first, the nice thing about filters is that you do have this ability to enable or disable them if you need to.

To enable a filter globally, you just need to follow these same steps in the bootstrap of your app. To hook into the beginning process of Symfony, we'll need an event listener.

I did the hard-work already and created a class called BeforeRequestListener:

... lines 1 - 7
class BeforeRequestListener
{
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function onKernelRequest(GetResponseEvent $event)
{
// ...
}
}

For Symfony peeps, you'll recognize the code in my services.yml:

services:
before_request_listener:
class: AppBundle\EventListener\BeforeRequestListener
arguments: ["@doctrine.orm.entity_manager"]
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest

It registers this as a service and the tags at the bottom says, "Hey, when Symfony boots, like right at the very beginning, call the onKernelRequest method." I'm also passing the EntityManager as the first argument to the __construct() function. Because, ya know, we need that to enable filters.

Let's go steal the enabling code from FortuneController, take it all out and paste it into onKernelRequest. Instead of simply $em, we have $this->em, since it's set on a property:

... lines 1 - 14
public function onKernelRequest(GetResponseEvent $event)
{
$filter = $this->em
->getFilters()
->enable('fortune_cookie_discontinued');
$filter->setParameter('discontinued', false);
}
... lines 22 - 23

Let's try it! Even though we took the enable() code out of the controller, the numbers don't change: our filter is still working. If we click into "Proverbs", we see only 1. But if I disable the filter, we see all 3.

That's it! You're dangerous. If you've ever built a multi-tenant site where almost every query has a filter, life just got easy.

Leave a comment!

46
Login or Register to join the conversation
Default user avatar
Default user avatar Lee Ravenberg | posted 5 years ago

If you use the Gedmo SoftDeletable filter, enabling and disabling it will not prevent the event listener to do its work. An issue has been created: https://github.com/Atlantic...

20 Reply
Almo Avatar

Note that getParameter returns a quoted string! A workarround regarding nullable values would be something like:

$expression = 0 == substr($this->getParameter('is_duplicate'), 1, -1) ? 'IS NULL' : 'IS NOT NULL';

return sprintf('%s.is_duplicate %s', $targetTableAlias, $expresion);
1 Reply
Default user avatar
Default user avatar Miryafa | posted 5 years ago

For anyone who just looked at this page without doing the rest of the course up to this point (as with me) and/or aren't using Symfony: You can access the current configuration with the following two lines:

$em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$config = $em->getConfiguration();

And you just put those lines in the indexAction of your controller (or whichever action you want). So the $config line would look like this:

public function indexAction()
{

// ... other code ...

// from http://doctrine-orm.readthe...
$em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$config = $em->getConfiguration();
$config->addFilter('locale', '\Doctrine\Tests\ORM\Functional\MyLocaleFilter');

// ... other code ...

}

1 Reply
Default user avatar
Default user avatar Tomáš Votruba | posted 5 years ago

If you prefer using only Filter class and keep Controllers clean, you might like this package https://github.com/Symplify...

1 Reply
Default user avatar
Default user avatar Tomáš Votruba | weaverryan | posted 5 years ago

Thanks!

Reply
Default user avatar
Default user avatar Simon Carr | posted 5 years ago

I need my filters to be enabled based on the role the user has. We have some data that due to commercial restrictions some users should NOT be able to see.

How do I access the currently logged in users roles in the filter class?

1 Reply

Yo Simon Carr!

When you enable your filter (like how we do it in an event listener), pass in the security.authorization_checker service as a "parameter", much like we do with the "discontinued" parameter in our example. You should then be able to use that check for security roles with the normal isGranted in the filter. Or, if you know exactly what roles to check, you could do the role-checking in your even listener, and then pass some other simpler "parameters" to help tell the filter how it should behave.

Hope that helps! Cheers!

1 Reply
Default user avatar

Many Thanks,

I did manage to find the answer in the end, just before you posted. What was confusing me for quite a while was that all the posts on Stack Exchange talk about injecting the tokenStorage or SecurityContext which just did not work for me. May be they are Symfony 2.x only. Any way security.authorization_checker worked just fine and in just a few minutes I had filters controlled via ROLE membership. Fantastic.

Reply

Hey Simon Carr!

Ah, nice debugging then! And you're right - SecurityContext is a Symfony 2 thing, and TokenStorage is the way to get the User, but not check roles (though sometimes people abuse the $user->getRoles() function). Anyways, you have the right way - super happy it's working :).

Cheers!

Reply

It will be great to add a new screencast about Doctrine Criteria to this tutorial. I think they awesome too! :)
What do you think, Ryan?

1 Reply

victor maybe...? :) I've never used them and avoid them on purpose because they always strike me as a really complicated way of doing something that could be done very easily inside a DQL string. So, instead of all the expression/criteria stuff (example http://stackoverflow.com/qu..., I'll just put what I need inside of an andWhere() string, like we do here: https://knpuniversity.com/s....

That's subjective - but it looks much easier to me than the Criteria stuff. But there may also be a use-case that can only be done with Critiera?

Cheers!

Reply

Wow.. :) I agree with you, Criteria more complex at first look and the beginners hard to use it. However, I could use criteria without custom entity repositories for minor things in any controller calling `matching()` method on entity repository. It's allowing me to avoid mixing layers. I mean using query builder in controllers is a wrong way because it mixing DB layer with controller business logic layer that violates MVC pattern, so this is a bad approach. Creating entity repository with many custom methods in some cases could be overhead. So in this cases criteria could help as well.

Also I like to easily apply criteria to already fetched Doctrine array collections calling `matching()` method on it. It's allowing me filtering any collection and avoid sending a new query to database again (filtering on PHP collection level).

Summing up, I think about *doctrine criteria* like about *array criteria* on steroids that I could passed to `fetchBy` and `fetchOneBy` methods :) It allowing me to use negotiation (NOT), partial matching (LIKE), checking for NULL and OR logical operations, >, < operators etc. that I can get directly in controller and that not supported by `fetchBy` and `fetchOneBy` methods out-of-the-box.

Reply

Criteria are a bit more interesting than I thought - I will admit I like the fluid interface that can be used to either make a query (with matching) or filter a collection (though I want to minimize doing this sort of thing).

If I start finding uses for it in my code, then I may add a chapter on it. It looks like a nice feature - but I'm not convinced yet that I'll have a pattern where I use it. Your point about using them in controllers is valid, but I still think I'll create custom repo methods. There's a lot of personal preference down at this level.

Cheers!

Reply
Markchicobaby Avatar
Markchicobaby Avatar Markchicobaby | posted 3 months ago

Hi team, I know this is an old lecture, but I'm wondering if the technique to use Doctrine filters in this way can still be used in Symfony 6.2? (do you have a recent tutorial using them?) If not, is there a new/recommended approach? Thank you!

Reply

Hey @Markchicobaby ,

We do have a new tutorial - it's in the finish phase before we start releasing it: https://symfonycasts.com/screencast/doctrine-queries - you can subscribe to it and we will notify you when it will be available, but it should happen pretty soon.

About your question about Doctrine filters - yes, it should work the same way... IIRC the interface only requires you to add some return types to match its signature. And also, registering a listener to enable it globally is also redundant now, it can be done via config now :) we will cover this in that tutorial I linked :)

Cheers!

1 Reply
Markchicobaby Avatar
Markchicobaby Avatar Markchicobaby | Victor | posted 3 months ago

Great I look forward to it! I have actually got it going but I'm also wondering if we could use the automatic EntityValueResolver and MapEntity... I look forward to learning more about them, hope they are also covered 😃

Reply
Default user avatar
Default user avatar Yamen Imad Nassif | posted 5 years ago

I loved this tutorial, but i've got a problem, i have like around 10 tables all have client_id which i set into session after a user login the problem is when i set the filter globally the client_id will not be in the users table and some other tables, but i need this filter to be applied to a number of tables only. how can i achieve this ?

Reply

Hey Yamen Imad Nassif!

Ah, cool! So, I have an idea :). Well, first, of course, you can use an if statement like this - https://knpuniversity.com/screencast/doctrine-queries/filters#adding-the-filter-logic - to check for all of your 10 classes. That's super simple, but manual, and laborious.

So, I have 2 ideas:

1) If you don't like that, then I would recommend creating an interface (e.g. ClientEntityInterface - it wouldn't even need to have any methods in it). Then, check for this interface in the filter:


$interfaces = class_implements($targetEntity->getReflectionClass()->name);

if (!isset($interfaces['App\Doctrine\ClientEntityInterface'])) {
    return;
}

// apply the filter

2) The second idea is to look at the class metadata itself to see if there is a client property that is a relationship. I don't have the exact code for this, but here's what you should do: dump($targetEntity);die; in your filter. You'll find that this object knows everything about the fields on every entity. You can use this to see if there is a client property, and if it is a relationship. If this property exists, apply the filter! If not, do nothing.

I hope that helps!

Cheers!

Reply
Default user avatar
Default user avatar Yamen Imad Nassif | weaverryan | posted 5 years ago

Thanks for your ideas, i applied the first idea actually but its still (dirty fix) i liked the interface one much better! so i am going to do it ;)
if(
$targetEntity->getTableName() == 'table' ||
$targetEntity->getTableName() == 'table2' ||
$targetEntity->getTableName() == 'table3' ||
$targetEntity->getTableName() == 'table4' ||
$targetEntity->getTableName() == 'table5'
){
return true;
}
return $targetTableAlias.'.'.Utils::CLIENT_ID_NAME.' = '. $this->getParameter(Utils::CLIENT_ID_NAME);

this is how i've done it for now, a let's say further question: this tutorial or idea is about querying or reading from database, how can we do the other way around? add an insert value to each/some insert/writing queries

Reply

Hey Yamen Imad Nassif

Could you explain a little bit about your use case? I'm not sure if you can achieve that with filters because filters are for filtering records

Reply
Default user avatar
Default user avatar Yamen Imad Nassif | MolloKhan | posted 5 years ago

Hi @Diego

Sorry i just saw your comment,

The idea is lets say you have an application which works for a specific company (they have their own users tables etc etc) and then you want to make the application for more than 1 company by changing colors and logos of the app as well you need to do for the users, so user1 from company1 will access with color1 logo1 and then have jobs1 and user2 from company2 will access with color2 logo2 and then have jobs2. And all of that should be regarding to the subdomain company1.website.com and company2.website.com . In order to do so you will need to do one of the following either duplicate your code over multi domains/subdomains and then have multi dbs which will cause a headache if you want to update your app -of course for 1 or 2 companies its fine but go for 1000 its gonna ba a hassle to update your code and manage all your domains/subdomains- so the idea was to filter every single query to the database by a client_id (company#) which will be added to the session after a user logs in in this case you dont have to worry about job1 or job2 or even color1 or color2 its all gonna be autodetected by the user login information.

So acutally applying the filters worked and it was nice, but there was a problem since every user have as well a client_id so you can tell to which company he belongs the problem was filtering only after the user logs in, so the filter had to be enabled only after the log in process so the previous code did it as well ^^

Reply

Alright, so the filtering worked as a charm. I believe you may find useful the "Blameable" Doctrine extension, so you can automatically attach the client_id to your records
https://github.com/Atlantic...

Cheers!

Reply
Default user avatar
Default user avatar Tomáš Votruba | posted 5 years ago

Recently I wrote and article about decoupled Filters and how to build them:
http://www.tomasvotruba.cz/...

It's easy if you try :)

Reply
avknor Avatar

Filters are grate! But I can't find any info about how to set one filter to one query many times with different parameters ((
Please help me if you can. Deadline is killing me ))

I made Q at Stackoverflow http://stackoverflow.com/qu...

Reply

Hey!

I'm not sure off the top of my head - but it looks like you got a reply on SO that makes sense to me. Let us know if it works out!

Cheers!

Reply
avknor Avatar

This is not actualy what I wanted. Let say: what if I wanted to filter by few values of field? For records of 2015 and 2016 years for example... So i need to set some sort of array-of-years in ->setParameter. But it want only strings! And sends an error when i'm trying to set something else.
How do you solve this?

Or even more complicated example. What if I need to filter by relational field. In this case I need to set entity as param of filter. Or even ArrayCollection of entities!

For now I'm decide it like this: I json_encode array before set it to setParameter in controller. And then I json_encode it in Filter class. BUT! There in Filter class I need to make one more step. I need to remove single and doublequotes from json string. Because they was added by setParameter to escape string (thats why we love it )) ).

Code hacks like this we call "crutches" here in Russia. So I'd like to avoid of them and write more elegant code )

Reply

Hey avknor!

Ok, let's see if we can work this out :).

First, some background - which I'm pretty sure you understand - but in case it's useful for others in the future :)

When you use a filter, what you're actually doing is writing raw SQL that will be applied to your query - i.e. you are not writing the same type of code that you might put into a query builder (where we're building DQL, which is a little friendlier). For example, in DQL, you can set a parameter to an array for a WHERE IN statement, and when Doctrine translates this to the SQL string, it properly takes that array and implodes it as expected. But, that won't happen in a filter: ultimately what we need to return from addFilterConstraint() is a raw SQL string.

In fact, if you dig into the core of Doctrine, here's what the (summarized) code looks like that calls your filter:


$filterClauses = array();
foreach ($filters as $filter) {
    if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
        $filterClauses[] = '(' . $filterExpr . ')';
   }
}

// this is ultimately added to the raw SQL string, after the WHERE
return implode(' AND ', $filterClauses);

Now, you tried to setParameter() and pass it an array... which makes perfect sense. The reason this doesn't work is simply because Doctrine apparently doesn't support this - you're not doing anything wrong. Here's the related issue: https://github.com/doctrine/doctrine2/issues/2624 and pull request to add the feature that was never merged (https://github.com/doctrine/doctrine2/pull/1168). So, hopefully that at least makes you feel better - you will need some sort of a hack to make this happen.

About the relational field comment, in that case, since we're ultimately building raw SQL, you'd just set the id of the related entity via setParameter() (not the entire object) or an array of ids, via the hack - or "crutches" as you call it - I like that term... but don't like that it's necessary :).

Let me know if this helps! This was a shortcoming of Doctrine filters that I honestly wasn't aware of - the use-case makes perfect sense, but I had never hit it myself.

Oh, and by the way, the Gedmo DoctrineExtensions library has an interesting SoftDeletableFilter for 2 reasons:

1) They don't use getParameter(), and instead use some low-level methods to do some quoting. They don't use it here, but they could also manually quote a value by using $conn->quote().

2) They have a $disabled array property (which is not meant to be a parameter, but is still configuration) and they allow you to set this via a few public methods. What I mean is, in your case, you could actually avoid setParameter() and instead call your own public method on your filter to set the array of ids. This is still a little bit of a "crutch" but less than needing to json_encode and then remove quotes afterwards.

Cheers!

1 Reply
avknor Avatar
avknor Avatar avknor | weaverryan | posted 5 years ago | edited

Thanks for your help weaverryan !

Reply

Hi Ryan, great video as usual! Many things learned on the journey. I have still two questions that relate to filtering:

1. can we fetch objects (not arrays) with "extra" columns using DQL? For example, an object "Category" with the property "totalCookies" automatically hydrated?
2. if we can, can we also "declare" this "automatic hydration" in annotations (or yml/xml)?

Thanks,
Pietro

Reply

Hi Pietrino!

To be honest, I've never tried this before! But as far as I can tell, this is not possible. The only possibility I can think of is with a custom hydrator. That looks possible, but might not be worth it. I think part of the issue would be that if you were able to put this in annotations (e.g. something attached to the Category entity), Doctrine might not be able to perform the query needed to do that count. For example, counting "totalCookies" for a Category would require a join. So Doctrine would need you to add that join, it would need to make it automatically, or it would need to do a separate query. And the last option is already sort of possible - e.g. count($category->getCookies()) - which will be done by doing a fast COUNT() query if you use the extra lazy association: http://doctrine-orm.readthe...

It's possible I'm over-looking something in Doctrine, but this is what I know :).

Cheers!

Reply

:))) Maybe I'm trying to do something strange, but what I'd like to accomplish is something like this: I want to view a list of all "parent" classes, with a "count" of all their children (such as "users" and their "posts"). Now, I think the choices I have are:

1. hydrate arrays (just as you made in chapter "Selecting specific fields";
2. perform multiple queries (1 + 1*"number of parents") to retrieve all the children of each parent;
3. create a custom hydrator;
4. use joins.

Cons:
1. I will miss the objects logic;
2. I will kill performances because of queries;
3. seems overkilling, looking at the Doctrine docs;
4. I will kill performances if children hydrating tons of objects

Any idea?

Reply

I think you've summarized the options (that I know of) nicely:

1. I agree with your con
2. I think you're over-estimating the performance loss. And if you hydrate "extra lazy" and do a count of the children, the extra query will be done automatically.
3. Agree its over-kill
4. Yea, this may be slower than option (2)

So, I would do 2 - I'm certain it's not that big of a deal. And if it were, I'd start to employ other strategies - e.g. caching. The only exception is if I were processing MANY rows (e.g. making a CSV download) where I needed to keep memory managed. In that case, I'd probably be doing a low-level query (e.g. something like option 1, but with a Doctrine "iterator").

Cheers!

Reply
Default user avatar
Default user avatar Stéphane Ratelet | posted 5 years ago | edited

Thank you so much for this tutorial to discover the filters, exactly what I was looking for. Bravo.

On comment for Symfony users, you can enable the filters globally without "kernel.event_listener". You can activate from the config.yml. See the example below:

[app/config/config.yml]


doctrine:
    orm:
        filters:
            fortune_cookie_discontinued:
                class: AppBundle\Doctrine\DiscontinuedFilter
                enabled: true

It is also possible to pass simple parameters, everything is explained in this webpage : https://symfony.com/doc/current/bundles/DoctrineBundle/configuration.html#filters-configuration

Reply

Ah, thanks for the post Stéphane Ratelet! I completely missed these configuration options - they're MUCH better :). I also updated your formatting so this is even easier for others to read.

Cheers!

Reply
Default user avatar
Default user avatar Stéphane Ratelet | weaverryan | posted 5 years ago

Thank you. Good job. Stef.

Reply
Andrzej S. Avatar
Andrzej S. Avatar Andrzej S. | posted 5 years ago

How can I use more then one parameters in Filter. Do I need to add another filter in config.yml with the same class name of filter?

Reply

Hey Monika!

Hmm, let's see. So, in this chapter, we have just one parameter: discontinued. Suppose we need another parameter - e.g. "discontinuedDate" (just for an example). You should be able to do this, simply by setting both parameters:


$filter = $this->em
    ->getFilters()
    ->enable('fortune_cookie_discontinued');
$filter->setParameter('discontinued', false);
$filter->setParameter('discontinuedDate', new \DateTime('-1 day'));

Does that help answer your question? Let me know either way :).

Cheers!

Reply
Andrzej S. Avatar

Thank you very much for your help.

And my addFilterConstraint function may look like that:

$parameters = array();
if($this->hasParameter('discontinued'))
{
$parameters[] = sprintf('%s.discontinued LIKE %s', $targetTableAlias, $this->getParameter('discontinued'));
}

if($this->hasParameter('discontinuedDate'))
{
$parameters[] = sprintf('%s.discontinuedDate = %s', $targetTableAlias, $this->getParameter('discontinuedDate'));
}

$result = implode("AND ", $parameters);

1 Reply
Default user avatar
Default user avatar jian su | posted 5 years ago

Hi Guys:

I still fail to see why we need a filter like this, you are going to edit and create multiple files which is error prone. where you can add a where statement for DQL. it looks hell lot easier. is it worth it to do this? sure, you ever built a multi-tenant site. maybe, but I would install third party bundle to handle multi-tenant, lot easer and I made few mistakes.

Reply

Hey jian su!

This is always my balance with filters: using them is "easy", but less explicit and seems more magic. Adding where statements to your queries gives you more control and is more explicit, but is easier to "forget". I think it comes down to personal preference. But both ultimately do the same thing: add WHERE clauses. The difference is just whether you configure this to happen globally, or if you want to manually add the WHERE on each query. The filter does have an added advantage that it will "filter" when you're using relationships, which is *probably* what you want in most cases, but could also surprise you if you're not expecting it.

tl;dr personal preference! We don't use Filters on KnpU, but we stay *very* organized and push all our queries through the repositories so we don't forget to filter. Works well for us :).

Cheers!

Reply
Default user avatar

Thank you Ryan! It makes sense :)

Reply
Default user avatar
Default user avatar WarGot Georg | posted 5 years ago

Good afternoon.
There is a problem. In the filter, I do not get the parameter from eventlistener. The problem is then I don`t use controller. What to do.
Doctrine filter work before event listener.
Thanks.
P.S. Sorry my english -)

Reply

Hey there!

I think your English is fine :). I don't know what your problem is, but the finished code for this tutorial *does* work (I just re-tried it). Specifically, in this sections - https://knpuniversity.com/s... - if you don't call setParameter() in the listener, then you'll get the error "Parameter 'discontinued' does not exist." If you do not get this error, then I think it *is* working.

But one warning: after you call setParameter(), Doctrine "escapes" the value before passing it to your filter. So, if you pass it the string hello, the parameter will become 'hello' (a string with quotes in the string). If you pass false (like I do), this will become 0. So, it's possible that you're passing some value and it's being escaped in some unexpected way.

Good luck!

Reply
Default user avatar

Thanks for answer.
Big big sorry. I write answer for you with my code, but I later understand what I use query for database, this query also call sql filter, before I send parameter to this filter. Dead loop.

Code after
http://codepen.io/anon/pen/...
before
http://codepen.io/anon/pen/... attention to the line 32

Thanks, good luck. Perfect article.

And as always, sorry my english -)

Reply
Default user avatar
Default user avatar Tomáš Votruba | WarGot Georg | posted 5 years ago

Hi, this might be a solution for you: https://github.com/Symplify...

It doesn't depend on Controller, nor Doctrine yaml configuration, nor Doctrine bundle. It is standalone and easily portable to any project.

Reply
Cat in space

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

This course is built on Symfony 2, but most of the concepts apply just fine to newer versions of Symfony. If you have questions, let us know :).

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3, <7.3.0",
        "symfony/symfony": "2.6.*", // v2.6.13
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.8
        "doctrine/doctrine-bundle": "~1.2", // 1.6.4
        "twig/extensions": "~1.0", // v1.5.4
        "symfony/assetic-bundle": "~2.3", // v2.8.2
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.12
        "symfony/monolog-bundle": "~2.4", // v2.12.1
        "sensio/distribution-bundle": "~3.0.12", // v3.0.36
        "sensio/framework-extra-bundle": "~3.0", // v3.0.29
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.3
        "hautelook/alice-bundle": "0.2.*" // 0.2
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3" // v2.5.3
    }
}
userVoice