From 15d4de2d3c2d0a6302be6168568fed00e18264e4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 7 Nov 2026 21:11:52 +0100 Subject: [PATCH 01/10] Add lang.reflection.Closure to reflect callable closures --- .../php/lang/reflection/Closure.class.php | 95 +++++++++++++++++++ src/main/php/lang/reflection/Member.class.php | 2 +- .../php/lang/reflection/Parameter.class.php | 2 +- .../php/lang/reflection/Parameters.class.php | 2 +- src/main/php/lang/reflection/Target.class.php | 7 ++ .../lang/reflection/TargetException.class.php | 4 +- .../reflection/unittest/ClosureTest.class.php | 90 ++++++++++++++++++ 7 files changed, 197 insertions(+), 5 deletions(-) create mode 100755 src/main/php/lang/reflection/Closure.class.php create mode 100755 src/main/php/lang/reflection/Target.class.php create mode 100755 src/test/php/lang/reflection/unittest/ClosureTest.class.php diff --git a/src/main/php/lang/reflection/Closure.class.php b/src/main/php/lang/reflection/Closure.class.php new file mode 100755 index 0000000..e3011ef --- /dev/null +++ b/src/main/php/lang/reflection/Closure.class.php @@ -0,0 +1,95 @@ +reflect= $arg; + } else { + $this->reflect= new ReflectionFunction($arg); + } + } + + /** 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))) + ; + } + + /** + * 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); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/reflection/Member.class.php b/src/main/php/lang/reflection/Member.class.php index 568b394..2dcb808 100755 --- a/src/main/php/lang/reflection/Member.class.php +++ b/src/main/php/lang/reflection/Member.class.php @@ -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; /** diff --git a/src/main/php/lang/reflection/Parameter.class.php b/src/main/php/lang/reflection/Parameter.class.php index cb11475..43d560f 100755 --- a/src/main/php/lang/reflection/Parameter.class.php +++ b/src/main/php/lang/reflection/Parameter.class.php @@ -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; diff --git a/src/main/php/lang/reflection/Parameters.class.php b/src/main/php/lang/reflection/Parameters.class.php index 6854099..66d6f8b 100755 --- a/src/main/php/lang/reflection/Parameters.class.php +++ b/src/main/php/lang/reflection/Parameters.class.php @@ -11,7 +11,7 @@ class Parameters implements \IteratorAggregate { private $method; - /** @param ReflectionMethod $method */ + /** @param ReflectionFunctionAbstract $method */ public function __construct($method) { $this->method= $method; } diff --git a/src/main/php/lang/reflection/Target.class.php b/src/main/php/lang/reflection/Target.class.php new file mode 100755 index 0000000..7303045 --- /dev/null +++ b/src/main/php/lang/reflection/Target.class.php @@ -0,0 +1,7 @@ +compoundName(), $cause); $this->target= $target; } /** Returns the target member */ - public function target(): Member { return $this->target; } + public function target(): Target { return $this->target; } } \ No newline at end of file diff --git a/src/test/php/lang/reflection/unittest/ClosureTest.class.php b/src/test/php/lang/reflection/unittest/ClosureTest.class.php new file mode 100755 index 0000000..ff9bc83 --- /dev/null +++ b/src/test/php/lang/reflection/unittest/ClosureTest.class.php @@ -0,0 +1,90 @@ + true]; + yield [function() { return true; }]; + yield [new ReflectionFunction('strtoupper')]; + } + + #[Test, Values(from: 'callables')] + public function can_create($callable) { + new Closure($callable); + } + + #[Test] + public function no_comment() { + Assert::null((new Closure(fn() => true))->comment()); + } + + #[Test] + public function with_comment() { + Assert::equals('Test', (new Closure(/** Test */ fn() => true))->comment()); + } + + #[Test] + public function empty_parameters() { + $parameters= (new Closure(fn() => true))->parameters(); + Assert::equals(0, $parameters->size()); + } + + #[Test] + public function parameters() { + $parameters= (new Closure(fn($name) => true))->parameters(); + Assert::equals(1, $parameters->size()); + } + + #[Test, Values([0, 'name'])] + public function parameter($arg) { + $parameter= (new Closure(fn($name) => true))->parameter($arg); + Assert::equals('name', $parameter->name()); + Assert::false($parameter->optional()); + } + + #[Test] + public function optional_parameter() { + $parameters= (new Closure(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 Closure(fn($name) => true))->returns(); + Assert::false($constraint->present()); + Assert::equals(Type::$VAR, $constraint->type()); + } + + #[Test] + public function return_type() { + $constraint= (new Closure(fn($name): bool => true))->returns(); + Assert::true($constraint->present()); + Assert::equals(Primitive::$BOOL, $constraint->type()); + } + + #[Test] + public function invoke() { + $closure= (new Closure(fn($name) => strtoupper($name))); + Assert::equals('TEST', $closure->invoke(['test'])); + } + + #[Test] + public function invoke_named() { + $closure= (new Closure(fn($name) => strtoupper($name))); + Assert::equals('TEST', $closure->invoke(['name' => 'test'])); + } + + #[Test, Expect(CannotInvoke::class)] + public function invoke_missing_argument() { + $closure= (new Closure(fn($name) => strtoupper($name))); + $closure->invoke([]); + } +} \ No newline at end of file From a139ede5c3ee507dbe63afb119dd4c724937c3a6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 20:57:38 +0100 Subject: [PATCH 02/10] Rename Closure -> Invocable --- ...{Closure.class.php => Invocable.class.php} | 2 +- ...Test.class.php => InvocableTest.class.php} | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) rename src/main/php/lang/reflection/{Closure.class.php => Invocable.class.php} (98%) rename src/test/php/lang/reflection/unittest/{ClosureTest.class.php => InvocableTest.class.php} (60%) diff --git a/src/main/php/lang/reflection/Closure.class.php b/src/main/php/lang/reflection/Invocable.class.php similarity index 98% rename from src/main/php/lang/reflection/Closure.class.php rename to src/main/php/lang/reflection/Invocable.class.php index e3011ef..eee2b9b 100755 --- a/src/main/php/lang/reflection/Closure.class.php +++ b/src/main/php/lang/reflection/Invocable.class.php @@ -3,7 +3,7 @@ use ReflectionException, ReflectionFunction, ArgumentCountError, TypeError, Throwable; use lang\{Reflection, Type}; -class Closure implements Target { +class Invocable implements Target { private $reflect; /** @param string|Closure|ReflectionFunction $arg */ diff --git a/src/test/php/lang/reflection/unittest/ClosureTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php similarity index 60% rename from src/test/php/lang/reflection/unittest/ClosureTest.class.php rename to src/test/php/lang/reflection/unittest/InvocableTest.class.php index ff9bc83..fa9be64 100755 --- a/src/test/php/lang/reflection/unittest/ClosureTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -1,11 +1,11 @@ true))->comment()); + Assert::null((new Invocable(fn() => true))->comment()); } #[Test] public function with_comment() { - Assert::equals('Test', (new Closure(/** Test */ fn() => true))->comment()); + Assert::equals('Test', (new Invocable(/** Test */ fn() => true))->comment()); } #[Test] public function empty_parameters() { - $parameters= (new Closure(fn() => true))->parameters(); + $parameters= (new Invocable(fn() => true))->parameters(); Assert::equals(0, $parameters->size()); } #[Test] public function parameters() { - $parameters= (new Closure(fn($name) => true))->parameters(); + $parameters= (new Invocable(fn($name) => true))->parameters(); Assert::equals(1, $parameters->size()); } #[Test, Values([0, 'name'])] public function parameter($arg) { - $parameter= (new Closure(fn($name) => true))->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 Closure(fn($name= null) => true))->parameters(); + $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 Closure(fn($name) => true))->returns(); + $constraint= (new Invocable(fn($name) => true))->returns(); Assert::false($constraint->present()); Assert::equals(Type::$VAR, $constraint->type()); } #[Test] public function return_type() { - $constraint= (new Closure(fn($name): bool => true))->returns(); + $constraint= (new Invocable(fn($name): bool => true))->returns(); Assert::true($constraint->present()); Assert::equals(Primitive::$BOOL, $constraint->type()); } #[Test] public function invoke() { - $closure= (new Closure(fn($name) => strtoupper($name))); - Assert::equals('TEST', $closure->invoke(['test'])); + $Invocable= (new Invocable(fn($name) => strtoupper($name))); + Assert::equals('TEST', $Invocable->invoke(['test'])); } #[Test] public function invoke_named() { - $closure= (new Closure(fn($name) => strtoupper($name))); - Assert::equals('TEST', $closure->invoke(['name' => 'test'])); + $Invocable= (new Invocable(fn($name) => strtoupper($name))); + Assert::equals('TEST', $Invocable->invoke(['name' => 'test'])); } #[Test, Expect(CannotInvoke::class)] public function invoke_missing_argument() { - $closure= (new Closure(fn($name) => strtoupper($name))); - $closure->invoke([]); + $Invocable= (new Invocable(fn($name) => strtoupper($name))); + $Invocable->invoke([]); } } \ No newline at end of file From b8c2f5b8b4437d3b238ac253548b77580ece6549 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:02:24 +0100 Subject: [PATCH 03/10] QA: Use imports for global classes --- src/main/php/lang/Reflection.class.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/Reflection.class.php b/src/main/php/lang/Reflection.class.php index 7c70851..392c980 100755 --- a/src/main/php/lang/Reflection.class.php +++ b/src/main/php/lang/Reflection.class.php @@ -1,5 +1,6 @@ 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]); } } From 9483a5b4d39abf175466eab3ea4a4b460ae3a6be Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:12:01 +0100 Subject: [PATCH 04/10] Add Invocable::name() --- src/main/php/lang/reflection/Invocable.class.php | 9 +++++++++ .../reflection/unittest/InvocableTest.class.php | 13 +++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/reflection/Invocable.class.php b/src/main/php/lang/reflection/Invocable.class.php index eee2b9b..7ab0e28 100755 --- a/src/main/php/lang/reflection/Invocable.class.php +++ b/src/main/php/lang/reflection/Invocable.class.php @@ -15,6 +15,15 @@ public function __construct($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.'()'; } diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index fa9be64..6024e0a 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -9,10 +9,10 @@ class InvocableTest { /** @return iterable */ private function callables() { - yield ['strtoupper']; - yield [fn() => true]; - yield [function() { return true; }]; - yield [new ReflectionFunction('strtoupper')]; + yield ['strtoupper', 'strtoupper']; + yield [fn() => true, null]; + yield [function() { return true; }, null]; + yield [new ReflectionFunction('strtoupper'), 'strtoupper']; } #[Test, Values(from: 'callables')] @@ -20,6 +20,11 @@ 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()); From f8dd92d548bdc5b43d3fda83b30ec9a0351d4e30 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:23:25 +0100 Subject: [PATCH 05/10] Allow creating Invocable instances from callables --- src/main/php/lang/reflection/Invocable.class.php | 6 ++++-- .../php/lang/reflection/unittest/InvocableTest.class.php | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/reflection/Invocable.class.php b/src/main/php/lang/reflection/Invocable.class.php index 7ab0e28..6496270 100755 --- a/src/main/php/lang/reflection/Invocable.class.php +++ b/src/main/php/lang/reflection/Invocable.class.php @@ -1,15 +1,17 @@ reflect= $arg; + } else if (is_array($arg)) { + $this->reflect= new ReflectionFunction(Closure::fromCallable($arg)); } else { $this->reflect= new ReflectionFunction($arg); } diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index 6024e0a..d3a4e26 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -1,6 +1,6 @@ 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']; } From 6e7bb7d6742030af2d7b376b3772d25b80a908c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:46:19 +0100 Subject: [PATCH 06/10] Add annotation support for invocables --- .../php/lang/meta/MetaInformation.class.php | 2 +- .../php/lang/reflection/Invocable.class.php | 15 +++++++++++++ .../unittest/InvocableTest.class.php | 22 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/meta/MetaInformation.class.php b/src/main/php/lang/meta/MetaInformation.class.php index 57f792c..7d18a02 100755 --- a/src/main/php/lang/meta/MetaInformation.class.php +++ b/src/main/php/lang/meta/MetaInformation.class.php @@ -7,7 +7,7 @@ * it to another source otherwise. */ class MetaInformation { - private $annotations; + public $annotations; public function __construct($annotations) { $this->annotations= $annotations; diff --git a/src/main/php/lang/reflection/Invocable.class.php b/src/main/php/lang/reflection/Invocable.class.php index 6496270..67d5116 100755 --- a/src/main/php/lang/reflection/Invocable.class.php +++ b/src/main/php/lang/reflection/Invocable.class.php @@ -5,6 +5,7 @@ class Invocable implements Target { private $reflect; + private $annotations= null; /** @param callable|ReflectionFunction $arg */ public function __construct($arg) { @@ -41,6 +42,20 @@ public function comment() { ; } + /** @return lang.reflection.Annotations */ + public function annotations() { + $this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofType($this->reflect); + return new Annotations($this->annotations); + } + + /** @return ?lang.reflection.Annotation */ + public function annotation(string $type) { + $this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofType($this->reflect); + + $t= strtr($type, '.', '\\'); + return isset($this->annotations[$t]) ? new Annotation($t, $this->annotations[$t]) : null; + } + /** * Looks up a parameter * diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index d3a4e26..6952446 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -38,6 +38,28 @@ public function with_comment() { Assert::equals('Test', (new Invocable(/** Test */ fn() => true))->comment()); } + #[Test] + public function no_annotations() { + Assert::equals([], iterator_to_array((new Invocable(fn() => true))->annotations())); + } + + #[Test] + public function with_annotation() { + Assert::equals( + 'test', + (new Invocable(#[Test] fn() => true))->annotation(Test::class)->name() + ); + } + + #[Test] + public function with_annotations() { + $annotations= []; + foreach ((new Invocable(#[Test, Values] fn() => true))->annotations() as $annotation) { + $annotations[]= $annotation->name(); + } + Assert::equals(['test', 'values'], $annotations); + } + #[Test] public function empty_parameters() { $parameters= (new Invocable(fn() => true))->parameters(); From 7aceae21e1acffac26386a5f20cb95adfb5ffb03 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:53:20 +0100 Subject: [PATCH 07/10] Annotations on invocables raise an exception in PHP 7 --- src/main/php/lang/meta/FromAttributes.class.php | 5 +++++ src/main/php/lang/meta/FromSyntaxTree.class.php | 5 +++++ src/main/php/lang/reflection/Invocable.class.php | 4 ++-- .../php/lang/reflection/unittest/InvocableTest.class.php | 7 ++++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/meta/FromAttributes.class.php b/src/main/php/lang/meta/FromAttributes.class.php index 4c77402..c973d7b 100755 --- a/src/main/php/lang/meta/FromAttributes.class.php +++ b/src/main/php/lang/meta/FromAttributes.class.php @@ -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); diff --git a/src/main/php/lang/meta/FromSyntaxTree.class.php b/src/main/php/lang/meta/FromSyntaxTree.class.php index 6a1b0a5..7892d40 100755 --- a/src/main/php/lang/meta/FromSyntaxTree.class.php +++ b/src/main/php/lang/meta/FromSyntaxTree.class.php @@ -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()); diff --git a/src/main/php/lang/reflection/Invocable.class.php b/src/main/php/lang/reflection/Invocable.class.php index 67d5116..e38d46e 100755 --- a/src/main/php/lang/reflection/Invocable.class.php +++ b/src/main/php/lang/reflection/Invocable.class.php @@ -44,13 +44,13 @@ public function comment() { /** @return lang.reflection.Annotations */ public function annotations() { - $this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofType($this->reflect); + $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->ofType($this->reflect); + $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; diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index 6952446..1a419dc 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -3,6 +3,7 @@ use Closure, ReflectionFunction; use lang\reflection\{Invocable, CannotInvoke}; use lang\{Type, Primitive}; +use test\verify\Runtime; use test\{Assert, Expect, Test, Values}; class InvocableTest { @@ -38,12 +39,12 @@ public function with_comment() { Assert::equals('Test', (new Invocable(/** Test */ fn() => true))->comment()); } - #[Test] + #[Test, Runtime(php: '>=8.0')] public function no_annotations() { Assert::equals([], iterator_to_array((new Invocable(fn() => true))->annotations())); } - #[Test] + #[Test, Runtime(php: '>=8.0')] public function with_annotation() { Assert::equals( 'test', @@ -51,7 +52,7 @@ public function with_annotation() { ); } - #[Test] + #[Test, Runtime(php: '>=8.0')] public function with_annotations() { $annotations= []; foreach ((new Invocable(#[Test, Values] fn() => true))->annotations() as $annotation) { From 596d3cedeca9dabeb9abf3bfc694a43b3706be13 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:55:46 +0100 Subject: [PATCH 08/10] PHP 7.4 syntax --- .../reflection/unittest/InvocableTest.class.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index 1a419dc..01405b4 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -46,16 +46,22 @@ public function no_annotations() { #[Test, Runtime(php: '>=8.0')] public function with_annotation() { - Assert::equals( - 'test', - (new Invocable(#[Test] fn() => true))->annotation(Test::class)->name() - ); + $closure= + #[Test] + fn() => true + ; + Assert::equals('test', (new Invocable($closure))->annotation(Test::class)->name()); } #[Test, Runtime(php: '>=8.0')] public function with_annotations() { + $closure= + #[Test, Values] + fn() => true + ; + $annotations= []; - foreach ((new Invocable(#[Test, Values] fn() => true))->annotations() as $annotation) { + foreach ((new Invocable($closure))->annotations() as $annotation) { $annotations[]= $annotation->name(); } Assert::equals(['test', 'values'], $annotations); From 3bb212d11e12c7ea6722173fa785be37a6c430cb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:57:53 +0100 Subject: [PATCH 09/10] Wrap closure annotations in `eval()` --- .../lang/reflection/unittest/InvocableTest.class.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index 01405b4..0b3fd0d 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -46,19 +46,13 @@ public function no_annotations() { #[Test, Runtime(php: '>=8.0')] public function with_annotation() { - $closure= - #[Test] - fn() => true - ; + $closure= eval('return #[Test] fn() => true;'); Assert::equals('test', (new Invocable($closure))->annotation(Test::class)->name()); } #[Test, Runtime(php: '>=8.0')] public function with_annotations() { - $closure= - #[Test, Values] - fn() => true - ; + $closure= eval('return #[Test, Values] fn() => true;'); $annotations= []; foreach ((new Invocable($closure))->annotations() as $annotation) { From 01cffa2a08d6deedbf03856555a2f803b5f37dca Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Nov 2026 21:59:15 +0100 Subject: [PATCH 10/10] Fully qualifiy annotation types --- src/test/php/lang/reflection/unittest/InvocableTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php index 0b3fd0d..97588eb 100755 --- a/src/test/php/lang/reflection/unittest/InvocableTest.class.php +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -46,13 +46,13 @@ public function no_annotations() { #[Test, Runtime(php: '>=8.0')] public function with_annotation() { - $closure= eval('return #[Test] fn() => true;'); + $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, Values] fn() => true;'); + $closure= eval('return #[\\test\\Test, \\test\\Values] fn() => true;'); $annotations= []; foreach ((new Invocable($closure))->annotations() as $annotation) {