diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 98d8f0ae7900..8e3b5449958b 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -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. * @@ -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])) { @@ -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 = []; } /** diff --git a/tests/Container/ContainerExtendTest.php b/tests/Container/ContainerExtendTest.php index a34c5c6ef9d6..f2c5353cb022 100644 --- a/tests/Container/ContainerExtendTest.php +++ b/tests/Container/ContainerExtendTest.php @@ -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