Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ class Container implements ArrayAccess, ContainerContract
*/
protected $abstractAliases = [];

/**
* The global extension closures for services.
*
* @var array[]
*/
protected $globalExtenders = [];

/**
* The extension closures for services.
*
Expand Down Expand Up @@ -378,14 +385,20 @@ public function singletonIf($abstract, $concrete = null)
/**
* "Extend" an abstract type in the container.
*
* @param string $abstract
* @param \Closure $closure
* @param string|\Closure $abstract
* @param \Closure|null $closure
* @return void
*
* @throws \InvalidArgumentException
*/
public function extend($abstract, Closure $closure)
public function extend($abstract, Closure $closure = null)
{
if ($abstract instanceof Closure) {
$this->globalExtenders[] = $abstract;

return;
}

$abstract = $this->getAlias($abstract);

if (isset($this->instances[$abstract])) {
Expand Down Expand Up @@ -1173,7 +1186,20 @@ public function getAlias($abstract)
*/
protected function getExtenders($abstract)
{
return $this->extenders[$this->getAlias($abstract)] ?? [];
return array_merge(
$this->globalExtenders,
$this->extenders[$this->getAlias($abstract)] ?? []
);
}

/**
* Remove all of the global extender callbacks.
*
* @return void
*/
public function forgetGlobalExtenders()
{
$this->globalExtenders = [];
}

/**
Expand Down
203 changes: 203 additions & 0 deletions tests/Container/ContainerExtendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,209 @@ public function testUnsetExtend()

$this->assertSame('foo', $container->make('foo'));
}

public function testGloballyExtendedBindings()
{
// Given a simple "foo" binding.
$container = new Container;
$container['foo'] = 'foo';

// When we append "bar" to all bindings.
$container->extend(function ($old, $container) {
return $old.'bar';
});

// Then we resolve "foobar".
$this->assertSame('foobar', $container->make('foo'));
}

public function testGloballyExtendedSingletons()
{
// Given an registered "foo" singleton.
$container = new Container;
$container->singleton('foo', function () {
return (object) ['name' => 'taylor'];
});

// When we add the age property to all bindings.
$container->extend(function ($old, $container) {
$old->age = 26;

return $old;
});

// Then the "foo" singleton has the "age" property.
$result = $container->make('foo');
$this->assertSame('taylor', $result->name);
$this->assertEquals(26, $result->age);

// And it stays the same instance no matter how many times we resolve it.
$this->assertSame($result, $container->make('foo'));
}

public function testResolvedInstancesAreNotAffectedByNewGlobalExtenders()
{
// Given an already resolved "foo" instance.
$container = new Container;
$container->instance('foo', (object) ['foo' => 'original']);

// When we add a new global extender.
$container->extend(function ($obj, $container) {
$obj->foo = 'extended';

return $obj;
});

// Then the "foo" instance was not extended.
$this->assertSame('original', $container->make('foo')->foo);
}

public function testGlobalExtendersAreLazyInitialized()
{
// Given an uninitialized class.
ContainerLazyExtendStub::$initialized = false;

// And a simple binding of that class.
$container = new Container;
$container->bind(ContainerLazyExtendStub::class);

// When we add a global extender that initializes that class.
$container->extend(function ($obj, $container) {
$obj->init();

return $obj;
});

// Then the class is still not initialized.
$this->assertFalse(ContainerLazyExtendStub::$initialized);

// but will be initialized when resolved from the container.
$container->make(ContainerLazyExtendStub::class);
$this->assertTrue(ContainerLazyExtendStub::$initialized);
}

public function testGlobalExtendersCanBeCalledBeforeBind()
{
// Given a registered global extenders that appends "bar" to all bindings.
$container = new Container;
$container->extend(function ($old, $container) {
return $old.'bar';
});

// When we, later on, bind "foo" to the container.
$container['foo'] = 'foo';

// Then it gets resolved to "foobar".
$this->assertSame('foobar', $container->make('foo'));
}

public function testGlobalExtendersDoNotTriggerInstanceRebindingCallbacks()
{
// Given an indicator that the instance has no rebound.
$testRebound = false;

// And a rebinding callback that toggles that indicator to true.
$container = new Container;
$container->rebinding('foo', function () use (&$testRebound) {
$testRebound = true;
});

// And a "foo" instance already bound.
$obj = new stdClass;
$container->instance('foo', $obj);

// When we register a new global extender.
$container->extend(function ($obj, $container) {
return $obj;
});

// Then the rebinding callback was not called.
$this->assertFalse($testRebound);
}

public function testGlobalExtendersDoNotTriggerBindRebindingCallback()
{
// Given an indicator that the instance has no rebound.
$testRebound = false;

// And a rebinding callback that toggles that indicator to true.
$container = new Container;
$container->rebinding('foo', function () use (&$testRebound) {
$testRebound = true;
});

// And an existing "foo" binding that has been resolved once.
$container->bind('foo', function () {
return new stdClass;
});
$container->make('foo');
$this->assertFalse($testRebound);

// When we register a new global extender.
$container->extend(function ($obj, $container) {
return $obj;
});

// Then the rebinding callback was not called.
$this->assertFalse($testRebound);
}

public function testGlobalExtensionWorksOnAliasedBindings()
{
// Given a singleton with an alias.
$container = new Container;
$container->singleton('something', function () {
return 'some value';
});
$container->alias('something', 'something-alias');

// When we register a global extender.
$container->extend(function ($value) {
return $value.' extended';
});

// Then both the singleton and its alias appear to be extended.
$this->assertSame('some value extended', $container->make('something'));
$this->assertSame('some value extended', $container->make('something-alias'));
}

public function testMultipleGlobalExtenders()
{
// Given a simple binding.
$container = new Container;
$container['foo'] = 'foo';

// When we register two global extenders.
$container->extend(function ($old, $container) {
return $old.'bar';
});
$container->extend(function ($old, $container) {
return $old.'baz';
});

// Then the binding has been extended by both global extenders.
$this->assertSame('foobarbaz', $container->make('foo'));
}

public function testForgetGlobalExtenders()
{
// Given a simple binding.
$container = new Container;
$container->bind('foo', function () {
return 'foo';
});

// And a global extender that appends "bar" to all bindings.
$container->extend(function ($obj, $container) {
return $obj.'bar';
});

// When we forget all global extenders.
$container->forgetGlobalExtenders();

// Then the global extender is not applied when we next resolve that binding.
$this->assertSame('foo', $container->make('foo'));
}
}

class ContainerLazyExtendStub
Expand Down