Refactor container to be more flexible

This commit is contained in:
Timothy Warren 2016-08-29 12:51:40 -04:00
parent 3c782af86b
commit 3db58c7066
5 changed files with 245 additions and 56 deletions

View File

@ -28,11 +28,18 @@ use Aviat\Ion\Di\Exception\NotFoundException;
class Container implements ContainerInterface { class Container implements ContainerInterface {
/** /**
* Array with class instances * Array of container Generator functions
*
* @var Callable[]
*/
protected $container = [];
/**
* Array of object instances
* *
* @var array * @var array
*/ */
protected $container = []; protected $instances = [];
/** /**
* Map of logger instances * Map of logger instances
@ -55,10 +62,10 @@ class Container implements ContainerInterface {
/** /**
* Finds an entry of the container by its identifier and returns it. * Finds an entry of the container by its identifier and returns it.
* *
* @param string $id Identifier of the entry to look for. * @param string $id - Identifier of the entry to look for.
* *
* @throws NotFoundException No entry was found for this identifier. * @throws NotFoundException - No entry was found for this identifier.
* @throws ContainerException Error while retrieving the entry. * @throws ContainerException - Error while retrieving the entry.
* *
* @return mixed Entry. * @return mixed Entry.
*/ */
@ -71,40 +78,88 @@ class Container implements ContainerInterface {
if ($this->has($id)) if ($this->has($id))
{ {
$item = $this->container[$id]; // Return an object instance, if it already exists
if (array_key_exists($id, $this->instances))
{
return $this->instances[$id];
}
if (is_callable($item)) // If there isn't already an instance, create one
{ $obj = $this->getNew($id);
return $this->applyContainer($item($this)); $this->instances[$id] = $obj;
} return $obj;
else
{
return $item;
}
} }
throw new NotFoundException("Item '{$id}' does not exist in container."); throw new NotFoundException("Item '{$id}' does not exist in container.");
} }
/** /**
* Add a value to the container * Get a new instance of the specified item
*
* @param string $id - Identifier of the entry to look for.
* @param array [$args] - Optional arguments for the factory callable
* @throws NotFoundException - No entry was found for this identifier.
* @throws ContainerException - Error while retrieving the entry.
* @return mixed
*/
public function getNew($id, array $args = NULL)
{
if ( ! is_string($id))
{
throw new ContainerException("Id must be a string");
}
if ($this->has($id))
{
// By default, call a factory with the Container
$args = (is_array($args)) ? $args : [$this];
$obj = call_user_func_array($this->container[$id], $args);
// Check for container interface, and apply the container to the object
// if applicable
return $this->applyContainer($obj);
}
throw new NotFoundException("Item '{$id}' does not exist in container.");
}
/**
* Add a factory to the container
* *
* @param string $id * @param string $id
* @param mixed $value * @param Callable $value - a factory callable for the item
* @return ContainerInterface * @return ContainerInterface
*/ */
public function set($id, $value) public function set($id, Callable $value)
{ {
$this->container[$id] = $value; $this->container[$id] = $value;
return $this; return $this;
} }
/**
* Set a specific instance in the container for an existing factory
*
* @param string $id
* @param mixed $value
* @throws NotFoundException - No entry was found for this identifier.
* @return ContainerInterface
*/
public function setInstance($id, $value)
{
if ( ! $this->has($id))
{
throw new NotFoundException("Factory '{$id}' does not exist in container. Set that first.");
}
$this->instances[$id] = $value;
return $this;
}
/** /**
* Returns true if the container can return an entry for the given identifier. * Returns true if the container can return an entry for the given identifier.
* Returns false otherwise. * Returns false otherwise.
* *
* @param string $id Identifier of the entry to look for. * @param string $id Identifier of the entry to look for.
*
* @return boolean * @return boolean
*/ */
public function has($id) public function has($id)
@ -114,37 +169,38 @@ class Container implements ContainerInterface {
/** /**
* Determine whether a logger channel is registered * Determine whether a logger channel is registered
* @param string $key The logger channel *
* @param string $id The logger channel
* @return boolean * @return boolean
*/ */
public function hasLogger($key = 'default') public function hasLogger($id = 'default')
{ {
return array_key_exists($key, $this->loggers); return array_key_exists($id, $this->loggers);
} }
/** /**
* Add a logger to the Container * Add a logger to the Container
* *
* @param LoggerInterface $logger * @param LoggerInterface $logger
* @param string $key The logger 'channel' * @param string $id The logger 'channel'
* @return ContainerInterface * @return ContainerInterface
*/ */
public function setLogger(LoggerInterface $logger, $key = 'default') public function setLogger(LoggerInterface $logger, $id = 'default')
{ {
$this->loggers[$key] = $logger; $this->loggers[$id] = $logger;
return $this; return $this;
} }
/** /**
* Retrieve a logger for the selected channel * Retrieve a logger for the selected channel
* *
* @param string $key The logger to retrieve * @param string $id The logger to retrieve
* @return LoggerInterface|null * @return LoggerInterface|null
*/ */
public function getLogger($key = 'default') public function getLogger($id = 'default')
{ {
return ($this->hasLogger($key)) return ($this->hasLogger($id))
? $this->loggers[$key] ? $this->loggers[$id]
: NULL; : NULL;
} }

View File

@ -16,43 +16,61 @@
namespace Aviat\Ion\Di; namespace Aviat\Ion\Di;
use Interop\Container\ContainerInterface as InteropInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
* Interface for the Dependency Injection Container * Interface for the Dependency Injection Container
*/ */
interface ContainerInterface extends \Interop\Container\ContainerInterface { interface ContainerInterface extends InteropInterface {
/** /**
* Add a value to the container * Add a factory to the container
* *
* @param string $key * @param string $id
* @param mixed $value * @param Callable $value - a factory callable for the item
* @return ContainerInterface * @return ContainerInterface
*/ */
public function set($key, $value); public function set($id, Callable $value);
/**
* Set a specific instance in the container for an existing factory
*
* @param string $id
* @param mixed $value
* @return ContainerInterface
*/
public function setInstance($id, $value);
/**
* Get a new instance of the specified item
*
* @param string $id
* @return mixed
*/
public function getNew($id);
/** /**
* Determine whether a logger channel is registered * Determine whether a logger channel is registered
* @param string $key The logger channel * @param string $id The logger channel
* @return boolean * @return boolean
*/ */
public function hasLogger($key = 'default'); public function hasLogger($id = 'default');
/** /**
* Add a logger to the Container * Add a logger to the Container
* *
* @param LoggerInterface $logger * @param LoggerInterface $logger
* @param string $key The logger 'channel' * @param string $id The logger 'channel'
* @return Container * @return Container
*/ */
public function setLogger(LoggerInterface $logger, $key = 'default'); public function setLogger(LoggerInterface $logger, $id = 'default');
/** /**
* Retrieve a logger for the selected channel * Retrieve a logger for the selected channel
* *
* @param string $key The logger to retreive * @param string $id The logger to retreive
* @return LoggerInterface|null * @return LoggerInterface|null
*/ */
public function getLogger($key = 'default'); public function getLogger($id = 'default');
} }

View File

@ -8,9 +8,22 @@ use Monolog\Logger;
use Monolog\Handler\TestHandler; use Monolog\Handler\TestHandler;
use Monolog\Handler\NullHandler; use Monolog\Handler\NullHandler;
class FooTest {
public $item;
public function __construct($item) {
$this->item = $item;
}
}
class FooTest2 {
use \Aviat\Ion\Di\ContainerAware;
}
class ContainerTest extends \Ion_TestCase { class ContainerTest extends \Ion_TestCase {
public function setUp() public function setUp()
{ {
$this->container = new Container(); $this->container = new Container();
@ -53,6 +66,94 @@ class ContainerTest extends \Ion_TestCase {
} }
} }
/**
* @dataProvider dataGetWithException
*/
public function testGetNewWithException($id, $exception, $message)
{
try
{
$this->container->getNew($id);
}
catch(ContainerException $e)
{
$this->assertInstanceOf($exception, $e);
$this->assertEquals($message, $e->getMessage());
}
}
public function dataSetInstanceWithException()
{
return [
'Non-existent id' => [
'id' => 'foo',
'exception' => 'Aviat\Ion\Di\Exception\NotFoundException',
'message' => "Factory 'foo' does not exist in container. Set that first.",
],
'Non-existent id 2' => [
'id' => 'foobarbaz',
'exception' => 'Aviat\Ion\Di\Exception\NotFoundException',
'message' => "Factory 'foobarbaz' does not exist in container. Set that first.",
],
];
}
/**
* @dataProvider dataSetInstanceWithException
*/
public function testSetInstanceWithException($id, $exception, $message)
{
try
{
$this->container->setInstance($id, NULL);
}
catch(ContainerException $e)
{
$this->assertInstanceOf($exception, $e);
$this->assertEquals($message, $e->getMessage());
}
}
public function testGetNew()
{
$this->container->set('footest', function($item) {
return new FooTest($item);
});
// Check that the item is the container, if called without arguments
$footest1 = $this->container->getNew('footest');
$this->assertInstanceOf('Aviat\Ion\Di\ContainerInterface', $footest1->item);
$footest2 = $this->container->getNew('footest', ['Test String']);
$this->assertEquals('Test String', $footest2->item);
}
public function testSetContainerInInstance()
{
$this->container->set('footest2', function() {
return new FooTest2();
});
$footest2 = $this->container->get('footest2');
$this->assertEquals($this->container, $footest2->getContainer());
}
public function testGetNewReturnCallable()
{
$this->container->set('footest', function($item) {
return function() use ($item) {
return $item;
};
});
// Check that the item is the container, if called without arguments
$footest1 = $this->container->getNew('footest');
$this->assertInstanceOf('Aviat\Ion\Di\ContainerInterface', $footest1());
$footest2 = $this->container->getNew('footest', ['Test String']);
$this->assertEquals('Test String', $footest2());
}
public function testGetSet() public function testGetSet()
{ {
$container = $this->container->set('foo', function() { $container = $this->container->set('foo', function() {
@ -61,6 +162,8 @@ class ContainerTest extends \Ion_TestCase {
$this->assertInstanceOf('Aviat\Ion\Di\Container', $container); $this->assertInstanceOf('Aviat\Ion\Di\Container', $container);
$this->assertInstanceOf('Aviat\Ion\Di\ContainerInterface', $container); $this->assertInstanceOf('Aviat\Ion\Di\ContainerInterface', $container);
// The factory returns a callable
$this->assertTrue(is_callable($container->get('foo'))); $this->assertTrue(is_callable($container->get('foo')));
} }

View File

@ -29,13 +29,13 @@ class Ion_TestCase extends PHPUnit_Framework_TestCase {
protected static $staticContainer; protected static $staticContainer;
protected static $session_handler; protected static $session_handler;
public static function setUpBeforeClass() /*public static function setUpBeforeClass()
{ {
// Use mock session handler // Use mock session handler
$session_handler = new TestSessionHandler(); $session_handler = new TestSessionHandler();
session_set_save_handler($session_handler, TRUE); session_set_save_handler($session_handler, TRUE);
self::$session_handler = $session_handler; self::$session_handler = $session_handler;
} }*/
public function setUp() public function setUp()
{ {
@ -86,7 +86,12 @@ class Ion_TestCase extends PHPUnit_Framework_TestCase {
// Set up DI container // Set up DI container
$di = require('di.php'); $di = require('di.php');
$container = $di($config_array); $container = $di($config_array);
$container->set('session-handler', self::$session_handler); $container->set('session-handler', function() {
// Use mock session handler
$session_handler = new TestSessionHandler();
session_set_save_handler($session_handler, TRUE);
return $session_handler;
});
$this->container = $container; $this->container = $container;
} }
@ -111,7 +116,7 @@ class Ion_TestCase extends PHPUnit_Framework_TestCase {
['Zend\Diactoros\ServerRequestFactory', 'fromGlobals'], ['Zend\Diactoros\ServerRequestFactory', 'fromGlobals'],
array_merge($default, $supers) array_merge($default, $supers)
); );
$this->container->set('request', $request); $this->container->setInstance('request', $request);
} }
} }
// End of Ion_TestCase.php // End of Ion_TestCase.php

View File

@ -12,20 +12,27 @@ use Aviat\Ion\Di\Container;
// Setup DI container // Setup DI container
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
return function(array $config_array = []) { return function(array $config_array = []) {
$container = new Container([ $container = new Container();
'config' => new Config($config_array),
]);
// Create Request/Response Objects $container->set('config', function() {
$request = ServerRequestFactory::fromGlobals( return new Config([]);
$_SERVER, });
$_GET,
$_POST, $container->setInstance('config', new Config($config_array));
$_COOKIE,
$_FILES $container->set('request', function() {
); return ServerRequestFactory::fromGlobals(
$container->set('request', $request); $_SERVER,
$container->set('response', new Response()); $_GET,
$_POST,
$_COOKIE,
$_FILES
);
});
$container->set('response', function() {
return new Response();
});
// Create session Object // Create session Object
$container->set('session', function() { $container->set('session', function() {