Multiple dependencies become troublesome


#1

As my App grow my Controllers are getting more and more dependencies. It is common to say that more than 3 dependencies is a sign to refactor your code.

But i already have 5 dependencies in my Controllers just by using basic DB, Session, Flash, Redis, Twig. Not all of them are using it but still, some do. And there come some other specific dependencies in couple of my Controllers.

I don’t want to go the singleton or registry way. How can i handle that in Slim?

Most irritating are DB and Redis dependencies as i handle my Mappers on my own, which forces me to inject DB and Redis to all of them (just like there: https://github.com/slimphp/Tutorial-First-Application/blob/master/src/classes/Mapper.php).

But since my Mappers need DB and Redis, i must pass those dependencies to Controllers, so they can pass it down to Mappers.


#2

You can add the mappers to the container and initialise them there:

$container['ticketMapper'] = function ($c) {
    return new TicketMapper($c->get('db'));
};

and then use the dependency

$app->get('/ticket/{id}', function (Request $request, Response $response, $args) {
    $mapper = $this->ticketMapper;
    // ...
}

#3

Yes, though i am not using Closures for routing. I’m using Controller classes where i inject my dependencies and i don’t want to change it since Closures at some point are hard to maintain, especially when there is a lot of them. It is also easier to move some common logic in Controller actions to methods.

While using Controllers i don’t want to inject mappers as dependencies as well since it doesn’t really solve original problem, where i need to inject PDO, Redis etc. It would just change place with Mappers.


#4

No problem, you can use container resolution to create a controller with the necessary dependencies instead of using closures.

The mapper dependency should replace the dependency on PDO and Redis in the controller, saving one dependency in the process. Also the logic for creating a mapper is removed in from all controllers.

If you need multiple mappers in one controller, you can instead create a service in which you inject the mappers and then inject that service into the controller.

Also, to save another dependency, perhaps Session and Flash can be combined into a separate service. like in Aura Session? But that may be a bit of a stretch.

Alternatively, you may want to look at the PHP DI container. It has a Slim bridge and allows for injecting the dependencies on the action method (controller parameters), so you can have different dependencies per action in the same controller.


#5

One way to manage dependencies when using controller classes is to use an abstract BaseController. This way you can share your common dependencies across all your controllers and only have custom dependencies in their respective controller. You can also create a few types of BaseControllers, one for public API controllers, one for admin controllers, and so on. Here is an example of what I mean, these are just excerpts of what I personally use, some libs may be missing. It’s for example only.

BaseController.php

namespace Controller;

use Core\Alert;
use Core\CSRF;
use Core\JS;
use Core\Input;
use Core\View;

abstract class BaseController
{
    protected $alert;
    protected $db;
    protected $csrf;
    protected $js;
    protected $input;
    protected $view;

    public function __construct($container)
    {
        $this->alert = new Alert;
        $this->db    = $container->get('db');
        $this->csrf  = new CSRF;
        $this->js    = new JS;
        $this->input = new Input($container->request);
        $this->view = new View($container->response);
        $this->view->setTemplatesDirectory($container->get('view.directory') . '/' . $container->get('view.theme'));
    }
}

UserController.php

namespace Controller

use Model\User;

class UserController extends BaseController
{
    public function get($req, $res, $args)
    {
        $users = new User($this->db);
        $json = $user->getById($args['id']);
        $view = [];
        $view['content_view'] = ($json === false) ? '{}': $this->view->getSafeJSON($json);

        // create new response headers
        return  $this->view->getJsonResponse($res, 'wrappers/json_view.php', $view);
    }
}

If you’re curious $container is used for, its because I use a Config class to manage all my app settings. I then pass the values to Slim’s container. This is how I gain access to them. For example I have a db.php config file that I include.

db.php

// setup db connection
$db = new PDO(
    "mysql:host=" . $config->get('db.hostname') . ";dbname=" . $config->get('db.database'),
    $config->get('db.username'),
    $config->get('db.password')
);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['Core\\PDOStatementDebugger', []]);

// store db connection
$config->set('db', $db);

Then in my index.php I return all my config values so slim can use them.

index.php

.
.
.
// instantiate config object
$config = new Config;
.
.
.
// load configs
require_once '../app/config/bootstrap.php';

$app = new App(new Container($config->getConfig()));

require_once '../app/config/routes.php';
.
.
.
.
// init
$app->run();

Hopefully this can help you ease your dependency management.


#6

I came back to this thread as i still can’t find proper solution for my mappers dependencies. I use my mappers a lot. Some of them use only PDO, some use PDO and Redis. Extending mapper to use Redis is painful as i must change all initializations of new mapper object to properly pass Redis in constructor next to PDO.

And so your answer could help but i can’t figure out any proper way to implement it. I can make a service to which i pass class name App\Mappers\User::class and it checks whether it extends abstract class AbstractMapper or AbstractCachedMapper (extending AbstractMapper) to inject only PDO or PDO and Redis. The problem is my mappers have its own unique methods, so service return type set on AbstractMapper is painful, since i am not getting methods typehint for each mapper specific methods (thats a pros of creating Mappers in Controllers, since mapper class is known).

So i thought maybe you can point me in some better direction as to how implement mappers service. I am trying to avoid adding new method for each new mapper, because on some point i might get 30-50+ (or even more) mappers depending on how many models (tables) i have in my project.


#7

Hi,

I wasn’t thinking of an abstract service, which could work, but more of a service that wraps multiple related wrappers and offers a coherent interface.

Lets say you have a mapper for orders and a mapper for order lines, it may make sense to create a service (repository?) that uses those mappers and abstracts that away for the controller, e.g.:

class OrderRepository
{
    public function __construct(OrderMapper $orderMapper, OrderLineMapper $orderLineMapper)
    {
        // ....
    }

    public function storeOrder(Order $order)
    {
        // use $this->orderMapper and $orderLineMapper
    }
}

Instead of injecting both the OrderMapper and OrderLineMapper, the OrderRepository is injected. From the standpoint of the controller there is one less dependency (one repository instead of two mappers). The orderRepository

I don’t think that 30-50 mapper service definitions is a big issue, if you have 30-50 mappers. Especially if you place these definitions in a separate file (e.g. mappers.php). If you change the constructor of a mapper, then you’d have two places where a change is needed: the mapper constructor and the service definition of the mapper.

You may want to check using a DI container that provides autowiring such as PHP-DI or the PHP League Container. With autowiring the DI container is then capable of constructing a service instance without having to specify how.

Hope that helps,

Lennaert