By my opinion one of the biggest problems in programming are dependencies. If you want to have a good well written application you should avoid dependencies between your modules/classes. There is a design pattern which could help and it's called Dependency Injection (DI). It allows you to inject objects into a class, instead of creating them there.
The files of that article could be found here
Actually I'm sure that you already used dependency injection in your code. And I bet that you are doing that pretty often. You just don't know that it is called dependency injection. For example let's take the following example:
class Person {
private $skills;
public function __construct() {
$this->skills = array("PHP", "HTML", "CSS", "JavaScript");
}
public function totalSkills() {
return count($this->skills);
}
}
$p = new Person();
echo $p->totalSkills();
There are three big problems with the class above.
- When we want to add some new skill we should edit the class, which is not good. Every class should be written as a black box. I.e. you should be able to fully operate with the class only by using its public properties and variables.
- What if we want to set different skills for the different instances of class Person. It is currently not possible, because the private property $skills is created inside the constructor of the class.
- What if we have something more then an array. For example another object and that object has its own needs. Then we should send the needed things to Person class so we can initialize our variable.
The solution of the problem could be something like:
class Person {
private $skills;
public function __construct($skills) {
$this->skills = $skills;
}
public function totalSkills() {
return count($this->skills);
}
}
$mySkills = array("PHP", "HTML", "CSS", "JavaScript");
$p = new Person($mySkills);
echo $p->totalSkills();
I.e. passing the skills from outside the class. We could say that we injected an object in class Person. The example is really simple, but it perfectly illustrates how helpful could be this approach.
In the example above we used injection to make our class looks better. We solved two problems, but another one comes. Now our class depends on the provided $skills variable. In our case that's just an array, but it could be something very complex or even worst - you could have several very complex objects and some of them could have another dependencies, which also have dependencies and so on. If that's the case then you should pass their dependencies too and your class will become bigger and bigger. You need something that will handle the dependencies for you and that thing is called Dependency Injection container. In the example below I'll show you how you can create and use DI container in PHP.
Let's imagine that we have a MVC written application and we want to show the users in our system.
We have a view which displays information to the visiter:
class View {
public function show($str) {
echo "<p>".$str."</p>";
}
}
A model which delivers the users' information:
class UsersModel {
public function get() {
return array(
(object) array("firstName" => "John", "lastName" => "Doe"),
(object) array("firstName" => "Mark", "lastName" => "Black")
);
}
}
A navigation of the page which uses the view to show main menu links:
class Navigation {
private $view;
public function __construct() {
$this->view = new View();
}
public function show() {
$this->view->show('
<a href="#" title="Home">Home</a> |
<a href="#" title="Home">Products</a> |
<a href="#" title="Home">Contacts</a>
');
}
}
A class which shows the content of the page:
class Content {
private $title;
private $view;
private $usersModel;
public function __construct($title) {
$this->title = $title;
$this->view = new View();
$this->usersModel = new UsersModel();
}
public function show() {
$users = $this->usersModel->get();
$this->view->show($this->title);
foreach($users as $user) {
$this->view->show($user->firstName." ".$user->lastName);
}
}
}
Controller which combines everything:
class PageController {
public function show() {
$navigation = new Navigation();
$content = new Content("Content title!");
$navigation->show();
$content->show();
}
}
At the end - running the controller:
$page = new PageController();
$page->show();
The result:
<p>
<a href="#" title="Home">Home</a> |
<a href="#" title="Home">Products</a> |
<a href="#" title="Home">Contacts</a>
</p>
<p>Content title!</p>
<p>John Doe</p>
<p>Mark Black</p>
Basically nothing wrong with the code. It works, shows the navigation, the title and the users. The problem here is that the classes are configure by themselfs and a lot of dependencies are created. For example if we want to use the Content with another model or another view we should make changes inside the constructor of the class. All the objects are tightly connected to each other and it is difficult to use them out of the current context.
The role of the DI container that we are going to create is to initializes objects instead of you and inject the dependencies which are required by the new object. We will start with the mapping mechanism. I.e. the setting of rules which will define what to be injected and where. Here is how the class looks in its first form:
class DI {
public static function getInstanceOf($className, $arguments = null);
public static function mapValue($key, $value);
public static function mapClass($key, $value, $arguments = null);
public static function mapClassAsSingleton($key, $value, $arguments = null);
}
- getInstanceOf - creates instance of type $className by passing $argumentsto the class's constructor.
- mapValue - associate $key with $value. The value could be anything - array, string or object. The key is used by the container to find out what exactly to inject.
- mapClass - same as mapValue, but class name should be passed as a value. We can also send $arguments to class's constructor.
- mapClassAsSingleton - same as mapClass, but once the class is created its instance is returned during the next injection.
Here is the mapping code:
class DI {
private static $map;
private static function addToMap($key, $obj) {
if(self::$map === null) {
self::$map = (object) array();
}
self::$map->$key = $obj;
}
public static function mapValue($key, $value) {
self::addToMap($key, (object) array(
"value" => $value,
"type" => "value"
));
}
public static function mapClass($key, $value, $arguments = null) {
self::addToMap($key, (object) array(
"value" => $value,
"type" => "class",
"arguments" => $arguments
));
}
public static function mapClassAsSingleton($key, $value, $arguments = null) {
self::addToMap($key, (object) array(
"value" => $value,
"type" => "classSingleton",
"instance" => null,
"arguments" => $arguments
));
}
}
So far so good. Now we have to find some way to write instructions to the container. These instructions should be placed inside the classes which will be used during the injection. The other programming languages offer various ways to handle this. For example in ActionScript we could use meta tags, but in PHP we don't have any simple and elegant solution. There are several implementations of Dependency Injection in PHP. Some of them are reading your php file as a plain text and parsing its content. I decided to use ReflectionClass and to put everything in the class's docs. For example:
/**
* @Inject view
*/
class Navigation {
public function show() {
$this->view->show( );
}
}
When you want to have an object of type Navigation you should call the getInstanceOf method of the container. The line @Inject view means that you will have a public property injected called view. The value of view depends of your mapping rules. In the example below $view is an instance of View class.
The code of getInstanceOf:
public static function getInstanceOf($className, $arguments = null) {
// checking if the class exists
if(!class_exists($className)) {
throw new Exception("DI: missing class '".$className."'.");
}
// initialized the ReflectionClass
$reflection = new ReflectionClass($className);
// creating an instance of the class
if($arguments === null || count($arguments) == 0) {
$obj = new $className;
} else {
if(!is_array($arguments)) {
$arguments = array($arguments);
}
$obj = $reflection->newInstanceArgs($arguments);
}
// injecting
if($doc = $reflection->getDocComment()) {
$lines = explode("\n", $doc);
foreach($lines as $line) {
if(count($parts = explode("@Inject", $line)) > 1) {
$parts = explode(" ", $parts[1]);
if(count($parts) > 1) {
$key = $parts[1];
$key = str_replace("\n", "", $key);
$key = str_replace("\r", "", $key);
if(isset(self::$map->$key)) {
switch(self::$map->$key->type) {
case "value":
$obj->$key = self::$map->$key->value;
break;
case "class":
$obj->$key = self::getInstanceOf(self::$map->$key->value, self::$map->$key->arguments);
break;
case "classSingleton":
if(self::$map->$key->instance === null) {
$obj->$key = self::$map->$key->instance