Software Highlight: PHP REST Service


Building an API in PHP? Look no further than PHP REST Service.

PHP REST Service is a fork of the 10 year old package, marcj/php-rest-service, with new features and updates for the modern PHP API.

For more about the how and why, check out the last article.

The quick summary is that I needed to build a PHP API for work and wanted something that worked kind of like express. marcj/php-rest-service was the best I could find, but it needed some love, so I made my own version.

PHP REST Service is a smart API server that can automatically generate API endpoints from a class (or manually specified callback functions), using PHP reflection. It’s extremely lightweight with zero dependencies and a single server file, and it packs a ton of features like regex url patterns, JSON, XML, plaintext and custom responses, middleware functions, request validation, and OpenAPI specification generation, to name a few. Most importantly, it’s easy to learn, simple to integrate with existing projects, and takes only seconds to get up and running.

Hello World

Let’s start with the basics: getting an API with a single endpoint up and running.

PHP REST Service is a single page app and handles all the routing internally, so first things first, you need to configure your web server to forward all requests to your server file.

If your server is in index.php and you are running an apache web server, use the following .htaccess file to setup redirection:

Apache
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.+) index.php/$1 [L,QSA]

Now, let’s create our API. Simply install the package from composer

Bash
php composer install cdgco/php-rest-service

and create your index.php file

PHP
<?php

include 'vendor/autoload.php';

use RestService\Server;

Server::create('api')
    ->addGetRoute('', function() {
        return "Hello World!";
    })
->run();

That’s it! You now have a GET endpoint at “api/” which will return “Hello World!” in JSON.

Using Classes

Now, writing every endpoint as a method callback function could get tiring and messy very quickly. Instead, we can pass a class to our constructor and use the “collectRoutes” method to automatically scan the class for endpoints.

Simply create a class with public member functions starting with “get”, “post”, “put”, etc. and PHP REST Service will automatically turn your functions into endpoints with the correct HTTP method based on the first part of the function name, and the URL as the second part, with camel case converted into dashes.

For example, here is the same endpoint as above, but written as a class member instead

PHP
<?php

namespace MyRestApi;

include 'vendor/autoload.php';

use RestService\Server;

class Test {
  public function get(){
    return 'Yay!';
  }
}

Server::create('', 'MyRestApi\Test')
    ->collectRoutes()
->run();

Want to override the url specified in the function name? Simply add a function comment with the “@url” annotation. If provided, this URL will be used instead.

Processing Parameters

Want to use query, body, or path parameters? Just add an argument to your function.

The name of your function argument will automatically be bound to the corresponding endpoint parameter. For example, a function “getTest($hello)” will bind “$hello” to “$_GET[‘hello’]”.

For path parameters, use the “@url” comment annotation. The first arguments in your function will automatically be bound to the first corresponding capture groups in your regex URL. For example the URL annotation “@url /test/(\d+)” for the function “getTestFunction($hello, $world)” will bind the regex capture group “(\d+)” to “$hello” and “$_GET[‘world’]” to “$world”.

If a function argument has a default value, it is marked at optional, otherwise it will be marked as required and will throw an error if not provided. Request validation can be specified with the “@param” annotation which will verify the correct data type.

Responses

By default, PHP REST Service will send all responses in JSON with a status code and data (if successful) or error and message (if unsuccessful). You can change the response format by using the “setFormat” method, or by passing your own formatted using the “setCustomFormat” method. Built in response formatters are “json”, “xml”, and “text”.

To respond with an error, simply throw an exception in your function. The exception message and error code will be passed to the response. Don’t want to use the built in error handler? Fine, use your own.

Using the “setExceptionHandler” method, you can define exactly how the API should handle exceptions and what to respond with.

Middleware

While I want PHP REST Service to be a one-size-fits-all sort of solution, I can’t do everything for you, and I don’t know exactly what you need. So you can build all sorts of helpful extensions to PHP REST Service as middleware.

Use the “setCheckAccess” method to define a function that will be executed before processing the request. As the name implies, this is meant for access control, but you can also use is for rate limiting, logging, caching, or anything else your heart desires.

In the future, I may change the name and make it so you can have multiple middleware functions, but for now, this is how we’re doing things.

OpenAPI

I love documentation, and I love standards. Every API I build has a corresponding OpenAPI specification, and/or mock API, but who wants to have to write and update their specification, documentation, and mock API with every revision?

One of my favorite NPM packages is “express-openapi-validator”, but I hate having to manually keep my API spec and express routes in line with each other.

Luckily, PHP REST Service can do it all for you. Simply use the “setApiSpec” method to enable the “/spec” endpoint which will automatically generate an always up-to-date, valid JSON OpenAPI specification for your API.

Use Cases

I built this package specifically to use in a single project, but it’s a large project and has some interesting use cases.

First off, my API is versioned. It’s very easy to build and maintain multiple versions of an API with PHP REST Service. In my case, I made a separate file with a different class for each version. Each file has it’s own server instance inside, and my index.php file is simply a require_once statement with each version’s file.

PHP
// index.php

require_once('APIv1.php');
require_once('APIv2.php');
PHP
// APIv2.php

namespace APIv2;

require_once('vendor/autoload.php');

class Core {
    ...
}
Server::create('v2', new Core)
    ->collectRoutes()
->run();

My API also has multiple instances of the same app running on different databases. In order to choose a specific database, I separated my API into different data pools that correspond to different class constructors and databases:

PHP
// APIv2.php

namespace APIv2;

require_once('vendor/autoload.php');

class Core {
    ...
}

class Pool {
    ...
}
Server::create('v2', new Core)
    ->collectRoutes()
    ->addSubController('Pool1', new Pool("Pool1"))
        ->collectRoutes()
    ->done()
    ->addSubController('Pool2', new Pool("Pool2"))
        ->collectRoutes()
    ->done()
->run();

Finally, I use the middleware functions to implement custom access control and caching systems.

More Information

Ready to get started or want to learn more?

GitHub: https://github.com/cdgco/php-rest-service

Documentation: https://cdgco.github.io/php-rest-service/

Packagist: https://packagist.org/packages/cdgco/php-rest-service