Using Dependency Injection in MVC PHP

1.9k views Asked by At

For loading classes I use PSR-4 autoload. In my index.php I use FastRoute component. In that index.php I create $db connection and pass it to the controllers.

call_user_func_array([new $class($db), $method], $vars);

In the controllers I receive it and pass it to the models and use it there. I know that is bad approach. DB connection that was created in index.php how can I use it in my models not pass it through controllers etc and without singleton instance of a connection? How can I use DI or DIC to set up db connection in index.php and get in all models?

index.php:

<?php

require_once 'vendor/autoload.php';

$db = new DbConnection(new DbConfig($config));

$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addRoute('GET', '/users', 'UserController/actionGetAllUsers');
    $r->addRoute('GET', '/users/{id:\d+}', 'UserController/actionGetUserById');
});

// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::NOT_FOUND:
        // ... 404 Not Found
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        $allowedMethods = $routeInfo[1];
        // ... 405 Method Not Allowed
        break;
    case FastRoute\Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        list($class, $method) = explode("/", $handler, 2);
        $module = strtolower(str_replace('Controller', '', $class));
        $class = 'Vendorname\\' . $module . '\\controllers\\' . $class;
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
        call_user_func_array([new $class($db), $method], $vars);
        break;
} 

controller:

class UserController extends BaseApiController
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
        parent::__construct();
    }

    public function actionGetAllUsers()
    {
        $model = new User($this->db);
        echo json_encode($model->getAllUsers());
    }

model:

class User
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function getAllUsers()
    {
        $stmt = $this->db->prepare('SELECT * FROM user');
        $stmt->execute();
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
1

There are 1 answers

0
tereško On

First of all, you have to understand, that "Dependency Injection" and "Dependency Injection Container" are two different things:

  • DI is a technique, that you use to separate two classes in OOP.
  • DIC is a piece of software, that assembles object graphs

It seems, that you are already, that you way of passing of DB connection is bad. The exact technical reason can be described as violation of LoD. Basically, your $db variable is crossing two layer boundaries.

But, the solution for this is not: "just add DI container". That won't solve the problem. Instead, you should improve the design of the model layer. You might do a quick read here, but I will give a bit shorter version here:

  • separate your persistence logic from your domain logic
  • controller should depend on service, which would be how controllers (that exist in UI layer) interact with the business logic.
  • the simplest way yo separate business logic is in three parts: datamappers (handle persistence logic), domain objects (for business rules) and services (interaction between domain objects and persistence objects)

The DB should be a dependency for a datamapper, that is specifically made for handling user instances (more here). That mapper should be either a direct dependency of your "account management service" (think of a good name) or be constructed using a factory inside that service (and this factory then would be the dependency of the service.

P.S. As for "which DIC to choose", I personally go with Symfony's standalone container, but that's purely personal preference.