Skip to content

Commit

Permalink
Merge pull request #30 from xp-forge/refactor/reflection
Browse files Browse the repository at this point in the history
Migrate to new reflection API
  • Loading branch information
thekid authored Jul 30, 2023
2 parents 7de191a + a2db1ac commit 413152c
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 42 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^11.0 | ^10.0 | ^9.0 | ^8.0 | ^7.0",
"xp-framework/reflection": "^2.0",
"php" : ">=7.0.0"
},
"require-dev" : {
Expand Down
54 changes: 22 additions & 32 deletions src/main/php/inject/Injector.class.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php namespace inject;

use lang\reflect\TargetInvocationException;
use lang\{IllegalArgumentException, Nullable, Primitive, Throwable, Type, TypeUnion, XPClass};
use lang\{IllegalArgumentException, Nullable, Primitive, Throwable, Type, TypeUnion, XPClass, Reflection};

/**
* Injector
Expand Down Expand Up @@ -103,47 +102,41 @@ private function provided($lookup) {
/**
* Retrieve arguments for a given routine
*
* @param lang.reflect.Routine $routine
* @param lang.reflection.Routine $routine
* @param [:var] $named
* @return inject.Binding
*/
private function argumentsOf($routine, $named= []) {
$args= [];
foreach ($routine->getParameters() as $i => $param) {
$name= $param->getName();
foreach ($routine->parameters() as $name => $param) {
if (isset($named[$name])) {
$args[]= $named[$name];
continue;
}

if ($param->hasAnnotation('inject')) {
$inject= $param->getAnnotation('inject');
} else if (0 === $i) {
$inject= $routine->hasAnnotation('inject') ? $routine->getAnnotation('inject') : null;
if ($annotation= $param->annotation(Inject::class)) {
$inject= $annotation->arguments();
} else if (0 === $param->position() && $annotation= $routine->annotation(Inject::class)) {
$inject= $annotation->arguments();
} else {
$inject= null;
$inject= [];
}

if (is_array($inject)) {
$type= isset($inject['type']) ? Type::forName($inject['type']) : ($param->getTypeRestriction() ?: $param->getType());
$lookup= $this->binding($type, $inject['name'] ?? null);
} else {
$type= $param->getTypeRestriction() ?: $param->getType();
$lookup= $this->binding($type, $inject);
}
$type= isset($inject['type']) ? Type::forName($inject['type']) : $param->constraint()->type();
$lookup= $this->binding($type, $inject['name'] ?? $inject[0] ?? null);

if ($binding= $this->provided($lookup) ?? $this->provided($this->binding($type, $name))) {
$args[]= $binding->resolve($this);
} else if ($param->isOptional()) {
$args[]= $param->getDefaultValue();
} else if ($param->optional()) {
$args[]= $param->default();
} else {
return new ProvisionException(sprintf(
'No bound value for type %s%s in %s\'s %s() parameter %s',
$type->getName(),
isset($inject['name']) ? ' named "'.$inject['name'].'"' : '',
$routine->getDeclaringClass()->getName(),
$routine->getName(),
$param->getName()
$routine->declaredIn()->name(),
$routine->name(),
$name
));
}
}
Expand All @@ -160,18 +153,16 @@ private function argumentsOf($routine, $named= []) {
* @throws inject.ProvisionException
*/
private function instanceOf($class, $named= []) {
if (!$class->hasConstructor()) return new InstanceBinding($class->newInstance());
$t= Reflection::type($class);
if (null === ($constructor= $t->constructor())) return new InstanceBinding($class->newInstance());

$constructor= $class->getConstructor();
$arguments= $this->argumentsOf($constructor, $named);
if (!$this->provided($arguments)) return $arguments;

// Wrap any exception caught during instance creation. These errors
// should not be simply returned as we risk them being overlooked!
try {
return new InstanceBinding($constructor->newInstance($arguments->resolve($this)));
} catch (TargetInvocationException $e) {
throw new ProvisionException('Error creating an instance of '.$class->getName(), $e->getCause());
} catch (Throwable $e) {
throw new ProvisionException('Error creating an instance of '.$class->getName(), $e);
}
Expand All @@ -183,7 +174,6 @@ private function instanceOf($class, $named= []) {
* @param string|lang.Type $type
* @param ?string $name
* @return inject.Binding
* @throws inject.ProvisionException
*/
public function binding($type, $name= null) {
$t= $type instanceof Type ? $type : Type::forName($type);
Expand All @@ -202,13 +192,13 @@ public function binding($type, $name= null) {
return $this->binding($t->underlyingType(), $name);
} else if (self::$PROVIDER->isAssignableFrom($t)) {
$literal= $t->genericArguments()[0]->literal();
if (isset($this->bindings[$literal][$name])) {
return new InstanceBinding($this->bindings[$literal][$name]->provider($this));
if ($binding= $this->bindings[$literal][$name] ?? null) {
return new InstanceBinding($binding->provider($this));
}
} else {
$literal= $t->literal();
if (isset($this->bindings[$literal][$name])) {
return $this->bindings[$literal][$name];
if ($binding= $this->bindings[$literal][$name] ?? null) {
return $binding;
} else if (null === $name && $t instanceof XPClass && !($t->isInterface() || $t->getModifiers() & MODIFIER_ABSTRACT)) {
return $this->instanceOf($t);
}
Expand All @@ -235,7 +225,7 @@ public function get($type, $name= null) {
/**
* Retrieve args for a given routine.
*
* @param lang.reflect.Routine $routine
* @param lang.reflection.Routine $routine
* @param [:var] $named Named arguments
* @return var[]
* @throws inject.ProvisionException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class AnnotatedConstructorTest extends AnnotationsTest {
public function with_inject_annotation_and_type() {
$this->inject->bind(Value::class, $this->newInstance([
'injected' => null,
'#[Inject(["type" => "inject.unittest.AnnotationsTest"])] __construct' => function($param) {
'#[Inject(type: "inject.unittest.AnnotationsTest")] __construct' => function($param) {
$this->injected= $param;
}
]));
Expand Down
3 changes: 1 addition & 2 deletions src/test/php/inject/unittest/AnnotationsTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

abstract class AnnotationsTest {
protected $inject;
protected $id= 0;

#[Before]
public function inject() {
Expand All @@ -27,7 +26,7 @@ public function inject() {
*/
protected function newInstance($definition) {
return ClassLoader::defineClass(
'inject.unittest.fixture.AnnotationsTest_'.($this->id++),
'inject.AnnotationsTest_'.uniqid(),
Value::class,
[],
$definition
Expand Down
15 changes: 8 additions & 7 deletions src/test/php/inject/unittest/NewInstanceTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

use inject\unittest\fixture\{FileSystem, Storage};
use inject\{Injector, ProvisionException};
use lang\{ClassLoader, IllegalAccessException, Runnable};
use lang\reflection\CannotInstantiate;
use lang\{ClassLoader, XPException, Runnable};
use test\{Assert, Before, Expect, Test};
use util\Currency;

Expand All @@ -13,11 +14,11 @@ class NewInstanceTest {
* Creates a unique and new fixture subclass with the given definition
*
* @param [:var] $definition
* @return inject.unittest.fixture.Storage
* @return inject.unittest.fixture.Fixture
*/
protected function newFixture($definition) {
return ClassLoader::defineClass(
'inject.unittest.fixture.T'.uniqid(),
'inject.NewInstanceTest_'.uniqid(),
'inject.unittest.fixture.Fixture',
[],
$definition
Expand Down Expand Up @@ -59,7 +60,7 @@ public function newInstance_performs_injection() {
public function newInstance_performs_named_injection_using_array_form() {
$inject= (new Injector())->bind(Storage::class, $this->storage, 'test');
$fixture= $this->newFixture([
'#[Inject(["name" => "test"])] __construct' => function(Storage $param) { $this->injected= $param; }
'#[Inject(name: "test")] __construct' => function(Storage $param) { $this->injected= $param; }
]);

Assert::equals($this->storage, $inject->newInstance($fixture)->injected);
Expand Down Expand Up @@ -117,8 +118,8 @@ public function newInstance_performs_partial_injection_with_optional_parameter()
Assert::equals([$this->storage, true], $inject->newInstance($fixture)->injected);
}

#[Test, Expect(class: IllegalAccessException::class, message: '/Cannot invoke private constructor/')]
public function newInstance_catches_iae_when_creating_class_instances() {
#[Test, Expect(class: CannotInstantiate::class, message: '/Cannot instantiate .+/')]
public function newInstance_catches_cannot_instantiate_when_creating_class_instances() {
$this->newInstance(new Injector(), $this->newFixture('{
#[Inject]
private function __construct() { }
Expand All @@ -128,7 +129,7 @@ private function __construct() { }
#[Test, Expect(class: ProvisionException::class, message: '/No bound value for type string named "endpoint"/')]
public function newInstance_throws_when_value_for_required_parameter_not_found() {
$this->newInstance(new Injector(), $this->newFixture('{
#[Inject(["type" => "string", "name" => "endpoint"])]
#[Inject(type: "string", name: "endpoint")]
public function __construct($uri) { }
}'));
}
Expand Down

0 comments on commit 413152c

Please sign in to comment.