Build a REST API with PHP SlimInit

4/21/2022

In this quick tutorial I will guide you through the setup of a simple REST API PHP Project using SlimInit, a wrapper around the Slim Framework.

Installation

First we need to create a blank PHP composer project. Create a new folder, execute composer init and follow the steps of the composer config generator. You can either choose to interactively define the dependencies and search for adeptoas/slim3-init there, or run composer require adeptoas/slim3-init after the setup. Please note that version ^4.0 of the SlimInit library is based on Slim4, despite the 3 in its current name.

Project Structure

This is an example directory structure for a sliminit project:

your-project/
    public/
        index.php               # your app's entry point
    src/                        # the core of your app (models, etc.)
    handlers/                   # folder for all API handlers (more on that later)
        ExampleHandler.php
    config/                     # optional config files, e.g. database or email credentials
        db.yml
    templates/                  # optional template files for emails, etc.
    vendor/                     # the installed composer dependencies
    composer.json               # your composer config
    composer.lock

Entrypoint

Your public/index.php needs to create a new SlimInit instance and load your API handlers.

<?php

use Adepto\Slim3Init\SlimInit;

require __DIR__ . '/../vendor/autoload.php';

$slim = new SlimInit(withCORS: true);

$slim
    ->getCORS()
    ->setAllowedOrigins(['https://example.com', 'http://localhost:3000']);

$slim
    ->addHandlersFromDirectory(__DIR__ . '/../handlers')
    ->run();

The convienient method addHandlersFromDirectory will automatically load and register the routes from all handlers in that specified folder. SlimInit also has built-in preflight/CORS support - so there’s no need for a custom middleware.

Handlers

A handler is a class that is responsible for one or more endpoints. Each handler returns an array of routes that are automatically being registered and map a route to a callback method.

This is a basic handler that creates a POST route for /example and returns a JSON body.

<?php

use Adepto\Slim3Init\{
    Request,
    Response,
    Handlers\Route,
    Handlers\Handler,
};

class ExampleHandler extends Handler {
    public function example(Request $request, Response $response, stdClass $args): Response {
        if (!empty($request->getParsedBody()['name'])) {
            $name = $request->getParsedBody()['name'];
            return $response->withJson(['message' => "Hi $name!"]);
        }

        return $response->withJson([
            'message' => 'Who are you?',
        ], 400);
    }

    public static function getRoutes(): array {
        return [
            new Route('POST','/example[/]', 'example'),
        ];
    }
}

SlimInit handlers are a great approach on structuring endpoints without having to register each route on the slim instance manually. In a simple todo app/API you could split the endpoints into handlers like AuthHandler (user authentication), TodoListHandler (manage lists), TodoHandler (manage todos).

Middleware

SlimInit also comes with a great support of middleware. For example, implementing a JWT authentication is very convienient. The SlimInit base handler class has a container, which can contain the user object inserted by the middleware. In every handler at any point you can then use $this->getClient(); to get the currently authenticated user.

This is an excerpt from a middleware that authenticates a user based on the Authorization header (actual authentication/validation is skipped):

<?php

namespace App\Middleware;

use Psr\Container\ContainerInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Adepto\Slim3Init\{
    Request,
    Response,
};

class UserAuthMiddleware {
    public function __construct(
        protected ContainerInterface $container,
    ) {}

    public function __invoke(Request $request, RequestHandlerInterface $handler): Response {
        session_write_close();

        if ($request->hasHeader('Authorization')) {
            $token = $request->getHeader('Authorization')[0];
            // validate token here and get the user object
            $this->container->Client = $user;
        }

        return $handler->handle($request);
    }
}

Then register the middleware in your public/index.php…

$slim
    ->addHandlersFromDirectory(BASE_DIR . '/handlers')
    ->addMiddleware(new UserAuthMiddleware($slim->getContainer()))
    ->run();

…and you are ready to go:

class ExampleHandler extends PrivilegedHandler {
    public function example(Request $request, Response $response, stdClass $args): Response {
        if ($this->hasClient()) {
            $name = $this->getClient()->getUsername();
            return $response->withJson(['message' => "Hi $name!"]);
        }

        return $response->withJson([
            'message' => 'Who are you?',
        ], 400);
    }

    ...
}

The SlimInit PrivilegedHandler comes with methods like hasClient() and getClient()- however your user object needs to implement the SlimInit Client interface. Alternatively you can also easily create your own Handler class to extend from. I am using the following for a little more flexibility and using my own UserInterface:

<?php

namespace App\API;

use Adepto\Slim3Init\Handlers\Handler as SlimInitHandler;
use App\Exceptions\UnauthorizedException;
use App\Models\User\{
    Session,
    UserInterface,
};

class Handler extends SlimInitHandler {
    /**
     * Require auth, throws an exception if user is not authenticated
     *
     * @return void
     */
    public function requireAuth(): void {
        if (empty($this->container->Client)) {
            throw new UnauthorizedException('User authentication failed.', 3);
        }
    }

    /**
     * Returns the currently authorized user (or null)
     *
     * @return UserInterface|null
     */
    public function getUser(bool $require = false): ?UserInterface {
        if ($require && empty($this->container->Client)) {
            throw new UnauthorizedException('User authentication failed.', 3);
        }

        return $this->container->Client ?? null;
    }

    /**
     * Returns the current session of the authorized user (or null)
     *
     * @return Session|null
     */
    public function getSession(): ?Session {
        return $this->container->UserSession ?? null;
    }
}

In addition to the user object my auth middleware is also checking the user’s session. And I can use the getUser(), requireAuth() and getSession() functionality everywhere in my handlers.

Exception Handling

Another cool feature of SlimInit is the ability of registering custom exception handlers. In nearly all projects I am using an InvalidValueException that is being thrown when data in a request is invalid. Instead of catching these exception in each handler (and generating a response from that) it's possible to create an exception handler:

<?php

namespace App\API\ExceptionHandlers;

use Adepto\Slim3Init\Handlers\ExceptionHandler;
use Adepto\Slim3Init\Request;
use Psr\Http\Message\ResponseInterface;
use App\Exceptions\InvalidValueException;
use Throwable;

class InvalidValueExceptionHandler extends ExceptionHandler {
    public function handle(Request $request, Throwable $t, bool $displayDetails): ResponseInterface {
        /**
         * @var InvalidValueException $t
         */
        return $this->createResponse(status: 400)->withJson([
            'status' => 'invalidValue',
            'key' => $t->getKey(),
        ]);
    }
}

It can be registered again in the public/index.php:

$slim->setException(InvalidValueException::class, InvalidValueExceptionHandler::class)

Whenever an (uncatched) InvalidValueException is thrown, it results in an error 400 with the json output from the handler. This saves a lot of duplicate code in handlers where user input is validated.

Same goes for other useful exception handling like being unauthorized, lack of permissions, exceeding rate limits, etc.

Conclusion

SlimInit is a really great enhancement of the slim framework that makes projects much cleaner and better structured. Check out the SlimInit Github repository for the full documentation! Also check out my yaml-config library for convieniently using .yml files for your config files (database credentials, etc.) that I use along with SlimInit.