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. diff --git a/src/main/php/inject/Implementations.class.php b/src/main/php/inject/Implementations.class.php new file mode 100755 index 0000000..452f14e --- /dev/null +++ b/src/main/php/inject/Implementations.class.php @@ -0,0 +1,72 @@ +inject= $inject; + $this->bindings= $bindings; + } + + /** 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 + * + * @param string $name + * @return T + * @throws inject.ProvisionException if there is no such implementation + */ + #[Generic(return: 'T')] + public function named(string $name) { + if ($binding= $this->bindings[$name] ?? null) { + return $binding->resolve($this->inject); + } + + 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 7959505..fe4723e 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'); } @@ -89,6 +90,20 @@ public function bind($type, $impl, $name= null) { return $this; } + /** + * Returns implementations for a given type + * + * @param string|lang.Type $type + * @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); + } + return null; + } + /** * Returns the lookup if it provides a value, null otherwise * @@ -194,6 +209,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 $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 new file mode 100755 index 0000000..ffb5963 --- /dev/null +++ b/src/test/php/inject/unittest/ImplementationsTest.class.php @@ -0,0 +1,52 @@ +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] + public function no_implementations_returning_null() { + Assert::null($this->fixture()->implementations(Endpoint::class)); + } + + #[Test, Expect(ProvisionException::class)] + public function unknown_named_implementation() { + $this->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/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/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 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