URL routing in PHP

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!

Josh Sherman - The Man, The Myth, The Avatar

About Josh

Husband. Father. Pug dad. Musician. Founder of Holiday API, Head of Engineering and Emoji Specialist at Mailshake, and author of the best damn Lorem Ipsum Library for PHP.


If you found this article helpful, please consider buying me a coffee.