Developers use WordPress to build all sorts of solutions. They can range from a small website to large application platforms. The larger the project gets, the more common it is to have the need for WordPress to handle custom URLs.
You might want to map a custom URL to a new template, a specific hook or both. These situations get more and more common as you work on larger WordPress projects. This type of problem is a bit of a growing up pain with WordPress.
In framework land, there’s a tool that helps you with that problem. It’s called the routing system. It’s a critical component of most frameworks. It lets you map URLs with different parts of your application.
It’s a tough problem to solve, but a good example of object-oriented design. That’s why we’re going to build one. It’ll show you how object-oriented programming helps you solve harder problems.
But isn’t this the job of the rewrite API?
Not quite. (But excellent question!) The rewrite API allows WordPress to understand human-readable URLs. There’s no question that it’s an important task.
That said, a routing system is a large enough problem that it’s made up of smaller sub-problems. Solving these sub-problems is necessary for the routing system to do its job. And, as you’ll see next, the rewrite API itself only takes care of one of these sub-problems.
The responsibilities of a routing system
To handle these problems, we’re going to break the routing system into more than one class. We’ll keep each class focused by limiting them to a single responsibility.
Let’s look at what these are.
Map custom URLs
The most basic need of a routing system is the ability to map custom URLs. It needs a way to know what you expect it to do when it receives a matching URL. This is what we call a route.
Routes are responsible for mapping the relationships between custom URLs and WordPress. They tell the routing system what to do when it matches a custom URL. Each route represents a single relationship and answers specific questions such as:
- What hook do you want to call?
- What template do you want to load?
Match a request to a route
The next thing the routing system needs to do is match a request to a route. This lets the routing system know which route it needs to process. The router is in charge of that job.
You can register routes with it. It’ll take a request that WordPress received and attempt to match it to one of them. But to do that, it needs to understand the request that WordPress received.
This is where the rewrite API becomes interesting. Its job is to transform a URL into something that WordPress can understand. The router also needs to recognize URLs to do its job.
Could the rewrite API help the router do that then? Of course, it could!
We’d have to design the router as a bridge between the routing system and the rewrite API. On one side, it would have to transform the registered routes so that the rewrite API could use them. On the other, it would have to take the information from the rewrite API and use it to match a route.
Process a route
Once the router matched a request to a route, the routing system needs to process it. This comes down to looking at the questions answered by the matched route.
- Does it want to call a hook?
- Does it want to load a template?
This is how the routing system will process the matched route. It’ll do it based on how it answered these questions.
So let’s say that the router matched a route. If it wants to call a hook, the routing system will call it. If it wants to load a template, the routing system will pass it to the WordPress template loader.
Limitations of the system
Routing isn’t an easy problem to solve. That said, we can make it easier by limiting what the routing system can do. Even with these limitations, the routing system is quite useful.
No route requirements
Route requirements let you create routes that are more granular. For example, you can create a route and restrict it to a specific HTTP method. It’s a powerful feature for sure.
That said, supporting route requirements demands more out of the routing system. It can’t just match a route to a URL anymore. It also has to make sure that the request matches the other requirements as well.
No path variables
Path variables let you define routes with dynamic URLs like /product/{id}
. When the routing system matches these routes, it extracts the variables from the URL. These variables then get sent to our hook as arguments or our template as variables.
There’s no doubt that this is an important feature. It’s even mandatory in some cases. But it also adds a tremendous amount of complexity to the routing system. That’s why we won’t support them here.
Building our routing system
Alright, we laid some initial groundwork for our routing system. It’s time to start building it! Now, you might be wondering, “Where do I start?”. That’s a valid question for you to have.
Finding a starting point
Building a routing system is a large task. It’s like looking up at a mountain that you have to climb. It’s normal to feel intimidated by it. You don’t always know what’s a good starting point. And, once you found one, how do you build out the entire system?
A good way to get over these issues is to try to start with the most isolated component in the system. You then work your way up from there. So what’s the most isolated component in the routing system?
It’s the route. A route just stores the mapping information for the routing system. It has no dependencies on other parts of the routing system.
So that’s where we’ll start.
Route class
The job of our Route
class is simple. We want it to be able to answer the two questions we saw earlier for a given URL. These were:
- Do you want to call a hook? If so which one?
- Do you want to load a template? If so which one?
That’s it! It’s not any more complicated than that. We just need to build it now.
Constructing a Route object
Let’s look at the constructor first as it sets the initial state of our Route
object. What does the __construct
method need to create a Route
object?
class Route { /** * The hook called when this route is matched. * * @var string */ private $hook; /** * The URL path that the route needs to match. * * @var string */ private $path; /** * The template that the route wants to load. * * @var string */ private $template; /** * Constructor. * * @param string $path * @param string $hook * @param string $template */ public function __construct($path, $hook = '', $template = '') { $this->hook = $hook; $this->path = $path; $this->template = $template; } }
As you can see above, the __construct
takes three parameters: path
, hook
and template
. The only required parameter is path
. It represents the URL path that we want the route to match. We can’t have a route without it.
hook
and template
are optional. That’s an intentional design decision. These two parameters represent the two questions we want our route to answer. We just don’t want to force a route to answer either question by default.
The method itself doesn’t do much. It assigns each parameter to internal variables with the same name. And each of these internal variables is private.
Getting information out of our Route object
Making our internal variables private has a (good) side-effect. Others can’t see what’s inside our Route
objects by default. We need to create ways for them to see what’s inside them.
class Route { // ... /** * Get the hook called when this route is matched. * * @return string */ public function get_hook() { return $this->hook; } /** * Get the URL path that the route needs to match. * * @return string */ public function get_path() { return $this->path; } /** * Get the template that the route wants to load. * * @return string */ public function get_template() { return $this->template; } }
That’s the job of these getter methods. They give others read access to the variables inside our Route
class. Each method starts with get_
and ends with the variable name.
As a note, we won’t be creating ways for others to change the variables inside our class. This is another design decision. We don’t want to allow others to change a Route
object once created. We call these types of objects: immutable objects.
Does a route answer a question?
Now that we have getter methods, there’s a way to get the route information out of our Route
objects. That said, there are situations where you don’t need to the answer to the question. Rather, you only want to know if it answers the question or not.
This isn’t hard to do. We know that the defaults for hook
and template
is an empty string. You can just use empty
function to check.
So you could just use the get method and do the empty check yourself. You’d have to do it every time you want to know if the Route
object answers the question not. But why should you have to do that? You shouldn’t have to.
class Route { // ... /** * Checks if this route want to call a hook when matched. * * @return bool */ public function has_hook() { return !empty($this->hook); } /** * Checks if this route want to load a template when matched. * * @return bool */ public function has_template() { return !empty($this->template); } }
That’s why has_hook
and has_template
are good helper methods to have. You remove code that you have to copy/paste everywhere. This improves the usability of our Route
class. This improves the quality of life of anyone that wants to use our class.
Router Class
Now that we have our Route
class, it’s time to move on to the largest piece of the puzzle: the Router
class. As we saw earlier, it has to do a lot of work for the routing system. It needs to:
- Manage the
Route
objects - Act as a bridge between the rewrite API and the routing system
- Match requests received by WordPress to a
Route
object
Handling these tasks is what gives it its size. They’re also a good way to break down the work we need to do to build the Router
class. But enough talking about it, let’s start building it!
Managing routes
The basic need of the Router
class is to manage Route
objects. Lucky for us, we don’t have to make it complicated. We can just do it by managing an internal array variable that contains our Route
objects.
class Router { /** * All registered routes. * * @var array */ private $routes; /** * Constructor. * * @param array $routes */ public function __construct(array $routes = array()) { $this->routes = $routes; } }
As you can see, there’s nothing fancy with our constructor. As you can see, our constructor reflects this basic need. It has a routes
parameter that lets you pass an initial set of routes. We assign these initial routes to the internal routes
variable.
That said, we shouldn’t limit adding routes to the initial Router
object construction. We might want to add more Route
objects to it later on. That’s a reasonable scenario to expect.
class Router { // ... /** * Add a route to the router. * * @param Route $route */ public function add_route(Route $route) { $this->routes[] = $route; } }
The add_route
method fixes the issue. It takes a Route
object as a parameter and then appends it to the routes
array.
We can also use our add_route
method in our constructor to make it safer.
class Router { /** * All registered routes. * * @var Route[] */ private $routes; /** * Constructor. * * @param Route[] $routes */ public function __construct(array $routes = array()) { $this->routes = array(); foreach ($routes as $name => $route) { $this->add_route($name, $route); } } // ... }
This is our updated __construct
method. The role of for
loop is to act as an extra validation step. It passes each element of the routes
array to the add_route
method.
Because add_route
uses type hinting, we’re now confident that routes
only contains Route
objects. We can also update the PHPDoc to reflect this by changing it to Route[]
. This implies that we expect an array of Route
objects as a parameter.
Bridging the rewrite API
So how do you use the rewrite API with a routing system? We’ve been dodging the question so far. The answer is that we want to use rewrite rules.
With the help of rewrite rules, the router can know when it should try to match a request to a Route
object. But how can the router know that? The trick is to add a unique name to each route.
add_rewrite_rule('^my-plugin$', 'index.php?route_name=my_plugin_index');
Here’s a simple example of a rewrite rule for a route with the path /my-plugin
. ^my-plugin$
is the regular expression that matches the path of our route. index.php?route_name=my_plugin_index
is the URL that WordPress redirects our route to.
The important part is route_name=my_plugin_index
. It’s what tells the router, “Hey! I’m a URL you should care about. LOOK AT ME.” In this case, it wants the router to look (Yes yes, we’re looking at you. Happy!?) for a route named my_plugin_index
.
Converting routes to rewrite rules
So now that we know what the rewrite API secret sauce is, we need our router to use it. First, we want to transform our registered Route
objects into rewrite rules. Let’s create a method to do that.
class Router { // ... /** * Adds a new WordPress rewrite rule for the given Route. * * @param string $name * @param Route $route */ private function add_rule($name, Route $route) { // ... } }
Here’s our first pass at the add_rule
method. We have two parameters: name
and route
. name
is the route name that we want to give it like my_plugin_index
.
We’re going to leave the inside empty for now. We need to lay down a bit of groundwork first. So let’s take care of that.
Assigning a name to a route
So far, we haven’t coded anything to track the name of a route for add_rule
. This is the first thing that we’re going to fix. The simplest way to do this is to change the structure of our routes array. We’ll make it an associative array where the name of the route is the key.
class Router { // ... /** * Add a route to the router. Overwrites a route if it shares the same name as an already registered one. * * @param string $name * @param Route $route */ public function add_route($name, Route $route) { $this->routes[$name] = $route; } // ... }
We updated our add_route
method to reflect this change. It now takes an extra name
parameter. name
is then used as the key when it adds route
to the routes
array.
class Router { /** * All registered routes. * * @var Route[] */ private $routes; /** * Constructor. * * @param Route[] $routes */ public function __construct(array $routes = array()) { $this->routes = array(); foreach ($routes as $name => $route) { $this->add_route($name, $route); } } // ... }
We also need to update our constructor. It now assumes that the given routes
array is an associative array with the route name as the key. The foreach loop now passes the name
key to add_route
.
Defining a route variable
As you saw earlier, route_name=
is what lets the rewrite rule identify a route. It’s not a good idea to hardcode the route variable name. So let’s give someone the option to override it.
class Router { /** * All registered routes. * * @var Route[] */ private $routes; /** * Query variable used to identify routes. * * @var string */ private $route_variable; /** * Constructor. * * @param string $route_variable * @param Route[] $routes */ public function __construct($route_variable = 'route_name', array $routes = array()) { $this->routes = array(); $this->route_variable = $route_variable; foreach ($routes as $name => $route) { $this->add_route($name, $route); } } // ... }
This brings us back to the __construct
method again. We’re going to add route_variable
as a method parameter. We’ll keep the route_name
as the default. route_variable
also gets assigned to an internal variable with the same name.
Converting our route path into a regular expression
We also need a way to transform our route path to a regular expression. This lets us turn /my-plugin
into ^my-plugin$
.
class Router { // ... /** * Generates the regex for the WordPress rewrite API for the given route. * * @param Route $route * * @return string */ private function generate_route_regex(Route $route) { return '^'.ltrim(trim($route->get_path()), '/').'$'; } }
generate_route_regex
takes a Route
object as a parameter. It uses that Route
object and gets the route path using get_path
. It then makes it all pretty for the rewrite API by applying a few trims here and there on it.
To do that, it starts by using trim
to remove all spaces from both sides of the route path. It also uses ltrim
to remove any /
at the beginning of the path. The rewrite API doesn’t want its regular expressions starting with them.
It finishes off by adding ^
to the beginning of the string and $
at the end. These are special regular expression characters. ^
only matches the beginning of a string while $
only matches at the end.
We add them to prevent matching errors. They force the rewrite API to do an exact match to our URL paths.
Going back to the add_rule method
So, let’s recap what we’ve done so far. We started tracking the name of every registered route. We added the internal route_variable
variable to let someone change the route variable name. We also created generate_route_regex
to convert a route path into a regular expression.
Now, we’ll use all these pieces to complete our add_rule
method.
class Router { // ... /** * Adds a new WordPress rewrite rule for the given Route. * * @param string $name * @param Route $route * @param string $position */ private function add_rule($name, Route $route, $position = 'top') { add_rewrite_rule($this->generate_route_regex($route), 'index.php?'.$this->route_variable.'='.$name, $position); } // ... }
The first you might have noticed is that we added position
as a third parameter. This lets us tweak the default position where the rewrite API add the rule for a route. By default, it would get appended to the all other rewrite rules. We’d rather have the rewrite API prepend it so that it can process it first. That’s why the default value is top
.
Looking at the completed add_rule
method, it’s just a wrapper around the add_rewrite_rule
function. It converts our three parameters (name
, route
and position
) into three arguments for add_rewrite_rule
.
The first argument of the add_rewrite_rule
is the regular expression of the new rule. So that’s where we’ll use the output of the generate_route_regex
method. The second argument is the redirect URL that WordPress will process. We create that redirect URL by concatenating route_variable
and name
inside a string. The third argument is the position
parameter described earlier.
Registering everything with the rewrite API
So far, we’ve seen how to convert a single Route
object into a rewrite rule. We still need a method that puts everything together. That’s the last piece of the puzzle.
class Router { // ... /** * Compiles the router into WordPress rewrite rules. */ public function compile() { foreach ($this->routes as $name => $route) { $this->add_rule($name, $route); } } // ... }
That’s the goal of the compile
method. It compiles everything in the Router
object into rewrite rules. It passes every registered route and its name in the router to add_rule
.
Right now, the router still wouldn’t work with the rewrite API. That’s because we didn’t register the route_variable
with it. Let’s fix that.
class Router { // ... /** * Compiles the router into WordPress rewrite rules. */ public function compile() { add_rewrite_tag('%'.$this->route_variable.'%', '(.+)'); foreach ($this->routes as $name => $route) { $this->add_rule($name, $route); } } // ... }
We added a call to add_rewrite_tag
to register route_variable
as a rewrite tag. We pass it the route_variable
prepended and appended with %
. We use (.+)
as the regular expression. This tells it to match any character 1 or more times.
Matching a route
At this point, we’ve handled everything bridging our routing system to the rewrite API. But that’s only one direction! We still need to handle converting information from the rewrite API to a route.
Lucky for us, this isn’t as complicated as what we’ve done so far. All the information that we need is in the internal WordPress query variables. We just need to look over them.
class Router { // ... /** * Tries to find a matching route using the given query variables. Returns the matching route * or a WP_Error. * * @param array $query_variables * * @return Route|WP_Error */ public function match(array $query_variables) { if (empty($query_variables[$this->route_variable])) { return new WP_Error('missing_route_variable'); } $route_name = $query_variables[$this->route_variable]; if (!isset($this->routes[$route_name])) { return new WP_Error('route_not_found'); } return $this->routes[$route_name]; } // ... }
And that’s exactly what the match
method does. It takes the array of query variables as a parameter. It then tries to find a matching route or returns a WP_Error
object with a relevant error code.
The first step is to check if there’s even a route name stored inside query_variables
. To do that, we do an empty check on query_variables
using route_variable
as the key. If it’s not empty, it’ll contain the name of the route we’re looking for. Otherwise, we send back a WP_Error
with missing_route_variable
as the error code.
Now that we have a route name, we just need to check if there’s a route registered with that name. We can do that using the isset
function. It lets us verify that there’s a Route
object stored in our routes
array. that using on our routes
. If there’s a route stored at route_name
location, we pass it back. Otherwise, we return another WP_Error
with the route_not_found
error code this time.
Processor Class
So far, we’ve stayed pretty far away from WordPress. Sure, we created a bridge between the routing system and the rewrite API, but that’s it. We still need to take care of the larger WordPress picture next.
What’s involved in doing that?
Well, we need to find out if there’s a route that matches the current request. Using that route, we have to call a specific hook, load a specific template or do both. There’s also the question of compiling our router and flushing the rewrite rules.
That’s still quite a bit of work left to do.
This will all be the job of our Processor
class. It’ll be in charge of wiring the routing system with the rest of WordPress using the plugin API. Let’s get to it!
Constructor and the plugin API
Since we’ll be using the plugin API, we have to design our class around that. You can pick the option that you prefer here. But for this example, we’ll use a custom constructor.
class Processor { /** * The router. * * @var Router */ private $router; /** * Constructor. * * @param Router $router */ public function __construct(Router $router) { $this->router = $router; } /** * Initialize processor with WordPress. * * @param Router $router */ public static function init(Router $router) { $self = new self($router); } }
This shows the initial setup for the Processor
class. There isn’t much to see there yet, but that’s intentional. We’ll fill the rest out as we add functionality to our class.
We have our __construct
method and init
as the custom constructor. Both take a Router
object as a parameter. That way the __construct
method can assign it to the router
internal variable.
$router = new Router('my_plugin_route_name'); Processor::init($router);
Processor::init($router);
initializes everything with WordPress. We create a Router
object outside the class and pass it to the constructor. This is just meant to serve as an example. The router
variable could come from anywhere in your plugin.
The important is that you pass it to the constructor. That way we don’t create a tight coupling between the two classes. We don’t want to create the router inside the Processor
class.
Loading routes
We have the skeleton of our Processor
class, but it’s still pretty empty. The next thing we want to do is add routes to it. That way we can load them into the router.
class Processor { /** * The router. * * @var Router */ private $router; /** * The routes we want to register with WordPress. * * @var Route[] */ private $routes; /** * Constructor. * * @param Router $router * @param Route[] $routes */ public function __construct(Router $router, array $routes = array()) { $this->router = $router; $this->routes = $routes; } /** * Initialize processor with WordPress. * * @param Router $router * @param Route[] $routes */ public static function init(Router $router, array $routes = array()) { $self = new self($router, $routes); } }
The first thing to do is to change our existing __construct
and init
methods. They now have routes
as a second parameter with an empty array as the default value. It’s an array of routes that we want to register with WordPress.
$router = new Router('my_plugin_route_name'); $routes = array( 'my_plugin_index' => new Route('/my-plugin'), ); Processor::init($router, $routes);
We also updated our external call to Processor::init
. We created an external array of routes with one Route
object in it. Our route has my_plugin_index
as its name and /my-plugin
as its path.
Why don’t we pass the routes into our Router
object right away? That’s a valid question to ask yourself. And there’s a good reason for that! (isn’t there always?)
We want to delay when our Processor
class added the routes to the router. That way any plugin or theme can filter them. This lets them either add or remove routes before they get registered with WordPress.
class Processor { // ... /** * Initialize processor with WordPress. * * @param Router $router * @param Route[] $routes */ public static function init(Router $router, array $routes = array()) { $self = new self($router, $routes); add_action('init', array($self, 'register_routes')); } /** * Register all our routes into WordPress. */ public function register_routes() { $routes = apply_filters('my_plugin_routes', $this->routes); foreach ($routes as $name => $route) { $this->router->add_route($name, $route); } } }
All this happens through the register_routes
method. We attached it to the init
hook in our init
method (a decision approved by the redundancy department of redundancy). This pushes the route registration to the last possible moment during loading.
Any plugin or theme can then use the my_plugin_routes
filter to change the array of routes. Once the routes filtered, the method loops through them and adds them to the router.
Compiling and flushing routes
Now that all our routes are in our router, there’s still the question of compiling it into rewrite rules. The good news is that we’ve already done all the work for it. We just need to call our compile
method in register_routes
.
class Processor { // ... /** * Register all our routes into WordPress. */ public function register_routes() { $routes = apply_filters('my_plugin_routes', $this->routes); foreach ($routes as $name => $route) { $this->router->add_route($name, $route); } $this->router->compile(); } }
So that was good news, but there’s also some bad news. Adding rewrite rules is just one of two steps when using the rewrite API. These rewrite rules won’t take effect until we flush them using flush_rewrite_rules
.
The problem is that flush_rewrite_rules
is an expensive function. We can’t always call it at the end of our compile
method. We need to add some logic to only do it when needed.
class Processor { // ... /** * Register all our routes into WordPress. */ public function register_routes() { $routes = apply_filters('my_plugin_routes', $this->routes); foreach ($routes as $name => $route) { $this->router->add_route($name, $route); } $this->router->compile(); $routes_hash = md5(serialize($routes)); if ($routes_hash != get_option('my_plugin_routes_hash')) { flush_rewrite_rules(); update_option('my_plugin_routes_hash', $routes_hash); } } }
The solution is to keep track of the routes that we registered the last time we flushed them. To do that, we need to hash our routes into a unique string. We start by serializing our routes into a string using the serialize
function. We then hash that string with the md5
function. This gives us a unique string to represent our routes.
We store this string in an option called my_plugin_routes_hash
. register_routes
compares that option to the value it gets from hashing our routes. If the value is different, it calls flush_rewrite_rules
. It also saves the routes_hash
as our new my_plugin_routes_hash
option.
Matching a request to a route
We’re almost at the finish line! We’ve done most of the heavy lifting. The rewrite API has all the information it needs to process our routes. We just need to check if it found anything.
Lucky for us, there’s an action hook designed just for that purpose: parse_request
. It fires when the WordPress finishes parsing the query variables of the incoming request. It passes the current WordPress environment as an argument.
class Processor { // ... /** * Initialize processor with WordPress. * * @param Router $router * @param Route[] $routes */ public static function init(Router $router, array $routes = array()) { $self = new self($router, $routes); add_action('init', array($self, 'register_routes')); add_action('parse_request', array($self, 'match_request'));; } /** * Attempts to match the current request to a route. * * @param WP $environment */ public function match_request(WP $environment) { $matched_route = $this->router->match($environment->query_vars); if ($matched_route instanceof Route) { $this->matched_route = $matched_route; } } // ... }
match_request
is the method that we’ll hook to parse_request
. We updated our init
method to register it. It takes a WP
object as a parameter. That’s the WordPress environment class.
The method itself is pretty simple. It calls the router match
method. It’ll return either a Route
or a WP_Error
. If we have a Route
object, we store it in the matched_route
internal variable. This is how the Processor
class knows if there’s a matched route or not.
class Processor { /** * Attempts to match the current request to a route. * * @param WP $environment */ public function match_request(WP $environment) { $matched_route = $this->router->match($environment->query_vars); if ($matched_route instanceof Route) { $this->matched_route = $matched_route; } if ($matched_route instanceof \WP_Error && in_array('route_not_found', $matched_route->get_error_codes())) { wp_die($matched_route, 'Route Not Found', array('response' => 404)); } } }
This last little bit is an optional step so feel free to ignore it. It’s an example of what you can do with the WP_Error
objects sent back by the router. In this case, we want to stop the WordPress process with a 404 error when we can’t find a route.
Why do that?
It’s just a way to prevent WordPress from doing something we don’t expect. We know from the match
method that it found our route variable. It just couldn’t find a route using it. We’re not sure how WordPress would handle it so we stop the process.
Processing the matched route
Let’s say that the match_request
method found something. We need to check which questions it answers. Does it want to call a hook, load a template or both?
Calling the route hook
class Processor { // ... /** * Initialize processor with WordPress. * * @param Router $router * @param Route[] $routes */ public static function init(Router $router, array $routes = array()) { $self = new self($router, $routes); add_action('init', array($self, 'register_routes')); add_action('parse_request', array($self, 'match_request')); add_action('template_redirect', array($self, 'call_route_hook')); } /** * Checks to see if a route was found. If there's one, it calls the route hook. */ public function call_route_hook() { // ... } // ... }
call_route_hook
handles whether the matched route wants to call a hook or not. The tricky question is “When do we want to call it?” While we could use any hook, template_redirect
is (in my opinion) the best one for the job.
Why? Well, it’s the last hook before the WordPress templating system goes into high gear. At that point, the WordPress process has done everything, but generate HTML. It’s the perfect location to create custom responses outside the templating system.
class Processor { // ... /** * Checks to see if a route was found. If there's one, it calls the route hook. */ public function call_route_hook() { if (!$this->matched_route instanceof Route || !$this->matched_route->has_hook()) { return; } do_action($this->matched_route->get_hook()); } // ... }
Now, let’s look at the call_route_hook
method. The first thing it does is to check if the router even found a route. We use an instanceof
check on matched_route
to see if it’s a Route
object. You also want to check if it even has a hook to call. If matched_route
passes both these tests, we call the route hook using do_action
.
Loading the route template
class Processor { // ... /** * Initialize processor with WordPress. * * @param Router $router * @param Route[] $routes */ public static function init(Router $router, array $routes = array()) { $self = new self($router, $routes); add_action('init', array($self, 'register_routes')); add_action('parse_request', array($self, 'match_request')); add_action('template_include', array($self, 'load_route_template')); add_action('template_redirect', array($self, 'call_route_hook')); } /** * Checks to see if a route was found. If there's one, it loads the route template. * * @param string $template * * @return string */ public function load_route_template($template) { if (!$this->matched_route instanceof Route || !$this->matched_route->has_template()) { return $template; } $route_template = get_query_template($this->matched_route->get_template()); if (!empty($route_template)) { $template = $route_template; } return $template; } // ... }
load_route_template
looks at whether the matched route wants to load a template or not. Unlike call_route_hook
, there’s only one good hook for this method. It’s the template_include
hook. It lets you filter the path of the template that the templating system wants to load.
We start by making similar tests as call_route_hook
. We check if we have matched route and if it has a template. If the tests fail, we return the template
value and stop there.
Now, if it does pass the test, we could just return the value from the get_template
method. But the truth is that it’s not a great solution. Instead, we’ll use get_query_template
to locate it. That way, the route only needs to pass a filename without the extension. The function will take care of the rest. If get_query_template
locates a template, we replace the value of template
with it.
The routing system at work
To finish things off, let’s see what our hard work looks like in practice. It shows two different situations where you can use the routing system. Each of them will have their own route.
$router = new Router('my_plugin_route_name'); $routes = array( 'my_plugin_index' => new Route('/my-plugin', '', 'my-plugin-index'), 'my_plugin_redirect' => new Route('/my-plugin/redirect', 'my_plugin_redirect'), ); Processor::init($router, $routes);
The my_plugin_index
route shows how to route to an index page. The routing system will use the route when URL path matches /my-plugin
. It doesn’t want to call any hook. Instead, it wants to load a template with the my-plugin-index.php
filename.
The my_plugin_redirect
route is a template-less route. It only wants the routing system to call the my_plugin_redirect
hook. It expects the hook to take care of generating a response.
function my_plugin_redirect() { $location = '/'; if (!empty($_GET['location'])) { $location = $_GET['location']; } wp_redirect($location); } add_action('my_plugin_redirect', 'my_plugin_redirect');
That’s what the my_plugin_redirect
function (I know, original name) does. We use /
as the default path. It then checks if the request had a location value. If it does, it redirects to that.
Looking at the big picture
This is a lot to digest. It’s not a walk in the park to build something like a routing system. Even more so if you’re struggling with object-oriented programming. It might even feel a bit insane.
It’s the catch-22 of object-oriented programming. It’s with these types of large systems where it shines brightest. Except that you need to feel comfortable with it to build them.
The good news is that our routing system is reusable. You can find all the code on GitHub. It’s all ready for you to use it on your own projects.
All that you need to do is create your own Route
objects. You don’t need to build anything else. You’re good to go!