Lumen: PHP REST API made easy

I researched PHP framework for an API and settled on Lumen for a few reasons:

  • It comes from a big name (Laravel). I have worked a little bit with Laravel before but always wanted to learn more as I consistently see it recommended as one of the best frameworks. This also gives it a very large community
  • A large number of 3rd party components are available for it should I need them. It is possible to pick and choose pieces from the Laravel framework for it (for example the Eloquent ORM)
  • It advertizes a very good performance
  • It follows modern PHP development practices (PSR compatible, installs with composer, etc)

I was torn between Slim and Lumen but settled on Lumen due to the slightly higher convenience – it was really a toss up between the 2, though. I usually like the option to hand pick the tools but in this case I was a little pressed for time so the convenience won. The downside is there are a lot more concepts to learn which somewhat offset the convenience advantage – a lot of the features are coming from the full Laravel framework. Then again, learning them is a big plus for my PHP skills, so, win-win! OK, I always have a bit of a weak spot for the underdog, so I gave a good try to Slim first, it is very good also, there are just a few more niceties around Lumen – the DI container is easier to use, the request and response are automatically parsed and formatted, and it comes with a bit more structure out of the box. The documentation is pretty good, though it requires a little bit of familiarity with the full framework, and it has a good community, being built on top of the very popular Laravel.

Creating the project

This is done with composer:
composer create-project –prefer-dist laravel/lumen blog

This will bootstrap the project. I liked the fact that out of the box it created a tests folder and some sample tests – many PHP projects have not yet embraced TDD. Though I was a little taken aback at the number of folders it generated – I was hoping for something a lot lighter for a “micro framework”. A lot of it is just examples, though.

I created a new MySQL database and configured it in the .env file.

Kicking the tires

Like an app using the Laravel framework, it has to be served from the “public” folder. I just used the php built in server to serve it (php -S localhost:8081), then I edited the route file (which is under app/Http/routes.php, though the documentation refers to a newer location web/routes.php) to change the default response:

$app->get('/', function () use ($app) {
    //return $app->version();
    return 'Hello world';
});

So that was easy.

How about a little database action though:

$app->get('/', function () use ($app) {
    return app('db')->select('SELECT * from users');
});

This gave me a nice error as I don’t have that table yet… so create it and insert some sample data, this gives me the results as JSON:

[{"id":1,"name":"Larry Laffer"},{"id":2,"name":"John Doe"}]

It’s worth noting that the framework comes with an optional ORM, Eloquent.

So far so good. I looked a little bit into the doc for Eloquent and it seemed a bit heavy but workable (more on that later).

Development!

Next I looked at how to implement the authentication. I needed to use Google web-based sign-in within my app and authenticate the user tokens with the API. Only some users were to be granted access so the API will do double duty:

  • Check the provided token against Google’s API, validating that it was indeed issued by Google
  • Check that the user’s email address has been recorded in our database
  • Then, it needs to issue a token that can be used for our own API requests.

I had one more slightly quirky requirement – some of the API methods could be accessed by unauthenticated users.

This is thus implemented in a few pieces.

Login Controller

Define a new route for the verification:

$app->get('/login/google_verify/{idToken}', 'LoginController@googleVerify');

You can define your routes using closures too and doing so is more “micro” style, but I like having a separate class for it.

At this point we can already write the controller and a test for it (I will come back to testing later):

class LoginController
{
    public function __construct(GoogleAuth $googleAuth)
    {
        $this->googleAuth = $googleAuth;
    }

    public function googleVerify($idToken) {
        $user = $this->googleAuth->validateSignin($idToken);
        $user->api_token = $idToken;
        $user->save();
        return $user->api_token;
    }
}

The DI container is well done and will automatically wire constructor dependencies. Actually they can be put on the request also:

Token Guard

Out of the box Lumen has support for an API token validation, it is just commented out (it is implemented in Providers/AuthServiceProvider, to activate one just needs to uncomment the line in bootstrappp.php).

$this->app['auth']->viaRequest('api', function (Request $request) {
        if ($request->hasHeader('X-API-Token')) {
            return User::where('api_token', $request->header('X-API-Token'))->first();
        }
    });

There are a few things to note:
The viaRequest just register a callback to be invoked on every request. I was confused because the line looks like it only applies to “api” requests, but the “api” string is actually just their configuration key and does not have any particular meaning
The $request parameter is an instance of Illuminate\Http\Request, which is a SymfonyRequest. It is not a standard PSR-7 request implementation, though it can be converted to one using an additional library
I changed it from the default “$request->input”, to use $request->header instead, this makes it easier to use from Ajax, and at the same time (very) slightly safer. The default input() will read from either $POST or $GET depending on the HTTP method used.
By declaring the “Request” type on the request parameter, you get type hint for it which is handy if you have an IDE that makes use of it, like PhpStorm.

Laravel actually has some support for an API token via a class named Token Guard, however this uses a few pieces that are not in place in the simpler version of Lumen (and is not needed anyway). There are a few more important concepts related to authentication in Laravel though they are not needed in this simple case.

Middleware

The last thing we need is applying a middleware that is going to filter out unauthenticated requests for specific route. Again, Lumen came with a simple but suitable example in the form of an “Authenticate” class – when that middleware is applied to a route, unauthenticated (“guest”) access will be prevented. I liked that the middleware was supplied as an example class leveraging only the existing framework feature, with very simple code, rather than supplying it as a framework class – it’s a subtle difference but it gives me more control over the code to extend it, and makes it easier to troubleshoot when things go wrong.
The middleware concept is simply to filter every request with a “handle” method that gets a reference to the request, and a reference to the next step in the pipeline (either the next middleware, or the request handler / controller proper). In addition it can receive other parameters provided by the DI.

And a few more features…

Logging

Lumen comes with Monolog out of the box, this can be configured by registering a callback in the app bootstrap, for example this is how I set it to log to STDERR, instead of a log file (which is the default but does not work on Heroku):

$app->configureMonologUsing(function(\Monolog\Logger $log) {
    $streamHandler = new \Monolog\Handler\StreamHandler('php://stderr', \Monolog\Logger::WARNING);
    $log->setHandlers([
        $streamHandler
    ]);
    return $log;
});

Eloquent ORM

So as soon as I wanted to do a little bit more with my data, I needed to use the ORM. I had looked at using RedBean ORM instead because it has a very cool “self-building” feature where it will create the tables and fields for you as you access them, but I actually rather define my models in advance, and some of the syntax was a little off-putting – a little bit too much naming-convention based magic in my (biased) opinion. And after all I am used to working with NHibernate so a little configuration does not scare me. The models are defined in the root of the app by default, which I changed to live in the Models namespace instead. It is possible to define migrations, these are created on the command line using a php script called “artisan”. However this does not tie to the model at all – it is only a way to define your tables. Care must then be taken to ensure that the table names, and the column names, match what eloquent is going to expect. For example, if a model named User has a property apiToken, this needs to be represented by a table “users” with a column for “api_token”. I found it pretty annoying that there was not a way to just generate the schema based on the model – at least a basic skeleton of it, where I would just have to fill in the column types (actually I might have to look at creating a script for that). Another thing that is somewhat related is that some of the functions within the app will take column names, some will take property names – which is a bit confusing. In fact most of the database access methods like create or where will take column names.

CORS

This is needed to make API requests that originate from another site. In my case the front end of the app was going to be hosted on another server altogether, so that was definitely needed. There are 2 parts to the puzzle:

  • Responses to requests to the site need to include headers to indicate it is OK to call it from other sites. We are basically telling the browser “it is OK to send arbitrary requests to this site, even if they were created somewhere else – we’ll have stuff in place to validate them ourselves”. This is easy to implement with a middleware that adds those headers on each request
  • OPTIONS request need to be answered in the same way. This is because for POST requests (really anything other than GET), the browser will send an OPTIONS request first to check the headers. The reason it does not do it for GET requests is that GET requests are supposed to not cause any side effect, so they are safe to send as is – the browser just needs to prevent the foreign script from accessing the result. I found a clever solution to this problem online – it works by automatically registering a handler when an OPTIONS request is received. Without that, we would have to declare a dummy OPTIONS route for every distinct route in our app, to prevent Lumen from answering with a 405 (method not allowed) error.

Testing

This is one area where Lumen really shines (get it? Shine! It’s a lumen! I’ll be here all week)
They have a base TestCase, that is based on PHP Unit, and include helper methods to send mock requests to the app, wrap tests so they can be executed without modifying the database, test that specific response or responses matching a specific pattern are received, access the services defined for the app, etc. And because the controller uses DI, it is easy to inject mock services. This is an example test that I am using for my signin method:

public function testGoogleVerifyValidUserShouldCreateToken() {
    $google = $this->createMock(\App\Services\GoogleAuth::class);
    $google->method('validateSignin')
        ->willReturn(new User());
    $ctrl = new LoginController();
    $result = $ctrl->googleVerify('123', $google);
    $this->assertInternalType('array', $result);
    $this->assertNotEmpty($result['apiToken']);
}

PhpStorm supports PHPUnit out of the box and will run the test with a decent UI, it is not as fancy as Resharper in Visual Studio but it is functional (and a lot faster)

Deploying to Heroku

Deployment to Heroku was without any problem, except I needed to update my composer installation, which I thought was odd since I had updated it a week or 2 ago. You just have to make sure to create a Procfile that will designate the public folder:

web: vendor/bin/heroku-php-apache2 public/

In Conclusion

Lumen is a great option for an API and it is really exciting to see such tools come to PHP. It is not trendy and is often treated as a second class language by those working with the more “in vogue” programming languages but it does have some great tools, it is very easy to find hosting for it and has a huge talent pool making it relatively easy to find resources (though as always with that quantity, come some quality problems). To quote a colleague, “php is the dirty whore of the internet”. Since the advent of Composer the ecosystem has really blossomed and I love that it is embracing techniques such as TDD and dependency management, even though they can still be a little rough around the edges.

I know I am looking forward to doing more with Lumen, and with PHP, in this and future projects.

Leave a Reply

Your email address will not be published. Required fields are marked *