Porting Silex to Slim
Well, here we are. Time to put the pedal to the metal and start exploring non-Silex options for my many side projects. I've always defaulted to Silex because it was a quick and easy way to stand up a simple project with a handful of routes.
Luckily, there are several actively-developed options for that in the PHP world.
SlimPHP looks to be the most in-line with Silex's microframework goals.
A while back, I made deref.link as a brutally simple testbed for experimenting with AngularJS. It consists of a homepage and a route for checking the redirect path of URLs. I'm going to rewrite it using Slim and, in a later post, React.
Owing to deref.link's simplicity, it should be very quick to port it to Slim. Replace the composer dependency, make a few small tweaks to the PHP files, and it should run.
Let's test that hypothesis. You can follow along via this diff on github.
composer remove silex/silex
composer require slim/slim
At this point composer is happy, but I suspect the application is not working.
Sure enough, running a test results in an unusable app:
Well, let's see what needs to change.
The Slim homepage shows this example as the purest, bare-bones-iest Slim app:
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
require 'vendor/autoload.php';
$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name");
return $response;
});
$app->run();
Parts of this will be useful for replacing the bootstrap.php
file for deref.link.
Line 5 of the existing bootstrap.php
looks like:
$app = new Silex\Application();
I'll change this to:
$app = new \Slim\App;
Uh oh, refreshing my app shows a new error:
From this message, I can see that array-based container access is not going to fly with Slim. Let's look at the Slim docs and see why ...
Ah! The documentation says that Slim is also using Pimple. That will make things easier. Also, it says I can access the container using $app->container()
. So on line 6, I'll add this code:
$container = $app->getContainer();
Now I'll update lines 7 and 13 (now 8 and 14):
8: $container['debug'] = false;
9: $container['displayErrorDetails'] = false;
15: $app['deref.config'] = is_array($config) ? $config : [];
OK, now I refresh the app again. Yay! A new error is waiting for me.
Well, that's not very informative. Mostly because the error details are disabled by default. I glance at the PHP console and see reams of error messages.
The most important part appears to be:
Message: Cannot use object of type Slim\App as array
File: /Users/kboyd/Projects/deref/web/index.php
Line: 10
Line 10 of index.php
looks like:
$config = $app['deref.config'];
That's definitely not going to work. OK, time to update index.php
to fix all array-based references to $app
so they look at the container instead.
At the top of the file, after loading $app
, I add a line to fetch the container:
$app = require __DIR__ . '/../bootstrap.php';
$container = $app->getContainer();
Then, I change the signature for the /
homepage route to use $container
instead of $app
:
$app->get('/', function() use ($container) {
$config = $container['deref.config'];
Hey, that was faster than I thought. Speaking of "brutally simple" -- there was only the one occurrence. Nice.
Now the page loads, but if I try to interact with it, I get a 500 Internal Server Error. Once again, I glance at PHP console for guidance.
It says:
Message: Argument 1 passed to Closure::{closure}() must be an instance of Symfony\Component\HttpFoundation\Request, instance of Slim\Http\Request given
File: /Users/kboyd/Projects/deref/web/index.php
Line: 111
What?
Oh, right. Slim and Silex use different request objects. I've probably got a line or two that is still using Silex/Symfony requests. Should be easy to fix. I'll just check the Slim docs again to see if it handles requests and responses similarly to Silex.
OK, the docs say that Slim uses PSR-7 interfaces. Cool. I add these use statements to the top of index.php
:
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
That should be enough to get things working, right? Well, first I have to update the code to use them. After that it should be fine.
As I'm doing this, I realize I also need to add the Slim-specific Response object. This lets me return a JSON response from my routes:
use Slim\Http\Response;
There we go. Now, Silex had a JsonResponse
object that I was calling directly. I'll have to find out how to return JSON in Slim. Should be fairly easy, though.
Look at that, there's a section in the docs for this.
Slim has a ->withJson()
helper to build a JSON response object. But, I'm kind of new to PSR-7. I learned something while reading these docs:
Slim doesn't rely on "instantiating" Response objects within the routes. Instead, it receives a pre-made Response object that you can transform and return. Something for me to keep in mind.
This is a good thing, though. Immutable objects result in fewer surprises when internal state is being tinkered with. That's why the "transform" step results in a new object.
It looks like ->withJson()
is Slim-specific. I'm a bit confused about how to reconcile type hinting the ResponseInterface
, while using logic specific to the Slim Response
object. For clarity's sake, I'll stick with the Slim-specific Response
type hint.
Now I can see that PHPStorm is telling me I can't ->get()
the URL from the request. OK. Looks like PSR-7 requires me to be a bit more verbose. I'll try using ->getAttribute()
instead, and see how that goes. Also, my existing code checks for JSON requests using Symfony's ->getContentType()
method.
Actually, the docs suggest I can use Slim's ->getParsedBody()
method to simplify all this logic:
$body = $request->getParsedBody();
$url = $body['url'] ?? null;
There we go. Time for another refresh.
Muahaha! It's alive!
So that's the basic approach to porting a brutally simple Silex app to Slim.
You can see that there are a few new quirks with Slim and PSR-7. Having to fetch the container before accessing it means some code will have to change. Also, you'll have to adjust controller flow to handle immutable response behaviours. Depending on the size of your application, that could result in some headaches.
This particular application isn't a very good example for a larger porting effort. It doesn't make extensive use of middleware or event listeners. There are no ControllerProviders or ServiceProviders. But the Slim documentation seems very well put together, so I'm sure it will be easy to find information.
I hope this was interesting for you. It was certainly fun for me. All told, it took less than an hour from starting this blog post to deploying the upgraded version of deref.link.
Published: May 12, 2018
Tags: coding, dev, development, howto, legacy, php, projects, silex, slim, maintenance