Buy
Buy

Controller Magic: Param Conversion

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!

  • 2018-12-09 weaverryan

    Hey Ahmed EBEN HASSINE!

    Yes, you're correct: if the Genus is not found, then a NotFoundHttpException is thrown, which will cause a 404 page. You can customize the 404 page (there are docs on this - it's pretty simple), but there will not be any simple way to know when you're customizing it what caused the 404. So, it's easy to have one 404 page for your site. But, if you want a different 404 error for difference sections of you're site, you'll need to do a bit more work, unfortunately:

    1) Do not use the ParamConverter
    2) Create a new Exception class that extends NotFoundHttpException - e.g. CustomMessageNotFoundHttpException - something like:


    class CustomMessageNotFoundHttpException
    {
    public function __construct(string $message = null, \Exception $previous = null, int $code = 0, array $headers = array())
    {
    parent::__construct($message, $previous, $headers, $code);
    }

    public function getCustomMessage()
    {
    return $this->getMessage();
    }
    }

    3) Manually query for the Genus and throw this exception if it's not found - use whatever message you want
    4) When you create your custom 404 template - https://symfony.com/doc/cur... - your template will be passed an exception variable. You can use it to see if your exception has a custom message or not:


    {% if exception.customMessage is defined %}
    {{ exception.customMessage }}
    {% endfor %}

    Yes, this is a bit of work - it could definitely be easier in Symfony. One alternative is to (A) NOT use the param converter, (B) check for the Genus in your class and then (C) just render some custom error template from right inside your controller. That is probably a fine solution. However, by NOT throwing an exception, you may lose some minor things, most notably, you won't see the 404 in your logs. That might not be a huge deal - just be aware that Symfony wants you to throw an exception for a 404, so if you just render your own template from a controller, you're working around some of the things that it wants to do.

    Cheers!

  • 2018-12-08 Ahmed EBEN HASSINE

    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.

  • 2018-12-04 James Davison

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

  • 2018-12-03 weaverryan

    Hey James Davison!

    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!

  • 2018-12-03 James Davison

    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?

  • 2018-12-01 weaverryan

    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!

  • 2018-11-29 James Davison

    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

  • 2018-11-29 James Davison

    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

  • 2018-11-28 weaverryan

    Hey James Davison!

    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!

  • 2018-11-27 James Davison

    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
    }

  • 2018-11-27 James Davison

    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

  • 2017-07-26 Nina

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

  • 2017-07-26 Victor Bocharsky

    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!

  • 2017-07-26 Nina

    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")?

  • 2017-04-11 Victor Bocharsky

    Hey maxii123 ,

    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!

  • 2017-04-10 maxii123

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

  • 2016-12-26 Victor Bocharsky

    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!

  • 2016-12-24 Nefi López García

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

  • 2016-03-29 weaverryan

    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!

  • 2016-03-24 Andrew Grudin

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