Created
November 22, 2014 02:53
-
-
Save jm42/81b170cef1bb56c390ef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| interface ContainerInterface { | |
| function get($key, array $arguments=[]); | |
| function has($key); | |
| } | |
| /** | |
| * Dependency Injection Container. | |
| */ | |
| class Container implements ContainerInterface { | |
| private $delegate; | |
| private $keys = []; | |
| private $values = []; | |
| private $aliases = []; | |
| private $factories = []; | |
| private $loading = []; | |
| public function __construct(ContainerInterface $delegate=null) { | |
| if ($delegate === null) { | |
| $delegate = $this; | |
| } | |
| $this->delegate = $delegate; | |
| } | |
| /** | |
| * Assign a value to the entry or setup a factory. | |
| * | |
| * Any type of value could be assigned directly and will be returned as is | |
| * without modification. | |
| * | |
| * ```php | |
| * $di->set('debug', true); | |
| * $di->set('users', ['root']); | |
| * $di->set('db', new PDO('sqlite::memory:')); | |
| * ``` | |
| * | |
| * When a factory is given, the second argument expect a list of parameters | |
| * that the factory will accept. | |
| * | |
| * ```php | |
| * $di->set('PDO', ['sqlite::memory:'], function($dsn) { | |
| * return new PDO($dsn); | |
| * }); | |
| * ``` | |
| * | |
| * Parameters will be lookup into the delegate container. | |
| * | |
| * ```php | |
| * $delegate = new Container; | |
| * $delegate->set('PDO.dsn', 'sqlite::memory:'); | |
| * | |
| * $container = new Container($delegate); | |
| * $container->set('PDO', ['PDO.dsn'], function($dsn) { | |
| * return new PDO($dsn); | |
| * }); | |
| * ``` | |
| * | |
| * @param string $key Identifier of the entry. | |
| * @param mixed $value Entry value or parameters to the factory. | |
| * @param Closure $factory Function to construct the entry. | |
| */ | |
| public function set($key, $value, \Closure $factory=null) { | |
| if ($factory === null) { | |
| $this->values[$key] = $value; | |
| return; | |
| } | |
| $this->keys[$key] = $hash = spl_object_hash($factory); | |
| $this->factories[$hash] = [$factory, (array) $value]; | |
| } | |
| /** | |
| * Return the entry for the given key. | |
| * | |
| * If the entry need to be constructed, given arguments will be used to | |
| * call the factory. | |
| * | |
| * @param string $key Identifier of the entry. | |
| * @param array $arguments Optional arguments to construct the entry. | |
| * | |
| * @throws OutOfBoundsException No entry was found. | |
| * @throws LogicException Error when retrieving the entry. | |
| * | |
| * @return mixed | |
| */ | |
| public function get($key, array $arguments=[]) { | |
| if (isset($this->aliases[$key])) { | |
| $key = $this->aliases[$key]; | |
| } | |
| if (isset($this->values[$key])) { | |
| return $this->values[$key]; | |
| } | |
| if (isset($this->keys[$key])) { | |
| if (isset($this->loading[$key])) { | |
| throw new \LogicException("Cyclic dependency for $key"); | |
| } | |
| $hash = $this->loading[$key] = $this->keys[$key]; | |
| list($factory, $parameters) = $this->factories[$hash]; | |
| foreach ($parameters as $index => $parameter) { | |
| if (isset($arguments[$index])) { | |
| continue; | |
| } | |
| if ($this->delegate->has($parameter)) { | |
| $arguments[$index] = $this->delegate->get($parameter); | |
| } else { | |
| $arguments[$index] = $parameter; | |
| } | |
| } | |
| ksort($arguments); | |
| $value = call_user_func_array($factory, $arguments); | |
| if (array_key_exists($key, $this->values)) { | |
| $this->values[$key] = $value; | |
| } | |
| unset($this->loading[$key]); | |
| return $value; | |
| } | |
| throw new \OutOfBoundsException("Entry for $key not found"); | |
| } | |
| /** | |
| * True if the container has the given key. False otherwise. | |
| * | |
| * @param string $key Identifier of the entry. | |
| * | |
| * @return boolean | |
| */ | |
| public function has($key) { | |
| return isset($this->values[$key]) || isset($this->keys[$key]); | |
| } | |
| /** | |
| * Mark the given key as a shared object. | |
| * | |
| * When marked as shared, the first time the object is created it will be | |
| * stored and returned for every other call. | |
| * | |
| * ```php | |
| * $di->share('PDO'); | |
| * $di->set('pdo.dsn', 'sqlite::memory:'); | |
| * $di->set('PDO', ['pdo.dsn'], function($dsn) { | |
| * return new PDO($dsn); | |
| * }); | |
| * $pdo = $di->get('PDO'); | |
| * $pdo === $di->get('PDO'); // assert true | |
| * ``` | |
| * | |
| * The first argument it cannot be an instance. Instead the object should | |
| * be setted as a value directly to the container. | |
| * | |
| * ```php | |
| * $di->set('PDO', new PDO('sqlite::memory:'); | |
| * $di->share('PDO'); | |
| * ``` | |
| * | |
| * @param string $key Identifier of the entry. | |
| */ | |
| public function share($key) { | |
| if (isset($this->values[$key])) { | |
| return; | |
| } | |
| $this->values[$key] = null; | |
| } | |
| /** | |
| * Treat the given alias as if it were the key. | |
| * | |
| * @param string $key Identifier of the entry. | |
| * @param string $alias Alias of the key. | |
| */ | |
| public function alias($key, $alias) { | |
| $this->aliases[$alias] = $key; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| class ContainerTest extends PHPUnit_Framework_TestCase { | |
| private $container; | |
| public function setUp() { | |
| $this->container = new Container; | |
| } | |
| public function testConstructor() { | |
| new Container( | |
| $this->getMock(ContainerInterface::class) | |
| ); | |
| } | |
| /** | |
| * @dataProvider getInvalidContainers | |
| * @expectedException PHPUnit_Framework_Error | |
| */ | |
| public function testConstructorWithInvalid($invalid) { | |
| new Container($invalid); | |
| } | |
| /** | |
| * @dataProvider getInvalidOffset | |
| * @expectedException PHPUnit_Framework_Error | |
| */ | |
| public function testSetWithInvalidKey($key) { | |
| $this->container->set($key, ''); | |
| } | |
| /** | |
| * @dataProvider getInvalidOffset | |
| * @expectedException PHPUnit_Framework_Error | |
| */ | |
| public function testGetWithInvalidKey($key) { | |
| $this->container->get($key); | |
| } | |
| /** | |
| * @expectedException OutOfBoundsException | |
| */ | |
| public function testGetWithInexistent() { | |
| $this->container->get('OutOfBounds'); | |
| } | |
| /** | |
| * @dataProvider getValues | |
| */ | |
| public function testValues($key, $value) { | |
| $null = $this->container->set($key, $value); | |
| $data = $this->container->get($key); | |
| $this->assertNull($null); | |
| $this->assertSame($value, $data); | |
| } | |
| public function testFactory() { | |
| $this->container->set('foo', [], | |
| function() { | |
| return 'bar'; | |
| }); | |
| $this->assertEquals('bar', | |
| $this->container->get('foo') | |
| ); | |
| } | |
| public function testFactoryWithParameters() { | |
| $this->container->set('foo', [], | |
| function($bar) { | |
| $this->assertEquals(2, func_num_args()); | |
| $this->assertEquals('baz', func_get_arg(1)); | |
| $this->assertEquals('bar', $bar); | |
| }); | |
| $this->container->get('foo', ['bar', 'baz']); | |
| } | |
| public function testFactoryWithDefaultParameter() { | |
| $this->container->set('foo', ['---', 'baz'], | |
| function($bar, $baz) { | |
| $this->assertEquals('bar', $bar); | |
| $this->assertEquals('baz', $baz); | |
| }); | |
| $this->container->get('foo', ['bar']); | |
| } | |
| public function testFactoryWithDelegateParameter() { | |
| $this->container->set('bar', 'baz'); | |
| $this->container->set('foo', ['bar'], | |
| function($bar) { | |
| $this->assertEquals('baz', $bar); | |
| }); | |
| $this->container->get('foo'); | |
| } | |
| /** | |
| * @expectedException PHPUnit_Framework_Error | |
| */ | |
| public function testFactoryWithMissingParameter() { | |
| $this->container->set('foo', [], | |
| function($bar) { | |
| // Should never be called | |
| }); | |
| $this->container->get('foo'); | |
| } | |
| /** | |
| * @expectedException LogicException | |
| */ | |
| public function testCyclicDependency() { | |
| $this->container->set('foo', ['bar'], | |
| function($bar) { | |
| return "foo{$bar}"; | |
| }); | |
| $this->container->set('bar', ['foo'], | |
| function($foo) { | |
| return "{$foo}bar"; | |
| }); | |
| $this->container->get('foo'); | |
| } | |
| public function testHas() { | |
| $this->container->set('env', 'test'); | |
| $this->container->set('foo', [], function() {}); | |
| $this->assertTrue($this->container->has('env')); | |
| $this->assertTrue($this->container->has('foo')); | |
| $this->assertFalse($this->container->has('debug')); | |
| } | |
| public function testShare() { | |
| $times = 0; | |
| $this->container->share('foo'); | |
| $this->container->set('foo', [], | |
| function() use (&$times) { | |
| return $times++; | |
| }); | |
| $this->container->get('foo'); | |
| $this->container->get('foo'); | |
| $this->container->share('foo'); | |
| $this->container->get('foo'); | |
| $this->container->get('foo'); | |
| $this->assertEquals(1, $times); | |
| } | |
| public function testAlias() { | |
| $this->container->alias('bar', 'foo'); | |
| $this->container->set('bar', 'baz'); | |
| $data = $this->container->get('foo'); | |
| $this->assertEquals('baz', $data); | |
| } | |
| public function getInvalidContainers() { | |
| return array( | |
| [123], | |
| [true], | |
| [array()], | |
| ['foobar'], | |
| [new \stdClass], | |
| [function() {}], | |
| ); | |
| } | |
| public function getInvalidOffset() { | |
| return array( | |
| [new \stdClass], | |
| [function() {}], | |
| ); | |
| } | |
| public function getValues() { | |
| return array( | |
| ['debug', true], | |
| ['env', 'test'], | |
| ['autoload', function() {}], | |
| ['baseclass', new stdClass], | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment