Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

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

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

Login Subscribe

If 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.

Leave a comment!

4
Login or Register to join the conversation
Bernard A. Avatar
Bernard A. Avatar Bernard A. | posted 1 year ago

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.

Reply

Hey Bernard A.!

Hmm. Unfortunately, my knowledge of the GraphQL implementation is too limited to help here :/. I simply don't know if this is a bug or the expected behavior. The GraphQL system *does* support filters in general - https://github.com/api-plat... - so I'm not sure what's going on here :/.

Sorry I can't help more on this one!

1 Reply
Bernard A. Avatar

Thanks anyways.

I will report this to API-Platform.

Unfortunately, there I will fall into the downside of this otherwise great tool.

The responsiveness of the team to the issues posted on Github is simply glacial. I had issues linger there for 2 years or longer.

So, I will post it, but with very low expectations.

1 Reply

Yes, they're quite busy - it's tricky. I'm guilty myself of having slow replies on my open source projects! I'm always trying to improve that, but it's tough. Good luck!

Reply
Cat in space

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

This tutorial also works great with API Platform 2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.5.10
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2.0", // 2.1.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.4.5", // 2.8.2
        "nelmio/cors-bundle": "^2.1", // 2.1.0
        "nesbot/carbon": "^2.17", // 2.39.1
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
        "ramsey/uuid-doctrine": "^1.6", // 1.6.0
        "symfony/asset": "5.1.*", // v5.1.5
        "symfony/console": "5.1.*", // v5.1.5
        "symfony/debug-bundle": "5.1.*", // v5.1.5
        "symfony/dotenv": "5.1.*", // v5.1.5
        "symfony/expression-language": "5.1.*", // v5.1.5
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "5.1.*", // v5.1.5
        "symfony/http-client": "5.1.*", // v5.1.5
        "symfony/monolog-bundle": "^3.4", // v3.5.0
        "symfony/security-bundle": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/validator": "5.1.*", // v5.1.5
        "symfony/webpack-encore-bundle": "^1.6", // v1.8.0
        "symfony/yaml": "5.1.*" // v5.1.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
        "symfony/browser-kit": "5.1.*", // v5.1.5
        "symfony/css-selector": "5.1.*", // v5.1.5
        "symfony/maker-bundle": "^1.11", // v1.23.0
        "symfony/phpunit-bridge": "5.1.*", // v5.1.5
        "symfony/stopwatch": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.5
        "zenstruck/foundry": "^1.1" // v1.8.0
    }
}
userVoice