From 5a9fa793dff9c3bfc0a8deacce5a30a8cd8c4794 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 16 Jul 2024 22:15:29 +0200 Subject: [PATCH 1/6] Add Injector::implementations() to select implementations for a given type --- src/main/php/inject/Implementations.class.php | 46 +++++++++++++++++++ src/main/php/inject/Injector.class.php | 16 +++++++ .../unittest/ImplementationsTest.class.php | 46 +++++++++++++++++++ .../unittest/fixture/Creation.class.php | 3 ++ .../php/inject/unittest/fixture/URI.class.php | 5 +- 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100755 src/main/php/inject/Implementations.class.php create mode 100755 src/test/php/inject/unittest/ImplementationsTest.class.php diff --git a/src/main/php/inject/Implementations.class.php b/src/main/php/inject/Implementations.class.php new file mode 100755 index 0000000..26426e2 --- /dev/null +++ b/src/main/php/inject/Implementations.class.php @@ -0,0 +1,46 @@ +inject= $inject; + $this->bindings= $bindings; + } + + /** + * Returns the default implementation + * + * @return T + */ + #[Generic(return: 'T')] + public function default() { + return current($this->bindings)->resolve($this->inject); + } + + /** + * Returns the implementation for a given name + * + * @param string $name + * @return T + * @throws inject.ProvisionException if there is no such implementation + */ + #[Generic(return: 'T')] + public function named($name) { + if ($binding= $this->bindings[$name] ?? null) { + return $binding->resolve($this->inject); + } + + throw new ProvisionException('No implementation named "'.$name.'"'); + } +} \ No newline at end of file diff --git a/src/main/php/inject/Injector.class.php b/src/main/php/inject/Injector.class.php index 7959505..36b4ac9 100755 --- a/src/main/php/inject/Injector.class.php +++ b/src/main/php/inject/Injector.class.php @@ -89,6 +89,22 @@ public function bind($type, $impl, $name= null) { return $this; } + /** + * Returns implementations for a given type + * + * @param string|lang.Type $type + * @return inject.Implementations + * @throws inject.ProvisionException + */ + public function implementations($type) { + $t= $type instanceof Type ? $type : Type::forName($type); + if ($bindings= $this->bindings[$t->literal()] ?? null) { + return new Implementations($this, $bindings); + } + + throw new ProvisionException('No implementations for type '.$t); + } + /** * Returns the lookup if it provides a value, null otherwise * diff --git a/src/test/php/inject/unittest/ImplementationsTest.class.php b/src/test/php/inject/unittest/ImplementationsTest.class.php new file mode 100755 index 0000000..99dc81f --- /dev/null +++ b/src/test/php/inject/unittest/ImplementationsTest.class.php @@ -0,0 +1,46 @@ +uris= [ + 'dev' => new URI('http://localhost'), + 'prod' => new URI('https://example.com'), + ]; + } + + /** @return inject.Injector */ + private function fixture() { + $fixture= new Injector(); + foreach ($this->uris as $name => $uri) { + $fixture->bind(URI::class, $uri, $name); + } + return $fixture; + } + + #[Test, Values(['dev', 'prod'])] + public function implementations_named($name) { + Assert::equals($this->uris[$name], $this->fixture()->implementations(URI::class)->named($name)); + } + + #[Test] + public function default_implementation() { + Assert::equals($this->uris['dev'], $this->fixture()->implementations(URI::class)->default()); + } + + #[Test, Expect(ProvisionException::class)] + public function no_implementations() { + $this->fixture()->implementations(Endpoint::class); + } + + #[Test, Expect(ProvisionException::class)] + public function unknown_implementation() { + $this->fixture()->implementations(URI::class)->named('stage'); + } +} \ No newline at end of file diff --git a/src/test/php/inject/unittest/fixture/Creation.class.php b/src/test/php/inject/unittest/fixture/Creation.class.php index c7771e7..2ae11e9 100755 --- a/src/test/php/inject/unittest/fixture/Creation.class.php +++ b/src/test/php/inject/unittest/fixture/Creation.class.php @@ -4,4 +4,7 @@ class Creation { /** Create fluent interface for URIs */ public function __construct(URI $uri) { /* ... */ } + + /** @return string */ + public function create() { /* ... */ } } \ No newline at end of file diff --git a/src/test/php/inject/unittest/fixture/URI.class.php b/src/test/php/inject/unittest/fixture/URI.class.php index fd782a0..9d3f6e5 100755 --- a/src/test/php/inject/unittest/fixture/URI.class.php +++ b/src/test/php/inject/unittest/fixture/URI.class.php @@ -1,7 +1,10 @@ backing= $arg instanceof Creation ? $arg->create() : (string)$arg; + } } \ No newline at end of file From 711538b75784c5fa91f67df4e3d59b735e0a0d2b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 16 Jul 2024 22:20:37 +0200 Subject: [PATCH 2/6] Type-hint named() --- src/main/php/inject/Implementations.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/inject/Implementations.class.php b/src/main/php/inject/Implementations.class.php index 26426e2..ab5cdfa 100755 --- a/src/main/php/inject/Implementations.class.php +++ b/src/main/php/inject/Implementations.class.php @@ -36,7 +36,7 @@ public function default() { * @throws inject.ProvisionException if there is no such implementation */ #[Generic(return: 'T')] - public function named($name) { + public function named(string $name) { if ($binding= $this->bindings[$name] ?? null) { return $binding->resolve($this->inject); } From 11b37c6f908f33ea594a483e3c7e2328230df02c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 19 Jul 2024 17:47:33 +0200 Subject: [PATCH 3/6] Use SVG badge, "implementations" --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 80aff3f..9d61072 100755 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ Inject [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) [![Requires PHP 7.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_4plus.svg)](http://php.net/) [![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/) -[![Latest Stable Version](https://poser.pugx.org/xp-forge/inject/version.png)](https://packagist.org/packages/xp-forge/inject) +[![Latest Stable Version](https://poser.pugx.org/xp-forge/inject/version.svg)](https://packagist.org/packages/xp-forge/inject) The inject package contains the XP framework's dependency injection API. Its entry point class is the "Injector". Binding ------- -Values can be bound to the injector by using its `bind()` method. It accepts the type to bind to, an optional name and these different scenarios: +Implementations can be bound to the injector by using its `bind()` method. It accepts the type to bind to, an optional name and these different scenarios: * **Binding a class**: The typical usecase, where we bind an interface to its concrete implementation. * **Binding an instance**: By binding a type to an existing instance, we can create a *singleton* model. From fb09473c7087eee1af8ab66bdb5477a1f3c6b95f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 20 Jul 2024 17:54:50 +0200 Subject: [PATCH 4/6] Make Implementations injectable --- src/main/php/inject/Implementations.class.php | 16 +++++++++++----- src/main/php/inject/Injector.class.php | 9 ++++++--- .../unittest/ImplementationsTest.class.php | 8 +++++++- .../inject/unittest/fixture/Service.class.php | 10 ++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) create mode 100755 src/test/php/inject/unittest/fixture/Service.class.php diff --git a/src/main/php/inject/Implementations.class.php b/src/main/php/inject/Implementations.class.php index ab5cdfa..fd34c76 100755 --- a/src/main/php/inject/Implementations.class.php +++ b/src/main/php/inject/Implementations.class.php @@ -18,16 +18,22 @@ public function __construct(Injector $inject, array $bindings) { $this->bindings= $bindings; } - /** - * Returns the default implementation - * - * @return T - */ + /** Returns the default implementation */ #[Generic(return: 'T')] public function default() { return current($this->bindings)->resolve($this->inject); } + /** Returns all implementations */ + #[Generic(return: '[:T]')] + public function all() { + $r= []; + foreach ($this->bindings as $name => $binding) { + $r[$name]= $binding->resolve($this->inject); + } + return $r; + } + /** * Returns the implementation for a given name * diff --git a/src/main/php/inject/Injector.class.php b/src/main/php/inject/Injector.class.php index 36b4ac9..51733a2 100755 --- a/src/main/php/inject/Injector.class.php +++ b/src/main/php/inject/Injector.class.php @@ -12,11 +12,12 @@ * @test inject.unittest.NewInstanceTest */ class Injector { - protected static $PROVIDER; + protected static $IMPLEMENTATIONS, $PROVIDER; protected $bindings= []; protected $protect= []; static function __static() { + self::$IMPLEMENTATIONS= Type::forName('inject.Implementations'); self::$PROVIDER= Type::forName('inject.Provider'); } @@ -93,13 +94,13 @@ public function bind($type, $impl, $name= null) { * Returns implementations for a given type * * @param string|lang.Type $type - * @return inject.Implementations + * @return inject.Implementations * @throws inject.ProvisionException */ public function implementations($type) { $t= $type instanceof Type ? $type : Type::forName($type); if ($bindings= $this->bindings[$t->literal()] ?? null) { - return new Implementations($this, $bindings); + return self::$IMPLEMENTATIONS->base()->newGenericType([$t])->newInstance($this, $bindings); } throw new ProvisionException('No implementations for type '.$t); @@ -210,6 +211,8 @@ public function binding($type, $name= null) { } } else if ($t instanceof Nullable) { return $this->binding($t->underlyingType(), $name); + } else if (self::$IMPLEMENTATIONS->isAssignableFrom($t)) { + return new InstanceBinding($this->implementations($t->genericArguments()[0])); } else if (self::$PROVIDER->isAssignableFrom($t)) { $literal= $t->genericArguments()[0]->literal(); if ($binding= $this->bindings[$literal][$name] ?? null) { diff --git a/src/test/php/inject/unittest/ImplementationsTest.class.php b/src/test/php/inject/unittest/ImplementationsTest.class.php index 99dc81f..1f18e17 100755 --- a/src/test/php/inject/unittest/ImplementationsTest.class.php +++ b/src/test/php/inject/unittest/ImplementationsTest.class.php @@ -1,6 +1,6 @@ fixture()->implementations(URI::class)->named('stage'); } + + #[Test] + public function inject() { + $fixture= $this->fixture(); + Assert::equals($this->uris, $fixture->get(Service::class)->uris); + } } \ No newline at end of file diff --git a/src/test/php/inject/unittest/fixture/Service.class.php b/src/test/php/inject/unittest/fixture/Service.class.php new file mode 100755 index 0000000..4c4801b --- /dev/null +++ b/src/test/php/inject/unittest/fixture/Service.class.php @@ -0,0 +1,10 @@ + $uris */ + public function __construct($uris) { + $this->uris= $uris->all(); + } +} \ No newline at end of file From ed38bea230d7405096a75d817ea5a822dbba60e7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Jul 2024 10:04:42 +0200 Subject: [PATCH 5/6] Optimize implementation bindings --- src/main/php/inject/Implementations.class.php | 22 ++++++++++++++++++- src/main/php/inject/Injector.class.php | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/php/inject/Implementations.class.php b/src/main/php/inject/Implementations.class.php index fd34c76..452f14e 100755 --- a/src/main/php/inject/Implementations.class.php +++ b/src/main/php/inject/Implementations.class.php @@ -4,7 +4,7 @@ /** @test inject.unittest.ImplementationsTest */ #[Generic(self: 'T')] -class Implementations { +class Implementations implements Binding { private $inject, $bindings; /** @@ -49,4 +49,24 @@ public function named(string $name) { throw new ProvisionException('No implementation named "'.$name.'"'); } + + /** + * Resolves this binding and returns the instance + * + * @param inject.Injector $injector + * @return var + */ + public function resolve($injector) { + return $this; + } + + /** + * Returns a provider for this binding + * + * @param inject.Injector $injector + * @return inject.Provider + */ + public function provider($injector) { + return $this; + } } \ No newline at end of file diff --git a/src/main/php/inject/Injector.class.php b/src/main/php/inject/Injector.class.php index 51733a2..2d65ddc 100755 --- a/src/main/php/inject/Injector.class.php +++ b/src/main/php/inject/Injector.class.php @@ -212,7 +212,7 @@ public function binding($type, $name= null) { } else if ($t instanceof Nullable) { return $this->binding($t->underlyingType(), $name); } else if (self::$IMPLEMENTATIONS->isAssignableFrom($t)) { - return new InstanceBinding($this->implementations($t->genericArguments()[0])); + return $this->implementations($t->genericArguments()[0]); } else if (self::$PROVIDER->isAssignableFrom($t)) { $literal= $t->genericArguments()[0]->literal(); if ($binding= $this->bindings[$literal][$name] ?? null) { From 2a20fb6b3c8b2461c2cd989697a9e8aefbca372a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 Jul 2024 10:13:07 +0200 Subject: [PATCH 6/6] Return null from implementations() rather than throwing --- src/main/php/inject/Injector.class.php | 8 +++----- .../php/inject/unittest/ImplementationsTest.class.php | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/php/inject/Injector.class.php b/src/main/php/inject/Injector.class.php index 2d65ddc..fe4723e 100755 --- a/src/main/php/inject/Injector.class.php +++ b/src/main/php/inject/Injector.class.php @@ -94,16 +94,14 @@ public function bind($type, $impl, $name= null) { * Returns implementations for a given type * * @param string|lang.Type $type - * @return inject.Implementations - * @throws inject.ProvisionException + * @return ?inject.Implementations */ public function implementations($type) { $t= $type instanceof Type ? $type : Type::forName($type); if ($bindings= $this->bindings[$t->literal()] ?? null) { return self::$IMPLEMENTATIONS->base()->newGenericType([$t])->newInstance($this, $bindings); } - - throw new ProvisionException('No implementations for type '.$t); + return null; } /** @@ -212,7 +210,7 @@ public function binding($type, $name= null) { } else if ($t instanceof Nullable) { return $this->binding($t->underlyingType(), $name); } else if (self::$IMPLEMENTATIONS->isAssignableFrom($t)) { - return $this->implementations($t->genericArguments()[0]); + return $this->implementations($t->genericArguments()[0]) ?? Bindings::$ABSENT; } else if (self::$PROVIDER->isAssignableFrom($t)) { $literal= $t->genericArguments()[0]->literal(); if ($binding= $this->bindings[$literal][$name] ?? null) { diff --git a/src/test/php/inject/unittest/ImplementationsTest.class.php b/src/test/php/inject/unittest/ImplementationsTest.class.php index 1f18e17..ffb5963 100755 --- a/src/test/php/inject/unittest/ImplementationsTest.class.php +++ b/src/test/php/inject/unittest/ImplementationsTest.class.php @@ -34,13 +34,13 @@ public function default_implementation() { Assert::equals($this->uris['dev'], $this->fixture()->implementations(URI::class)->default()); } - #[Test, Expect(ProvisionException::class)] - public function no_implementations() { - $this->fixture()->implementations(Endpoint::class); + #[Test] + public function no_implementations_returning_null() { + Assert::null($this->fixture()->implementations(Endpoint::class)); } #[Test, Expect(ProvisionException::class)] - public function unknown_implementation() { + public function unknown_named_implementation() { $this->fixture()->implementations(URI::class)->named('stage'); }