Buy

Time to refactor our code to use the slug in the URLs. I'll close up a few files and then open GenusController. The "show" page we just saw in our browser comes from showAction(). And yep, it has {genusName} in the URL. Gross:

120 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 64
/**
* @Route("/genus/{genusName}", name="genus_show")
*/
public function showAction($genusName)
{
... lines 70 - 92
}
... lines 94 - 118
}

Change that to {slug}:

113 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 64
/**
* @Route("/genus/{slug}", name="genus_show")
*/
... lines 68 - 111
}

And now, because slug is a property on the Genus entity, we don't need to manually query for it anymore. Instead, type-hint Genus as an argument:

113 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 64
/**
* @Route("/genus/{slug}", name="genus_show")
*/
public function showAction(Genus $genus)
{
... lines 70 - 85
}
... lines 87 - 111
}

Now, Symfony will do our job for us: I mean, query for the Genus automatically.

That means we can clean up a lot of this code. Just update the $genusName variable below to $genus->getName():

113 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 64
/**
* @Route("/genus/{slug}", name="genus_show")
*/
public function showAction(Genus $genus)
{
$em = $this->getDoctrine()->getManager();
$markdownTransformer = $this->get('app.markdown_transformer');
$funFact = $markdownTransformer->parse($genus->getFunFact());
$this->get('logger')
->info('Showing genus: '.$genus->getName());
$recentNotes = $em->getRepository('AppBundle:GenusNote')
->findAllRecentNotesForGenus($genus);
return $this->render('genus/show.html.twig', array(
'genus' => $genus,
'funFact' => $funFact,
'recentNoteCount' => count($recentNotes)
));
}
... lines 87 - 111
}

We just Broke our App!

Cool! Except, we just broke our app! By changing the wildcard from {genusName} to {slug}, we broke any code that generates a URL to this route. How can we figure out where those spots are?

My favorite way - because it's really safe - is to search the entire code base. In this case, we can search for the route name: genus_show. To do that, find your terminal and run:

git grep genus_show

Ok! We have 1 link in list.html.twig and we also generate a URL inside GenusController.

Search for the route in the controller. Ah, newAction() - which just holds some fake code we use for testing. Change the array key to slug set to $genus->getSlug():

113 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 42
return new Response(sprintf(
'<html><body>Genus created! <a href="%s">%s</a></body></html>',
$this->generateUrl('genus_show', ['slug' => $genus->getSlug()]),
$genus->getName()
));
}
... lines 49 - 111
}

Next, open app/Resources/views/genus/list.html.twig. Same change here: set slug to genus.slug:

27 lines app/Resources/views/genus/list.html.twig
... lines 1 - 2
{% block body %}
<table class="table table-striped">
... lines 5 - 11
<tbody>
{% for genus in genuses %}
<tr>
<td>
<a href="{{ path('genus_show', {'slug': genus.slug}) }}">
{{ genus.name }}
</a>
</td>
... lines 20 - 21
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

Project, un-broken!

There's one other page whose URL still uses name. In GenusController, find getNotesAction(). This is the AJAX endpoint that returns all of the notes for a specific Genus as JSON.

Change the URL to use {slug}:

113 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 87
/**
* @Route("/genus/{slug}/notes", name="genus_show_notes")
* @Method("GET")
*/
public function getNotesAction(Genus $genus)
{
... lines 94 - 110
}
}

The automatic query will still work just like before. Now, repeat the careful searching we did before: copy the route name, find your terminal, and run:

git grep genus_show_notes

This is used in just one place. Open the genus/show.html.twig template. Change the path() argument to slug set to genus.slug:

42 lines app/Resources/views/genus/show.html.twig
... lines 1 - 25
{% block javascripts %}
... lines 27 - 32
<script type="text/babel">
var notesUrl = '{{ path('genus_show_notes', {'slug': genus.slug}) }}';
... lines 35 - 39
</script>
{% endblock %}

That's it! That's everything. Go back to /genus in your browser and refresh. Now, click on Octopus. Check out that lowercase o on octopus in the URL. And since the notes are still displaying, it looks like the AJAX endpoint is working too.

So slugs are the proper way to do clean URLs, and they're really easy if you set them up from the beginning. You can also use {id} in your URLs - it just depends if you need them to look fancy or not.

Ok, let's get back to the point of this course: time to tackle - queue dramatic music - ManyToMany relations.

Leave a comment!

  • 2017-08-14 Diego Aguiar

    Hey Pieter Meyvaert!

    Actually that would be nice to have! but as I know it is not a default behaviour. You could create your own twig's "path" function to implement it :)

    Have a nice day

  • 2017-08-14 Pieter Meyvaert

    Is there a way to ommit the specific field when generating urls?

    now you have: {{ path('genus_show', {'slug': genus.slug}) }}
    Would it be possible to use:

    {{ path('genus_show',genus) }}
    and let the helper figure out that the "genus_show" route uses a "slug" field that is publicly available on the "genus" object?

  • 2017-01-02 weaverryan

    Both good options :). "git grep" is able to take advantage of its internal git cache to make the search faster than normal (but PhpStorm may do the same thing).

    Btw, off-topic, there is another utility called the "silver searcher" which is *lightning* fast at searching through stuff. But, it all comes down to preference!

    Cheers!

  • 2017-01-02 the_nuts

    I would have used the PhpStorm's "Find in Path" feature instead of git grep