7

I'm using Slim Framework 3 to create an API. The app structure is: MVCP (Model, View, Controller, Providers).

Is it possible to have Slim Dependency Inject all my classes?

I'm using composer to autoload all my dependencies.

My directory structure looks like this:

/app
   - controllers/
   - Models/
   - services/
   index.php
/vendor
 composer.json

Here's my composer.json file.

{
  "require": {
    "slim/slim": "^3.3",
    "monolog/monolog": "^1.19"
  },
  "autoload" : {
    "psr-4" : {
        "Controllers\\" : "app/controllers/",
        "Services\\" : "app/services/",
        "Models\\" : "app/models/"
    }
  }
}

Here's my index.php file. Again, the dependencies are being auto injected by composer

<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

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

$container = new \Slim\Container;
$app = new \Slim\App($container);

$app->get('/test/{name}', '\Controllers\PeopleController:getEveryone');

$app->run();

My controller looks like this

<?php #controllers/PeopleController.php

namespace Controllers;

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


class PeopleController
{
    protected $peopleService;

    protected $ci;
    protected $request;
    protected $response;

    public function __construct(Container $ci, PeopleService $peopleService)
    {
        $this->peopleService = $peopleService;
        $this->ci = $ci;
    }

    public function getEveryone($request, $response)
    {
        die($request->getAttribute('name'));

        return $this->peopleService->getAllPeoples();
    }
}

My PeopleService file looks like this:

<?php

namespace Services;

use Model\PeopleModel;
use Model\AddressModel;
use Model\AutoModel;


class PeopleService
{
    protected $peopleModel;
    protected $autoModel;
    protected $addressModel;

    public function __construct(PeopleModel $peopleModel, AddressModel $addressModel, AutoModel $autoModel)
    {
        $this->addressModel = $addressModel;
        $this->autoModel = $autoModel;
        $this->peopleModel = $peopleModel;
    }

    public function getAllPeopleInfo()
    {
        $address = $this->addressModel->getAddress();
        $auto = $this->autoModel->getAutoMake();
        $person = $this->peopleModel->getPeople();

        return [
            $person[1], $address[1], $auto[1]
        ];
    }
}

Models/AddressModels.php

<?php

namespace Model;

class AddressModel
{

    public function __construct()
    {
        // do stuff
    }

    public function getAddress()
    {
        return [
            1 => '123 Maple Street',
        ];
    }
}

Models/AutoModel.php

namespace Model;

class AutoModel
{

    public function __construct()
    {
        // do stuff
    }

    public function getAutoMake()
    {
        return [
            1 => 'Honda'
        ];
    }
}

Models/PeopleModel.php

<?php
namespace Model;

class PeopleModel
{

    public function __construct()
    {
        // do stuff
    }

    public function getPeople()
    {
        return [
            1 => 'Bob'
        ];
    }

}

ERROR I'm getting the following error now:

PHP Catchable fatal error:  Argument 2 passed to Controllers\PeopleController::__construct() must be an instance of Services\PeopleService, none given, called in /var/www/vendor/slim/slim/Slim/CallableResolver.php on line 64 and defined in /var/www/app/controllers/PeopleController.php on line 21

THE QUESTION How do I dependency inject all my classes? Is there a way to automagically tell Slim's DI Container to do it?

alexw
  • 7,044
  • 6
  • 46
  • 81
Kearney Taaffe
  • 580
  • 6
  • 16
  • 2
    So you want to inject or autoload them? If you mean autoloading, you probably forgot to include `vendor/autoload.php` file at the beginning of `index.php`. It contains generated autoloaders. – Jakub Matczak Apr 25 '16 at 16:21
  • Well, that was one of my issues.Thank you! I've updated the index.php to include the vendor autoload PHP file. I've updated the question to include the error I'm now getting. Again, the issue is how to Dependency Inject my classes. – Kearney Taaffe Apr 25 '16 at 17:57
  • Well, the error is self-explaining. What's not clear here? – Jakub Matczak Apr 25 '16 at 18:03
  • What's not clear is how to pass parameter to my controller's constructor method. `$app->get('/test', '\Controllers\PeopleController:getEveryone(new Service\PeopleService');` doesn't work – Kearney Taaffe Apr 25 '16 at 18:08
  • Here's an example that does exactly what you want: http://www.slimframework.com/docs/objects/router.html#container-resolution You need to register your controller as a service in DIC, and pass dependencies in its deifnition. – Jakub Matczak Apr 25 '16 at 18:20
  • I followed that code exactly. As long as the only argument to the controller's constructor method is (Slim\Container $ci), then the code works fine. I was wanting to do constructor injection. So, every time I instantiate my constructor the service is called along with all the necessary models – Kearney Taaffe Apr 25 '16 at 19:02
  • No, you didn't. It's all here: Slim **first looks** for an entry of \HomeController **in the container**, if it’s found it will use that instance otherwise it will call it’s constructor with the container as the first argument. You didn't register your controller in the container, did you? – Jakub Matczak Apr 25 '16 at 19:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/110184/discussion-between-kearney-taaffe-and-dragoste). – Kearney Taaffe Apr 25 '16 at 19:46

1 Answers1

8

When you reference a class in the route callable Slim will ask the DIC for it. If the DIC doesn't have a registration for that class name, then it will instantiate the class itself, passing the container as the only argument to the class.

Hence, to inject the correct dependencies for your controller, you just have to create your own DIC factory:

$container = $app->getContainer();
$container['\Controllers\PeopleController'] = function ($c) {
    $peopleService = $c->get('\Services\PeopleService');
    return new Controllers\PeopleController($c, $peopleService);
};

Of course, you now need a DIC factory for the PeopleService:

$container['\Services\PeopleService'] = function ($c) {
    $peopleModel = new Models\PeopleModel;
    $addressModel = new Models\AddressModel;
    $autoModel = new Models\AutoModel;
    return new Services\PeopleService($peopleModel, $addressModel, $autoModel);
};

(If PeopleModel, AddressModel, or AutoModel had dependencies, then you would create DIC factories for those too.)

Aidan Casey
  • 3
  • 1
  • 4
Rob Allen
  • 12,444
  • 1
  • 35
  • 46
  • does Slim require you to pass the container to *every* class that needs it? That doesn't seem like a very DRY approach. I'm used to working with CakePHP and it seems like things are "globally" available when using that (possibly due to the AppModel and AppController). It seems in Slim that you're required to pass things such as the container every single time you need them? So if I have 100 classes, I have to use code equivalent to what you put above 100 times? – Andy Oct 19 '16 at 11:35
  • It's up to you. There's nothing to stop you creating a global static function that allows you to grab the container. Personally, I dislike that level of coupling, so don't do it that way. – Rob Allen Oct 21 '16 at 14:19