I have recently been looking into the concepts/patterns behind dependency injection, inversion of control, and registries/service locators. I have searched about the internet on the subjects and have come up with some decent understanding about them, but some things still seem a little cloudy. To start, I would like to know if my understanding on the subjects is correct:
- Dependency Injection - injecting a dependent class into another class that needs the dependency to operate correctly. e.g.
ClassA
depends onClassB
, soClassB
is injected intoClassA
through either a method or constructor ofClassA
. - Registries/Service Locators - Basically an array or object with keys/properties that hold other objects that can be called up when needed by other classes.
- Inversion of Control - I actually don't really know what this is completely, yet. From what I can understand IoC is simply dependency inject exploited and enhanced to another level. IoC seems to utilize
Closures
to create "definitions" of what objects should look like (with dependencies) that can be called later in an application.
I'm interested in some clarifications on what these three things are in relation to each other and if I have the meanings/concepts behind each understood correctly. Each seem to have their uses, but I see mixed responses on the subjects. So perhaps some simple examples or situations on where they each have their use could help clear the air for me.
I learn best with practice, trial by error, and good 'ol hands-on approach. The internet, along with many posts over on Stack Overflow, reveal a number of tried and true IoC/DIC plugins you can use for your PHP apps, however I feel the need to make my own for understanding, experience, and personal enrichment. So to better understand all of these concepts I have tried to created my own IoC/DIC to get a grip on how all of this works and if I can use this sort of thing in future developments. I would like some feedback on my code and need to know if I am missing some considerations from an implementation standpoint. I have made this as a static
class, as it seems to make sense. I know I will likely shamed and stoned for making such a thing static
, but to me the end result if I were to use it would be to throw it behind a namespace and not pollute the global space. Making the class static also means I don't have to inject the IoC into other classes as well. But perhaps there are some considerations there I am not seeing right away either, so again, feedback is much appreciated.
Here is my IoC/DIC code I have in IoC.php
:
<?php
/**
* Dependancy Injector Container / Inversion of Control
*
* Learning project for DIC/IoC design pattern.
*
*/
class IoC {
private static $definitions = array();
private static $registry = array();
/**
* Register a Closure for use in creating an object later.
*
* @param string $identifier String to identify the registered definition.
* @param Closure $closure A Closure to be called that will create an object.
*/
public static function register($identifier, Closure $closure) {
if(array_key_exists($identifier, self::$definitions)) {
throw new Exception("Identifier '$identifier' already defined in registry.");
} else {
self::$definitions[$identifier] = $closure;
}
}
/**
* Check if a registered definition exists.
*
* @param string $identifier The definition's indentifier to search for.
* @return bool TRUE if a definition was found or FALSE if it was not.
*/
public static function exists($identifier) {
if(array_key_exists($identifier, self::$definitions)) {
return true;
}
return false;
}
/**
* Launches the Closure definition to create a specified object.
*
* @param string $identifier The Closure definition to locate and call.
* @return mixed An object created by a Closure definition.
*/
public static function create($identifier) {
if(static::exists($identifier)) {
$item = self::$definitions[$identifier];
return $item();
} else {
throw new Exception("No item registered with the identifier of '$identifier'.");
}
}
/**
* Launches a Closure definition and stores the created object in the registry.
*
* @param string $identifier String to identify the created object in the registry.
* @param string $object A Closure definition to look for and call.
*/
public static function store($identifier, $object) {
if(static::exists($object)) {
$item = self::create($object);
self::$registry[$identifier] = $item;
} else {
throw new Exception("Unable to store item. No item registered with the identifier of '$identifier'.");
}
}
/**
* Gets an object stored in the registry.
*
* @param string $identifier The object identifier to find in the registry.
* @return mixed Returns the requested object or FALSE if the object wasn't found.
*/
public static function get($identifier) {
if(array_key_exists($identifier, self::$registry)) {
return self::$registry[$identifier];
} else {
throw new Exception("No item registered with the identifier of '$identifier'.");
}
return false;
}
}
?>
I have created a file for this, which works as expected, but I haven't thrown anything behind a namespace to see how it works there. For my test I define a number of objects and then register a few with the IoC container and then print out some results. I start by creating a class called FruitBasket
which uses some setter injection to obtain Fruit
objects. I then create a Fruit
parent class and some child classes Apple
, Orange
, Banana
, and Pomegranate
. One function in the IoC is called register()
which I create a definition of a closure in and give it a string to identify that closure by. After a definition has been made I can have that closure called by making a call to create()
, which only needs the identifier for the closure. The IoC container also contains a registry where objects can be instantiated and preserved using the store()
function. store()
needs an identifier to be used to identify the newly created object by in the registry and the identifier for a closure definition that has already been made using register()
.
Here are the classes used to create the FruitBasket
and Fruits
:
<?php
class FruitBasket {
/**
* Array of fruits in the basket
*/
private $fruits = array();
/**
* Constructor - no arguements
*/
public function __construct() {}
/**
* Add a fruit to the basket.
*
* In order for the fruit basket to be useful it should
* have some fruit in it. The fruits are the dependancies.
*
* @param Fruit $fruit A type of Fruit to add to the basket.
*/
public function addFruit(Fruit $fruit) {
$this->fruits[] = $fruit;
}
/**
* Show the flavors of all the fruit in the basket
*/
public function show() {
if(!empty($this->fruits)) {
foreach($this->fruits as $fruit) {
$fruit->getFlavor();
}
} else {
throw new Exception("Nothing to show, no fruits!");
}
}
}
/**
* Parent class: Fruit
*/
class Fruit {
private $flavor = "";
public function __construct($str) {
$this->flavor = $str;
}
public function getFlavor() {
print("The fruit " . get_class($this) . " tastes " . $this->flavor . ".\n");
}
}
/**
* Child class: Apple
*/
class Apple extends Fruit {
public function __construct() {
parent::__construct("sweet and juicy");
}
}
/**
* Child class: Orange
*/
class Orange extends Fruit {
public function __construct() {
parent::__construct("tangy - but in a citris way");
}
}
/**
* Child class: Banana
*/
class Banana extends Fruit {
public function __construct() {
parent::__construct("a bit on the bland side of sweetness");
}
}
/**
* Child class: Pomegrante
*/
class Pomegranate extends Fruit {
public function __construct() {
parent::__construct("juicy, seedy, and delicious followed by a little bit of sour-like sweetness");
}
}
?>
Here is the test that I did to check some of the functionality:
<?php
// - SET REGISTRATIONS
//Register the definition for an empty basket
//This definition doesn't do any injections.
IoC::register('emptyBasket', function () {
$basket = new FruitBasket();
return $basket;
});
//Register the definition with a basket that
//has some fruit. This would hopefully be
//an example of how to utilize setter injection.
IoC::register('fruitBasket', function () {
$basket = new FruitBasket();
$basket->addFruit(new Apple());
$basket->addFruit(new Orange());
return $basket;
});
//Register another definition but will all the
//fruits injected into the basket.
IoC::register('fullBasket', function () {
$basket = new FruitBasket();
$basket->addFruit(new Apple());
$basket->addFruit(new Orange());
$basket->addFruit(new Banana());
$basket->addFruit(new Pomegranate());
return $basket;
});
//Store an empty basket in the registry for shared use.
//Equivalent to storing a database connection.
IoC::store('emptyBasket', 'emptyBasket');
//Now it is time to do stuff with the things that have
//been created.
//Create a 'fruitBasket' and show its contents.
print("\nShowing contents of 'fruitBasket'. \n");
$basket = IoC::create('fruitBasket');
$basket->show();
//Create a 'fullBasket' and show its contents.
print("\nShowing contents of 'fullBasket'. \n");
$basket = IoC::create('fullBasket');
$basket->show();
//Get the 'emptyBasket' stored in the registry and add some fruits.
print("\nShowing contents of 'emptyBasket' - after some additions. \n");
$basket = IoC::get('emptyBasket');
$basket->addFruit(new Orange());
$basket->addFruit(new Pomegranate());
$basket->addFruit(new Apple());
$basket->show();
?>
This produced the following (expected) output:
Showing contents of 'fruitBasket'. The fruit Apple tastes sweet and juicy. The fruit Orange tastes tangy - but in a citris way. Showing contents of 'fullBasket'. The fruit Apple tastes sweet and juicy. The fruit Orange tastes tangy - but in a citris way. The fruit Banana tastes a bit on the bland side of sweetness. The fruit Pomegranate tastes juicy, seedy, and delicious followed by a little bit of sour-like sweetness. Showing contents of 'emptyBasket' - after some additions. The fruit Orange tastes tangy - but in a citris way. The fruit Pomegranate tastes juicy, seedy, and delicious followed by a little bit of sour-like sweetness. The fruit Apple tastes sweet and juicy.
All in all I am happy with what the results are so far, but since the subjects are new to me I am reaching out the community here on Code Review for feedback. I am basically concerned with whether or not my understanding of DI/IoC/Registries/Service Locators are sound and whether or not I am overlooking various design considerations. I would also like to know if I am going in the right direction with my IoC code as it is now or if I am completely missing the point on how to create such a thing in PHP.
IoC
members non-static, only difference is the addition of$ctx = new IoC();
. And it is a step forward, as it demonstrates I can now have many contexts in an application. \$\endgroup\$