How to build a simple REST server in PHP.
For this example I tried to avoid any frameworks targeting at a simple PHP code.
If you want to skip my explanations, all the code is here: https://github.com/tan-tan-kanarek/php-rest-server-example
1. Apache configuration
In order to direct all REST paths into a single entry point, I defined rewrite rule:
Listen 82 <VirtualHost *:82> DocumentRoot "C:/My/Projects/php-rest-server-example/web" ErrorLog "C:/My/Projects/php-rest-server-example/log/apache-error.log" CustomLog "C:/My/Projects/php-rest-server-example/log/apache-access.log" combined RewriteEngine on RewriteRule ^/(.+)$ /index.php/$1 [PT] <Directory "/"> DirectoryIndex index.php Order allow,deny Allow from all </Directory "/"> </VirtualHost *:82>
2. Single entry point – index.php
The entry point triggering request-deserializer that returns a request object:
$request = RestRequestDeserializer::deserialize();
Executing the request:
$response = $request->execute();
And finally echoing the response:
3. Deserializing the request – RestRequestDeserializer.class.php
The request-deserializer is a static class, its most important method is parse that currently just deserialize the JSON, no matter if it arrived as application/json content-type or as json field inside multipart/form-data content-type (which could be useful if you want to upload files and send JSON in the same request).
The output of the serialization process is always a request-object.
Note that the request-deserializer is very simple thanks to the fact that all the routing points are always built of single service and single action, that enables me to implement controllers without defining a unique routing path for each action. I also don’t have to check the HTTP method because the action is part of the URL.
4. Request objects
I defined three types of requests.
Invalid request accepts the exception object in its constructor and just sets the response error, it’s instantiated by the request-deserializer in case of invalid inputs.
Controller request accepts controller, action (method) and arguments. The controller-request executes the action and set the response object or the response error in case of exception.
The execute method returns the request serializer that later will serialize the response object and response errors into a well formed JSON or XML.
Scheme request Parses the entire API and provides an XML descriptor that describe the entire API scheme, including controllers, actions, objects and enumerators. The scheme is auto-generated based on the code.
5. Services (controllers)
I created a simple users-service example with CRUD action (add, update, delete, get and search). The users-service is performing DB actions on a simple sqlite example file (run install.php to create it).
Normally I would have a single service for each data object while each service has CRUD actions for its data object, trying as much as I can to avoid nested objects, for example, if user has devices, I won’t add devices array on user object, I will prefer to create a new service called user-device that manages each user-device separately.
I also created a multi-request-controller that handles a series of requests, that can save the HTTP round-trip in case you want to execute several requests together.
Note that this model force me to have flat list of services, it simplifies the routing URLs as well as the server code.
6. Data model
An example user data model implemented in order to add, update, delete and search users.
7. Response serializer
Note that the response already contains errors and warnings in addition to the response content, so you don’t have to choose between response and error, you can return them both, that also means that the HTTP response code is always 200 OK, unless there is a true HTTP error, this way I distinguish between applicative error and HTTP protocol errors.
I used PHPUnit to test all my user-service CRUD action as well as pagination and multi-request.