Straightforward Routing Requirements
In the previous video we looked at ways to get the request parameters in Symfony websites.
Along the way we covered various properties of the Request
object, including the request
, query
, and attributes
public properties, and also accepting JSON via the request body content.
The thing is, whilst we can accept this data, we were doing so in a way that left ourselves open for mistakes... at the very least.
What we ideally want to do is ensure only some specific values are acceptable.
Let's take an example route:
class DemoController extends Controller
{
/**
* @Route("/demo/{userId}", name="demo")
*/
public function demoAction($userId)
{
dump($userId);
return $this->render('::base.html.twig');
}
As we saw in the previous video, this will allow us to accept a parameter (or multiple parameters) via our URL that Symfony will convert for us (via the RouterListener
) into variable(s) such as $userId
.
The problem we have is that we haven't explicitly stated what kind of information is acceptable. By default, any character sequence that can come via the URL would therefore be 'valid'.
That's Numberwang
Thinking about it, a $userId
is commonly only going to be a number. Well, we may be using GUIDs, but for the moment, let's make life simple for ourselves and say in our application, user id
data is always numerical.
Thankfully, Symfony makes restricting this value to a number very easy. We just need to add in the appropriate requirements
annotation:
class DemoController extends Controller
{
/**
* @Route(
* "/demo/{userId}",
* name="demo",
* requirements={
* "userId": "\d+"
* }
* )
*/
public function demoAction($userId)
{
dump($userId);
return $this->render('::base.html.twig');
}
I've stretched this annotation out over multiple lines, purely for the sake of readability, but you are free to make it a one-liner.
Be careful with annotations. As they aren't code, it is really easy to make trivial mistakes, and the errors are not exactly beginner friendly:
[Syntax Error] Expected Doctrine\Common\Annotations\DocLexer::T_CLOSE_PARENTHESIS, got 'defaults' at position 82 in method AppBundle\Controller\DemoController::demoAction() in /var/www/twig.symfony-3.dev/src/AppBundle/Controller/ (which is being imported from "/var/www/twig.symfony-3.dev/app/config/routing.yml").
Translation: you missed a comma.
Meeting Your Requirements
Adding in requirements
is easy enough. The syntax is always the same. You just need to match up the routing placeholder (i.e. userId
) with the same key under your requirements
section.
The harder part is figuring out the correct regular expression to use.
Now, regular expressions (regex for short) are, frankly, not beginner friendly.
However, they are powerful, and largely universal, so at least spending a little time learning the basics is information that you can put to use in a whole slew of programming languages, not to mention config files, and debugging painful .htaccess
issues.
Fortunately, as regex are so common and so tricky, a whole bunch of websites have sprung up to make working with them that little bit less difficult. One really nice site is regexr.com, but plenty of other alternatives exist
When using a site like regexr, you can either build up your own, or if you are like me, find a decent looking starting point on a site like StackOverflow, then paste that in and hit the 'explain' tab, which will handily break down what initially looks like hieroglyphics, into something even my simple brain can comprehend.
Common Regex Patterns
There are some common regex patterns that, with a bit of use, will stick in your head.
Once you know a handful, it's really just a case of smushing a few patterns together until they meet your needs.
Of course, good tests will validate you didn't make unexpected mistakes, and again, sites like regexr will help 'explain' the patterns.
Here are a few common regex patterns I use:
\d+
- matches one or more digits\w+
- matches one or more alphanumerical characters and underscores[a-z0-9]+
- matches one ore more letters or numbersthis|that
- matches eitherthis
orthat
, the pipe being theor
part
And then you can combine them, such as [a-z0-9\-]+
which will match one or more alphanumerical characters and hyphens. The \
part before the -
is called an escape character, and won't be matched directly.
No Matches
The next question you may have might be: "what happens if we don't meet the requirements?"
Good question, even if I did put the words into your mouth.
Simply put, the route won't match.
And as a result, the user would see a 404
page.
This can be a little confusing as the output of your php bin/console debug:router
won't actually reveal this to you:
-------------------------- -------- -------- ------ -----------------------------------
Name Method Scheme Host Path
-------------------------- -------- -------- ------ -----------------------------------
demo ANY ANY ANY /demo/{userId}
You would need to look at the routing annotations directly to figure this out.
Or, well, there is another way of doing this - but it's better illustrated with a more complex example.
Example++
Chances are you commonly need to accept more than just a single parameter.
Let's take a more advanced example:
class DemoController extends Controller
{
/**
* @Route(
* "/demo/{userId}/{pageName}",
* name="demo",
* requirements={
* "userId": "\d+",
* "pageName": "this|that"
* }
* )
*/
public function demoAction($userId, $pageName)
{
dump($userId);
dump($pageName);
return $this->render('::base.html.twig');
}
Here we have two routing placeholders - userId
and pageName
.
As it stands, neither are optional. We must supply a value for both.
http://mysite.com/demo
-404
http://mysite.com/demo/123
-404
http://mysite.com/demo/123/other
-404
http://mysite.com/demo/123/that
-200
(only one that works)
We can provide some default values here.
class DemoController extends Controller
{
/**
* @Route(
* "/demo/{userId}/{pageName}",
* name="demo",
* requirements={
* "userId": "\d+",
* "pageName": "this|that"
* },
* defaults={
* "userId": "987",
* "pageName": "this"
* }
* )
*/
public function demoAction($userId, $pageName)
{
dump($userId);
dump($pageName);
return $this->render('::base.html.twig');
}
And now we can hit the following URLs:
http://mysite.com/demo
-200
> outputs987
, andthis
http://mysite.com/demo/123
-200
> outputs123
, andthis
http://mysite.com/demo/123/that
-200
> outputs123
, andthat
http://mysite.com/demo/123/other
-404
(other
is still invalid)http://mysite.com/demo/some-string
-404
(some-string
is not a number)
But there's one way to spoil your fun.
We can set a default to an 'invalid' value:
class DemoController extends Controller
{
/**
* @Route(
* "/demo/{userId}/{pageName}",
* name="demo",
* requirements={
* "userId": "\d+",
* "pageName": "this|that"
* },
* defaults={
* "userId": "tim",
* "pageName": "anything-we-like"
* }
* )
*/
public function demoAction($userId, $pageName)
{
dump($userId);
dump($pageName);
return $this->render('::base.html.twig');
}
And now, should we hit the base URL:
http://mysite.com/demo
-200
> outputstim
, andanything-we-like
So just be careful.
There are valid use cases for the above, but you can also inadvertently torpedo your good intentions.
Again, robust tests should catch such unexpected / unwanted outcomes.
Debugging
Lastly I did mention that there is an approach to debugging your routes that is better illustrated with a more complex example.
php bin/console debug:router
Is a really common one. It's useful for many reasons.
You can also test your routes more specifically.
The command would be:
php bin/console router:match {your route here}
Let's try that in our case, and see the various output:
php bin/console router:match /demo/999/this
[OK] Route "demo" matches
+--------------+--------------------------------------------------------------+
| Property | Value |
+--------------+--------------------------------------------------------------+
| Route Name | demo |
| Path | /demo/{userId}/{pageName} |
| Path Regex | #^/demo(?:/(?P<userId>\d+)(?:/(?P<pageName>this|that))?)?$#s |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | ANY |
| Requirements | pageName: this|that |
| | userId: \d+ |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: AppBundle:Demo:demo |
| | pageName: anything-we-like |
| | userId: tim |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+--------------------------------------------------------------+
Plenty of useful output here.
It shows our 'bad' defaults, the regex that Symfony will use behind the scenes, our routing requirements and more.
php bin/console router:match /demo/765
[OK] Route "demo" matches
+--------------+--------------------------------------------------------------+
| Property | Value |
+--------------+--------------------------------------------------------------+
| Route Name | demo |
| Path | /demo/{userId}/{pageName} |
| Path Regex | #^/demo(?:/(?P<userId>\d+)(?:/(?P<pageName>this|that))?)?$#s |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | ANY |
| Requirements | pageName: this|that |
| | userId: \d+ |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: AppBundle:Demo:demo |
| | pageName: this |
| | userId: 123 |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+--------------------------------------------------------------+
When we don't supply a value (pageName
in this case), we don't get any immediate notification. Instead, we must manually consult the defaults. In my opinion, this could be a little more user friendly.
php bin/console router:match /demo
[OK] Route "demo" matches
+--------------+--------------------------------------------------------------+
| Property | Value |
+--------------+--------------------------------------------------------------+
| Route Name | demo |
| Path | /demo/{userId}/{pageName} |
| Path Regex | #^/demo(?:/(?P<userId>\d+)(?:/(?P<pageName>this|that))?)?$#s |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | ANY |
| Requirements | pageName: this|that |
| | userId: \d+ |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: AppBundle:Demo:demo |
| | pageName: this |
| | userId: 123 |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+--------------------------------------------------------------+
Again, our route still matches here, but it's not perhaps immediately obvious why. You know, because you've been through this video, but imagine if you hadn't - it does require a few brain cycles to figure it out.
Of course it sounds trivial during a tutorial, but remember that this sort of thing is usually done whilst debugging - and you usually have a few things in your head already when tracking down problems.
php bin/console router:match /demo/bad
Route "demo" almost matches but requirement for "userId" does not match (\d+)
[ERROR] None of the routes match the path "/demo/bad"
Lastly when we pass in bad data, we will see the standard Symfony big red [ERROR]
command line output. Pity that I can't copy the formatting in, as without it, a lot of the impact is lost.