Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Controller Magic: Param Conversion

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

Time to finally make these genus notes dynamic! Woo!

Remember, those are loaded by a ReactJS app, and that makes an AJAX call to an API endpoint in GenusController. Here it is: getNotesAction():

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 90
/**
* @Route("/genus/{genusName}/notes", name="genus_show_notes")
* @Method("GET")
*/
public function getNotesAction($genusName)
{
... lines 97 - 106
}
}

Step 1: use the genusName argument to query for a Genus object. But you guys already know how to do that: get the entity manager, get the Genus repository, and then call a method on it - like findOneBy():

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 54
/**
* @Route("/genus/{genusName}", name="genus_show")
*/
public function showAction($genusName)
{
$em = $this->getDoctrine()->getManager();
$genus = $em->getRepository('AppBundle:Genus')
->findOneBy(['name' => $genusName]);
... lines 64 - 88
}
... lines 90 - 107
}

Old news.

Let's do something much cooler. First, change {genusName} in the route to {name}, but don't ask why yet. Just trust me:

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 90
/**
* @Route("/genus/{name}/notes", name="genus_show_notes")
* @Method("GET")
*/
public function getNotesAction(Genus $genus)
{
... lines 97 - 107
}
}

This doesn't change the URL to this page... but it does break all the links we have to this route.

To fix those, go to the terminal and search for the route name:

git grep genus_show_notes

Oh cool! It's only used in one spot. Open show.html.twig and find it at the bottom. Just change the key from genusName to name:

... lines 1 - 23
{% block javascripts %}
... lines 25 - 30
<script type="text/babel">
var notesUrl = '{{ path('genus_show_notes', {'name': genus.name}) }}';
... lines 33 - 37
</script>
{% endblock %}

Using Param Conversion

So... doing all of this didn't change anything. So why did I make us do all that? Let me show you. You might expect me to add a $name argument. But don't! Instead, type-hint the argument with the Genus class and then add $genus:

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 90
/**
* @Route("/genus/{name}/notes", name="genus_show_notes")
* @Method("GET")
*/
public function getNotesAction(Genus $genus)
{
... lines 97 - 107
}
}

What? I just violated one of the cardinal rules of routing: that every argument must match the name of a routing wildcard. The truth is, if you type-hint an argument with an entity class - like Genus - Symfony will automatically query for it. This works as long as the wildcard has the same name as a property on Genus. That's why we changed {genusName} to {name}. Btw, this is called "param conversion".

Tip

Param Conversion comes from the SensioFrameworkExtraBundle.

Dump the $genus to prove it's working:

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 94
public function getNotesAction(Genus $genus)
{
dump($genus);
... lines 98 - 107
}
}

Go back and refresh! We don't see the dump because it's actually an AJAX call - one that happens automatically each second.

Seeing the Profiler for an AJAX Request

But don't worry! Go to /_profiler to see a list of the most recent requests, including AJAX requests. Select one of these: this is the profiler for that AJAX call, and in the Debug panel... there's the dump. It's alive!

So be lazy: setup your routes with a wildcard that matches a property name and use a type-hint to activate param conversion. If a genus can't be found for this page, it'll automatically 404. And if you can't use param conversion because you need to run a custom query: cool - just get the entity manager and query like normal. Use the shortcut when it helps!

Leave a comment!

22
Login or Register to join the conversation

If the identifier of an Genus does not necessarily exist then the paramConvertor will trigger an exception. Is it sensible to treat this exception or not ? If you want to send a simple message to the user showing that this category does not exist.

1 Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 3 years ago

Is it possible to access multiple entitys through a single @Route annotation?
By example with this param conversion?

I want to achieve something like: /search/$term/categorie1,categorie2
Which should show all articles with the word $term and which are in categorie1 and categorie2

At the moment SF only has access to one category entity via:
* @Route("/search/{term}/{category}/{page}", name="app_search")

Reply
Mike P. Avatar

I think I've found the answer myself :)
https://stackoverflow.com/a...

UPDATE://
This works:

* @Entity("category", class="App\Entity\Category", options={
* "repository_method" = "findAllBySlug",
* })

But it throws a deprecation, that repository_method will be unavailable in sf 6.
But if I use instead this code:

* @Entity("category", expr="repository.findAllBySlug(slug)")

Or this:

* @Entity("category", expr="category.findAllBySlug(slug)")

nothing happens.
(SF doesn't event go into my findAllBySlug method)

Update2://
Final Code:
composer require symfony/expression-language

* @Entity("category", class="App\Entity\Category", expr="repository.findAllBySlug(slug)")

I find it a bit disappointing, that I need to install another service for (deprecated) built in methods.
Because I can measure a nagative speed impact (approx. -5 ms) on other sites where I don't use @Entity.
I haven't had that negative speed impact with the built in repository_method option.

Anyway I'am thankful that SF provides a clean solution :)

Reply

Hey Mike,

I'm glad you found the solution by yourself, well done! Solution sounds easy, or you can always write custom queries inside your controller/action to find proper objects instead of doing it via param converter, it always works for complex tasks and easier to do ;)

Cheers!

Reply
James D. Avatar
James D. Avatar James D. | posted 4 years ago

Hi guys,

I am trying to use the Param Converter. Is there a way to find a result from a joint relation?

I have created an Entity called URL which is connected to Page.

Code below:
/**
* @Route("/{url}.html", name="show_page")
* @ParamConverter("url", options={"mapping"={"url"="url.key"}})
*/
public function showPageAction(Page $page, $request)
{
// return a page
}

Is it something which is possible?

Thanks

James

Reply
James D. Avatar

Would this be a possible solution:

Repository
class PageRepository extends EntityRepository
{
public function findUrl($urlKey)
{
return $this->findBy(array(), array('url' => $urlKey));
}
}

Controller
/**
* @Route("/{url}.html", name="show_page")
* @Entity("page", expr="repository.findUrl(url)")
*/
public function showPageAction(Page $page, Request $request)
{
// return a page if it exist or go to next route available
}

Reply

Hey James D.!

Hmm, interesting! Does the second option work? I actually don't know if it does immediately because, in general, if I have a situation that seems too complex for the ParamConverter, I just skip it :). Instead I add the few extra lines at the top of my method to query manually - it's not much more work, and it's very clear.

But, in general, I think your approach should work. Except that you will probably need a join in your repository method because it looks like you actually have a Url entity that needs the WHERE clause. So, you would probably need to create a custom query builder, innerJoin over to this Url entity, and add the andWhere on its key property. Here is some info about those types of queries: https://symfonycasts.com/sc...

Let me know if that helps! Cheers!

Reply
James D. Avatar

Hi Ryan,

Just to be sure I understand. If I do the same for my product controller and category controller, Symfony will first look for an url related to page, if does not find will move on the next possible match ...

Thanks

Reply

Hey James!

Ah, excellent question. So actually, no. By the time the param converter is called, Symfony has determined that *this* route *has* matches the URL. And so, if the param converter fails, it will be a 404 - it will *not* continue and try to match the next route. I don’t know the full situation, but my recommendation (because your setup is a bit complex) is to skip the param converter and put the logic in your controller. But, you can organize things nicely. For example, if you find the Page, you can then call some private function to do the work for rendering that page - e.g. return $this->renderPage($page);. If you did not find the page, query for the Category and then call some other function that renders the category. It should keep things nicely organized.

Let’s me know how it all works! Cheers!

Reply
James D. Avatar

Hey Ryan,

Maybe it is more complex than it needs to be. I am trying to create an entity URL to have only unique URL. Then I link this URL to specific page/product/category. Does it make sense to do it that way?

Reply

Hey James D.!

Ah! So basically, you have a Url entity, which allows you to define 100% custom URLs, and when you go to a specific URL, it will then display the page/product/category (whatever "thing" is related to that URL object). Here is what I would do:


/**
 * The requirements= part allows the URL key to contain "/" and still match this route - https://symfony.com/doc/current/routing/slash_in_parameter.html
 *
 * @Route("/{key}, name="app_url_show", requirements={"key"=".+"})
 */
public function showPage(Url $url)
{
    // getPageType() would return one of these constants, depending on what type of object it's related to
    switch ($url->getPageType()) {
        // Create 3 constants for convenience - e.g. const PAGE = 'page';
        Url::PAGE:
            // create 3 separate functions that each render the page type
            // if you also need the Url object, pass that as a second argument
            return $this->renderPage($url->getPage());
        Url::PRODUCT
            return $this->renderProduct($url->getProduct());
        Url::CATEGORY
            return $this->renderCategory($url->getCategory());
        default:
            throw new \Exception('Invalid page type');
    }
}

Let me know if that makes sense :).

Cheers!

Reply
James D. Avatar

THAT IS AWESOME! I was looking everywhere for this "requirements=" to keep it DRY!
Thanks loads
Always find great answers here!

Reply
James D. Avatar

Hi Ryan,

Thanks for taking the time to answer me.

I have created an innerJoin as below, I will let you know if it works.

public function findUrl($urlKey)
{
return $this->createQueryBuilder('page')
->andWhere('url.key LIKE :searchTerm')
->innerJoin('page.url', 'url')
->setParameter('searchTerm', $urlKey)
->getQuery()
->execute();
}

Thanks again

Reply
Default user avatar

Hello, please help me.
I go to the terminal and search for the route name
use command :
git grep genus_show_notes

but see nothing just go to the next line
Why? And how to fix this? How to find links on this route ("genus_show_notes")?

Reply

Hey Nina,

"git grep genus_show_notes" command will work if you have committed changes about "genus_show_notes" route. If you add this route but do not commit yet - it won't work, because this command search in the current repo state, i.e. only changes you committed. You can use PhpStorm's "Find in path" feature (Edit -> Find -> Find in path) to search for uncommitted changes in your project or commit them and use "git grep".

Or you can use Symfony Console command:
$ bin/console debug:router genus_show_notes

to see the full information about genus_show_notes route in your app.

Cheers!

Reply
Default user avatar

Thanks for the quick and detailed reply.
That's very useful.

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago

The param matching is all well and good but god knows I wouldnt want to maintain this stuff down the road ;)

Reply

Hey Richard ,

Yeah :) Anyway, it's just a quick start. Of course, if you have more complex logic - you can do it manually, but it saves you time because probably you need a complex logic not always. And btw, even if you have more complex logic - you can write your custom param converter and still continue using this magic ;)

Cheers!

Reply
Default user avatar
Default user avatar Nefi López García | posted 5 years ago

dump($genus) don't work for me in the controller generates the next error, Attempted to call function "dump" from namespace "AppBundle\Controller". :(

Reply

Hey Nefi,

Hm, you can try to add "\" before like "\dump($genus)" - I hope it helps. But it depends, please, take a look at this explanation: http://stackoverflow.com/a/...

Cheers!

Reply
Default user avatar
Default user avatar Andrew Grudin | posted 5 years ago

No code is bad news for me. It's difficult to follow.

Reply

Hey Andrew!

The code is up now - you were working ahead of us :). Sometimes a few of the tutorials are "secretly" available for a few days before we actually put the code blocks up.

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice