Skip to content

Commit

Permalink
Merge pull request #24 from xp-forge/refactor/reflection
Browse files Browse the repository at this point in the history
Migrate to new reflection library
  • Loading branch information
thekid authored Jul 25, 2023
2 parents 39bc616 + 6c6377c commit 41febbe
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 46 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",
"xp-forge/web": "^3.0 | ^2.0 | ^1.0",
"xp-forge/marshalling": "^1.0 | ^0.3 | ^0.2",
"xp-forge/json": "^5.0 | ^4.0 | ^3.1",
Expand Down
55 changes: 27 additions & 28 deletions src/main/php/web/rest/Delegate.class.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php namespace web\rest;

use io\streams\{InputStream, Streams};
use lang\IllegalArgumentException;
use lang\reflect\TargetInvocationException;
use lang\reflection\{Method, TargetException};
use lang\{IllegalArgumentException, Reflection, Type};
use web\Request;

class Delegate {
Expand Down Expand Up @@ -31,67 +31,66 @@ static function __static() {
* Creates a new delegate
*
* @param object $instance
* @param lang.reflect.Method $method
* @param string|lang.reflection.Method $method
* @param string $source Default source
*/
public function __construct($instance, $method, $source) {
$this->instance= $instance;
$this->method= $method;
foreach ($method->getParameters() as $param) {
$this->method= $method instanceof Method ? $method : Reflection::type($instance)->method($method);
foreach ($this->method->parameters() as $param) {

// Source explicitely set by annotation
foreach ($param->getAnnotations() as $source => $name) {
if (isset(self::$SOURCES[$source])) {
$this->param($param, $name ?? $param->getName(), $source);
foreach ($param->annotations() as $annotation) {
if ($accessor= self::$SOURCES[$annotation->name()] ?? null) {
$this->param($param, $name ?? $param->name(), $accessor);
continue 2;
}
}

// Source derived from parameter type
$type= $param->getType();
if ('var' === $type->getName()) {
$type= $param->constraint()->type();
if (Type::$VAR === $type) {
// NOOP
} else if ($type->isAssignableFrom(InputStream::class)) {
$source= 'stream';
} else if ($type->isAssignableFrom(Request::class)) {
$source= 'request';
}
$this->param($param, $param->getName(), $source);
$this->param($param, $param->name(), self::$SOURCES[$source]);
}
}

/**
* Adds parameter request reader for a given parameter
* Adds parameter request accessor for a given parameter
*
* @param lang.reflect.Parameter $param
* @param lang.reflection.Parameter $param
* @param string $name
* @param function(web.Request, web.rest.format.EntityFormat, string): var $source
* @param function(web.Request, web.rest.format.EntityFormat, string): var $accessor
* @return void
* @throws lang.IllegalArgumentException
*/
private function param($param, $name, $source) {
$extract= self::$SOURCES[$source];

if ($param->isOptional()) {
$default= $param->getDefaultValue();
$read= function($req, $format) use($extract, $name, $default) {
return $extract($req, $format, $name) ?? $default;
private function param($param, $name, $accessor) {
if ($param->optional()) {
$default= $param->default();
$read= function($req, $format) use($accessor, $name, $default) {
return $accessor($req, $format, $name) ?? $default;
};
} else {
$read= function($req, $format) use($extract, $name) {
if (null === ($value= $extract($req, $format, $name))) {
$read= function($req, $format) use($accessor, $name) {
if (null === ($value= $accessor($req, $format, $name))) {
throw new IllegalArgumentException('Missing argument '.$name);
}
return $value;
};
}
$this->params[$name]= ['type' => $param->getType(), 'read' => $read];
$this->params[$name]= ['type' => $param->constraint()->type(), 'read' => $read];
}

/** @return string */
public function name() { return nameof($this->instance).'::'.$this->method->getName(); }
public function name() { return nameof($this->instance).'::'.$this->method->name(); }

/** @return [:var] */
public function annotations() { return $this->method->getAnnotations(); }
/** @return lang.reflection.Annotations */
public function annotations() { return $this->method->annotations(); }

/** @return [:var] */
public function params() { return $this->params; }
Expand All @@ -106,7 +105,7 @@ public function params() { return $this->params; }
public function invoke($args) {
try {
return $this->method->invoke($this->instance, $args);
} catch (TargetInvocationException $e) {
} catch (TargetException $e) {
throw $e->getCause();
}
}
Expand Down
13 changes: 9 additions & 4 deletions src/main/php/web/rest/Delegates.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace web\rest;

use lang\IllegalArgumentException;
use lang\{IllegalArgumentException, Reflection};

/**
* Matches request and routes to correct delegate
Expand Down Expand Up @@ -31,16 +31,21 @@ public function with($instance, $base= '/') {
}

$base= rtrim($base, '/');
foreach (typeof($instance)->getMethods() as $method) {
foreach (array_intersect_key($method->getAnnotations(), self::$METHODS) as $verb => $segment) {
foreach (Reflection::type($instance)->methods() as $method) {

foreach ($method->annotations() as $annotation) {
$verb= $annotation->name();
if (null === ($source= self::$METHODS[$verb] ?? null)) continue;

$segment= $annotation->argument(0);
if (null === $segment) {
$pattern= $base.'(/.+)?';
} else if ('/' === $segment || '' === $segment) {
$pattern= $base.'/?';
} else {
$pattern= $base.preg_replace(['/\{([^:}]+):([^}]+)\}/', '/\{([^}]+)\}/'], ['(?<$1>$2)', '(?<$1>[^/]+)'], $segment);
}
$this->patterns['#^'.$verb.$pattern.'$#']= new Delegate($instance, $method, self::$METHODS[$verb]);
$this->patterns['#^'.$verb.$pattern.'$#']= new Delegate($instance, $method, $source);
}
}
return $this;
Expand Down
10 changes: 8 additions & 2 deletions src/main/php/web/rest/MethodsIn.class.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<?php namespace web\rest;

use lang\Reflection;

/**
* Creates routing based on a given instance
*/
class MethodsIn extends Delegates {

/** @param object $instance */
public function __construct($instance) {
$class= typeof($instance);
$this->with($instance, $class->hasAnnotation('resource') ? $class->getAnnotation('resource') ?? '' : '/');
$class= Reflection::type($instance);
if ($annotation= $class->annotation(Resource::class)) {
$this->with($instance, (string)$annotation->argument(0));
} else {
$this->with($instance, '/');
}
uksort($this->patterns, function($a, $b) { return strlen($b) - strlen($a); });
}
}
14 changes: 7 additions & 7 deletions src/main/php/web/rest/ResourcesIn.class.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<?php namespace web\rest;

use lang\reflect\Package;
use lang\reflection\Package;

/**
* Creates routing based on resource classes in a given package
*
* @test xp://web.rest.unittest.ResourcesInTest
* @test web.rest.unittest.ResourcesInTest
*/
class ResourcesIn extends Delegates {

/**
* Creates this delegates instance
*
* @param lang.reflect.Package|string $package
* @param lang.reflection.Package|string $package
* @param function(lang.XPClass): object $new Optional function to create instances
*/
public function __construct($package, $new= null) {
$p= $package instanceof Package ? $package : Package::forName($package);
foreach ($p->getClasses() as $class) {
if ($class->hasAnnotation('resource')) {
$this->with($new ? $new($class) : $class->newInstance(), $class->getAnnotation('resource') ?? '');
$p= $package instanceof Package ? $package : new Package($package);
foreach ($p->types() as $type) {
if ($resource= $type->annotation(Resource::class)) {
$this->with($new ? $new($type->class()) : $type->newInstance(), (string)$resource->argument(0));
}
}
uksort($this->patterns, function($a, $b) { return strlen($b) - strlen($a); });
Expand Down
6 changes: 3 additions & 3 deletions src/test/php/web/rest/unittest/InvocationsTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use lang\{ElementNotFoundException, IllegalStateException};
use test\{Assert, Test};
use web\rest\unittest\api\Users;
use web\rest\unittest\api\{Users, Cached};
use web\rest\{Interceptor, Response, RestApi};

class InvocationsTest extends RunTest {
Expand Down Expand Up @@ -49,12 +49,12 @@ public function intercepting_catching_exceptions() {
#[Test]
public function intercepting_can_access_annotations() {
$invocations= function($invocation, $args) use(&$cached) {
$cached= $invocation->target()->annotations()['cached'];
$cached= $invocation->target()->annotations()->type(Cached::class);
return $invocation->proceed($args);
};

$this->run((new RestApi(new Users()))->intercepting($invocations), 'GET', '/users/1549/avatar');
Assert::equals(['ttl' => 3600], $cached);
Assert::equals(['ttl' => 3600], $cached->arguments());
}

#[Test]
Expand Down
4 changes: 2 additions & 2 deletions src/test/php/web/rest/unittest/ResourcesInTest.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace web\rest\unittest;

use lang\reflect\Package;
use lang\reflection\Package;
use test\{Assert, Test};
use web\rest\ResourcesIn;

Expand All @@ -14,7 +14,7 @@ public function using_package_name() {

#[Test]
public function using_package_instance() {
$r= new ResourcesIn(Package::forName('web.rest.unittest.api'));
$r= new ResourcesIn(new Package('web.rest.unittest.api'));
Assert::notEquals(null, $r->target('get', '/monitoring/status'));
}

Expand Down

0 comments on commit 41febbe

Please sign in to comment.