Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Wildcard Routes

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.

The homepage will eventually be the place where a user can design and build their next sweet mix tape. But in addition to creating new tapes, users will also be able to browse other people's creations.

Creating a Second Page

Let's make a second page for that. How? By adding a second controller: public function, how about browse: the name doesn't really matter. And to be responsible, I'll add a Response return type.

Above this, we need our route. This will look exactly the same, except set the URL to /browse. Inside the method, what do we always return from a controller? That's right: a Response object! Return a new Response... with a short message to start.

... lines 1 - 7
class VinylController
{
... lines 10 - 15
#[Route('/browse')]
public function browse(): Response
{
return new Response('Breakup vinyl? Angsty 90s rock? Browse the collection!');
}
}

Let's try it! If we refresh the homepage, nothing changes. But if we go to /browse... we're crushing it! A second page in under a minute! Dang!

On this page, we'll eventually list mix tapes from other users. To help find something we like, I want users to also be able to browse by genre. For example, if I go to /browse/death-metal, that would show me all the death metal vinyl mix tapes. Hardcore.

Of course, if we try this URL right now... it doesn't work.

Not Route found

No matching routes were found for this URL, so it shows us a 404 page. By the way, what you're seeing is Symfony's fancy exception page, because we're currently developing. It gives us tons of details whenever something goes wrong. When you eventually deploy to production, you can design a different error page that your users would see.

{Wildcard} Routes

Anyways, the simplest way to make this URL work is just... to change the URL to /browse/death-metal.

... lines 1 - 7
class VinylController
{
... lines 10 - 15
#[Route('/browse/death-metal')]
public function browse(): Response
{
return new Response('Breakup vinyl? Angsty 90s rock? Browse the collection!');
}
}

But... not super flexible, right? We would need one route for every genre... which could be hundreds! And also, we just killed the /browse URL! It now 404's.

What we really want is a route that match /browse/<ANYTHING>. And we can do that with a wildcard. Replace the hard-coded death-metal with {} and, inside, slug. Slug is just a technical word for a "URL-safe name". Really, we could have put anything inside the curly-braces, like {genre} or {coolMusicCategory}: it makes no difference. But whatever we put inside this wildcard, we are then allowed to have an argument with that same name: $slug.

... lines 1 - 7
class VinylController
{
... lines 10 - 15
#[Route('/browse/{slug}')]
public function browse(): Response
{
return new Response('Breakup vinyl? Angsty 90s rock? Browse the collection!');
}
}

Yup, if we go to /browse/death-metal, it will match this route and pass the string death-metal to that argument. The matching is done by name: {slug} connects to $slug.

To see if it's working, let's return a different response: Genre then the $slug.

... lines 1 - 7
class VinylController
{
... lines 10 - 15
#[Route('/browse/{slug}')]
public function browse($slug): Response
{
return new Response('Genre: '.$slug);
//return new Response('Breakup vinyl? Angsty 90s rock? Browse the collection!');
}
... lines 23 - 24

Testing time! Head back to /browse/death-metal and... yes! Try /browse/emo and yea! I'm that much closer to my Dashboard Confessional mix tape!

Oh, and it's optional, but you can add a string type to the $slug argument. That doesn't change anything... it's just a nice way to program: the $slug was already always going to be a string.

... lines 1 - 7
class VinylController
{
... lines 10 - 15
#[Route('/browse/{slug}')]
public function browse(string $slug): Response
{
... lines 19 - 21
}
... lines 23 - 24

A bit later, we'll learn how you could turn a number wildcard - like the number 5 - into an integer if you want to.

Using Symfony's String Component

Let's make this page a bit fancier. Instead of printing out the slug exactly, let's convert it to a title. Say $title = str_replace() and replace any dashes with spaces. Then, down here, use title in the response. In a future tutorial, we're going to query the database for these genres, but, for right now, we can at least make it look nicer.

... lines 1 - 7
class VinylController
{
... lines 10 - 15
#[Route('/browse/{slug}')]
public function browse(string $slug): Response
{
$title = str_replace('-', ' ', $slug);
return new Response('Genre: '.$title);
... line 23
}
... lines 25 - 26

If we try it, Emo doesn't look any different... but death metal does. But I want more fancy! Add another line with $title = then type u and auto-complete a function that's literally called... u.

We don't use many functions from Symfony, but this is a rare example. This comes from a Symfony library called symfony/string. As I mentioned, Symfony is many different libraries - also called components - and we're going to leverage those libraries all the time. This one helps you make string transformations... and it happens to already be installed.

Move the str_replace() to the first argument of u(). This function returns an object that we can then do string operations on. One method is called title(). Say ->title(true) to convert all words to title case.

... lines 1 - 6
use function Symfony\Component\String\u;
... line 8
class VinylController
{
... lines 11 - 15
#[Route('/browse/{slug}')]
public function browse(string $slug): Response
{
$title = u(str_replace('-', ' ', $slug))->title(true);
return new Response('Genre: '.$title);
... lines 23 - 24
}
... lines 26 - 27

Now whe n we try it... sweet! It uppercases the letters! The string component isn't particularly important, I just want you to see how we can already leverage parts of Symfony to get our job done.

Making the Wildcard Optional

Ok: one last challenge. Going to /browse/emo or /browse/death-metal works. But just going to /browse... does not work. It's broken! A wild card can match anything, but, by default, a wild card is required. We have to go to /browse/<something>.

Can we make a wildcard optional? Absolutely! And it's delightfully simple: make the corresponding argument optional.

... lines 1 - 8
class VinylController
{
... lines 11 - 15
#[Route('/browse/{slug}')]
public function browse(string $slug = null): Response
{
... lines 20 - 24
}
... lines 26 - 27

As soon as we do that, it tells Symfony's routing layer that the {slug} does not need to be in the URL. So now when we refresh... it works. Though, that's not a great message for the page.

Let's see. If there's a slug, then set the title the way we were. Else, set $title to "All genres". Oh, and move the "Genre:" up here... so that down in the Response we can just pass $title.

... lines 1 - 8
class VinylController
{
... lines 11 - 15
#[Route('/browse/{slug}')]
public function browse(string $slug = null): Response
{
if ($slug) {
$title = 'Genre: '.u(str_replace('-', ' ', $slug))->title(true);
} else {
$title = 'All Genres';
}
return new Response($title);
... lines 27 - 28
}
... lines 30 - 31

Try that. On /browse... "All Genres". On /browse/emo... "Genre: Emo".

Next: putting text like this into a controller.... isn't very clean or scalable, especially if we start including HTML. Nope, we need to render a template. To do that, we're going to install our first third-party package and witness the massively important Symfony recipe system in action.

Leave a comment!

11
Login or Register to join the conversation
Default user avatar
Default user avatar boban_dj | posted 5 months ago

I have to add: use function Symfony\Component\String\u; to make it work. VSCodium does not show autocomplete, but here is well explained too https://symfony.com/doc/cur...

2 Reply

Hey boban_dj

The fuction import is not shown in the video but if you look on the code blocks from the script, you'll see Ryan used exactly that function. Good job finding it out.

Cheers!

Reply
Prim N. Avatar

i really want to say i appreciate these videos ...for someone who is new on symfony your way of doing things makes super sense

1 Reply
Sebastien D. Avatar
Sebastien D. Avatar Sebastien D. | posted 1 month ago

Hi!
My application has to manage multiple domains. When I'm using the 'host' route parameter, it matches differently if user entered 'www' or not in the browser: www.example.com and example.com are not considered as the same host. How can I add a wildcard that could solve that?
Thanks!

Reply

Hey Sebastien D.

I think a better solution would be to add a redirect at the web server level, for example all requests coming from www.example.com should redirect to example.com, or viceversa.

Cheers!

Reply
Default user avatar
Default user avatar Eskinder | posted 4 months ago

Hi, I am working on Ubuntu 20.04 and installed apache2,php, mysql and composer. On symfony 6 , #[Route('/browse')] doesn't work. I have 404 page "The requested URL was not found on this server." I have installed "symfony/apache-pack". I still have the same result.

#[Route('/browse')]
public function browse(): Response
{
return new Response('Browse page');
}

Best regards

Reply

Hey Eskinder,

First of all, please, make sure the route is exist in the system - for this, run:

$ bin/console debug:router | grep browse

Do you see the route in the output? is it just "/browse" or does it has a prefix? Also, make sure this route is registered for GET (or any) requests. If so, then most probably the problem in your Apache config. First of all, make sure you set the public/ directory as your document root. Then, double-check the other config. Actually, Symfony gives the correct Apache/Nginx configs in the docs, see: https://symfony.com/doc/cur... - there's important to know if you configure it via PHP-FPM or in a different way - there are different configs. But basically, if the route exist and you configured the host well using the config from the docs - it should work. Also, don't forget to restart your Apache server after tweaking any config - that's important, most users forget to do this :)

I hope this helps!

Cheers!

Reply

Hi,

I have a strange question, I hope you will understand me.

In your example, "/browse" is hard coded.

Is it possible to save "/ browse" in the database and call from there.

Problem is if user wants to change "/browse" to "/somethig-else" he can't do that from cms it must be changed in code.

Best regards

Reply
Paul R. Avatar

Yes, that is why you have the {wildcard} route. So all you need to do is change the /browse route to /{browse} now it matches anything you pass to it and you can check if that route or slug exists in your database

Reply

So /{browse} cen be value from data base?

Reply
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
    }
}