diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 0f1becdfdc28..ef504fc7123b 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -133,6 +133,13 @@ class Container implements ArrayAccess, ContainerContract */ protected $afterResolvingCallbacks = []; + /** + * All the abstract keys which are bound with class names. + * + * @var array + */ + protected $boundToClassName = []; + /** * Define a contextual binding. * @@ -222,6 +229,7 @@ public function isAlias($name) public function bind($abstract, $concrete = null, $shared = false) { $this->dropStaleInstances($abstract); + $this->decideAboutConcreteType($abstract, $concrete); // If no concrete type was given, we will simply set the concrete type to the // abstract type. After that, the concrete type to be registered as shared @@ -674,7 +682,11 @@ protected function resolve($abstract, $parameters = []) $this->instances[$abstract] = $object; } - $this->fireResolvingCallbacks($abstract, $object); + // We don't fire "resolving" callbacks for interface bound with class path syntax + // since the callback will fire later, when the bounded concrete is resolving. + if (! $this->isAnInterfaceBoundedWithClassName($abstract)) { + $this->fireResolvingCallbacks($abstract, $object); + } // Before returning, we will also set the resolved flag to "true" and pop off // the parameter overrides for this build. After those two things are done @@ -1269,4 +1281,31 @@ public function __set($key, $value) { $this[$key] = $value; } + + /** + * Determine if the abstract is both an interface and is bounded to a class path (not a closure). + * + * @param string $abstract + * @return bool + */ + protected function isAnInterfaceBoundedWithClassName($abstract) + { + return interface_exists($abstract) && array_key_exists($abstract, $this->boundToClassName); + } + + /** + * Detects what type of concrete is provided for the abstract + * + * @param string $abstract + * @param \Closure|string|null $concrete + * @return void + */ + protected function decideAboutConcreteType($abstract, $concrete) + { + if ($concrete instanceof Closure) { + unset($this->boundToClassName[$abstract]); + } else { + $this->boundToClassName[$abstract] = null; + } + } } diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index d54d71dc000b..2ee9c0700f12 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -1022,6 +1022,301 @@ public function testResolvingCallbacksShouldBeFiredWhenCalledWithAliases() $this->assertEquals('taylor', $instance->name); } + public function testResolvingCallbacksAreCalledOnceForImplementation() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + } + + public function testGlobalResolvingCallbacksAreCalledOnceForImplementation() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(IContainerContractStub::class); + $this->assertEquals(2, $callCounter); + } + + public function testAfterResolvingCallbacksAreCalledOnceForImplementation() + { + $container = new Container; + + $callCounter = 0; + $container->afterResolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(IContainerContractStub::class); + $this->assertEquals(2, $callCounter); + } + + public function testResolvingCallbacksAreCalledOnceForSingletonConcretes() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + $container->bind(ContainerImplementationStub::class); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + + $container->make(IContainerContractStub::class); + $this->assertEquals(3, $callCounter); + } + + public function testResolvingCallbacksCanStillBeAddedAfterTheFirstResolution() + { + $container = new Container; + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(ContainerImplementationStub::class); + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + } + + public function testResolvingCallbacksAreCanceledWhenInterfaceGetsBoundToSomeOtherConcrete() + { + $container = new Container; + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $callCounter = 0; + $container->resolving(ContainerImplementationStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + + $container->bind(IContainerContractStub::class, ContainerImplementationStubTwo::class); + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + } + + public function testResolvingCallbacksAreCalledOnceForStringAbstractions() + { + $container = new Container; + + $callCounter = 0; + $container->resolving('foo', function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind('foo', ContainerImplementationStub::class); + + $container->make('foo'); + $this->assertEquals(1, $callCounter); + + $container->make('foo'); + $this->assertEquals(2, $callCounter); + } + + public function testResolvingCallbacksAreCalledOnceForImplementation2() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, function () { + return new ContainerImplementationStub; + }); + + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(3, $callCounter); + + $container->make(IContainerContractStub::class); + $this->assertEquals(4, $callCounter); + } + + public function testRebindingDoesNotAffectResolvingCallbacks() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + $container->bind(IContainerContractStub::class, function () { + return new ContainerImplementationStub; + }); + + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(3, $callCounter); + + $container->make(IContainerContractStub::class); + $this->assertEquals(4, $callCounter); + } + + public function testRebindingDoesNotAffectMultipleResolvingCallbacks() + { + $container = new Container; + + $callCounter = 0; + + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->resolving(ContainerImplementationStubTwo::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + // it should call the callback for interface + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + + // it should call the callback for interface + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + + // should call the callback for the interface it implements + // plus the callback for ContainerImplementationStubTwo. + $container->make(ContainerImplementationStubTwo::class); + $this->assertEquals(4, $callCounter); + } + + public function testResolvingCallbacksAreCalledForInterfaces() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(IContainerContractStub::class); + + $this->assertEquals(1, $callCounter); + } + + public function testResolvingCallbacksAreCalledForConcretesWhenAttachedOnInterface() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(ContainerImplementationStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + } + + public function testResolvingCallbacksAreCalledForConcretesWhenAttachedOnConcretes() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(ContainerImplementationStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->bind(IContainerContractStub::class, ContainerImplementationStub::class); + + $container->make(IContainerContractStub::class); + $this->assertEquals(1, $callCounter); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + } + + public function testResolvingCallbacksAreCalledForConcretesWithNoBinding() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(ContainerImplementationStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + } + + public function testResolvingCallbacksAreCalledForInterFacesWithNoBinding() + { + $container = new Container; + + $callCounter = 0; + $container->resolving(IContainerContractStub::class, function ($some) use (&$callCounter) { + $callCounter++; + }); + + $container->make(ContainerImplementationStub::class); + $this->assertEquals(1, $callCounter); + $container->make(ContainerImplementationStub::class); + $this->assertEquals(2, $callCounter); + } + public function testMakeWithMethodIsAnAliasForMakeMethod() { $mock = $this->getMockBuilder(Container::class)