The other day I was checking out what folks are querying to get to my site.

Of course a ton of people get here looking for VPS comparison, but what surprised me was the volume of queries for “PHP routing”.

Back in 2015 I had posted a basic page rounding in PHP guide and as you’d expect, it’d pretty basic.

I’m not one of those people that goes back and massages old posts (outside of typos, thanks Justin) so I thought I’d take a moment to discuss some more advanced PHP routing techniques, specifically exploring optional and variable parts of the URI.

Please keep in mind that this is a discussion of how to handle URL routing in PHP without any additional dependencies.

It doesn’t have to be this way though! There are loads of framework-specific and stand-alone routing libraries out there, so if you’re just looking for a package to drop in, head over to Packagist and install Slim or whatever else suite you.

Okay, so last time I talked about using a big ol’ switch statement to handle the routing based on the URI coming in.

First things first, I think it’s good practice to get a clean, non-query string polluted URI. To do so, all you need to do is grab everything before the ? in the request URI:

$uri = explode('?', $\_SERVER['REQUEST\_URI'], 2)[0];

Great, now that we have the URI sans-query string, let’s take a look at the different ways we could check against it:

Exact Match

Exact matches are easy, just check that the value is exactly that:

if ($uri === '/some/awesome/page') {
    // Do awesome stuff here
}

Obviously this would fall apart if you allow mixed-case URIs, if that’s the case, you can use a regular expression with the case-insensitive flag turned on:

if (preg_match('/^\/Some\/Awesome\/Page$/i', $uri)) {
    // Do awesome stuff here
}

Partial Match

Sometime you may want to only match part of a URI, generally the first part so that you can hand things off to another router. Works well if you like to group things a bit instead of having a single monolithic router.

Partial matches are very simple to accomplish and are very similar to our case-insensitive exact match:

if (preg_match('/^\/grouping//i', $uri)) {
    // Hand off to another router to handle /grouping/* URIs
}

Variable Match

Both exact and partial matches are easy to pull off. Where things get a bit more complex is when you start introducing variables.

It’s not hard to pull off a fuzzy match, where things get tricky is when you want access to the variable so you can use it in your code:

Of course, back at it again with the RegEx:

if (preg_match('/^\/person/(.\*)$/i', $uri, $vars)) {
  // $vars[1] contains our value!
}

This isn’t without problems though. That regular expression will accept anything for the variable’s value. If we know the value will only ever be a number, we can improve things a bit (and gain a bit of validation in the process!):

if (preg_match('/^\/puppy/(\d+)$/i', $uri, $vars)) {
  // $vars[1] contains our NUMERIC value!
}

Optional Variable

Sometimes variable, sometimes not.

We’ve all done, it built out a page that handles adding a new entry as well as editing an existing entry. In those situations, we need to match the URI without a variable as well as with one:

if (preg_match('/^\/kitten(\/(\d+))?$/i', $uri, $vars)) {
  // $vars[2] contains our NUMERIC value!
}

This is where things get a bit messy. Since the regular expression has nested parentheses, to group the slash with our variable and make the whole thing optional, the variable winds up in the 3rd index of the $vars array.

Not a deal break, just something to be aware of if you’re writing more advanced regular expressions to handle your routing.

Putting it all together

This is all well and good, but what’s it look like if we put it all together?

Easiest approach would be to just do a series of conditionals with a final else that handles 404 not found routing:

$uri = explode('?', $\_SERVER['REQUEST\_URI'], 2)[0];

if ($uri === '/') {
    // Home page
} else if (preg_match('/^\/users$/i', $uri)) {
    // Get a list of users
} else if (preg_match('/^\/user/[a-z0-9\_]{1,16}$/i', $uri)) {
    // Display a user's profile
} else if (preg_match('/^\/join(\/(\d+))?$/, $uri, vars)) {
    // Sign up page, with an optional referral code in $vars[2]
} else if (preg_match('/^\/admin\//i', $uri)) {
    // Hands off to a secret admin router
} else {
    // Handle that 404
}

Is it elegant? Oh hell no. But it gets the job done.

If you are working with a very small project that has zero intention of ever getting larger, this is more than sufficient. But if you’re dealing with more than a handful of pages, I’d be shopping around for a routing library!