Skip to content

Instantly share code, notes, and snippets.

@jm42
Created November 22, 2014 02:53
Show Gist options
  • Save jm42/81b170cef1bb56c390ef to your computer and use it in GitHub Desktop.
Save jm42/81b170cef1bb56c390ef to your computer and use it in GitHub Desktop.
<?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;
}
}
<?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