Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Generate Urls & bin/console

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.

There are two different ways that we can interact with our app. The first is via the web server... and that's what we've been doing! We got to a URL and... behind the scenes, it executes public/index.php, which boots up Symfony, calls the routing and runs our controller.

Hello bin/console

What's the second way we can interact with our app? We haven't seen it yet: it's via a command line tool called bin/console. At your terminal run:

php bin/console

... to see a bunch of commands within this script. I love this thing. It's full of stuff to help us debug, eventually it will have code-generation commands, commands for setting secrets: all kinds of good stuff that we're going to discover little-by-little.

But I do want to point out that... there's nothing special about this bin/console script! It's just a file: there's literally a bin/ directory with a console file inside. You'll probably never need to open this file or think about it, but it is useful. Oh, and on most systems, you can just run:


... which looks cooler. Or sometimes you may see me run:

symfony console

... which is just another way to execute this file. We'll talk more about this in a future tutorial.

bin/console debug:router

The first command I want to check out inside of bin/console is debug:router:

php bin/console debug:router

This is awesome. It shows us every route in our app, like our two routes for / and /browse/{slug}. What are these other routes? They come form the web debug toolbar and profiler system... and they're only here while we're developing locally.

Ok, back on our site.... at the top of the page, we have two non-functional links to the homepage and browse page. Let's hook these up. Open templates/ base.html.twig... and search for a tags. Here we go.

So it would be really easy to get this working by just href="/". But instead, whenever we link to a page in Symfony, we're going to ask the routing system to generate a URL for us. We'll say:

Please generate the URL to the homepage's route, or the browse page's route.

Then, if we ever change the URL of a route, all our links will instantly update. Magic.

Naming your Route

Let's start with the homepage. How do we ask Symfony to generate a URL to this route? Well first, we need to give the route a name. Surprise! Every route has an internal name. You can see it back in debug:router. Our route's are named app_vinyl_homepage and app_vinyl_browse. Huh, those are the exact names of my pet turtles when I was kid.

Where did these names come from? By default, Symfony automatically generates a name for us, which is fine. The name isn't used at all until we generate a URL to it. And as soon as we do need to generate a URL to a route, I highly recommend taking control of this name... just to make sure it never accidentally changes.

To do this, find the route and add an argument: name set to, how about, app_homepage. I like using the app_ prefix: it makes it easier to search for a route name later.

... lines 3 - 9
class VinylController extends AbstractController
#[Route('/', name: 'app_homepage')]
public function homepage(): Response
... lines 15 - 27
... lines 29 - 38

By the way, PHP 8 attributes - like this Route attribute - are represented by actual, physical PHP classes. If you hold command or ctrl, you can open it and look inside. This is great: the __construct() method shows all of the different options you can pass to the attribute.

For example, there's a name argument... and then we're using PHP's named argument syntax to pass this into the attribute. Opening up an attribute is a great way to learn about its options.

Generating a URL from Twig

Anyways, now that we've given this a name, go back to our terminal and run debug:router again:

php bin/console debug:router

This time... yea! The route is named app_homepage! Copy that, then head back to base.html.twig. To generate a URL inside of twig, say {{ - because we're going to print something - and then use a Twig function called path(). Pass this the route name.

<!DOCTYPE html>
... lines 3 - 26
... lines 28 - 31
<a class="navbar-brand" href="{{ path('app_homepage') }}">
<i class="fas fa-record-vinyl"></i>
Mixed Vinyl
... lines 36 - 84

Done! Refresh... and the link up here works!

One more link to go. We know step one: give the route a name. So name: and, how about, app_browse.

... lines 3 - 9
class VinylController extends AbstractController
... lines 12 - 29
#[Route('/browse/{slug}', name: 'app_browse')]
public function browse(string $slug = null): Response
... lines 33 - 37

Copy that, and... scroll down a bit. Here it is: "Browse Mixes". Change that to {{ path('app_browse') }}.

<!DOCTYPE html>
... lines 3 - 26
... lines 28 - 40
<li class="nav-item">
<a class="nav-link" style="margin-top: 20px;" href="{{ path('app_browse') }}">Browse Mixes</a>
... lines 44 - 84

And now... that link works too!

Generating URLs with Wildcards

Oh, but on this page, we have some quick links to go to the browse page for a specific genre. And these do not work yet.

This is interesting. We want to generate a URL like before... but this time we need to pass something to the {slug} wildcard. Open browse.html.twig. Here's how we do that. The first part is the same: {{ path() }} and then the name of the route: app_browse.

If we stopped here, it would generate /browse. To pass values to any wildcards in a route, path() has a second argument: an associative array of those value. And, again, just like JavaScript, to create an "associative array", you use { and }. I'm going to hit enter to break this onto multiple lines... just to keep things readable. Inside add a slug key to the array... and since this is the "Pop" genre, set it to pop.

Cool! Let's repeat this two more times: {{ path('app_browse') }}, pass curly braces for an associative array, with slug set to rock. And then one more time down here... which I'll do really quickly.

... lines 1 - 2
{% block body %}
... lines 4 - 7
<ul class="genre-list ps-0 mt-2 mb-3">
<li class="d-inline">
<a class="btn btn-primary btn-sm" href="{{ path('app_browse', {
slug: 'pop'
}) }}">Pop</a>
<li class="d-inline">
<a class="btn btn-primary btn-sm" href="{{ path('app_browse', {
slug: 'rock'
}) }}">Rock</a>
<li class="d-inline">
<a class="btn btn-primary btn-sm" href="{{ path('app_browse', {
slug: 'all-accordion'
}) }}">All Accordion</a>
... lines 25 - 52
{% endblock %}

Let's see if it works! Refresh. Ah! Variable rock doesn't exist. I bet some of you saw me do that. I forgot my quotes, so this looks like a variable.

Try it again. There we go. And try the links... yes! They work!

Next: we've created two HTML pages. Now let's see what it looks like to create a JSON API endpoint.

Leave a comment!

Login or Register to join the conversation
Default user avatar

Hello, it looks like JsonResponse is not (anymore?) a Response:
App\Controller\SongController::getSong(): Return value must be of type App\Controller\Reponse, Symfony\Component\HttpFoundation\JsonResponse returned

Benoit-L Avatar
Benoit-L Avatar Benoit-L | posted 6 months ago

hi there, for some reason in phpstorm when I hold ctrl on the Route term I am not seing the Route.php file.


Hey Benoit L.

But what happens when you hold and click on it?


Benoit-L Avatar

Hey Vladmir,
The message is Cannot find declaration to go to.


My guess is that you PHPStorm cache is broken, or vendors are not installed, or maybe some PHPStorm options with language level and composer deps are not configured, so it doesn't know where to look

Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3