10.
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.
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
Whoops, an error! Please, try again later.
46 Comments
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...
Note that getParameter returns a quoted string! A workarround regarding nullable values would be something like:
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 ...
}
If you prefer using only Filter class and keep Controllers clean, you might like this package https://github.com/Symplify...
I like it!
Thanks!
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?
Yo Simon Carr!
When you enable your filter (like how we do it in an event listener), pass in the
security.authorization_checkerservice 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 normalisGrantedin 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!
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.
Hey Simon Carr!
Ah, nice debugging then! And you're right -
SecurityContextis a Symfony 2 thing, andTokenStorageis 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!
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?
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!
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.
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!
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!
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!
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 😃
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 ?
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: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!
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
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
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 ^^
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!
Recently I wrote and article about decoupled Filters and how to build them:
http://www.tomasvotruba.cz/...
It's easy if you try :)
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...
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!
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 )
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:
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
$disabledarray 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!
Thanks for your help weaverryan !
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
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!
:))) 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?
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!
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]
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
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!
Thank you. Good job. Stef.
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?
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:
Does that help answer your question? Let me know either way :).
Cheers!
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);
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.
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!
Thank you Ryan! It makes sense :)
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 -)
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!
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 -)
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.
"Houston: no signs of life"
Start the conversation!