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 method 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 now usually used as the web server.

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.

diagram

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();
    }
}
Rest.php

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';
    }
}
FooResource.php
<?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';
    }
}
BarResource.php

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();
index.php

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-bar-resource
Output from index.php

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;
noClass-will-fail.php
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
Output of noClass-will-fail.php

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
{
}
noMethod-will-fail.php
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
Output of noMethod-will-fail.php

Of course, we could check both the existence of 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:

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.