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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.9
        "sensio/framework-extra-bundle": "^5.5", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.5
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.3.1", // v1.6.2
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/var-dumper": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.0.*" // v5.0.4
    },
    "require-dev": {
        "symfony/profiler-pack": "^1.0" // v1.0.4
    }
}