Autoloading controllers with Composer in SlimPHP
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/Controllertouch 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
<?phpnamespace 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",}],"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
<?phpnamespace 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
<?phpnamespace 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.