Isolated macadamia on white background

Why a framework?

We live in agile times. We want to get our features out into the world fast. We can always refine away mistakes and rough edges with new iterations. So why bother to use a framework when I could simply create a page, name it index.php and get to work?

<?php
print "hello world";


And there you are. It took me more like five seconds than twenty minutes. But the beguiling friendliness of PHP can be the undoing of a project. As soon as I create other pages, begin to mix in my logic and presentation, I’m well on my way to spaghetti code. To quote my own book:

The problem is that PHP is just too easy. It tempts you to try out your ideas , and flatters you with good results. You write much of your code straight into your web pages, because PHP is designed to support that. You add utility functions (such as database access code) to files that can be included from page to page, and before you know it you have a working web application.

You are well on the road to ruin. You don’t realize this, of course, because your site looks fantastic. It performs well, your clients are happy, and your users are spending money.

Trouble strikes when you go back to the code to begin a new phase. Now you have a larger team, some more users, a bigger budget. Yet, without warning, things begin to go wrong. It’s as if your project has been poisoned.

Your new programmer is struggling to understand code that is second nature to you, although perhaps a little byzantine in its twists and turns. She is taking longer than you expected to reach full strength as a team member. A simple change, estimated at a day, takes three days when you discover that you must update 20 or more web pages as a result.

Matt Zandstra PHP: Objects, Patterns, and Practice

So, hacking out a prototype is not always a good idea. On the other hand, do you really want to use a fully-featured framework for a couple of templates and a controller or two? There’s absolutely nothing wrong with monsters like Symfony and Laravel. They come with lots of incredibly useful tools and libraries, which is kind of the point. It’s just that a big fat framework can be overkill if you only intend to use a tiny fraction of the functionality on offer.

The middle way is a micro-framework. A framework that handles the core task of routing and a few other essentials and leaves you to layer in additional features as you need them.

In this article I’ll cover the bare minimum I think you need for scaleable fast prototyping. That is:

  • Routing
  • Controller methods
  • Configuration
  • Basic templating

Introducing Slim

For a long time, my go to micro-framework was Silex. The project was discontinued in 2018, however. It is still possible to put together something nice and minimal using Symfony components – which is what I did as the project headed for end of life. In fact, though, I could have jumped ship to Slim which was already going strong and has been since 2011.

I missed Slim altogether until, one day, I noticed it used in documentation for The League of Extraordinary Packages’ OAuth2.0 Server. I promptly used it for a demo project and loved it straightaway. It does what I want a framework to do for me – manage routing, requests and responses without fuss or bother. The rest I can bolt on as I need it.

NOTE If you’re interested, there is a Silex resource still available on this site which gets a lot of traffic even now.

So, let’s get started. A working system in twenty minutes.

From an empty directory to Hello World (5 minutes)

As with most PHP packages, the easiest way to get Slim is via composer. Here is my composer.json file.

{
    "require": {
        "slim/slim": "^4.11",
        "slim/psr7": "^1.6"
    },
    "autoload": {
        "psr-4": {
            "getinstance\\myapp\\": "src/"
        }
    }
}

Of these, the really essential package is Slim itself: slim/slim. The slim/psr7 is needed so that we can work with HTTP requests and responses.

NOTE Composer is the best way of installing packages, handling dependencies and managing autoloading. It’s out of scope for this article, but there’s a good introduction at the Composer site.

Once I have a composer.json file, I can go ahead and run composer update to get the libraries.

Let’s create a directory, web, which will be the public-facing part of our app, and start a file: index.php.

$ mkdir web/
$ vi web/index.php

Now we are ready to output our first “Hello World”.

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->get('/', function (Request $request, Response $response, $args) {
    $response->getBody()->write("Hello World");
    return $response;
});

$app->run();


Not a hugely more complex file than our dumb ‘Hello World’ example. Let’s work through it quickly.

PSR-7 is the PHP standard for HTTP messages – it lays down requirements for Request and Response classes which, as you might expect, encapsulate the request that comes in to the server, and the response we send out to the client. By requiring the slim/psr7 package we have included implmentations for those PSR interfaces. AppFactory will generate the core Slim\App object that will manage our application.

We get a Slim\App instance with AppFactory::create(). Armed with that, I can set up routing. The App::get() method can be called any number of times to define the endpoints we want to implement for GET requests. In this case, I’m only interested in a default: ‘/’ which I pass as the first argument. My second argument is a function to be invoked if the request matches the pattern defined in the first argument.

NOTE As you might expect, Slim\App provides calls for all the HTTP methods: most commonly of course get(), post(), and put(). You can find full routing documentation on the Slim site.

The callable will be passed PSR-7 RequestInterface and ResponseInterface implementations. We can use the Request to get any query data we need and, by writing to the Response object’s StreamInterface implementation (that’s what getBody() returns) we can have content sent back to the client. The function should always return the Response object so that Slim can wrap things up. You’ll be hit with an exception if you fail to do this.

Finally, having configured Slim we call Slim\App::run() to make it all happen. If you don’t call run() you’ll end up with an anticlimactically blank browser window.

Running the code

I no longer like to assume that you’re coding in an environment that includes a particular toolset. However, if you do have PHP 8 installed you can go ahead and run

php -S 0.0.0.0:8080 -t web


to launch PHP’s built-in development server. Or, more flexibly, we can create a docker-compose.yml document and run our application in a container which will provide PHP 8 (and, with a bit more work, any other platform tools we might require).

NOTE Watch this space for much more on Docker in coming weeks.

Here’s my bare-bones Docker compose set up, which is based on the equivalent file generated by Slim-Skeleton (a little more on this later).

version: '3.7'

services:
    slim:
        image: php:8-alpine
        working_dir: /var/www
        command: php -S 0.0.0.0:8080 -t web
        ports:
            - "8080:8080"
        volumes:
            - .:/var/www


This is not a Docker article, but, in brief, I define a service named slim. php:8-alpine is an offical PHP image. The tag part, which follows the colon, specifies PHP version 8 installed on Alpine, a lightweight Linux distro. I define both a working directory and the command I’d like to run within it. I map a local port to the port that’s exposed (which is useful where the port within the container might conflict with a port already in use within the host enviroment). Finally, with the volumes directive, I mount my development directory (.) to /var/www within the container. Thanks to this, any changes I make to my code will be instantly available.

Let’s power it up:

$ docker compose up

Here’s the output as the container starts up, and the PHP server is run.

[+] Running 1/0
 ⠿ Container slim01-slim-1  Created                                                                                                     0.0s
 Attaching to slim01-slim-1
 slim01-slim-1  | [Thu Jan 12 11:14:53 2023] PHP 8.2.0 Development Server (http://0.0.0.0:8080) started

Whether you’ve run the built-in PHP web server directly or invoked a Docker container you should be able to access the code via your loopback host or IP: probably http://localhost:8080.

Hello World in browser

Creating a controller (5 minutes)

So, we have reached parity with our naive hello world one-liner. We’re not yet quite at the scaleable state I’d like, though. Although the closure (anonymous function) we pass to get() does the job for a single string output, this approach would soon become unwieldy in a single file. It’s time to grow the application.

Let’s start by creating a controller base class for shared functionality:

namespace getinstance\myapp\controllers;

abstract class Controller
{

}


Nothing going on there for now. We can leave it as a placeholder.

NOTE It’s worth noting that I created the Controller class in a file named Controller.php in a newly created src/controllers directory. Thanks to my composer.json file, the package getinstance\myapp maps implicitly to the src directory. Thereafter namespace parts will map to directories under src. That’s a long-winded way of saying that the class getinstance\myapp\controllers\Controller will be autoloaded if found in src\controllers\Controller.php.

Of course, I would not have created an empty base class if I wasn’t pretty convinced I’d soon be putting some shared code there. For now, though, I’ll add some actual functionality to a concrete subclass: MainController

namespace getinstance\myapp\controllers;

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

class MainController extends Controller
{
    public function renderForm(Request $request, Response $response, array $args): Response
    {
        $response->getBody()->write("hats");
        return $response;
    }
}


At present, this implementation is almost identical to the closure from my previous example – it has simply been moved to MainController::renderForm(). All I need to do now is tell Slim where to find the method:

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use getinstance\myapp\controllers\MainController;

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

$app = AppFactory::create();

$app->get('/', MainController::class . ":renderForm");

$app->run();


The key here is that, instead of passing a closure to get(), I pass it a string that points to my new controller method. This resolves to getinstance\myapp\controllers\MainController:renderForm.

I would show you another screenshot here, but you can guess that it would only show a browser window empty apart from the word hats so let’s crack on.

Configuration (5 minutes)

So I now have routing and a controller. A Web app won’t get far without configuration, though. I’ll need it eventually for database passwords, service keys, and so on. First of all, then, I’ll create a super-simple configuration class that wraps an array.

namespace getinstance\myapp\util;

class Conf
{
    public function __construct(private array $data)
    {
    }

    public function get($key): mixed
    {
        return ($this->data[$key] ?? null);
    }
}


As you can see from the namespace and the class name, this will end up living in the file src\util\Conf.php. To make it available to controllers we can demand that it’s passed in to the abstract Controller class’s constructor.

namespace getinstance\myapp\controllers;

use getinstance\myapp\util\Conf;

abstract class Controller
{
    public function __construct(private Conf $conf)
    {
    }

    public function getConf(): Conf
    {
        return $this->conf;
    }
}


Of course, as is often the way with accessing object, we beg the question here: where is the object actually created?

(Re)introducing dependency injection

We could very easily create some kind of registry to store values and objects but the fashion at present is to use dependency containers for this sort of work. Since Slim is very much oriented around this strategy, it makes sense to leverage the pattern.

NOTE If the phrase dependency container sounds like trendy gobbledegook to you, don’t worry – it’s actually pretty straightforward. What’s more there’s another article on this very site, that covers the implementation we’re using here.

First of all, we need to add support for a dependency container to our system. I’m going with PHP-DI, which is a pretty standard choice. We could amend the composer.json file manually, or have Composer do it for us with

$ composer require php-di/php-di


Now, we need to configure and create a container and then pass it to the AppFactory::setContainer() method before creating our Slim\App object.

$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->addDefinitions([
    "settings" => [
        "salutation" => "welcome!"
    ],
    Conf::class => \DI\autowire()->constructor(\DI\get("settings")),
]);

$container = $containerBuilder->build();
AppFactory::setContainer($container);

$app = AppFactory::create();

$app->get('/', MainController::class . ":renderForm");

$app->run();


I’m using DI\ContainerBuilder to configure my container. I create a settings array (which, later on, I might put in a file on my system in JSON or INI format so that I don’t have to commit any secrets to my version control system). I set up the instantiation of Conf wrapper class so that the settings array will be passed in to its constructor if and when it is needed.

NOTE Again, I provide more information about PHP-DI in a recent article. In short, though, the DI\autowire() helper function returns an AutowireDefinitionHelper object. AutowireDefinitionHelper::constructor() accepts definitions that will resolve to the arguments required by the target class’s constructor when needed. That’s a complicated way of saying that when a Conf class is required by our system it will automatically be instantiated with the provided settings array. Dependency containers are way harder to describe than they are to use!

Finally, I build the container and pass it to AppFactory. And that’s it.

Using Conf

Of course I still have to be able to use the Conf object. This is where the magic of the dependency container comes into play. I don’t actully need to do any further work to get my hands on Conf. Let’s see this in action.

public function renderForm(Request $request, Response $response, array $args): Response
{
    $salutation = $this->getConf()->get("salutation");
    $response->getBody()->write($salutation);
    return $response;
}

Because the MainController class inherits from the abstract Controller class, it has the getConf() method which gives it access to the Conf class. From that, I’m able to get at the salutation configuration element.

Time for another screengrab.

Salutation in browser

But wait! Just when and how was the MainController class instantiated? If you’re anything like me, you’re probably suspicious of things that happen seemingly by magic, even if they’re convenient. The answer is that deep within the workings of Slim lies a class named Slim\CallableResolver that is responsible for invoking the code attached to routing. Thanks to the routing process which marries the incoming request to our configuration (via Slim\App::get()), it knows it needs to acquire an instance of MainController and it asks the container to provide it. In turn, the container is smart enough to discover our MainController class and, through reflection, it knows that the constructor requires Conf. We have already told it how to create that class, so it has all the pieces it needs.

Creating a view (5 minutes)

Right now, I’m piping a string straight into the Response object’s output stream. Once again, that won’t scale very well for a real world application.

It would be pretty easy to roll my own templating system based on that, but let’s not reinvent another wheel. Slim does not support templating at core, but really it’s only a sub-package away.

$ composer require slim/php-view


NOTE Although I’m using slim/php-view to include PHP templates, there is also a slim/twig-view package that provides support for the Twig template language.

Now that we have provided support for slim/php-view, let’s create a simple renderer in the parent Controller class.

namespace getinstance\myapp\controllers;

use getinstance\myapp\util\Conf;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\PhpRenderer;

abstract class Controller
{
    public function __construct(private Conf $conf)
    {
    }

    protected function render(Response $response, string $template, array $args): Response
    {
        $renderer = new PhpRenderer(__DIR__ . '/../../views/');
        $renderer->render($response, $template, $args);
        return $response;
    }

    public function getConf(): Conf
    {
        return $this->conf;
    }
}


The render() method requires a Response object, a string representing the template to write to, and an array of arguments to pass along to the template. I create a PhpRenderer object (as provided by the php-view package) with a path to a new views directory at the top level of my project. I call PhpRenderer::render() and then return the response.

Now, let’s call render() in the MainController::renderForm() method.

public function renderForm(Request $request, Response $response, array $args): Response
{
    $args['salutation'] = $this->getConf()->get("salutation");
    return $this->render($response, "main.php", $args);
}

I reference a template named main.php which, no surprise, should be placed in the views directory. I also pass an $args array which includes a salutation key.

Here is main.php

<h1><?= $salutation ?></h1>";


Note how the $args array which we passed to Controller::render() and then on to PhpRenderer::render() has been unpacked. Within the template, what started out as $args['salutation'] has become $salutation.

Taking stock

So we’ve reached a stable point. We’re still at a variation on hello world, but it’s a much firmer footing for fast prototyping – and we can turn a spike into a feature without too much backtracking.

Let’s take it just a little further to see things in action.

First of all, I’ve been developing a method named renderForm() without a form in sight. Let’s fix that.

<h1><?= $salutation ?></h1>

<form method="post" action="/process">
<input name="msg" type="text">
</form>

Now, because we’ve specified a /process endpoint, we need to add it to routing:

$app = AppFactory::create();
$app->addErrorMiddleware(true, true, true);

$app->get('/', MainController::class . ":renderForm");
$app->post('/process', MainController::class . ":processForm");

$app->run();


Note that I’ve used Slim\App::post() for /process. We are using the POST method for our form, and we need to match that with our routing.

I have specified a controller method: MainController::processForm(). I’ll need to create that.

public function processForm(Request $request, Response $response, array $args): Response
{
    $params = (array)$request->getParsedBody();
    $args['msg'] = $params['msg'];
    return $this->render($response, "process.php", $args);
}

I get the form input via Request::getParsedBody() and then call render() as before.

I’ll create a quick process.php template to ouput the msg form element (which I pass in to the template via the $args array).

<h2><?= $msg ?></h2>

So now I’ll see a form to submit when I view my application.

Form in browser

When I submit the form, my input will be reflected in the template rendering:

Output in browser

Even faster startup

There is much more to Slim than I’ve had time to cover here of course. Luckily there’s clear and comprehensive documention on the Slim site.

If you want a fast and somewhat richer site skeleton you could do a lot worse than the officially supported slim/slim-skeleton package which can be generated using the composer create-project command.

You can find source code for all my recent articles on Github. The most complete version of the code for this article is in the slim.05 directory.

Conclusion

Slim is a fantastic resource if, like me, you’d rather start with a very minimal framework and build in complexity only as you need it. In future articles, I’ll be using this base to create some more extensive tools. Watch this space!

Photo by Mockup Graphics on Unsplash