Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Refactoring Carefully

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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:

... 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}:

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

... 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():

... 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():

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

... 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}:

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

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

4
Login or Register to join the conversation
Default user avatar
Default user avatar Pieter Meyvaert | posted 5 years ago

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?

Reply

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

Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted 5 years ago

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

Reply

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!

1 Reply
Cat in space

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

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice