Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions src/main/php/inject/Implementations.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php namespace inject;

use lang\Generic;

/** @test inject.unittest.ImplementationsTest */
#[Generic(self: 'T')]
class Implementations implements Binding {
private $inject, $bindings;

/**
* Creates a new instance
*
* @param inject.Injector $inject
* @param [:inject.Binding] $bindings
*/
public function __construct(Injector $inject, array $bindings) {
$this->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;
}
}
19 changes: 18 additions & 1 deletion src/main/php/inject/Injector.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<?>');
}

Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions src/test/php/inject/unittest/ImplementationsTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php namespace inject\unittest;

use inject\unittest\fixture\{URI, Endpoint, Service};
use inject\{Injector, ProvisionException};
use test\{Assert, Before, Expect, Test, Values};

class ImplementationsTest {
private $uris;

#[Before]
private function uris() {
$this->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);
}
}
3 changes: 3 additions & 0 deletions src/test/php/inject/unittest/fixture/Creation.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ class Creation {

/** Create fluent interface for URIs */
public function __construct(URI $uri) { /* ... */ }

/** @return string */
public function create() { /* ... */ }
}
10 changes: 10 additions & 0 deletions src/test/php/inject/unittest/fixture/Service.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php namespace inject\unittest\fixture;

class Service {
public $uris;

/** @param inject.Implementations<inject.unittest.fixture.URI> $uris */
public function __construct($uris) {
$this->uris= $uris->all();
}
}
5 changes: 4 additions & 1 deletion src/test/php/inject/unittest/fixture/URI.class.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?php namespace inject\unittest\fixture;

class URI {
private $backing;

/** @param string|inject.unittest.fixture.Creation $arg */
public function __construct($arg) { /* ... */ }
public function __construct($arg) {
$this->backing= $arg instanceof Creation ? $arg->create() : (string)$arg;
}
}