Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Smart Routes: POST-only & Validate {Wildcards}

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.

Inside our JavaScript, we're making a POST request to the endpoint. And that makes sense. The topic of "which HTTP method" - like GET, POST, PUT, etc - you're supposed to use for an API endpoint... can get complicated. But because our endpoint will eventually change something in the database, as a best-practice, we don't want to allow people to make a GET request to it. Right now, we can make a GET request by just putting the URL in our browser. Hey! I just voted!

To tighten this up, in CommentController, we can make our route smarter: we can tell it to only match if the method is POST. To do that add methods="POST".

... lines 1 - 8
class CommentController extends AbstractController
{
/**
* @Route("/comments/{id}/vote/{direction}", methods="POST")
*/
public function commentVote($id, $direction)
{
... lines 16 - 25
}
}

As soon as we do that, when we refresh... 404 not found! The route no longer matches.

Tip

Actually, it's a 405 response code! HTTP Method Not Allowed.

The router:match Command

Another cool way to see this is at your terminal. Run: php bin/console router:match. Then go copy the URL... and paste it.

php bin/console router:match /comments/10/vote/up

This fun command tells us which route matches a given URL. In this case, no routes match, but it tells us that it almost matched the app_comment_commentvote route.

To see if a POST request would match this route, pass --method=POST:

php bin/console router:match /comments/10/vote/up --method=POST

And... boom! It shows us the route that matched and ALL its details, including the controller.

Restricting what a {Wildcard} Matches

But there's something else that's not quite right with our route. We're expecting that the {direction} part will either be up or down. But... technically, somebody could put banana in the URL. In fact, let's try that: change the direction to banana:

php bin/console router:match /comments/10/vote/banana --method=POST

Yes! We vote "banana" for this comment! This isn't the end of the world... if a bad user tried to hack our system and did this, it would just be a down vote. But we can make this better.

As you know, normally a wildcard matches anything. However, if you want, you can control that with a regular expression. Inside the {}, but after the name, add <>. Inside, say up|down.

... lines 1 - 8
class CommentController extends AbstractController
{
/**
* @Route("/comments/{id}/vote/{direction<up|down>}", methods="POST")
*/
public function commentVote($id, $direction)
{
... lines 16 - 25
}
}

Now try the router:match command:

php bin/console router:match /comments/10/vote/banana --method=POST

Yes! It does not match because banana is not up or down. If we change this to up, it works:

php bin/console router:match /comments/10/vote/up --method=POST

Making id Only Match an Integer?

By the way, you might be tempted to also make the {id} wildcard smarter. Assuming we're using auto-increment database ids, we know that id should be an integer. To make this route only match if the id part is a number, you can add <\d+>, which means: match a "digit" of any length.

... lines 1 - 8
class CommentController extends AbstractController
{
/**
* @Route("/comments/{id<\d+>}/vote/{direction<up|down>}", methods="POST")
*/
public function commentVote($id, $direction)
{
... lines 16 - 25
}
}

But... I'm actually not going to put that here. Why? Eventually, we're going to use $id to query the database. If somebody puts banana here, who cares? The query won't find any comment with an id of banana and we will add some code to return a 404 page. Even if somebody tries an SQL injection attack, as you'll learn later in our database tutorial, it will still be ok, because the database layer protects against this.

... lines 1 - 8
class CommentController extends AbstractController
{
/**
* @Route("/comments/{id}/vote/{direction<up|down>}", methods="POST")
*/
public function commentVote($id, $direction)
{
... lines 16 - 25
}
}

Let's make sure everything still works. I'll close one browser tab and refresh the show page. Yea! Voting still looks good.

Next, let's get a sneak peek into the most fundamental part of Symfony: services.

Leave a comment!

11
Login or Register to join the conversation
hanen Avatar

Hi, When I run bin/console router:match /comments/10/vote/up --method=POST
//dir Path: hanen@hanen-PC /d/symfony5/cauldron_overflow (master)
I got an error [ERROR] None of the routes match the path "C:/Users/hanen/Downloads/vendor/git-for-windows/comments/10/vote/up"
the issue is missing vendors libraries ?!!

1 Reply
Default user avatar
Default user avatar JoxTheFox | hanen | posted 3 years ago

I have the same problem. Seems like the Git Bash console I'm using is interpeting "/" as git-bash.exe location folder. Not sure how make it stop adding the "C:/Users/hanen/Downloads/vendor/git-for-windows" in front of the route. Seems like a Git Bash console thing. Works fine in cmd.exe

2 Reply

Hey guys,

Try to wrap that path in quotes! Probably single quotes may not work in windows, I suppose you need to wrap it in double quotes. But just in case, it's a good idea to try both, some quotes should work for you I think. So, basically you need to run the next command:

$ bin/console router:match "/comments/10/vote/up" --method=POST

This should do the trick. And thanks JoxTheFix for some ideas about why it may happen, it's clearer for me where the problem might be.

I hope this helps!

Cheers!

1 Reply
Joao P. Avatar

Hi, I'm getting the same error with git bash (MINGW64) and single or doubles quotes didn't help. As with Jox, with windows prompt it works. Thanks.

Reply

Hey Joao,

Thanks for confirming that JoxTheFox solution works for you.

Cheers!

Reply
hanen Avatar
hanen Avatar hanen | Victor | posted 3 years ago | edited

@victor thanks :)))

Reply

Hey hanen

Let's double check a couple of things
1) Check that you have defined the route /comments/{id}/vote/up. You can run bin/console debug:router to see all your routes
2) Check that you have a comment in your database with ID 10

Let me know if you still have problems. Cheers!

Reply
hanen Avatar

The problem comes from my route where I specify that I only want to run it in POST so I add methods={"POST", "GET"}
/**
* @Route("/comments/{id}/vote/{direction<up|down>}", methods={"POST", "GET"})
*/

Reply

Did Victor comment answered to your problem? If not, could you please check if you are importing the right Route class? It should be this one Symfony\Component\Routing\Annotation\Route

1 Reply
hanen Avatar

I add double quotes and execute the command in the terminal..everything works fine:)

Reply
hanen Avatar
hanen Avatar hanen | posted 3 years ago | edited

Hi everyone , Thanks for your reply :))).. there Actually I'm using cmder software package on Windows but when I go to terminal from phpstorm run command adding double quotes: php bin/console router:match "/comments/10/vote/up" --method=POST
looks like it should work fine
[OK] Route "app_comment_commentvote" matches

+--------------+-------------------------------------------------------------+
| Property | Value |
+--------------+-------------------------------------------------------------+
| Route Name | app_comment_commentvote |
| Path | /comments/{id}/vote/{direction} |
| Path Regex | #^/comments/(?P<id>[^/]++)/vote/(?P<direction>up|down)$#sDu |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | POST |
| Requirements | direction: up|down |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: App\Controller\CommentController::commentVote |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
| | utf8: true |
+--------------+-------------------------------------------------------------+

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.9
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}
userVoice