Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Param Converter & 404's

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

We've programmed the happy path. When I go to /mix/13, my database does find a mix with that id and... life is good. But what if I change this to /99? Yikes. That's a 500 error: not something we want our site to ever do. This really should be a 404 error. So, how do we trigger a 404?

Triggering a 404 Page

Over in the method, this $mix variable will either be a VinylMix object or null if one isn't found. So we can say if (!$mix), and then, to trigger a 404, throw $this->createNotFoundException(). You can give this a message if you want, but it'll only be seen by developers.

... lines 1 - 11
class MixController extends AbstractController
{
... lines 14 - 35
public function show($id, VinylMixRepository $mixRepository): Response
{
... lines 38 - 39
if (!$mix) {
throw $this->createNotFoundException('Mix not found');
}
... lines 43 - 46
}
}

This createNotFoundException(), as the name suggests, creates an exception object. So we're actually throwing an exception here... which is nice, because it means that code after this won't be executed.

Now, normally if you or something in your code throws an exception, it will trigger a 500 error. But this method creates a special type of exception that maps to a 404. Watch! Over here, in the upper right, when I refresh... 404!

By the way, this is not what the 404 or 500 pages would look like on production. If we switched to the prod environment, we'd see a pretty generic error page with no details. Then you customize how those look, even making separate styles for 404 errors, 403 Access Denied errors, or even... gasp ... 500 errors if something goes really wrong. Check out the Symfony docs for how to customize error pages.

Param Converter: Automatic Query

Okay! We've queried for a single VinylMix object and even handled the 404 path. But we can do this with way less work. Check it out! Replace the $id argument with a new argument, type-hinted with our entity class VinylMix. Call it, how about, $mix to match the variable below. Then... delete the query... and also the 404. And now, we don't even need the $mixRepository argument at all.

... lines 1 - 35
public function show(VinylMix $mix): Response
{
return $this->render('mix/show.html.twig', [
'mix' => $mix,
]);
}
... lines 42 - 43

This... deserves some explanation. So far, the "things" that we are "allowed" to have as arguments to our controllers are (1) route wildcards like $id or (2) services. Now we have a third thing. When you type-hint an entity class, Symfony will query for the object automatically. Because we have have a wildcard called {id}, it will take this value (so "99" or "16") and query for a VinylMix whose id is equal to that. The name of the wildcard - id in this case - needs to match the property name it should use for the query.

But if I go back and refresh... it doesn't work!?

Cannot autowire argument $mix of MixController::show(): it references VinylMix but no such service exists.

We know this isn't a service... so that make sense. But... why isn't it querying for the object like I just said it would?

Because... to get this feature to work, we need to install another bundle! Well, if you're using Symfony 6.2 and a new enough DoctrineBundle - probably version 2.8 - then this should work without needing anything else. But since we're using Symfony 6.1, we need one extra library.

Find your terminal and say:

composer require sensio/framework-extra-bundle

This is a bundle full of nice little shortcuts that, by Symfony 6.2, will all have been moved into Symfony itself. So eventually, you won't need this.

And now... without doing anything else... it works! It automatically queried for the VinylMix object and the page renders! And if you go to a bad ID, like /99... yes! Check it out! We get a 404! This feature is called a "ParamConverter"... which is mentioned in the error:

VinylMix object not found by the @ParamConverter annotation.

Anyways, I love this feature. If I need to query for multiple objects, like in the browse() action, I'll use the correct repository service. But if I need to query for a single object in a controller, I use this trick.

Next, let's make it possible to up vote and down vote our mixes by leveraging a simple form. To do this, for the first time, we will update an entity in the database.

Leave a comment!

10
Login or Register to join the conversation
Default user avatar
Default user avatar unknown | posted 1 month ago | edited
Comment was deleted.
t5810 Avatar
t5810 Avatar t5810 | posted 4 months ago | edited

Hi

I had downloaded the code from this course (folder start), and I am trying to code along. When I attempt to run the command "composer require sensio/framework-extra-bundle ", I get the following error:

In App_KernelDevDebugContainer.php line 772:
!!
!! Attempted to call an undefined method named "registerLoader" of class "Doctrine\Common\Annotations\AnnotationRegistry".

To continue coding while I get answer to this question, i did the following:

  1. I run the command: "composer remove sensio/framework-extra-bundle"
  2. revert the function show as it was.

Any advice how to proceed?

Thanks!

Reply

Hey @t5810!

Ah, sorry for the trouble and the my glacially slow reply! Ok, so I see the issue - there was a bug between older version of Symfony and doctrine/annotations v2 (which is required by the sensio/framework-extra-bundle library). Anyways, the fix is to update one Symfony package:

composer up symfony/framework-bundle

That should fix the issue :). I'm running that on our course code right now so that the downloaded code works for everyone again without issues.

Cheers!

3 Reply
Ted Avatar

Hello, I had the same problem and when I ran composer up symfony/framework-bundle I have this new error :

Attempted to load class "Locale" from the global namespace.
Did you forget a "use" statement?

Reply
Ted Avatar

this is where it says the error is :

C:\Users\Fourt\Desktop\sites\mixedVinyl\vendor\symfony\translation\LocaleSwitcher.php:37

 `        $this->defaultLocale = $locale;    }    public function setLocale(string $locale): void    {        \Locale::setDefault($this->locale = $locale);        $this->requestContext?->setParameter('_locale', $locale);        foreach ($this->localeAwareServices as $service) {            $service->setLocale($locale);        } `
Reply

Hey @Ted

I believe you need to install the PHP intl extension. If you're in a Linux alike environment, you can install by running these commands
sudo apt-get update -y
sudo apt-get install -y php-intl

Cheers!

Reply
Michael-S Avatar
Michael-S Avatar Michael-S | posted 5 months ago

Let's say there are multiple Users that have their own Mixes, and you can only view other Users' Mixes if you have some kind of active relationship with said User, say if you are "subscribed" to them.

How would you protect against someone viewing a Mix that they're not supposed to? Would you still use the ParamConverter and check that the logged in User is subscribed to the User that owns the Mix that is passed in, or would you not use the ParamConverter and instead query based on the combo of the Mix ID and logged-in User ID, going through some bridge table that keeps track of subscriptions between Users?

Then you either display the Mix if found (knowing at that point the logged-in User has access to it), or NULL would be returned from the query either if the Mix doesn't exist, or the logged-in User has no active subscription, at which point you'd 404.

Or would you approach this case some other way entirely?

Thanks ahead of time.

Reply

Hey Michael,

It depends, but as you see there are many good options, and mostly it's the matter of taste. Sometimes you can prefer some option more just because the solution is easier with it. So, just use that one you would like the most. If in your code, it's easier to check it from the entity that returned by the param converter - great, that's a good way I think. But if it's easier for you to write a custom query and this way looks cleaner for you - great, go for it :) No matter what way you choose, Symfony is flexible enough and allows you to throw 404 error yourself, so both ways are valid technically, depending on which one you like the most, or which one looks cleaner to you, to your specific code.

P.S. usually calling a repository method directly is more obvious, flexible, and straightforward than param converter that mostly used for a very simple fetch. But if you can write a nice code with param converter and throw a custom 404 or even 403 - I don't see any strong arguments against it ;)

Cheers!

Reply
MaxiCom Avatar

Hello,

I was wondering, why on the param converter documentation and on this video, you refers to it as @ParamConverter?
Why is the need for the @, is it because the param converters are services?

Thanks for your time!

Reply

Hey MaxiCom,

The @ syntax in the comments is called PHP annotations, they were pretty popular before the new PHP 8 attributes that starts with # like #[Route('/mix/new')]. So, it's a special syntax that is read by the Symfony system. And yeah, almost everything in Symfony are services ;) but basically those @ParamConverter is just an alternative way of configuration :)

Cheers!

1 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0
    }
}
userVoice