A simple REST API
Before PHP became object-oriented, it was common practice to create applications as a collection of individual PHP scripts. Often with PHP, HTML and SQL mixed in one file. The file names of the scripts then corresponded to their URL paths because they were called directly from the web server.
Today, we know that this is not exactly easy to maintain and that it is also not that easy to do this consistently and securely. We therefore started using the so-called tunnelling process for new applications a long time ago and, with the spread of frameworks, it quickly became a quasi-standard to tunnel all requests to PHP through a single script.
Normally, we configure the web server so that it first tries to deliver existing static content such as CSS, JavaScript or images directly without PHP being involved. It would not really make sense from a performance point of view to load a PHP binary only to then deliver a file directly from the file system without using PHP at all.
Incidentally, the integration of PHP into the Apache web server used to work in exactly the same way: PHP was always loaded as part of the web server process, regardless of whether a static or dynamic request was being processed. Because this was not exactly resource-friendly, we now normally use PHP-FPM to separate the PHP processes from the web server process. Instead of Apache, the leaner and faster nginx is usually used as the web server today.
If our web server is now to process an HTTP request that does not retrieve an existing static file, this request is sent to the tunneling script. For nostalgic reasons, we will simply call it index.php
, although we could also choose any other name.
The corresponding configuration for nginx
could look something like this:
location / { try_files $uri @php; } location @php { fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /project/index.php; }
Before we take a look at the index.php
file itself, let's first write a class that represents our small REST API. After all, we want to work as object-oriented as possible:
<?php declare(strict_types=1);
class Rest
{
private array $routes = [
'/foo' => FooResource::class,
'/bar' => BarResource::class
];
public function run(): string
{
$class = $this->routes[$_SERVER['REQUEST_URI']];
$resource = new $class;
$method = strtolower($_SERVER['REQUEST_METHOD']);
return $resource->$method();
}
}
This is a very simplified example and definitely not an example of good code. But before we list everything that needs to be improved here, let's talk about what actually happens here - and why.
Our REST API only supports static URLs for the time being, and we have defined two resources, namely foo
and bar
. We only want to implement GET
and POST requests
for both resources for now, but we will see in a moment that it is very easy to implement other HTTP methods as well.
In the property routes
, we define an associative array that assigns the URL path of a resource to the class that represents this resource and will process the HTTP requests directed to this resource.
In the run()
method, we first check which URL path was called and retrieve the corresponding class name from routes
. Then we create an object of this class and determine which method $method
we want to call in it. Finally, we actually call this method and return the result.
The resources look like this:
<?php declare(strict_types=1);
class FooResource
{
public function get(): string
{
return 'GET /the-foo-resource';
}
public function post(): string
{
return 'POST /the-foo-resource';
}
}
<?php declare(strict_types=1);
class BarResource
{
public function get(): string
{
return 'GET /the-bar-resource';
}
public function post(): string
{
return 'POST /the-bar-resource';
}
}
And this is what our index.php
looks like:
<?php
require __DIR__ . '/autoload.php';
$_SERVER['REQUEST_URI'] = random_int(0, 1) === 0 ? '/foo' : '/bar';
$_SERVER['REQUEST_METHOD'] = random_int(0, 1) === 0 ? 'GET' : 'POST';
print (new Rest)->run();
We use autoload, as befits modern code. Then, to make our tag a little more interesting, we choose whether to simulate a request on /foo
or /bar
and whether this should be a GET
or POST request
. Normally I wouldn't write anything in superglobal variables, but for a quick test on the command line you can do it this way.
Let's try out our REST API:
POST /the-foo-resource
The dynamic routing works. Whether we call resources, controllers, handlers or whatever: in PHP, we can very elegantly determine both class and method names and call methods at runtime. More or less every framework does this.
If it doesn't work
Of course, the price we pay for this dynamic is that things can go wrong at runtime. For example, we could try to instantiate a non-existent class:
<?php declare(strict_types=1);
$class = 'doesNotExist';
$object = new $class;
PHP Fatal error: Uncaught Error: Class "doesNotExist" not found in noClass-will-fail.php:4 Stack trace: #0 {main} thrown in noClass-will-fail.php on line 4
The value of the variable $class
is only known at runtime. This means that the compiler has no chance to check whether we want to instantiate an existing class. Therefore, the attempt to instantiate nonsense does not lead to a translation error, but to a runtime error.
We have the same problem again with the names of the methods that we call:
<?php declare(strict_types=1);
$method = 'doesNotExist';
$object = new Something;
$object->$method();
class Something
{
}
PHP Fatal error: Uncaught Error: Call to undefined method Something::doesNotExist() in noMethod-will-fail.php:5 Stack trace: #0 {main} thrown in noMethod-will-fail.php on line 5
Of course, we could check the existence of both a class and a method using reflection, but in PHP 7 the corresponding runtime errors have become exceptions, which we could simply intercept and handle accordingly.
In general, however, it is in the nature of the PHP programming language to produce runtime errors rather than translation errors. We have to react to this with suitable error handling.
We are not finished yet
There are a few things that we have (over)simplified or omitted in this example:
- We would probably normally load the
routes
array from a configuration file and passRest
as a constructor parameter. - In a real REST API, we would have to support URLs with parameters, such as
/bars/<id>
, in addition to static URLs. To incorporate this, we could expand the router into a chain of responsibility. - We would have to write code for the case that the called route does not exist. Here we would probably return a 404 response code.
- We would probably delegate the generation of the resource to a factory because the resources have dependencies that need to be resolved during generation.
- We do not currently filter which request methods are supported. The use of an enum would probably make sense here. For security reasons, it is generally not a good idea to use external input more or less directly as method names.
- It is not specified anywhere which methods a resource must implement. We should define an interface for this.
- We simply return a string without worrying about the format.
- We deliberately ignore the fact that our HTTP response needs any headers.
- We directly access superglobal variables. Instead, we should use a request object.
- We do not pass a request to the resources. Normally, we would have to pass an
HTTPRequest
as a parameter to theget()
andpost()
methods. - As always in my example programs, there is no error handling at all because we want to focus on the essentials here, namely the happy path.
Of course, all the productive code is still missing in the individual resources, after all, our resources should not just return prefabricated texts. In reality, we would probably delegate the resource classes to services in a similar way to MVC controllers.
Outlook
This type of tunneling script is also known as a front controller
. In this way, we avoid duplication because we carry out the entire bootstrapping in a central location. If we also wanted to check whether the resource can be accessed at all, the front controller would be a good place to incorporate this. The individual resources would then not need to know anything about access rights, which means in particular that the API developer cannot make the mistake of simply forgetting the rights check for an individual resource or not implementing it properly.
Not all types of rights checks can be implemented in this way, but that would be going too far here. Routing for a website is also usually different because we mostly process GET requests
there.