PHP — Leveraging the Reflection API to Build a Simple Request Router
PHP may be an aging platform compared to so-called modern alternatives like Node.js, it remains largely used. Some major websites like Facebook are still running critical parts of their infrastructure with it. Why? Because it is a simple, fun and powerful programming language with some very cool features to uncover, notably thanks to the Reflection API.
What is Reflection?
At this point, you might think of light and mirrors. And you wouldn’t be completely wrong.
A reflexive programming language is capable of examining or describing itself, just like a mirror will send you back your own image. In more specific terms, reflection allows you to inspect declared types and variables dynamically. In practice, among many other things, you can create an instance of a class just by its name. To name a few, Java, C#, Objective-C and of course PHP are reflexive programming languages.
The downside of reflection is performance. Obviously, calling a function or a constructor through reflection (inspecting containing type then invoking) is way longer than calling it directly. Used extensively, reflection may cause real slow-downs. However, in most cases, the declared types won’t change their structure during the course of execution of your program, so caching reflected elements will compensate the initial loss.
Router, you Said?
In web development, a router is a single point of entry for all requests from the users of your website. It will interpret requests and then route them to the appropriate handling resource.
In PHP, it is generally the main index.php file that takes care of this task.
The Regular Way
Originally, to setup the routes for your website, you needed a configuration file and scripts that handled it accordingly. The configuration file was generally written in YAML or XML, then parsed by the router to understand what request routes were allowed or not and, finally, your corresponding code would be executed.
So when you needed to change the name of a route of your website, you had at least two loosely related files to update.
The Cooler Way
What if we could link our route configuration directly to our code? Wouldn’t it be great? Well, that’s exactly what the Reflection API will allow us to do. We just need to find a simple way of describing route configuration.
Fortunately, other programming languages have some very useful features to use as an inspiration, such as annotations in Java or attributes in C#. In both cases, annotations and attributes assign metadata to the described element of the language that can be inspected at runtime.
Annotations in PHP
Unfortunately, there is no support of annotations in PHP, even if all major frameworks like Symfony use annotations in PHP. How so?
Annotation support in PHP is what we could call a “metafeature” or an “emergent feature”. It relies extensively on the Reflection API and it’s ability to read documentation comments (doccomments).
Like many other programming languages, PHP differentiates regular comments
// This is a single-line comment/*
* This is a block comment
from documentation comments.
* This is a documentation block comment
* Notice the double star at the beginning
But unlike most programming languages, PHP’s Reflection API can recover raw doccomments at runtime.
Annotations in doccomments
Now we have a way to recover a doccomment as a raw string, we can include descriptive metadata in its body. By unformal convention, annotations are generally prefixed with @, just like in Java. It comes historically from source code annotations of phpDocumentor and other documentation generators like Doxygen.
* @complexAnnotation(param1, 'param2')
* @complexButDifferentAnnotation param1 param2
However, there is no official or monolithic definition for the syntax of annotations. Some efforts are done to standardize them, but unless using a specific framework that handle annotations for you, you’re free to use any syntax you like.
Building Our Router
We will need three “blocks” interacting with each other for our router to work:
- an autoloader method,
- at least one class to contain routes,
- an index.php file acting as the main entry point and router manager.
The Reflection API works like a charm with autoload. When you want to reflect a class, PHP will check if the class is already loaded. If not, it will try to load the class through autoloading, just the same as if you were invoking the class from regular code.
Let’s say our classes are stored inside a folder conveniently named classes and that file names match class names exactly, with respect of the case. Here is the code for this autoloader function:
The Route Class
This class only serves as an example for a route container. For the sake of clarity, only public static methods are allowed to be routes. This rule is enforced by requiring the presence of the @route annotation inside the doccomments of legitimate route methods.
The Main Entry Point
The first question to ask yourself is How do I tell the script the route I want to execute? IMVHO, the best answer is to use the path info.
In the above URL, every element past index.php compose the path info. You can retrieve this information in your PHP script and respond accordingly. Once the path info is recovered and parsed, your last step is only to reflect the specified class and method, check the presence of the @route annotation and invoke the method if valid.
The following code represent all you need in your index.php file to achieve this simple router:
Testing the Router
You can call your router main script with different parameters to test it. Here’s what you should see:
This is a valid route!> http://localhost/my-sample-router/index.php/Routes/invalidRoute
Method is not an allowed route> http://localhost/my-sample-router/index.php/Something/wrong
Route does not exist