phpoopdependency-injectionslim

Unable to use container from parent with slim


I am trying to build a project using SLIM and below is my HomeController

HomeController.php

<?php

namespace Controllers;

use DI\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;


class HomeController extends Controller{

   private $db;

   private $view;

   private $container;

    public function __construct(Container $container){
    
      $this->container = $container;

       $this->db = $container->get('db');

       $this->view = $container->get('view');
    }

    public function index(Request $request, Response $response, $args){

      echo $this->test; // 'hello world'; This is from Controller.php

      echo $this->container->get('container-test'); // I am from container is the string i pass is container and i am getting output.

      echo $this->container_from_controller?'Captured':'Not Captured'; // Not Captured i.e the container in Controller.php is empty
      
      return $this->view->render($response, 'home/index.html', [

          'name' => 'from home controller'

      ]);
    }
}

Controller.php

<?php

namespace Controllers;

use DI\Container;



class Controller{

    protected $container_from_controller;

    protected $test = 'hello world';

    public function __construct(Container $container){
      
        $this->container_from_controller = $container;
    }


}

The codes are working fine. As an improvement, I thought of moving the HomeController construct function to Controller.php so I dont want to repeat the construct code on every controller.

i.e. I want to move

   $this->container = $container;

   $this->db = $container->get('db');

   $this->view = $container->get('view');

to Controller.php and so I can directly access $this->view and $this->db as I could access $this->test

But I could see Controller.php __construct is not executing.

I tried to put parent::construct() in HomeController __construct function and got Too few arguments error.

What is the best possible way I can use to access container in controller, so i dont want to repeat the db and view codes.

composer.json

{
    "require": {
        "slim/slim": "4.*",
        "slim/psr7": "^1.6",
        "php-di/slim-bridge": "^3.4",
        "dcblogdev/pdo-wrapper": "^2.0",
        "slim/twig-view": "^3.3"
    },
    "autoload": {
        "psr-4": {
            "Controllers\\": "src/Controllers"
        }
    }
}

Update of codes as per hakre answer

I renamed the Controller to BaseController, which makes more sense.

HomeController

<?php

namespace Controllers;

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


class HomeController extends BaseController{



    public function index(Request $request, Response $response, $args){

      $sql = "SELECT * FROM list";

      $rows = $this->db->rows($sql);

      var_dump($rows);
      
      return $this->view->render($response, 'home/index.html', [

          'name' => 'from home controller'

      ]);
    }
}

BaseController

<?php

namespace Controllers;

use DI\Container;



abstract class BaseController{

    protected $db;

    protected $view;

    public function __construct(Container $container){
      
        $this->db = $container->get('db');

        $this->view = $container->get('view');
    }


}

Solution

  • In general if you move the constructor method __construct() from a child class to a parent class, like from HomeController to Controller, you have to remove it from the child class, HomeController, too.

    If you don't delete it, the child class constructor overrides the parent constructor, the parent constructor is not called.

    As you have already found out, you would then need to call parent::__construct(). (With the appropriate arguments.)

    Both ways can be correct, but as you want to use the superclass (the one you extend from, the parent/grandparent/grandgrand...) __construct() method only for starters, remove it from the child class.

    class Controller {
    
        public function __construct(Container $container)
        {
            ...
        }
    
        ...
    }
    
    class HomeController extends Controller {
    
        # deleted:
        # public function __construct(Container $container) 
        # { 
        #  ... 
        # }
    
        ...
    }
    

    More Information

    As you have the Controller as the superclass (or base class) to template code for other controllers to extend from, you may want to communicate that more clear by making the Controller class abstract.

    As the Controller class does not work on it's own (it is incomplete and only contains code for re-use by extending it), the abstract keyword will show that and ensures that a Controller alone can never be created (no new Controller($container)).

    abstract class Controller {
        ...
    }
    

    Additionally as you do not treat the __construct() method as an implementation detail any longer, you can use the final keyword to prevent any child-class to override the constructor.

    This could for example prevent the original problem you were running into, PHP then has the guardrails and you protect the object hierarchies you build from child-classes that implement wrong on extension.

    abstract class Controller {
    
        final public function __construct(Container $container)
        {
            ...
        }
    
        ...
    }
    

    Furthermore to finish the extends hierarchy, child classes can also be sealed by making them final.

    final class HomeController extends Controller {
    
        ...
    
    }
    

    https://www.php.net/manual/en/language.oop5.basic.php

    Forms of Templating code

    You further ask:

    What is the best possible way I can use to access container in controller, so i don't want to repeat the db and view codes.

    Using abstract classes to extend from are a common way to handle template code. They are designed for re-use by extending from them.

    Some say, when using abstract baseclasses and especially the superclass (the superclass is the topmost class, every other class extends from, Controller in the example) that they should have only ([abstract] protected) methods and do not contain any property handling nor provide them. So the superclass is without the properties you currently have in your superclass.

    That is, you provide a method interface to the container and the container is only the detail of the superclass, classes extending from it only call those methods:

    abstract class Controller
    {
        final public function __construct(private Container $container)
        {
        }
    
        protected function db() : Db {
           return $this->container->get('db');
        }
    
        private function view() : View {
           return $this->container->get('view');
        }
    
        protected function render(...$args)
        {
            return $this->view()->render(...$args);
        }
    
        ...
    }
    

    The child classes then communicate with the superclass only via the (protected) methods, the protocol.

    Additionally the superclass may itself define part of the protocol as abstract which then would make any childclass to implement such methods (not shown here).

    final class HomeController extends Controller
    {
         ...
    
         public function index(Request $request, Response $response, $args)
         {
            ...
    
            $db = $this->db();
    
            ...
    
            return $this->render($response, 'home/index.html', [
              'name' => 'from home controller'
            ]);
         }
    
         ...
    }
    

    Using the protocol only allows to write better templates as the extending class must not deal with the implementation details of the template class.

    This helps to better communicate and allows to better identify what templating code you need and what the abstractions are you have.

    Composition vs. Inheritance for Templating

    Another approach you can take is to never use the extends keyword and implement all your classes final.

    This automatically forces you to never use inheritance, therefore you need to use composition.

    Template code you can pull in via traits though. But using them can ruin composition again, only using traits then has given you some sort of inheritance (copy and paste during loading) on a more granular level.

    It is normally that you first start to create your class hierarchy and when things settle, you consider higher level objects for composition. You then do this by refactoring your code.

    In the example, the DI $container that is injected into the constructor function for example is used for composition. The Controller does not extend from it.

    By using composition you build an object hierarchy that is independent to the class hierarchy.

    Composition is normally more flexible, however it is harder to implement right. Using the final keyword on all classes and committing yourself to the requirement to have interfaces with all the classes normally supports this form well, as this methodology enforces you to program against interfaces, not concrete class abstractions, so you can more easily replace functionality and change the implementation, not the abstraction.

    But you loose the templating you're looking for by extends, too.

    Both forms require you to do your experiments with them, I'd therefore recommend you start with a low-level class hierarchy for your controllers, e.g. only have one superclass to extend from an make the child classes directly final.

    If you find areas of your application where all controllers are doing merely the same, you perhaps want to make use of composition, too, by providing a service, a method or a gateway object handling those actions by the DI $container.