Filter Class Arguments
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIf we enter an invalid from
date in the URL, it's simply ignored and we return everything. We did that on purpose in DailyStatsDateFilter
:
// ... lines 1 - 7 | |
class DailyStatsDateFilter implements FilterInterface | |
{ | |
// ... lines 10 - 11 | |
public function apply(Request $request, bool $normalization, array $attributes, array &$context) | |
{ | |
// ... lines 14 - 19 | |
$fromDate = \DateTimeImmutable::createFromFormat('Y-m-d', $from); | |
if ($fromDate) { | |
$fromDate = $fromDate->setTime(0, 0, 0); | |
$context[self::FROM_FILTER_CONTEXT] = $fromDate; | |
} | |
} | |
// ... lines 27 - 40 | |
} |
But another option is to return a 400 status code so that the user knows they messed up. How could we do that?
Returning a 400 any time you want
It's pretty simple actually! Symfony has a bunch of built-in exception classes that map to various status codes. For example, before this, we could say, if not $fromDate
, then throw new BadRequestHttpException
:
// ... lines 1 - 6 | |
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
class DailyStatsDateFilter implements FilterInterface | |
{ | |
// ... lines 11 - 12 | |
public function apply(Request $request, bool $normalization, array $attributes, array &$context) | |
{ | |
// ... lines 15 - 20 | |
$fromDate = \DateTimeImmutable::createFromFormat('Y-m-d', $from); | |
// you could optionally return a 400 error | |
if (!$fromDate) { | |
throw new BadRequestHttpException('Invalid "from" date format'); | |
} | |
// ... lines 27 - 31 | |
} | |
// ... lines 33 - 46 | |
} |
That exception - which you can throw whenever you want - maps to a 400 status code. And there are a bunch of other ones in that same directory for other status codes. Pass this a message:
Invalid from date format.
// ... lines 1 - 8 | |
class DailyStatsDateFilter implements FilterInterface | |
{ | |
// ... lines 11 - 12 | |
public function apply(Request $request, bool $normalization, array $attributes, array &$context) | |
{ | |
// ... lines 15 - 23 | |
if (!$fromDate) { | |
throw new BadRequestHttpException('Invalid "from" date format'); | |
} | |
// ... lines 27 - 31 | |
} | |
// ... lines 33 - 46 | |
} |
Cool! I know, I don't need this other if statement down here, but I'll leave it.
Anyways, let's see what this looks like. Refresh with the bad date and... cool! A nice JSON error message with: "Invalid from date format" and a 400 status code. On production, the stack trace wouldn't be here, but the user would see this message.
ApiFilter arguments Option
Let's do an extra challenge. Pretend that we want to make this behavior - whether to throw a 400 error on an invalid date format - something that is configurable when we activate the filter.
Okay, this may not be needed unless you're building a reusable filter, but it will reveal some cool stuff about how filters work.
The @ApiFilter()
annotation has several options that we can pass to it:
// ... lines 1 - 11 | |
/** | |
// ... lines 13 - 22 | |
* @ApiFilter(DailyStatsDateFilter::class) | |
*/ | |
class DailyStats | |
{ | |
// ... lines 27 - 58 | |
} |
Hold Command
or Control
and click to jump into that core annotation class. Yep! All of these public properties are options that we can technically pass to this annotation. But for the purposes of building a custom filter, the only options that really matter are arguments
and properties
. We'll talk about properties
later.
Close that class. Try this: add arguments={}
and then pass a new argument called throwOnInvalid
set to true
:
// ... lines 1 - 11 | |
/** | |
// ... lines 13 - 22 | |
* @ApiFilter(DailyStatsDateFilter::class, arguments={"throwOnInvalid"=true}) | |
*/ | |
class DailyStats | |
{ | |
// ... lines 27 - 58 | |
} |
What does this do? I don't know! Let's refresh and see what happens. Ah, error!
Class
DailyStatsDateFilter
does not have argument$throwOnInvalid
arguments Option Maps to Constructor Arguments
API platform does a cool, but kind of strange thing: if you pass an arguments
option to a filter, it tries to pass that argument - by name - to the constructor of your filter.
Check it out: in DailyStatsDateFilter
, add a constructor: public function __construct()
with bool
and then copy the name of the argument and paste it: $throwOnInvalid
. Default this to false
in case someone uses the filter without that option:
// ... lines 1 - 8 | |
class DailyStatsDateFilter implements FilterInterface | |
{ | |
// ... lines 11 - 14 | |
public function __construct(bool $throwOnInvalid = false) | |
{ | |
// ... line 17 | |
} | |
// ... lines 19 - 53 | |
} |
Next, hit Alt
+Enter
and go to "Initialize properties" to create that property and set it:
// ... lines 1 - 8 | |
class DailyStatsDateFilter implements FilterInterface | |
{ | |
// ... lines 11 - 12 | |
private $throwOnInvalid; | |
public function __construct(bool $throwOnInvalid = false) | |
{ | |
$this->throwOnInvalid = $throwOnInvalid; | |
} | |
// ... lines 19 - 53 | |
} |
Finally, in the if
statement, add if not $fromDate
and $this->throwOnInvalid
, then we want to throw that exception:
// ... lines 1 - 8 | |
class DailyStatsDateFilter implements FilterInterface | |
{ | |
// ... lines 11 - 19 | |
public function apply(Request $request, bool $normalization, array $attributes, array &$context) | |
{ | |
// ... lines 22 - 29 | |
// you could optionally return a 400 error | |
if (!$fromDate && $this->throwOnInvalid) { | |
throw new BadRequestHttpException('Invalid "from" date format'); | |
} | |
// ... lines 34 - 38 | |
} | |
// ... lines 40 - 53 | |
} |
Let's try it! Move back over, refresh and.... got it! We're back to the 400 status code.
The properties Option
So it's kind of weird, but any arguments
on the annotation map to constructor arguments by name. Oh, and the one other option that we could pass to the annotation is properties={}
if you want to configure that this is supposed to use a certain set of properties. And if you ever put @ApiFilter
above a property instead of on top of the class, this properties
option is automatically set for you.
Either way, if the properties
option is set, it's passed to your filter as an argument called $properties
.
So... cool! We now know how we can pass configuration to a filter. But there's more going on than it seems. Next: we'll reveal something that will make our filters a lot more powerful.
This is another issue I found when applying this to graphql.
It would seem that the apply method in DailyStatDateFilter is not called at all when using graphql.
I tried to dd($context) there and it simply ignored it.
Not sure if any other interface should be applied in the case of graphql, though the one used seem to be generic.
In the specific use case of this course, the apply method is used for getting the Request query parameters, validating them and throwing or not an error depending on the argument set on the Entity ("throwOnInvalid").
The former is not an issue if the apply method is not called with graphql, as the same query parameters are available on the DataProvider on context["filters"], as I already pointed out on another comment.
But passing the "throwOnInvalid" argument is an issue. It does not seem to be available on DailyStatDateFilter, as apparently apply() is ignored and I could not find a way to make it available on the DataProvider. It certainly is NOT included in the $context, at least not with the current interfaces.
So any help trying to find a solution for issues above on graphql are welcome.