Lumen is a micro-framework written in PHP by the designers of Laravel. I needed to learn how to use an API that is written PHP. The reason for this is that unlike Python or Java frameworks, PHP can be dropped on almost any web hosting site there is, whereas other languages require more specialised hosting.

API Setup

Setting up Lumen is a fairly well documented process but I will go over some of the parts that tripped me up initially.

Project Creation

Using composer

compoer create-project --prefer-dist laravel/lumen <project folder>

Environment Variables

First up editing the included .env file. We will be setting up the database later so you can delete the entries for anything database related.

APP_NAME=Project
APP_ENV=local
APP_KEY=MakeSureYouFillThisInWithARandomValue
APP_DEBUG=true
APP_URL=http://localhost
APP_TIMEZONE=UTC

LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=

CACHE_DRIVER=file
QUEUE_CONNECTION=sync

Database Configuration

Create a new file under the config directory and call it database.php. This file will tell lumen how to connect to ou chosen database.

<?php
return [
    'default' => env('DB_CONNECTION', 'mysql'),
    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
             'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => LEAGUE_CODE . '_',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],
        'sqlite' => [
            'driver' => 'sqlite',
            'database' => env('DB_DATABASE', storage_path('database.sqlite')),
            'prefix' => env('DB_PREFIX', ''),
        ],
    ],
];

Bootstrapping the App

To ensure the application will run correctly there are a few minor edits we need to change in the bootstrap/app.php file. This file is responsible for setting up the application on each request.

First up we need to enable facades and eloquent. SO uncomment the following lines:

$app->withFacades();
$app->withEloquent();

Whilst we are here, we can also add teh configuration required for the swagger-lume library that we will configure next. To do this we need to add a new line underneath the app config line

$app->configure('app');
// Add this line just here
$app->configure('swagger-lume');

Swagger Documentation

For documenting APIs that I create I prefer to use Swagger. By using swagger we can also get a live, intewractive, hosted version of our docs using swagger-ui. The library for this is called swagger-lume, which can be installed by using composer

composer require "darkaonline/swagger-lume:7.*"

Once we have the library downloaded we need to create the configuration file. For simplicity, you can download my config file:

Some important sections to note here are as follows:

  • All the URL routes are behind a common endpoint /api.
  • No middleware is included for protecting the documentation

The resasons for this are to do with the reverse proxy that is explained later in the post.

Adding a Test Route

Here we will add a a dummy route that we can query to ensure that our site is setup correctly. We need to add this because once behind the proxy, the default route included by lumen (that prints the app verion), should be inaccessible. This route is added to the file routes/web.php

$router->group(['prefix' => 'api'], function() use ($router) {
    $router->get('/', function() use ($router) {
        return response()->json([
            "hello" => "world",
        ], 200);
    });
});

Docker and Nginx

docker-compose is used here to connect and run all the services. There are 3 main services registered here: nginx, backend and db.

  • Nginx is the reverse proxy that routes traffic
  • Backend is our Lumen API
  • DB is a MySQL database for our backend to use

Also defined are two networks, front and back. The front network is accessible to the world and the backend network is private. The main use here is to ensure that our database is not world accessible.

version: "3"

volumes:
  db:

networks:
  front:
  back:

services:
  nginx:
    image: lumenapi/nginx:v1
    build:
      dockerfile: docker/nginx/Dockerfile
      context: .
    depends_on:
      - backend
      - frontend
    networks:
      - front
    ports:
      - "80:80"
    volumes:
      - ./backend:/var/www/html/api
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro

  backend:
    image: lumenapi/backend:v1
    build:
      context: .
      dockerfile: docker/backend/Dockerfile
    depends_on:
      - redis
      - db
    environment:
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=lumenapi
      - DB_USERNAME=lumenapi
      - DB_PASSWORD=helloworld
    expose:
      - 9000
    networks:
      - front
      - back

  db:
    image: linuxserver/mariadb
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - MYSQL_ROOT_PASSWORD=helloworld
      - TZ=Europe/London
      - MYSQL_DATABASE=lumenapi
      - MYSQL_USER=lumenapi
      - MYSQL_PASSWORD=helloworld
    volumes:
      - db:/config
    networks:
      - back
    ports:
      - 3306:3306

Directory Structure
.
|- backend/
|  |- app/
|  |- artisan
|  |- bootstrap/
|  |- ...
|- docker/
|  |- backend/
|  |  |- Dockerfile
|  |- nginx/
|  |  |- Dockerfile
|  |  |- default.conf
|- docker-compose.yml

docker/backend/Dockerfile

FROM composer as composer
WORKDIR /app
COPY backend/ /app
RUN composer install

FROM php:7.4-fpm-alpine
RUN docker-php-ext-install mysqli pdo pdo_mysql
COPY backend/ /var/www/html/api
COPY --from=composer /app/vendor /var/www/html/api/vendor
RUN chown -R www-data:www-data /var/www/html

WORKDIR /var/www/html
EXPOSE 9000

docker/nginx/Dockerfile

FROM nginx:latest
WORKDIR /var/www/html

COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

docker/nginx/default.conf

There is configuration here for a frontend as well. This could be, for example, some kind
of JavaScript frontend (like Vue or React). This config is included so that if you decide to sping up a frontend int eh docker-compose, you can just uncomment these lines and the app should work

Something that tripped me up for a while was the fact that the paths must be the same in both the nginx and backend container e.g. the code should be stored in /var/www/html in both containers

server {
    listen      80;
    server_name _;
    index       index.php index.html index.html;
    charset     utf-8;

    error_log   /var/log/nginx/error.log;
    access_log  /var/log/nginx/access.log;

#    # Requests to the frontend
#    location / {
#        proxy_set_header Host $host;
#        proxy_set_header X-Real-IP $remote_addr;
#        proxy_pass http://frontend:8080;
#    }

    # Requests to the Lumen API
    location ^~ /api {
        root      /var/www/html;
        index     index.php;
        try_files $uri /api/public/index.php;

        location ~ \.php {
            try_files $uri =404;
            fastcgi_pass backend:9000;
            fastcgi_index index.php;
            fastcgi_split_path_info ^(.+\.php)(.*)$;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
}

Running the API

To run the API you should now be able to run:

docker-compose up

We can then test our API by heading over to the browser and navigating to

  • http://localhost/
    • Should return a 404 page
  • http://localhost/api
    • Should return our hello world route as defined in backend/route/web.php