Search code examples
phpmodel-view-controllerdependency-injectionlaw-of-demeter

PHP MVC DI practical exaple of Law of Demeter with Router, Controller and Model


Struggling with MVC in PHP. My concerns grew even bigger after watching this: https://www.youtube.com/watch?v=RlfLCWKxHJ0

According to LoD my Router class should only know the Request Uri to load proper Controller class. However my Conroller should know which Model class it should use and a View class to present data. Or better, Controller should know ModelFactory that will handle object creation using selected data storage.

This all breaks LoD to me.

So my question is:

  1. How Router should init Controller class not knowing which parameters it needs? Even if it is DI container, we don't know object parameters to be passed to constructor inside Router. If we pass DI container to Router constructor (or any other class) we get back to Service Locator. How should this be done?

Maybe this is all wrong, but my starting point is:

// ... retrieve settings, available languages, start session,...

$router     = new Router($settings);
$router->loadController();

Router.php

class Router 
{
    public function __construct(Settings $settings)
    {
        $this->settings = $settings;
    }

    // some other methods

    public function loadController()
    {   
        try
        {
            // Loading controller
            $controller = $this->getController();

            if (is_callable(array($controller, $this->method)) == false)
                $this->method = 'init';

            // Running controller
            $controller->{$this->method}();
        }
        catch (Exception $e)
        {
        $e->displayMessage();
        }
    }
}

And from here on I can do nothing inside my Controller class, cause I need to call new Model, new View and have to do it explicitly inside constructor or method, which is bad.

More Questions:

  1. How should I get instance of Model class in Controller? Should I use static method to load View?

Solution

  • First, the model is a layer, not a specific class. The controller itself will initiate classes necessary to start processing the business logic of your application.

    Take a look at these answers. The 2nd has some examples of how these classes may be called...

    How should a model be structured in MVC?

    Properly calling the database from Model in an MVC application?

    Second, the router just routes the requested url to a specific controller and view. There are different ways to do it, but my router(s) check from matches against an existing resource map. Based on a few other things, it will either return a successful "resource" name for the page, a resource name for pages not found, or resource for redirection, etc.

    Some basic code from a bootstrap for illustration...

    //snip
    
    $routeLoader = new \Routing\RouteLoader();
    $match = $routeLoader->getMatchedRoute( $request['pageName'] );
    
    $router = new \Routing\Router( $request );
    $resource = $router->getResource( $match );
    
    //snip
    
    $viewName       = '\View\\' . $resource . 'View';
    $controllerName = '\Controller\\' . $resource . 'Controller';
    
    $view = new $viewName();
    $controller = new $controllerName( $view, $request );
    $controller->{$router->getCommand()}();
    
    $view->response();
    

    So, to directly answer your questions..

    1) Do not initiate the controller and view inside the router class. Do it in the bootstrap.

    Also, for web applications, you may or may not need a DI, a service factory, or even find it necessary to use a specific design pattern. For most, I think its overkill and adds unnecessary complexity.

    2) The controller will initiate classes to start processing the business logic (if its even necessary.)

    ADDITIONAL

    To add a database connection to the above and inject it into the controller, below is 1 way to do it..

    $DCM = new \Database\DatabaseConnectionManager( new \Config\DatabaseConfig() );
    $AppCache = new \Cache\AppCache();
    $DAM = new \DataAccess\DataAccessManager( $AppCache, $DCM );
    

    In the above code, the DataAccessManager object would be responsible for retrieving data from the either the cache first, or the database second. This $DAM object can now be injected into the controller, like so...

    $controller = new $controllerName( $DAM, $view, $request );
    

    Instead of connecting to the database in the bootstrap and passing the connection around the application, I prefer to use a DataAccessManager, which will make the connection only when it is actually needed. Once it is needed, a PDO object (or whatever) is initiated and stored in the object to be retrieved and used again if necessary. I can also make connections to other databases if necessary..

    // method from DataAccessManager class
    
    private function connectToDatabase( $server = 'slave' )
    {
        if (!array_key_exists( $server, $this->dbObject )) {
            // use the DataConnectionManager to connect and store the connection here
            $this->dbObject[$server] = $this->DCM->connect( $server );
        }
    
        return $this->dbObject[$server];
    }