development
Build a REST API with PHP SlimInit
4/21/2022In 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.