olamileke.dev

All ArticlesGuidesSide Projects

olamileke.dev

All ArticlesGuidesSide Projects
Guides

A Guide to PHP Slim

#php-slim

#php

Fambegbe Olamileke. 2024/04/11, 2:47 pm

With frameworks in general (both frontend and backend) I tend to group them into two different categories. In the first category, there are heavyweight, batteries included frameworks that aim to offer an ecosystem with every single provision one might need. Frameworks like these generally tend to recommend or enforce a defined file structure and architecture for its applications. You essentially work within the framework's defined rules when using it. In this sense, they can be referred to as opinionated frameworks. They also typically include CLI tools for generating relevant files relating to the framework (models, services, migrations). Examples of frameworks like these are Laravel, Django, Nest JS on the backend and Angular 2 on the frontend (It's been ages since I wrote Angular but back then, this applied). On the other hand, there are micro frameworks which adopt more of a bring your own tooling approach. They aim to provide the basic foundation for your application, leaving you to make the relevant decisions regarding what other building blocks to constitute the rest of your application. There are no defined ways to structure files or define architecture. Whatever paradigm you think suits your use case best, you are free to use. They can be seen as routing mechanisms which provide a means to route endpoints to defined methods/functions/resolvers with the other bits and pieces decided on and supplied on you the engineer. Examples include Express, Flask, FastAPI on the backend and React (I use the word framework a bit loosely here because technically React is a library).


I typically reach for frameworks in the first category for work projects, or personal projects that are a bit more fully featured. Essentially, projects in which I would prefer to focus the bulk of my time on the unique business logic underpinning the application and would prefer to just follow the framework's conventions which have been decided by very smart people after making a ton of research. For the second category of frameworks, I typically make use of for smaller projects, small microservices or just instances where I would like to expose some functionality behind an API. With PHP, lately I have been itching to work with it outside of Laravel (a framework definitely in the first category), something I have not done since late 2019. Also, with Laravel being a framework that has a lot of abstractions, I have wanted for a while to get back to writing PHP on a lower level without a lot of the heavy lifting that Laravel does. I remember initially learning Object oriented PHP and using it to build out a few projects. Nothing I would be proud to have my name associated with today but the learning experience was fun plus it meant that when I picked up Laravel, I had some insight into its abstractions and what was really happening underneath the hood. To satisfy my itch, I decided to learn Slim, a PHP microframework that has been around for a while now, with a decent community behind it and is reasonably supported. As I always do, I built a small API to fully understand Slim, to fit all its different pieces in my head and grasp how they all fit together. The code is here if anyone wants to have a look.


Introduction

Slim is a fast and flexible microframework for building APIs. It is suited for a variety of use cases right from quick application prototypes to full stack web applications with templating engines like Twig. The core framework itself does not have a lot of code and instead aims to provide a starting point for your applications. As such it is quite extensible both with packages maintained by the official Slim team and by third party packages in the PHP package registry (Packagist). You can basically start with what you need for the current state of your application and add to it as requirements evolve. This differs from say Laravel, where you are installing the entire framework with so much functionality you are unlikely to ever need if all you are building is a simple service or a small API. Your entire Slim application can really be just 1 file as seen below


    
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

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

$app = AppFactory::create();

$app->post('/', function (Request $request, Response $response, array $args) {
    $response->getBody()->write("POST '/' route from my demo Slim app");
    return $response;
});

$app->get('/', function (Request $request, Response $response, array $args) {
    $response->getBody()->write("GET '/' route from my demo Slim app");
    return $response;
});

$app->run();
    


Your entire application is really just an instance of the Slim\App class, which is the instance returned by the Slim\Factory\AppFactory::create() method. Relevant endpoints, middleware and methods are called on the $app object at the end of which the application is started by calling $app->run().


Core Concepts

To put it succinctly, Slim is about dealing with and manipulating request and response objects. In fact, the official documentation describes the framework as "a dispatcher that receives an HTTP request, invokes an appropriate callback routine, and returns an HTTP response". It provides a whole host of methods to obtain information and change these two objects. Some of these methods for the request object used to get information from it are


     
// returns the HTTP method for the request (GET, POST, PUT, PATCH, DELETE, OPTIONS)
$request->getMethod(): String 

// returns the header's value as an array (headers can have multiple values)
$request->getHeader(String $header): Array

// returns the header's value as a comma separated list
$request->getHeaderLine(String $header): String

// returns the request body as an associative array (this only works if the body parsing middleware
// has been added, more on middleware later)
$request->getParsedBody(): Array
    


While a few corresponding methods for the response object are


    
// returns the response object's status code
$response->getStatusCode(): Int

// allows the definition of the JSON payload to be delivered in the response
$response->getBody()->write(String $payload): void
    


With regards to mutation, Slim provides the following methods on both the request and response objects


    
// returns the request or response object with the specified header ($name) having its 
// value equal to the $value parameter specified
$request->withHeader(String $name, $Mixed $value): Request
$response->withHeader(String $name, $Mixed $value): Response

// returns the request or response object with the specified header ($name) having its value equal to its 
// previous value  with the passed in $value parameter appended to it (headers can have multiple values)
$request->withAddedHeader(String $name, $Mixed value): Request
$response->withAddedHeader(String $name, $Mixed value): Response
    
// returns the request or response object with the specified header ($name) removed
$request->withoutHeader(String $name): Request
$response->withoutHeader(String $name): Response
    


The following methods are available on only the request object for changing it


    
// returns the request object with its query params equal to the passed in $queryParams parameter
$request->withQueryParams(Array $queryParams): Request

// returns the request object with the specified attribute embedded in it. This embedded attribute can be 
// obtained via the $request->getAttribute(String $name) method
$request->withAttribute(String $name, Mixed $value): Request    
   
// returns the request object without the specified attribute ($name)
$request->withoutAttribute(String $name): Request
    


For the response object, the following method is available on it for changing it


    
// returns the response object with the status code equal to the specified $code parameter
$response->withStatus(Int $code): Response
    


This is only a sample of the available methods, more are available and explained in the official documentation. Also, you'll notice that every one of these methods begin with a "with" prefix. This is because in Slim, the request and response objects are immutable value objects. This basically means that the objects themselves cannot be changed. Think of it as an object with all its properties defined as private without setters to change them. Instead, what happens is that when one of these "with" methods is called, it returns a brand new clone of the object with the only difference being whatever property has been changed.


Dependency Injection

In Dependency injection, the aim is to delegate the responsiblity of instantiating and managing class dependencies from the class itself to an external source. The thinking is that classes should focus on their core responsibilities only. Any external dependencies they may need would be supplied at run time by the external dependency injection mechanism. Class A needs Class B and Class C ?, not an issue, Class A go on ahead and get started with your tasks, when you reach the point at which you need Classes B and C, they'll be ready and available for you to use. To learn more about dependency injection, check out this article. Slim does not enforce the use of a dependency injection mechanism, it is entirely optional. And if you do choose to make use of one, in accordance with its flexible approach, it allows you to pick anyone so far as it implements the PSR-11 standard. Examples of these include PHP-DI and uma/dic.


Middleware

Middleware is a general concept in programming, available in a lot of backend frameworks and Slim is no different. It offers several packaged middleware out of the box and also makes it possible to write your own custom middleware. Examples of the packaged middleware include

  • Error Handling Middleware - Slim offers a default error handler to catch all uncaught exceptions thrown by your application.


                
    <?php
    
    use Slim\Factory\AppFactory;
    
    require __DIR__ . '/../vendor/autoload.php';
    
    $app = AppFactory::create();
    
    // Default error middleware
    $app->addErrorMiddleware(true, true, true);
    
    $app->run();
                
            


    As with every other feature offered by the framework, this middleware is easily customizable. Say you want to log errors and exceptions to an external service like Sentry, you could easily do that by extending the default error handler provided by Slim. If you also wanted, you could define and use your own custom error middleware. More on this and defining custom exceptions can be found here.

  • Method Overriding Middleware - Sometimes when building out applications, you might want to override the default method used for your HTTP request. For example, when writing tests and you want to simulate a particular scenario or in your controller action. Slim provides a really easy way to do this via its method overriding middleware. A bit more on this can be found here.
  • Body Parsing Middleware -By default in Slim the request body can be accessed via the getBody() method on the request object. However, this method returns an an instance of the Psr\Http\Message\StreamInterface class. It basically returns a stream. However, working with streams can be a bit tricky and ideally we would want to interact with the request body as a native PHP data structure such as an array. This allows for easier access and manipulation. You really only want to access the request body as a stream when it is really large so you can deal with it in chunks instead of trying to access it all at once and running the risk of your application running out of memory. Slim provides an out of the box middleware to convert the request body from a stream into an associative array. Simply call the addBodyParsingMiddleware() on the $app object as seen below.


                
    <?php
    
    use Slim\Factory\AppFactory;
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    $app = AppFactory::create();
    
    // Parse json, form data and xml
    $app->addBodyParsingMiddleware();
    
    $app->addErrorMiddleware(true, true, true);
    
    $app->run();
                
            


    With this the request body can be accessed via the getParsedBody() method as seen below


                
    $body = $request->getParsedBody();
                
            


Writing custom middleware to suit your application's unique requirements is also extremely easy in Slim. Say you had a bunch of endpoints in your API which only authenticated users should be able to access. In a situation like this, you don't want to be duplicating this logic to check for authentication in every single controller action. Instead, simply write an auth middleware and have it run before the action is invoked. Here is a minimal example of an auth middleware in action


    
<?php

// src/Middleware/AuthMiddleware.php

declare(strict_types=1);

namespace Motif\Middleware;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Exception;

class AuthMiddleware
{
    public function __invoke(Request $request, RequestHandler $handler): Response
    {
        $response = new Response();
        $response = $response->withStatus(401);
        $payload = json_encode(
            [
            "code" => "auth_001",
            "message" => "Unauthenticated."
            ]
        );
        $authHeader = $request->getHeaderLine("Authorization");
        $authHeaderSplits = explode(" ", $authHeader ?? "");

        if (!$authHeader || count($authHeaderSplits) !== 2) {
            $response->getBody()->write($payload);
            return $response;
        }

        try {
            JWT::decode($authHeaderSplits[1], new Key($_ENV["JWT_KEY"], "HS256"));
        }
        catch (Exception $exception) {
            $response->getBody()->write($payload);
            return $response;   
        }
        return $handler->handle($request);
    }
}
    


And here it is being used on a group of endpoints.


    
$app->group(
    "/readings", function (RouteCollectorProxy $group) {
        $group->post("", [ReadingController::class, "create"]);
        $group->get("", [ReadingController::class, "get"]);
        $group->put("/{uuid}", [ReadingController::class, "update"]);
        $group->delete("/{uuid}", [ReadingController::class, "delete"]);
    }
)->add("ReadingMiddleware")->add("AuthMiddleware");
    


In the example above, the AuthMiddleware has been added to the dependency container (dependency injection mechanism) and is referenced by its token in the container ("AuthMiddleware"). At runtime, the container injects an instance of the AuthMiddleware class which is then executed. You'll also notice that there's another middleware ("ReadingMiddleware") added to the same group of endpoints. In Slim, you can add multiple middlewares to either a single endpoint or a group of endpoints. It's also important to note that middleware execution flows from out to in. This means that the last middleware added is executed first while the first middleware is executed last. Much like a stack data structure (LIFO - Last In First Out or FILO - First In Last Out). Another way to think of it is as a concentric circle with each layer in the circle representing middleware being executed in turn until it reaches your controller action at the center.

Databases

Being a PHP framework, Slim works quite well with MySQL. It leaves you free to decide how you would like to connect to your database and manage your CRUD operations. It works quite well with ORMs (Object Relational Mappers) like Doctrine and Eloquent. Also, if you would like to ignore ORMs altogether and manage your database connections directly via PDOs (PHP Data Objects), you can do so with Slim. The choice really is yours.


Conclusion

I had a lot of fun learning about Slim and exploring it. It's extremely powerful and flexible. I always wanted a lightweight alternative to Laravel. I really liked Lumen (a Laravel microframework) and used it in a few projects but after support for it was dropped, I found myself lacking a PHP microframework in my arsenal. In the same way I could reach out for Express or Flask/FastAPI anytime I wanted to write a quick, small API in Typescript or Python, I wanted to be able to do same with PHP and Slim has more than met my expectations. Here's a link to the code for the API I wrote to learn Slim, I had initially intended to deploy it and build an accompanying frontend for it in Vue but I decided against that, I have other plans I intend to explore.

Share this article

More from Guides

olamileke.dev © 2024