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
15 changes: 8 additions & 7 deletions src/main/php/lang/Reflection.class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php namespace lang;

use ReflectionClass, ReflectionObject, ReflectionException;
use lang\meta\{MetaInformation, FromSyntaxTree, FromAttributes};
use lang\reflection\{Type, Package};
use lang\{ClassLoader, ClassNotFoundException, IllegalArgumentException};
Expand Down Expand Up @@ -46,11 +47,11 @@ public static function type($arg) {
} else if ($arg instanceof Type) {
return $arg;
} else if (is_object($arg)) {
return new Type(new \ReflectionObject($arg));
return new Type(new ReflectionObject($arg));
} else {
try {
return new Type(new \ReflectionClass(strtr($arg, '.', '\\')));
} catch (\ReflectionException $e) {
return new Type(new ReflectionClass(strtr($arg, '.', '\\')));
} catch (ReflectionException $e) {
throw new ClassNotFoundException($arg, [ClassLoader::getDefault()]);
}
}
Expand Down Expand Up @@ -101,19 +102,19 @@ public static function of($arg) {
} else if ($arg instanceof Type) {
return $arg;
} else if (is_object($arg)) {
return new Type(new \ReflectionObject($arg));
return new Type(new ReflectionObject($arg));
} else {
$cl= ClassLoader::getDefault();
$name= strtr($arg, '\\', '.');
if ($cl->providesClass($name)) {
return new Type(new \ReflectionClass($cl->loadClass0($name)));
return new Type(new ReflectionClass($cl->loadClass0($name)));
} else if ($cl->providesPackage($name)) {
return new Package($name);
}

try {
return new Type(new \ReflectionClass(strtr($arg, '.', '\\')));
} catch (\ReflectionException $e) {
return new Type(new ReflectionClass(strtr($arg, '.', '\\')));
} catch (ReflectionException $e) {
throw new ClassNotFoundException($name, [$cl]);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/php/lang/meta/FromAttributes.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ private function annotations($reflect, $context) {
return $r;
}

/** @return iterable */
public function ofInvocable($reflect) {
return $this->annotations($reflect, $reflect);
}

/** @return iterable */
public function ofType($reflect) {
return $this->annotations($reflect, $reflect);
Expand Down
5 changes: 5 additions & 0 deletions src/main/php/lang/meta/FromSyntaxTree.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ public function imports($reflect) {
return $imports;
}

/** @return iterable */
public function ofInvocable($reflect) {
throw new IllegalStateException('Invocables cannot have annotations in PHP 7');
}

/** @return iterable */
public function ofType($reflect) {
$tree= $this->tree($reflect, $reflect->getFileName());
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/meta/MetaInformation.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* it to another source otherwise.
*/
class MetaInformation {
private $annotations;
public $annotations;

public function __construct($annotations) {
$this->annotations= $annotations;
Expand Down
121 changes: 121 additions & 0 deletions src/main/php/lang/reflection/Invocable.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php namespace lang\reflection;

use ReflectionException, ReflectionFunction, ArgumentCountError, Closure, TypeError, Throwable;
use lang\{Reflection, Type};

class Invocable implements Target {
private $reflect;
private $annotations= null;

/** @param callable|ReflectionFunction $arg */
public function __construct($arg) {
if ($arg instanceof ReflectionFunction) {
$this->reflect= $arg;
} else if (is_array($arg)) {
$this->reflect= new ReflectionFunction(Closure::fromCallable($arg));
} else {
$this->reflect= new ReflectionFunction($arg);
}
}

/** Returns the name, null for anonymous functions */
public function name(): ?string {
if (PHP_VERSION_ID < 80200 ? strpos($this->reflect->name, '{closure}') : $this->reflect->isAnonymous()) {
return null;
} else {
return $this->reflect->name;
}
}

/** Returns a compound name consisting of `[NAME]()` */
public function compoundName(): string { return $this->reflect->name.'()'; }

/**
* Returns this functions's doc comment, or NULL if there is none.
*
* @return ?string
*/
public function comment() {
return false === ($c= $this->reflect->getDocComment())
? null
: trim(preg_replace('/\n\s+\* ?/', "\n", substr($c, 3, -2)))
;
}

/** @return lang.reflection.Annotations */
public function annotations() {
$this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofInvocable($this->reflect);
return new Annotations($this->annotations);
}

/** @return ?lang.reflection.Annotation */
public function annotation(string $type) {
$this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofInvocable($this->reflect);

$t= strtr($type, '.', '\\');
return isset($this->annotations[$t]) ? new Annotation($t, $this->annotations[$t]) : null;
}

/**
* Looks up a parameter
*
* @param int|string $arg Either a position or a name
* @return ?lang.reflection.Parameter
*/
public function parameter($arg) {
if (is_int($arg)) {
$p= $this->reflect->getParameters()[$arg] ?? null;
} else {
$p= null;
foreach ($this->reflect->getParameters() as $parameter) {
if ($arg !== $parameter->name) continue;
$p= $parameter;
break;
}
}
return null === $p ? null : new Parameter($p, $this->reflect);
}

/**
* Returns all parameters
*
* @return lang.reflection.Parameters
*/
public function parameters(): Parameters {
return new Parameters($this->reflect);
}

/** @return lang.reflection.Constraint */
public function returns() {
$present= true;
$absent= function($set) use(&$present) { $present= false; };

$t= Type::resolve($this->reflect->getReturnType(), [], $absent);
return new Constraint($t ?? Type::$VAR, $present);
}

/**
* Invokes this closure
*
* @param var[] $args
* @return var
* @throws lang.reflection.CannotInvoke if prerequisites to the invocation fail
* @throws lang.reflection.InvocationFailed if invocation raises an exception
*/
public function invoke($args= []) {
try {
$pass= PHP_VERSION_ID < 80000 && $args ? Routine::pass($this->reflect, $args) : $args;
return $this->reflect->invokeArgs($pass);
} catch (ReflectionException|ArgumentCountError|TypeError $e) {
throw new CannotInvoke($this, $e);
} catch (Throwable $e) {

// This really should be an ArgumentCountError...
if (0 === strpos($e->getMessage(), 'Unknown named parameter $')) {
throw new CannotInvoke($this, $e);
}

throw new InvocationFailed($this, $e);
}
}
}
2 changes: 1 addition & 1 deletion src/main/php/lang/reflection/Member.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use lang\{XPClass, Reflection, Value};

/** Base class for constants, properties and methods */
abstract class Member implements Annotated, Value {
abstract class Member implements Annotated, Target, Value {
protected $reflect, $annotations;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/reflection/Parameter.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Parameter implements Annotated {
* Creates a new parameter
*
* @param ReflectionParameter $reflect
* @param ReflectionMethod $method
* @param ReflectionFuntionAbstract $method
*/
public function __construct($reflect, $method= null) {
$this->reflect= $reflect;
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/reflection/Parameters.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class Parameters implements \IteratorAggregate {
private $method;

/** @param ReflectionMethod $method */
/** @param ReflectionFunctionAbstract $method */
public function __construct($method) {
$this->method= $method;
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/php/lang/reflection/Target.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php namespace lang\reflection;

interface Target {

/** Returns a compound name */
public function compoundName(): string;
}
4 changes: 2 additions & 2 deletions src/main/php/lang/reflection/TargetException.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ abstract class TargetException extends XPException {
private $target;

/** Creates a new instance */
public function __construct(Member $target, $cause= null) {
public function __construct(Target $target, $cause= null) {
parent::__construct(static::MESSAGE.' '.$target->compoundName(), $cause);
$this->target= $target;
}

/** Returns the target member */
public function target(): Member { return $this->target; }
public function target(): Target { return $this->target; }

}
121 changes: 121 additions & 0 deletions src/test/php/lang/reflection/unittest/InvocableTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php namespace lang\reflection\unittest;

use Closure, ReflectionFunction;
use lang\reflection\{Invocable, CannotInvoke};
use lang\{Type, Primitive};
use test\verify\Runtime;
use test\{Assert, Expect, Test, Values};

class InvocableTest {

/** @return iterable */
private function callables() {
yield ['strtoupper', 'strtoupper'];
yield [fn() => true, null];
yield [function() { return true; }, null];
yield [[$this, 'name'], 'name'];
yield [[Closure::class, 'fromCallable'], 'fromCallable'];
yield [Closure::fromCallable('strtoupper'), 'strtoupper'];
yield [new ReflectionFunction('strtoupper'), 'strtoupper'];
}

#[Test, Values(from: 'callables')]
public function can_create($callable) {
new Invocable($callable);
}

#[Test, Values(from: 'callables')]
public function name($callable, $expected) {
Assert::equals($expected, (new Invocable($callable))->name());
}

#[Test]
public function no_comment() {
Assert::null((new Invocable(fn() => true))->comment());
}

#[Test]
public function with_comment() {
Assert::equals('Test', (new Invocable(/** Test */ fn() => true))->comment());
}

#[Test, Runtime(php: '>=8.0')]
public function no_annotations() {
Assert::equals([], iterator_to_array((new Invocable(fn() => true))->annotations()));
}

#[Test, Runtime(php: '>=8.0')]
public function with_annotation() {
$closure= eval('return #[\\test\\Test] fn() => true;');
Assert::equals('test', (new Invocable($closure))->annotation(Test::class)->name());
}

#[Test, Runtime(php: '>=8.0')]
public function with_annotations() {
$closure= eval('return #[\\test\\Test, \\test\\Values] fn() => true;');

$annotations= [];
foreach ((new Invocable($closure))->annotations() as $annotation) {
$annotations[]= $annotation->name();
}
Assert::equals(['test', 'values'], $annotations);
}

#[Test]
public function empty_parameters() {
$parameters= (new Invocable(fn() => true))->parameters();
Assert::equals(0, $parameters->size());
}

#[Test]
public function parameters() {
$parameters= (new Invocable(fn($name) => true))->parameters();
Assert::equals(1, $parameters->size());
}

#[Test, Values([0, 'name'])]
public function parameter($arg) {
$parameter= (new Invocable(fn($name) => true))->parameter($arg);
Assert::equals('name', $parameter->name());
Assert::false($parameter->optional());
}

#[Test]
public function optional_parameter() {
$parameters= (new Invocable(fn($name= null) => true))->parameters();
Assert::equals(0, $parameters->size($required= true));
Assert::equals(1, $parameters->size($required= false));
}

#[Test]
public function without_return_type() {
$constraint= (new Invocable(fn($name) => true))->returns();
Assert::false($constraint->present());
Assert::equals(Type::$VAR, $constraint->type());
}

#[Test]
public function return_type() {
$constraint= (new Invocable(fn($name): bool => true))->returns();
Assert::true($constraint->present());
Assert::equals(Primitive::$BOOL, $constraint->type());
}

#[Test]
public function invoke() {
$Invocable= (new Invocable(fn($name) => strtoupper($name)));
Assert::equals('TEST', $Invocable->invoke(['test']));
}

#[Test]
public function invoke_named() {
$Invocable= (new Invocable(fn($name) => strtoupper($name)));
Assert::equals('TEST', $Invocable->invoke(['name' => 'test']));
}

#[Test, Expect(CannotInvoke::class)]
public function invoke_missing_argument() {
$Invocable= (new Invocable(fn($name) => strtoupper($name)));
$Invocable->invoke([]);
}
}
Loading