Search code examples
phpmodel-view-controllerdependency-injectionphp-8

How to structure my own MVC PHP app with dependency injection?


Sorry if the title of the post is unclear, I will try to be as simple as possible, because I really think this question has a simple answer.

First of all I will add some code(not real code, just for testing):

Model:

class User{
    public $id;
    public $name;
    public $age;
}

Interface:

interface IData{
   public function search(User $user)

}

Class Repository:

class DataRepository{
    protected $db;
    public $conn;
    public $data;


    // Passing the interface DB to the constructor
    public function __construct(DB $db)
    {
        $this->db = $db;
    }


    public function search(User $user){
        $query = " SELECT * FROM test_db WHERE id_user = ". $user->id . " and name = '%" . $user->name . "%'";
        $this->conn = $this->db->connection();
        $this->data = $this->conn->query($query);
        $dataList = [];

        foreach ($this->data as $row)
        {
            $dataList [] = (object)array(
                "name" => $row["name"],
                "age" => $row["age"],
            );
        }

        return $dataList;
    }

}

Controller:

class UserController{
    public $Idata;


    // Passing the interface IData to the constructor
    function __construct(IData $Idata) {
        $this->Idata = $Idata;
    }


    // Function to search the user and return an array with the data
    function printData(){
        $Mdata = new User();
        $Mdata->id = 5;
        $Mdata->name = "Carl";
        $Mdata->age = 39;

        $result = $this->Idata->search($Mdata);

        return $result;
    }
}

View:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test Page</title>
</head>
<body>

<?php


// We create an instance of UserController

$UserData = new UserController();


// Print the data
echo "<pre>";
    print_r($UserData);
echo "</pre>"

?>
    
</body>
</html>

So I was trying to learn the repository pattern using MVC, to sepearate my DB layer from the controller and encapsulate, all was looking great, until the moment i needed to use the IData interface on the main Controller(UserController), on my view i had an error

$UserData = new UserController() -> returns an error: Expected 1 arguments. Found 0.

I know the meaning of this error, that it requires a parameter(interface), hovewer it feels weird to instance the interface on the view side, for later on pass it as an argument to the contructor.

I tried to:

<?php

$IData = new IData();

$UserData = new UserController($IData);

echo "<pre>";
    print_r($UserData);
echo "</pre>"

?>

it returns this error: Fatal error: Uncaught Error: Cannot instantiate interface IData.

My main question is, is there any way to use the interface only on the constructor of UserController, without having to define it on the view?

if it's not possible(for sure it will be the case), what should I do?( i can't use the keyword implemante, because i'am trying to use the repository pattern, making dependecy injection from the constructor)


Solution

  • This is more a question of how dependency injection is done in PHP.

    Prerequsities

    Before anything, you should fix your code by declaring DataRepository implements the interface:

    class DataRepository implements IData
    {
        // ...
    }
    

    There is no way around it. PHP requires explicit declaration of it.

    Think first...

    Then think about the followings:

    1. UserController depends on IData.
    2. DataRepository is supposed to implements IData.
    3. Your code should know, when instantiate UserController, it needs an IData implementation. But there can be more than one of them.
    4. How would your code know which IData to use for UserController?

    For (4), you'd need a way to declare that either:

    • all classes that want IData should have DataRepository; or

    • some of the classes that want IData, like UserController, should have DataRepository.

    Dependency Injection Container

    A simple way is to make use of dependency injection container like PHP-DI. They are flexible in declaring recipes of how an interface dependency should be fulfilled.

    Assuming you have "php-di/php-di" installed with composer, then you should have this piece of code run in some sort of common php / kernel.

    require_once __DIR__ . '/vendor/autoload.php';
    
    $db = getDB(); // some way to create the database object
    $container = new DI\Container();
    $container->set(IData::class, \DI\create(DataRepository::class));
    $container->set(DB::class, \DI\value($db));
    
    // ...
    

    Should you pass the $container variable to your view file, you can simply do this:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Test Page</title>
    </head>
    <body>
    <?php
    
    // We create an instance of UserController
    $UserData = $container->get(UserController::class);
    
    // Print the data
    echo "<pre>";
        print_r($UserData);
    echo "</pre>"
    
    ?>
    </body>
    </html>
    

    Note that you do not need to manually create the dependencies of UserController. When asked for UserController, the container here will search for all the recipes to fulfil all the dependencies required by UserController. Thus the view do not need to have knowledge of the implementation details of the controller.

    More on This

    As I mentioned before, you may need to only use DataRepository as IData for some of the controllers. PHP-DI, and other dependency injection container, would have ways for you to specify the recipe to create DataRepository to do that.

    For PHP-DI, you should read documentation for that, or for other more advanced usages.

    Common MVC frameworks in PHP

    I guess a more common MVC design would probably have the controller call for the view. And have controller instantiation (using container) as the implementation details:

    Routing:

    // Pseudo-code only
    // But all MVC framework has its own router / routing code like this
    
    // some container initialization
    $container = dummyGetContainer();
    
    // router will probably need container
    $router = new MyRouter($container);
    
    // register the handler of GET request to "/userData" 
    $router->get('/userData', 'UserController::viewData');
    
    // actually execute the routing
    // internally will do:
    // 1. find the handler of the request method and path
    // 2. instantiate the controller with dependencies.
    // 3. execute the handler against the request.
    $router->route(dummyGetRequestFromEnvironment());
    

    Controller:

    class UserController{
        public $Idata;
    
        // Passing the interface IData to the constructor
        function __construct(IData $Idata) {
            $this->Idata = $Idata;
        }
    
        function viewData($request) {
            $userData = $this->getData();
            include __DIR__ . '/../views/userDataView.php';
        }
    
        // Function to search the user and return an array with the data
        function getData(){
            $Mdata = new User();
            $Mdata->id = 5;
            $Mdata->name = "Carl";
            $Mdata->age = 39;
    
            $result = $this->Idata->search($Mdata);
    
            return $result;
        }
    }
    

    View:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Test Page</title>
    </head>
    <body>
    <pre>
    <?php print_r($userData); ?>
    </pre>
    
    

    This design is even better because view should not be required to know anything about the controller. It should simply represent the data it is given.