From 3db58c7066a78db32a8232963b2382512ed5ad94 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 29 Aug 2016 12:51:40 -0400 Subject: [PATCH] Refactor container to be more flexible --- src/Ion/Di/Container.php | 112 ++++++++++++++++++++++-------- src/Ion/Di/ContainerInterface.php | 40 ++++++++--- tests/Ion/Di/ContainerTest.php | 103 +++++++++++++++++++++++++++ tests/Ion_TestCase.php | 13 ++-- tests/di.php | 33 +++++---- 5 files changed, 245 insertions(+), 56 deletions(-) diff --git a/src/Ion/Di/Container.php b/src/Ion/Di/Container.php index f324c69..221dded 100644 --- a/src/Ion/Di/Container.php +++ b/src/Ion/Di/Container.php @@ -28,11 +28,18 @@ use Aviat\Ion\Di\Exception\NotFoundException; class Container implements ContainerInterface { /** - * Array with class instances + * Array of container Generator functions + * + * @var Callable[] + */ + protected $container = []; + + /** + * Array of object instances * * @var array */ - protected $container = []; + protected $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. * - * @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 ContainerException Error while retrieving the entry. + * @throws NotFoundException - No entry was found for this identifier. + * @throws ContainerException - Error while retrieving the entry. * * @return mixed Entry. */ @@ -71,40 +78,88 @@ class Container implements ContainerInterface { 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)) - { - return $this->applyContainer($item($this)); - } - else - { - return $item; - } + // If there isn't already an instance, create one + $obj = $this->getNew($id); + $this->instances[$id] = $obj; + return $obj; } 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 mixed $value + * @param Callable $value - a factory callable for the item * @return ContainerInterface */ - public function set($id, $value) + public function set($id, Callable $value) { $this->container[$id] = $value; 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 false otherwise. * * @param string $id Identifier of the entry to look for. - * * @return boolean */ public function has($id) @@ -114,37 +169,38 @@ class Container implements ContainerInterface { /** * Determine whether a logger channel is registered - * @param string $key The logger channel + * + * @param string $id The logger channel * @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 * * @param LoggerInterface $logger - * @param string $key The logger 'channel' + * @param string $id The logger 'channel' * @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; } /** * Retrieve a logger for the selected channel * - * @param string $key The logger to retrieve + * @param string $id The logger to retrieve * @return LoggerInterface|null */ - public function getLogger($key = 'default') + public function getLogger($id = 'default') { - return ($this->hasLogger($key)) - ? $this->loggers[$key] + return ($this->hasLogger($id)) + ? $this->loggers[$id] : NULL; } diff --git a/src/Ion/Di/ContainerInterface.php b/src/Ion/Di/ContainerInterface.php index 694d31e..a1df05a 100644 --- a/src/Ion/Di/ContainerInterface.php +++ b/src/Ion/Di/ContainerInterface.php @@ -16,43 +16,61 @@ namespace Aviat\Ion\Di; +use Interop\Container\ContainerInterface as InteropInterface; use Psr\Log\LoggerInterface; /** * 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 mixed $value + * @param string $id + * @param Callable $value - a factory callable for the item * @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 - * @param string $key The logger channel + * @param string $id The logger channel * @return boolean */ - public function hasLogger($key = 'default'); + public function hasLogger($id = 'default'); /** * Add a logger to the Container * * @param LoggerInterface $logger - * @param string $key The logger 'channel' + * @param string $id The logger 'channel' * @return Container */ - public function setLogger(LoggerInterface $logger, $key = 'default'); + public function setLogger(LoggerInterface $logger, $id = 'default'); /** * Retrieve a logger for the selected channel * - * @param string $key The logger to retreive + * @param string $id The logger to retreive * @return LoggerInterface|null */ - public function getLogger($key = 'default'); + public function getLogger($id = 'default'); } \ No newline at end of file diff --git a/tests/Ion/Di/ContainerTest.php b/tests/Ion/Di/ContainerTest.php index 402fc9d..c826538 100644 --- a/tests/Ion/Di/ContainerTest.php +++ b/tests/Ion/Di/ContainerTest.php @@ -8,9 +8,22 @@ use Monolog\Logger; use Monolog\Handler\TestHandler; 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 { + public function setUp() { $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() { $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\ContainerInterface', $container); + + // The factory returns a callable $this->assertTrue(is_callable($container->get('foo'))); } diff --git a/tests/Ion_TestCase.php b/tests/Ion_TestCase.php index 9b407e4..875dcff 100644 --- a/tests/Ion_TestCase.php +++ b/tests/Ion_TestCase.php @@ -29,13 +29,13 @@ class Ion_TestCase extends PHPUnit_Framework_TestCase { protected static $staticContainer; protected static $session_handler; - public static function setUpBeforeClass() + /*public static function setUpBeforeClass() { // Use mock session handler $session_handler = new TestSessionHandler(); session_set_save_handler($session_handler, TRUE); self::$session_handler = $session_handler; - } + }*/ public function setUp() { @@ -86,7 +86,12 @@ class Ion_TestCase extends PHPUnit_Framework_TestCase { // Set up DI container $di = require('di.php'); $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; } @@ -111,7 +116,7 @@ class Ion_TestCase extends PHPUnit_Framework_TestCase { ['Zend\Diactoros\ServerRequestFactory', 'fromGlobals'], array_merge($default, $supers) ); - $this->container->set('request', $request); + $this->container->setInstance('request', $request); } } // End of Ion_TestCase.php \ No newline at end of file diff --git a/tests/di.php b/tests/di.php index d113371..d464735 100644 --- a/tests/di.php +++ b/tests/di.php @@ -12,20 +12,27 @@ use Aviat\Ion\Di\Container; // Setup DI container // ----------------------------------------------------------------------------- return function(array $config_array = []) { - $container = new Container([ - 'config' => new Config($config_array), - ]); + $container = new Container(); - // Create Request/Response Objects - $request = ServerRequestFactory::fromGlobals( - $_SERVER, - $_GET, - $_POST, - $_COOKIE, - $_FILES - ); - $container->set('request', $request); - $container->set('response', new Response()); + $container->set('config', function() { + return new Config([]); + }); + + $container->setInstance('config', new Config($config_array)); + + $container->set('request', function() { + return ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES + ); + }); + + $container->set('response', function() { + return new Response(); + }); // Create session Object $container->set('session', function() {