diff --git a/CHANGELOG.md b/CHANGELOG.md index e6de815f..32a8d294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ to [Semantic Versioning](http://semver.org/). ## [unreleased] Unreleased +### Added +- The container now registers Service Providers using its own `Container::get()` method, instead of the `new` keyword. This allows Service Providers to utilize dependency injection. (thanks @defunctl). +- Additional contextual binding examples for primitives + service provider documentation in the README. + ## [3.0.2] 2023-01-20; ### Added diff --git a/README.md b/README.md index bad8c96b..70140139 100644 --- a/README.md +++ b/README.md @@ -554,6 +554,14 @@ $container->bind(DbCache::class, function($container){ $container->when(TransactionManager::class) ->needs(CacheInterface::class) ->give(DbCache::class); + +/* + * We can also bind primitives where the container doesn't know how to auto-wire + * them. + */ +$container->when(PaginationManager::class) + ->needs('$per_page') + ->give(25); ``` ## Binding decorator chains @@ -771,3 +779,87 @@ $legacyOne = $container->get(LegacyClassOne::class); // Will not be called again here, done already. $legacyTwo = $container->get(LegacyInterfaceTwo::class); ``` + +### Dependency injection with service providers + +The container now supports additional dependency injection for service providers. Auto-wiring +will work the same as any class, simply override the service provider's constructor and add any additional concrete dependencies (don't forget to call the parent!): + +```php +// file ProviderOne.php + +use lucatume\DI52\ServiceProvider; + +class ProviderOne extends ServiceProvider { + + /** + * @var ConfigHelper + */ + protected $config; + + public function __construct(\lucatume\DI52\Container $container, ConfigHelper $config) + { + parent::__construct($container); + + $this->config = $config; + } + + public function register() + { + $this->container->when(ClassFour::class) + ->needs('$value') + ->give($this->config->get('value')); + } + +} + +// Application bootstrap file. +use lucatume\DI52\Container; + +$container = new Container(); + +$container->register(ProviderOne::class); +``` +If you want to inject primitives into a service provider, you need to utilize the `when`, `needs`, `gives` methods **_before_** registering the provider in the container: + +```php +// file ProviderOne.php + +use lucatume\DI52\ServiceProvider; + +class ProviderOne extends ServiceProvider { + + /** + * @var bool + */ + protected $service_enabled; + + public function __construct(\lucatume\DI52\Container $container, $service_enabled) + { + parent::__construct($container); + + $this->service_enabled = $service_enabled; + } + + public function register() + { + if (!$this->service_enabled) { + return; + } + + $this->container->bind(InterfaceOne::class, ClassOne::class); + } + +} + +// Application bootstrap file. +use lucatume\DI52\Container; + +$container = new Container(); + +$container->when(ProviderOne::class) + ->needs('$service_enabled') + ->give(true); + +$container->register(ProviderOne::class); +``` diff --git a/src/Container.php b/src/Container.php index 0c1e7d5c..22ccbe0c 100644 --- a/src/Container.php +++ b/src/Container.php @@ -88,6 +88,7 @@ public function __construct($resolveUnboundAsSingletons = false) { $this->resolver = new Builders\Resolver($resolveUnboundAsSingletons); $this->builders = new Builders\Factory($this, $this->resolver); + $this->singleton(Container::class, $this); } /** @@ -450,7 +451,7 @@ protected function checkClassIsInstantiatable($class) public function register($serviceProviderClass, ...$alias) { /** @var ServiceProvider $provider */ - $provider = new $serviceProviderClass($this); + $provider = $this->get($serviceProviderClass); if (!$provider->isDeferred()) { $provider->register(); } else { diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 9030693e..c0becc80 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -129,4 +129,25 @@ public function should_throw_when_getting_provider_from_non_provider_binding() $this->assertInstanceOf(NotFoundException::class, $e); } } + + /** + * The container now registers itself inside itself (containerception). + * + * @test + */ + public function it_should_contain_the_containers_own_instance() + { + $container = new Container(); + + $this->assertTrue($container->has(Container::class)); + $this->assertSame($container, $container->get(Container::class)); + + $container->get(ClassOne::class); + $container->get(ClassOneOne::class); + + $this->assertTrue($container->has(ClassOne::class)); + $this->assertTrue($container->has(ClassOneOne::class)); + $this->assertSame($container, $container->get(Container::class)); + } + } diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php index 8f4d4f84..ff46d7b7 100644 --- a/tests/ServiceProviderTest.php +++ b/tests/ServiceProviderTest.php @@ -104,6 +104,71 @@ public function register() } } +class TestDependency +{ + + public function getValue() + { + return 'test'; + } +} + +class TestProviderInjection extends ServiceProvider +{ + + /** + * @var TestDependency + */ + protected $dependency; + + public function __construct(Container $container, TestDependency $dependency) + { + parent::__construct($container); + + $this->dependency = $dependency; + } + + public function register() + { + // TODO: Implement register() method. + } + + public function getContainer() { + return $this->container; + } + + public function getDependency() + { + return $this->dependency; + } +} + +class TestProviderPrimitiveInjection extends ServiceProvider +{ + + /** + * @var string + */ + protected $value; + + public function __construct(Container $container, $value) + { + parent::__construct($container); + + $this->value = $value; + } + + public function register() + { + // TODO: Implement register() method. + } + + public function getValue() + { + return $this->value; + } +} + class ServiceProviderTest extends TestCase { /** @@ -293,4 +358,43 @@ public function should_allow_registering_a_service_provider_with_multiple_aliase $container->getProvider('other-service-provider') ); } + + /** + * Test we can create service providers with additional concrete dependencies. + * + * @test + */ + public function should_automatically_inject_concrete_instances_into_extended_providers() + { + $container = new Container(); + + $container->register(TestProviderInjection::class); + $this->assertTrue($container->has(TestProviderInjection::class)); + $this->assertTrue($container->has(TestDependency::class)); + + $provider = $container->get(TestProviderInjection::class); + + $this->assertSame($container, $provider->getContainer()); + $this->assertInstanceOf(TestDependency::class, $provider->getDependency()); + $this->assertSame('test', $provider->getDependency()->getValue()); + } + + /** + * Test we can configure the container with providers that have primitives + * in their constructor. + * + * @test + */ + public function should_allow_binding_providers_with_primitive_values() + { + $container = new Container(); + + $container->when(TestProviderPrimitiveInjection::class) + ->needs('$value') + ->give('test'); + + $container->register(TestProviderPrimitiveInjection::class); + $this->assertTrue($container->has(TestProviderPrimitiveInjection::class)); + $this->assertSame('test', $container->get(TestProviderPrimitiveInjection::class)->getValue()); + } }