Skip to content

Commit

Permalink
Automatic resolution doc+example+test
Browse files Browse the repository at this point in the history
  • Loading branch information
dakujem committed May 19, 2020
1 parent f5cbf88 commit 0357733
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 6 deletions.
73 changes: 67 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ $factory = function(MyServiceInterface $s1, MyOtherSeviceInterface $s2){
// and provides them to the callable when you call the `invoke` method:
$complexService = $genie->provide('myService', 'my-other-service')->invoke($factory);

// Repo genie will only be able to fetch repositories, the following fails:
// Repo genie will only be able to fetch repositories,
// the following call would fail
// if 'my-system-service' service did not implement RepositoryInterface:
$repoGenie->provide('my-system-service');
```

Expand All @@ -83,22 +85,26 @@ You now have means to allow a service
on-demand access to services of a certain type without injecting them all.\
This particular use-case breaks IoC, though.
```php
// using $repoGenie from the previous snippet
new RepositoryUser($repoGenie);

// inside RepositoryUser
$repoGenie->provide(
ThisRepo::class,
ThatRepo::class
ThisRepo::class, // or 'this' ✳
ThatRepo::class // or 'that' ✳
)->invoke(function(
ThisRepository $r1,
ThatRepository $r2
){
// do stuff with the repos...
});
```
> Note the _callable_ returned by `WireGenie::provide()` method and its immediate invocation.
> ✳ the actual keys depend on the container in use
> and the way the services are registered in it.\
> These identifiers are bare strings without their own semantics.
> They may or may not be related to the actual instances that are fetched from the container.
As you can see, it is important to limit access
In use cases like the one above, it is important to limit access
to certain services only to keep your app layers in good shape.


Expand Down Expand Up @@ -137,6 +143,61 @@ $genie->provide( ... )->invoke(function( ... ){ ... });
```


## Automatic dependency resolution

You might consider implementing reflection-based automatic dependency resolution.

> Note that using reflection might have negative performance impact
> if used heavily.
This code would work for closures, provided the type-hinted class names are
equal to the identifiers of services in the DI container,
i.e. the container will fetch correct instances if called with class name
argument like this `$container->get(ClassName::class)`:
```php
final class ArgumentReflector
{
public static function types(Closure $closure): array
{
$rf = new ReflectionFunction($closure);
return array_map(function (ReflectionParameter $rp): string {
$type = ($rp->getClass())->name ?? null;
if ($type === null) {
throw new RuntimeException(
sprintf(
'Unable to reflect type of parameter "%s".',
$rp->getName()
)
);
}
return $type;
}, $rf->getParameters());
}
}

// Implement a method like this:
function wireAndExecute(Closure $closure)
{
$genie = new WireGenie( $this->container ); // get a Wire Genie instance
return
$genie
->provide(...ArgumentReflector::types($closure))
->invoke($closure);
}

// Then use it to call closures without explicitly specifying the dependencies:
$result = $foo->wireAndExecute(function(DepOne $d1, DepTwo $d2){
// do or create stuff
return $d1->foo() + $d2->bar();
});

// The PSR-11 container will be asked to
// ::get(DepOne::class)
// ::get(DepTwo::class)
// instances.
```


## Contributing

Ideas or contribution is welcome. Please make a PR or file an issue.
Ideas or contribution is welcome. Please send a PR or file an issue.
71 changes: 71 additions & 0 deletions tests/AutomaticResolutionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);

namespace Dakujem\Tests;

use Closure;
use Dakujem\WireGenie;
use Dakujem\WireLimiter;
use Error;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use ReflectionFunction;
use ReflectionParameter;
use RuntimeException;

require_once 'ContainerProvider.php';

final class ArgumentReflector
{
public static function types(Closure $closure): array
{
$rf = new ReflectionFunction($closure);
return array_map(function (ReflectionParameter $rp): string {
$type = ($rp->getClass())->name ?? null;
if ($type === null) {
throw new RuntimeException(sprintf('Unable to reflect type of parameter "%s".', $rp->getName()));
}
return $type;
}, $rf->getParameters());
}
}

/**
* AutomaticResolutionTest
*/
final class AutomaticResolutionTest extends TestCase
{
private function wireAndExecute(Closure $closure)
{
$genie = new WireGenie(ContainerProvider::createContainer());
return $genie->provide(...ArgumentReflector::types($closure))->invoke($closure);
}

public function testCorrectResolution(): void
{
$run = false;
$this->wireAndExecute(function (?WireGenie $wg = null, ?Error $e = null) use (&$run) {
$run = true;
$this->assertSame(WireGenie::class, get_class($wg));
$this->assertSame(Error::class, get_class($e));
});
$this->assertTrue($run);
}

public function testFailedResolution(): void
{
$run = false;
$this->wireAndExecute(function (?WireLimiter $foo = null) use (&$run) {
$run = true;
$this->assertSame(null, $foo);
});
$this->assertTrue($run);
}

public function testFailedReflection(): void
{
$this->expectException(RuntimeException::class);
$this->wireAndExecute(function (int $foo = null) {
});
}
}

0 comments on commit 0357733

Please sign in to comment.