Slim 4 Tutorial

05 Nov 2019

This tutorial shows you how to work with the powerful and lightweight Slim 4 framework.

Table of contents

Requirements

Introduction

Slim Framework is a great micro framework for web application, RESTful API’s and websites.

Our aim is to create a RESTful API with routing, business logic and database operations.

Standards like PSR and best practices are very imported and integrated part of this tutorial.

Installation

Create a new project directory and run this command to install the Slim 4 core components:

composer require slim/slim

In Slim 4 the PSR-7 implementation is decoupled from the App core. This means you can also install other PSR-7 implementations like nyholm/psr7.

In our case we are installing the Slim PSR-7 implementations using this command:

composer require slim/psr7

As next we need a PSR-11 container implementtion for dependency injection and autowiring.

Run this command to install PHP-DI:

composer require php-di/php-di

For testing purpose we are installing phpunit as development dependency with the --dev option:

composer require phpunit/phpunit --dev

Ok nice, now we have installed the most basic dependencies for our project. Later we will add more.

Note: Please don’t commit the vendor/ to your git repository. To set up the git repository correctly, create a file called .gitignore in the project root folder and add the following lines to this file:

vendor/
.idea/

Directory structure

A good directory structure helps you organize your code, simplifies setup on the web server and increases the security of the entire application.

Create the following directory structure in the root directory of your project:

.
├── config/             Configuration files
├── public/             Web server files (DocumentRoot)
│   └── .htaccess       Apache redirect rules for the front controller
│   └── index.php       The front controller
├── templates/          Twig templates
├── src/                PHP source code (The App namespace)
├── tmp/                Temporary files (cache and logfiles)
├── vendor/             Reserved for composer
├── .htaccess           Internal redirect to the public/ directory
└── .gitignore          Git ignore rules

In a web application, it is important to distinguish between the public and non-public areas.

The public/ directory serves your application and will therefore also be directly accessible by all browsers, search engines and API clients. All other folders are not public and must not be accessible online. This can be done by defining the public folder in Apache as DocumentRoot of your website. But more about that later.

Apache URL rewriting

To run a Slim app with apache we have to add url rewrite rules to redirect the web traffic to a so called front controller.

The front controller is just a index.php file and the entry point to the application.

# Redirect to front controller
RewriteEngine On
# RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]
<?php

(require __DIR__ . '/../config/bootstrap.php')->run();

The front controller is the entry point to your slim application and handles all requests by channeling requests through a single handler object.

Configuration

The directory for all configuration files is: config/

The file config/settings.php is the main configuration file and combines the default settings with environment specific settings.

<?php

// Error reporting
error_reporting(0);
ini_set('display_errors', '0');

// Timezone
date_default_timezone_set('Europe/Berlin');

// Settings
$settings = [];

// Path settings
$settings['root'] = dirname(__DIR__);
$settings['temp'] = $settings['root'] . '/tmp';
$settings['public'] = $settings['root'] . '/public';

return $settings;

Boostrapping

Boostrapping is the first code that is executed when the application (request) is started.

The bootstrap procedure starts with the composer autoloader and then continues to build the container, create the app and register the routes + middleware entries.

Create the bootstrap file config/bootstrap.php and copy/paste this content:

<?php

use DI\ContainerBuilder;
use Slim\App;
use Symfony\Component\Translation\Translator;

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

$containerBuilder = new ContainerBuilder();

// Set up settings
$containerBuilder->addDefinitions(__DIR__ . '/container.php');

// Build PHP-DI Container instance
$container = $containerBuilder->build();

// Create App instance
$app = $container->get(App::class);

// Register routes
(require __DIR__ . '/routes.php')($app);

// Register middleware
(require __DIR__ . '/middleware.php')($app);

return $app;

Routing setup

Create a file for all routes config/routes.php and copy/paste this content:

<?php

use Slim\App;

return static function (App $app) {
    // empty
};

Middeware

What is a middleware?

A middleware can be executed before and after your Slim application to manipulate the request and response object according to your requirements.

Read more

Routing and error middleware

Create a file to load global middleware handler config/middleware.php and copy/paste this content:

<?php

use Slim\App;

return static function (App $app) {
    // Parse json, form data and xml
    $app->addBodyParsingMiddleware();

    // Add global middleware to app
    $app->addRoutingMiddleware();

    // Error handler
    $displayErrorDetails = true;
    $logErrors = true;
    $logErrorDetails = true;

    $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails);
};

Container

A quick guide to the container

Dependency injection is passing dependency to other objects. Dependency injection makes testing easier. The injection can be done through a constructor.

A dependencies injection container (DIC) is a tool for injecting dependencies.

A general rule: The application itself should not use the container. Injecting the container into a class is an anti-pattern. Please declare all class dependencies in your constructor explicitly instead.

Why is injecting the container (in the most cases) an anti-pattern?

In Slim 3 the Service Locator (anti-pattern) was the default “style” to inject the whole (Pimple) container and fetch the dependencies from it. However, there are the following disadvantages:

Q: How can I make it better?

A: Use composition over inheritance and (explicit) constructor dependency injection.

Dependency injection is a programming practice of passing into an object it’s collaborators, rather the object itself creating them.

Since Slim 4 you can use modern tools like PHP-DI with the awesome autowire feature. This means: Now you can declare all dependencies explicitly in your constructor and let the DIC inject these dependencies for you.

To be more clear: Composition has nothing to do with the “autowire” feature of the DIC. You can use composition with pure classes and without a container or anything else. The autowire feature just uses the PHP Reflection classes to resolve and inject the dependencies automatically for you.

Container definitions

Slim 4 uses a dependency injection container to prepare, manage and inject application dependencies.

You can add any container library that implements the PSR-11 interface.

Create a new file for the container entries config/container.php and copy/paste this content:

<?php

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Slim\App;
use Slim\Factory\AppFactory;

return [
    'settings' => static function () {
        return require __DIR__ . '/settings.php';
    },

    App::class => static function (ContainerInterface $container) {
        AppFactory::setContainer($container);
        $app = AppFactory::create();

        // Optional: Set the base path to run the app in a subdirectory.
        //$app->setBasePath('/slim4-tutorial');

        return $app;
    },

    ResponseFactoryInterface::class => static function (ContainerInterface $container) {
        return $container->get(App::class)->getResponseFactory();
    },
];

Your first route

Open the file config/routes.php and insert the code for the first route:

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;

return static function (App $app) {
    $app->get('/', function (Request $request, Response $response) {
        $response->getBody()->write('Hello, World!');

        return $response;
    });
};

Now open your website, e.g. http://localhost and you should see the message Hello, World!.

If you get a 404 error (not found), you should define the correct basePath in config/container.php.

Example:

$app->setBasePath('/slim4-tutorial');

PSR-4 autoloading

For the next steps we have to register the \App namespace for the PSR-4 autoloader.

Add this autoloading settings into composer.json:

"autoload": {
    "psr-4": {
        "App\\": "src"
    }
},
"autoload-dev": {
    "psr-4": {
        "App\\Test\\": "tests"
    }
}

The complete composer.json file should look like this:

{
    "require": {
        "slim/slim": "^4.3",
        "slim/psr7": "^0.6.0",
        "php-di/php-di": "^6.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^8.4"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Test\\": "tests"
        }
    },
    "config": {
        "process-timeout": 0,
        "sort-packages": true
    }
}

Run composer update for the changes to take effect.

Action

In an ADR system, each Action is represented by a individual class or closure.

The Action mediates between the Domain and the Responder.

The Action interacts with the Domain in the same way a Controller interacts with a Model but does not interact with a View or template system. It sends data to the Responder and invokes it so it can build the HTTP response.

“Single Action Controllers” means: One action per class.

The Action does only these things:

All other logic, including all forms of input validation, error handling, and so on, are therefore pushed out of the Action and into the Domain (for domain logic concerns) or the Responder (for presentation logic concerns).

The Responder creates the response, not the Action.

A Responder might be HTML-responder for a standard web request; or it might be something like a JSON-responder for RESTful API requests.

Closures (functions) as routing handlers are quite “expensive” because PHP has to create all closures for each request. The use of class names is more lightweight, faster and scales better for larger applications.

<?php

namespace App\Action;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class HomeAction
{
    private $responseFactory;
    
    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }
    
    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        $response = $this->responseFactory->createResponse();
        $response->getBody()->write('Hello, Action!');

        return $response;
    }
}

Then open config/routes.php and replace the route closure for / with this line:

$app->get('/', \App\Action\HomeAction::class);

The complete config/routes.php should look like this now:

<?php

use Slim\App;

return static function (App $app) {
    $app->get('/', \App\Action\HomeAction::class);
};

Now open your website, e.g. http://localhost and you should see the message It works!.

Writing JSON to the response

Instead of calling json_encode everytime we are using a specific JSON responder for this task.

<?php

namespace App\Responder;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use UnexpectedValueException;

/**
 * A generic JSON responder.
 */
final class JsonResponder
{
    /**
     * @var ResponseFactoryInterface
     */
    private $responseFactory;

    /**
     * Constructor.
     *
     * @param ResponseFactoryInterface $responseFactory The response factory
     */
    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

    /**
     * Generate a json response.
     *
     * @param array|null $data The data
     *
     * @throws UnexpectedValueException
     *
     * @return ResponseInterface
     */
    public function render(array $data = null): ResponseInterface
    {
        $json = json_encode($data);
        if ($json === false) {
            throw new UnexpectedValueException('Malformed UTF-8 characters, possibly incorrectly encoded.');
        }

        $response = $this->responseFactory->createResponse()->withHeader('Content-Type', 'application/json');

        $response->getBody()->write($json);

        return $response;
    }
}

Now replace the generic ResponseFactoryInterface with the JsonResponder in src/Action/HomeAction.php:

<?php

namespace App\Action;

use App\Responder\JsonResponder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class HomeAction
{
    private $responder;

    public function __construct(JsonResponder $responder)
    {
        $this->responder = $responder;
    }

    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        return $this->responder->render([
            'success' => true,
        ]);
    }
}

Open your website, e.g. http://localhost and you should see the JSON response {"success":true}.

To change to http status code, just use the response withStatus(x) method:

$result = ['error' => ['message' => 'Validation failed']];
        
return $this->responder->render($result)->withStatus(422);

Domain

Services

The Domain is the place for the complex business logic.

Instead of putting the logic into gigantic (fat) “Models”, we but the logic into smaller, specialized Service classes, aka (Application Service, Transaction Script etc).

A service provides a specific functionality or a set of functionalities, such as the retrieval of specified information or the execution of a set of operations, with a purpose that different clients can reuse for different purposes.

There can be multiple clients for a service, e.g. the action (request), another service, the CLI (console) and the unit-test environmet (phpunit).

A service class is not a “Manager” or “Utility” class.

Each service class should have only one responsibility, e.g. to transfer money from A to B, and not more.

Separate data from behavior by using services for the behavior and DTO’s for the data.

The directory for all (domain) modules and sub-modules is: src/Domain

Pseudo example:

use App\Domain\User\Data\UserData;
use App\Domain\User\Service\UserGenerator;

$user = new UserData();
$user->username = 'john.doe';
$user->firstName = 'John';
$user->lastName = 'Doe';
$user->email = 'john.doe@example.com';

$service = new UserGenerator();
$service->createUser($user);

Data Transfer Objects (DTO)

A DTO contains only pure data. There is no business or domain specific logic, only simple validation logic. There is also no database access within a DTO. A service fetches data from a repository and the repository (or the service) fills the DTO with data. A DTO can be used to transfer data inside or outside the domain.

Create a DTO class to hold the data in this file: src/Domain/User/Data/UserData.php

<?php

namespace App\Domain\User\Data;

final class UserData
{
    /** @var string */
    public $username;
    
    /** @var string */
    public $firstName;

    /** @var string */
    public $lastName;

    /** @var string */
    public $email;
}

Create the code for the service class src/Domain/User/Service/UserGenerator.php:

<?php

namespace App\Domain\User\Service;

use App\Domain\User\Data\UserData;
use App\Domain\User\Repository\UserGeneratorRepository;
use UnexpectedValueException;

/**
 * Service.
 */
final class UserGenerator
{
    /**
     * @var UserGeneratorRepository
     */
    private $repository;

    /**
     * The constructor.
     *
     * @param UserGeneratorRepository $repository The repository
     */
    public function __construct(UserGeneratorRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * Create a new user.
     *
     * @param UserData $user The user data
     *
     * @return int The new user ID
     */
    public function createUser(UserData $user): int
    {
        // Validation
        if (empty($user->username)) {
            throw new UnexpectedValueException('Username required');
        }

        // Insert user
        $userId = $this->repository->insertUser($user);

        // Logging here: User created successfully

        return $userId;
    }
}

Take a look at the constructor! You can see that we have declared the UserGeneratorRepository as a dependency, because the service can only interact with the database through the repository.

Repositories

A repository is responsible for the data access logic, communication with database(s).

There are two types of repositories: collection-oriented and persistence-oriented repositories. In this case, we are talking about persistence-oriented repositories, since these are better suited for processing large amounts of data.

A repository is the source of all the data your application needs and mediates between the service and the database. A repository improves code maintainability, testing and readability by separating business logic from data access logic and provides centrally managed and consistent access rules for a data source. Each public repository method represents a query. The return values represent the result set of a query and can be primitive/object or list (array) of them. Database transactions must be handled on a higher level (service) and not within a repository.

Creating a repository

For this tutorial we need a test database with a users table. Please execute this SQL statement in your test database.

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `first_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `last_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Create a new directory: src/Domain/User/Repository

Create the file: src/Domain/User/Repository/UserGeneratorRepository.php and insert this content:

<?php

namespace App\Domain\User\Repository;

use App\Domain\User\Data\UserData;
use PDO;

/**
 * Repository.
 */
class UserGeneratorRepository
{
    /**
     * @var PDO The database connection
     */
    private $connection;

    /**
     * Constructor.
     *
     * @param PDO $connection The database connection
     */
    public function __construct(PDO $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Insert user row.
     *
     * @param UserData $user The user
     *
     * @return int The new ID
     */
    public function insertUser(UserData $user): int
    {
        $row = [
            'username' => $user->username,
            'first_name' => $user->firstName,
            'last_name' => $user->lastName,
            'email' => $user->email,
        ];

        $sql = "INSERT INTO users SET username=:username, first_name=:first_name, last_name=:last_name, email=:email;";
        $this->connection->prepare($sql)->execute($row);

        return (int)$this->connection->lastInsertId();
    }
}

Note that we have declared PDO as a dependency, because the repository requires a database connection.

The PDO object itself is created and injected by the dependency inject container (PHP-DI). For this we have to add the PDO settings and a container definition.

Add the PDO settings to: config/settings.php:

// Database settings
$settings['db'] = [
    'driver' => 'mysql',
    'host' => 'localhost',
    'username' => 'root',
    'database' => 'test',
    'password' => '',
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'flags' => [
        // Turn off persistent connections
        PDO::ATTR_PERSISTENT => false,
        // Enable exceptions
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        // Emulate prepared statements
        PDO::ATTR_EMULATE_PREPARES => true,
        // Set default fetch mode to array
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        // Set character set
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'
    ],
];

Insert a PDO::class container definition to config/container.php:

PDO::class => static function(ContainerInterface $container) {
    $settings = $container->get('settings');

    $host = $settings['db']['host'];
    $dbname = $settings['db']['database'];
    $username = $settings['db']['username'];
    $password = $settings['db']['password'];
    $charset = $settings['db']['charset'];
    $flags = $settings['db']['flags'];
    $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";

    return new PDO($dsn, $username, $password, $flags);
},

From now on, PHP-DI will always inject this PDO instance as soon as we declare PDO in a constructor as a dependency.

The last part is to register a new route for POST /users.

Create a new action class in: src/Action/RegisterUserAction.php:

<?php

namespace App\Action;

use App\Domain\User\Data\UserData;
use App\Domain\User\Service\UserGenerator;
use App\Responder\JsonResponder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class CreateUserAction
{
    private $userGenerator;

    private $responder;

    public function __construct(UserGenerator $userGenerator, JsonResponder $responder)
    {
        $this->userGenerator = $userGenerator;
        $this->responder = $responder;
    }

    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        // Collect input from the HTTP request
        $data = (array)$request->getParsedBody();

        $user = new UserData();
        $user->username = $data['username'];
        $user->firstName = $data['first_name'];
        $user->lastName = $data['last_name'];
        $user->email = $data['email'];

        // Invoke the Domain with inputs and retain the result
        $userId = $this->userGenerator->createUser($user);

        // Invoke the Responder with any data the Responder needs to build an HTTP response
        return $this->responder->render(['user_id' => $userId]);
    }
}

Add the new route in config/routes.php:

$app->post('/users', \App\Action\CreateUserAction::class);

The complete project structure should look like this now:

image

Now you can test the POST /users route with Postman to see if it works.

If successful, the result should look like this:

image

Value Objects

Use it only for “small things” like Date, Money, CustomerId and as replacement for primitive data types like string, int, float, bool and array.

A value object must be immutable and is responsible for keeping their state consistent.

A value object should only be filled using the constructor.

Wither methods are allowed, but setter methods are not allowed.

Example:

public function withEmail(string $email): self { ... }

A getter method name does not contain a get prefix.

Example:

public function email(): string { return $this->email; } 

All properties must be protected or private accessed by the getter methods.

Example:

<?php

final class CustomerId
{
    private $id;
    
    public function __construct(int $id)
    {
        $this->id = $id;
    }
    
    public function equals(CustomerId $customerId): bool
    {
        return $this->id === $customerId->id;
    }
    
    public function __toString()
    {
        return (string)$this->id;
    }
}

Read more

Parameter objects

If you have a lot of parameters that fit together, you can replace them with a parameter object. See DTO

Types and enums

Don’t use strings or fix integer codes as values. Instead use public class constants.

Example:

<?php

final class LevelType
{
    public const LOW = 1;
    public const MEDIUM = 2;
    public const HIGH = 3;
}

Deployment

For deployment on a productive server, there are some important settings and security releated things to consider.

You can use composer to generate an optimized build of your application. All dev-dependencies are removed and the Composer autoloader is optimized for performance.

Run this command in the same directory as the project’s composer.json file:

composer install --no-dev --optimize-autoloader

You don’t have to run composer on your production server. Instead you should implement a build pipeline that creates an so called “artifact”. An artifact is an ZIP file you can upload and deploy on your production server. selective-php/artifact is a tool to build artifacts from your source code.

For security reason you should turn of all error details in production:

$app->addErrorMiddleware(false, false, false);

If you have to run your Slim application in a sub-directory, you could try this library: selective-php/basepath

Important: It’s very important to set the Apache DocumentRoot to the public/ directory. Otherwise, it may happen that someone else could access internal files from outside. More details

/etc/apache2/sites-enabled/000-default.conf

DocumentRoot /var/www/example.com/htdocs/public

Tip: Never store secret passwords in your git / SVN repository. Instead you could store them in a file like env.php and place this file one directory above your application directory. e.g.

/var/www/example.com/env.php

Conclusion

Remember the relationships.

Comments