Autoloading controllers with Composer in SlimPHP

30 Jan 2017 in Tech

In the last post, we started using controllers rather than anonymous functions for our routes. In this post, we're going to be moving that controller class out of index.php and in to it's own file.

The first thing we need to do is create a folder for all of our controllers to live in. Let's call it src/Controller and create a file called HomeController in there:

bash
mkdir -p src/Controller
touch src/Controller/HomeController.php

The next thing to do is to move the code for HomeController out of index.php and in to src/Controller/HomeController.php. You can take it as it is, but you'll need to add two things to the beginning of src/Controller/HomeController.php - an opening PHP tag and a namespace. The PHP tag is expected, but what's this namespace thing?

We won't go in to too much detail, but a namespace is a logical grouping of code by a specific author. You've seen it in action already when using Slim. When we do new SlimApp, we're actually creating a new instance of the App class, which lives in the Slim namespace.

We should create a namespace for our code now and add it to HomeController. You can use whatever you like, but I'm going to choose the namespace DemoController. Once I've added this, my HomeController file looks like the following (we've also imported Request and Response as we need them in our hello function):

php
<?php
namespace DemoController;
use PsrHttpMessageServerRequestInterface as Request;
use PsrHttpMessageResponseInterface as Response;
class HomeController {
protected $view;
public function __construct($view) {
$this->view = $view;
}
public function hello(Request $request, Response $response) {
return $this->view->render($response, 'index.html', [
"name" => "Michael"
]);
}
}

Next, we need to update index.php to use our namespaced class rather than just HomeController. Edit controller.home in your container and add the Demo namespace to the class name so that it looks like the following:

php
$container['controller.home'] = function($container) {
return new DemoControllerHomeController($container['view']);
};

Now our application will try and instantiate DemoHomeController, which is what we wanted it to do. Sadly it won't work just yet - we have one final thing to do. We have to tell composer where our code lives so that it can automatically load it. To do this, we edit composer.json and add what's called an autoload section:

json
"autoload": {
"psr-4": {
"Demo\": "src"
}
}

PSR 4 is a PHP autoloading standard, but you don't really need to understand all of the details. All that we're saying is that if the namespace starts with Demo, look in the src folder. DemoControllerHomeController becomes src/Controller/HomeController automatically. DemoFoo becomes src/Foo.php. DemoALongClassPlease becomes src/A/Long/ClassPlease.php. Once you learn the rule, it's easy to follow.

Once you've added that, your composer.json should look like the following:

json
{
"name": "you/slim-demo",
"description": "A demo slim project",
"type": "project",
"require": {
"slim/slim": "^3.7",
"slim/twig-view": "^2.2"
},
"license": "MIT",
"authors": [
{
"name": "You",
"email": "[email protected]"
}
],
"autoload": {
"psr-4": {
"Demo\": "src"
}
}
}

If it does, run composer dump-autoload to rewrite Composer's autoloading cache and your changes should start magically working. Run php -t public -S localhost:8000 and visit http://localhost:8000 to see your homepage just like you left it.

This seems like a lot of effort, but you only have to do it once. Now, you can create new classes in src/Controller and add them to the container in just a few lines of code. Your index.php should just be application configuration and routing - no controller logic at all!

To show you how easy it is, let's do the same with the weather route too. Create a file at src/Controller/WeatherController.php with the following contents:

php
<?php
namespace DemoController;
use PsrHttpMessageServerRequestInterface as Request;
use PsrHttpMessageResponseInterface as Response;
class WeatherController {
protected $view;
public function __construct($view) {
$this->view = $view;
}
public function index(Request $request, Response $response) {
return $this->view->render($response, 'weather.html');
}
}

Just like last time, we pass in the view engine and we add a function to render our response. This time the function is called index.

Edit index.php to add this controller to our container by adding the following code before $app = new SlimApp($container);:

php
$container['controller.weather'] = function($container) {
return new DemoControllerWeatherController($container['view']);
};

Then finally, update your route to point to this new container entry.

php
$app->get('/weather', "controller.weather:index");

Notice how it's controller.weather:index, not controller.weather:hello? That's because our method name in WeatherController is index, not hello.

That brings us to the end of using controllers with Slim. As we're passing in view to every controller, you may want to create src/Controller/BaseController.php with the following contents, then remove the contructor from HelloController and WeatherController:

php
<?php
namespace DemoController;
class BaseController {
protected $view;
public function __construct($view) {
$this->view = $view;
}
}

You can see the final result of all of these changes in this commit on GitHub.