I just learned about dependency injection/dependency injection containers, and I realize that the controller in my MVC web application is hard to test.
My original controller class:
class Controller
{
protected $db;
private $article;
private $nav;
public function __construct($db)
{
$this->db = $db;
$this->article = new Article($this->db);
$this->nav = new Nav($this->db);
}
public function render($template)
{
var_dump($this->article->getRow());
var_dump($this->nav->getRows());
}
}
This is my test using DI/DIC in my MVC app:
Controller
class Controller
{
private $container;
public function __construct()
{
}
public function render($container,$template)
{
$this->container = $container;
var_dump($this->container->getArticle($url = 'home'));
var_dump($this->container->getNav());
}
}
Container: holds all objects which generate data from the database
class Container
{
protected $db;
private $article;
private $nav;
public function __construct($db)
{
$this->db = $db;
}
public function setArticle()
{
if (is_null($this->article))
{
$this->article = new Article($this->db);
}
return $this->article;
}
public function setNav()
{
if (is_null($this->nav))
{
$this->nav = new Nav($this->db);
}
return $this->nav;
}
public function getArticle($url)
{
return $this->setArticle()->getRow($url);
}
public function getNav()
{
return $this->setNav()->getRows();
}
}
Models
class Article
{
public function __construct($db)
{
}
public function getRow($url)
{
return "row 1";
}
}
class nav
{
public function __construct($db)
{
}
public function getRows()
{
return "rows nav";
}
}
Usage
$db = 'pdo connection';
$template = 'template.phtml';
$container = new Container($db);
$controller = new Controller();
$controller->render($container,$template);
Questions:
- Am I doing it correctly or not?
- Do the improved
Controller
andContainer
above make sense to you if you were a developer going to pick up this application's development? - Do I need any interfaces or traits as part of the app? If so, why?
3 Answers 3
Building upon Gufran's answer, here is what the Container
class using constructor injection would look like. It instantiates all objects of the application and then "wires" them together.
class Container {
protected $db;
private $article;
private $nav;
private $controller;
public function __construct($db) {
$this->db = $db;
}
public function getArticle() {
if (is_null($this->article)) {
$this->article = new Article($this->db);
}
return $this->article;
}
public function getNav() {
if (is_null($this->nav)) {
$this->nav = new Nav($this->db);
}
return $this->nav;
}
public function getController() {
if (is_null($this->controller)) {
$this->controller = new Controller(getArticle(), getNav());
}
return $this->controller;
}
}
You are not even doing Dependency Injection here. In your controller constructor for example
public function __construct($db)
{
$this->db = $db;
$this->article = new Article($this->db);
$this->nav = new Nav($this->db);
}
sure you are injecting the $db
but that is not a dependency for as long as it does not enforce the type of object required. In this case I can inject anything, say, a string, a number, an object of some other class, array or pretty much everything else. How is constructor going to control the behaviour of this object ?
Furthermore, you are 'newing' up the objects in constructor. Don't you think the objects you are initialising inside the constructor should instead be injected into it ? That is what IoC does.
The solution to first problem is Type Hinting. What you do here is you define the type of argument and only defined type can be passed to the constructor as argument. For more flexibility (which you'd more likely want in case of database connection) you can define an interface
and inject an implementation of it into the constructor. So for example you define an interface DatabaseConnection
like this:
interface DatabaseConnection {
public function prepareQuery($queryString);
public function addQueryBindings(array $queryBindings);
public function run();
}
Now you can create multiple implementations of this interface and have IoC inject them into constructor. So now you can have MySQLDatabaseConnection
, PostgreDatabaseConnection
, MSSQLDatabaseConnection
, SQLiteDatabaseConnection
each one implementing DatabaseConnection
and then you type hint the constructor of controller to accept DatabaseConnection
public function __construct(DatabaseConnection $db)
{
$this->db = $db;
$this->article = new Article($this->db);
$this->nav = new Nav($this->db);
}
This constructor will allow any object which implements the DatabaseConnection
interface and throw error if you try to inject anything else that is not a descendent of this interface.
Now that type hinting and interface is out of the way, I hope you can already see this but let me point it out.
The objects you are initialising inside controller constructor can be injected by IoC, this will also eliminate the need to have DatabaseConnection
injected into a controller (which looks a terrible idea to me). So here is the controller constructor freshen up
public function __construct(Article $article, Nav $nav)
{
$this->article = $article;
$this->nav = $nav;
}
and you'll also need to change the constructor of both Article
and Nav
class to type hint the DatabaseConnection
so that IoC can do its things
class Article
{
public function __construct(DatabaseConnection $db)
{
//...
}
}
and
class Nav
{
public function __construct(DatabaseConnection $db)
{
//...
}
}
The container, as you call it, is also not an IoC container (Inversion Of Control), It is not doing dependency injection just holding pre-initialised objects into itself. If you want Dependency Injection you need an IoC Container for that.
I dont think I can explain the architecture of an IoC container in a post here but with IoC in place you'll be able to initialise objects auto-magically with proper dependency already injected into it. So for example when you initialise our new controller from IoC container you will have an object of Controller
with Article
and Nav
injected into it and both of them will have DatabaseConnection
injected into them.
There are many IoC containers available on Github, some standalone and some as a part of a framework.
-
\$\begingroup\$ thanks for the answer. I think you reviewing on my older version of controller. please read my post again carefully. thanks. \$\endgroup\$Run– Run2014年08月01日 09:35:05 +00:00Commented Aug 1, 2014 at 9:35
-
1\$\begingroup\$ @lauthiamkok saw your edit, the answer still stands. \$\endgroup\$Gufran– Gufran2014年08月01日 15:54:04 +00:00Commented Aug 1, 2014 at 15:54
DI Container
Dependancy Injection Container is a name for something different than you have here. It's holding construction parameters and object instances (composed and single) not it's methods and itself is not injected - it only fetches required objects for injection. See the difference in examples:
class SomeObject
{
// constructor tells me that if I want to create/use/test this object
// I need another one that is implementing iNavClass interface
public function __construct(iNavClass $nav) {
$this->nav = $nav;
}
...
}
// I've programmed my container earlier that it returns an apprioprate instance of
// iNavClass implementation and it's not limited to concerete class
// I could choose from many classes that use required interface (at runtime)
new SomeObject($this->container->getObject('NavClassImplementation'));
"Injected container" is Service Locator and is considered an anti-pattern
class SomeObject
{
// constructor tells me that if I want to create/use/test this object
// I need other object(s), but can't tell what are they.
// Sometimes (not here) I need deep code inspection to find out
// how and what kind of objects are used.
// If I want to test this class I need to mock ServiceLocator
// along with object it should return.
public function __construct(ServiceLocator $locator) {
$this->nav = $locator->getObject('NavClassImplementation');
}
...
}
// Can't tell what this object would really need
new SomeObject($this->locator);
I'll explain rest in short (for now) - maybe will expand later (or maybe someone else will):
- Controll is going general-to-specific, but object instantiation (with some exceptions) the other way - that's why you use dependency injection. You can encapsulate creation with Factory Method pattern, Abstract Factory pattern or more dynamically with DI Container (this movie shows how it works).
- Your controller-envolved "Container" is slowly becoming Model layer.
Create ModelFactory class that instantiates (in this order): $db,
$article($db), $nav($db) (I'd go for CommonPageElements here - nav
only is too small for a class imo), $articlePage($article, $nav)
and have a method that returns last element - that was your "container". Invoke that method and inject result into new Controller. - The "View" and communication between these three are not resolved yet and there's still much more to learn (through practice & theory).
controller is hard to test because it instantiates the objects
but I have reworked on it if you read further down my post. thanks! \$\endgroup\$Controller
, duh! :/ Gufran's answer still stands IMHO. YourController
shouldn't know about theContainer
. I'm not familiar with PHP dependency injection frameworks, but when using Spring (Java), objects instantiated by the DI container do not depend on it. \$\endgroup\$