Skip to content

Commit 44e530d

Browse files
committed
Add Invokable interface
Analogous to the Stringable interface, Invokable is automatically implemented for classes defining __invoke(), but can also be explicitly implemented. This addresses a gap in PHP's type system: - `callable` is too broad (accepts strings, arrays) and cannot be used as a property type. - `Closure` is too narrow (excludes objects with __invoke()). Invokable provides a proper OOP type for invokable objects, enabling property types, intersection types, and instanceof checks. Key behaviors: - Marker interface with no methods to avoid LSP conflicts across varying __invoke() signatures. - Auto-implemented in three phases: compilation, trait binding, and internal class registration (mirrors Stringable exactly). - Can also be explicitly implemented: - Explicitly implementing Invokable requires an __invoke() method, or a fatal error is raised. - Abstract classes and interfaces are exempt, deferring the requirement to concrete subclasses. - Closure explicitly implements Invokable. - Invokable is covariant to callable in return type checks. - Cached `ce->__invoke` field added to zend_class_entry for consistency with other magic methods, improving runtime lookup.
1 parent 73c4690 commit 44e530d

26 files changed

+712
-7
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Abstract class can implement Invokable with abstract or concrete __invoke()
3+
--FILE--
4+
<?php
5+
6+
/* Abstract class with abstract __invoke forces signature on subclasses */
7+
abstract class TypedHandler implements Invokable {
8+
abstract public function __invoke(int $x): int;
9+
}
10+
11+
class Doubler extends TypedHandler {
12+
public function __invoke(int $x): int { return $x * 2; }
13+
}
14+
15+
var_dump(new Doubler() instanceof Invokable);
16+
var_dump((new Doubler())(5));
17+
18+
/* Abstract class with concrete __invoke */
19+
abstract class BaseHandler implements Invokable {
20+
public function __invoke(string $s): string { return strtoupper($s); }
21+
}
22+
23+
class MyHandler extends BaseHandler {}
24+
25+
var_dump(new MyHandler() instanceof Invokable);
26+
var_dump((new MyHandler())("hello"));
27+
28+
/* Abstract class implementing Invokable without __invoke at all (deferred to child) */
29+
abstract class DeferredHandler implements Invokable {}
30+
31+
class ConcreteHandler extends DeferredHandler {
32+
public function __invoke(): string { return "concrete"; }
33+
}
34+
35+
var_dump(new ConcreteHandler() instanceof Invokable);
36+
var_dump((new ConcreteHandler())());
37+
38+
?>
39+
--EXPECT--
40+
bool(true)
41+
int(10)
42+
bool(true)
43+
string(5) "HELLO"
44+
bool(true)
45+
string(8) "concrete"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
Anonymous class with __invoke() auto-implements Invokable
3+
--FILE--
4+
<?php
5+
6+
$obj = new class {
7+
public function __invoke(): string {
8+
return "anonymous";
9+
}
10+
};
11+
12+
var_dump($obj instanceof Invokable);
13+
var_dump($obj());
14+
15+
/* Anonymous class with explicit implements */
16+
$obj2 = new class implements Invokable {
17+
public function __invoke(): int {
18+
return 42;
19+
}
20+
};
21+
22+
var_dump($obj2 instanceof Invokable);
23+
var_dump($obj2());
24+
25+
?>
26+
--EXPECT--
27+
bool(true)
28+
string(9) "anonymous"
29+
bool(true)
30+
int(42)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
--TEST--
2+
Invokable is automatically implemented
3+
--FILE--
4+
<?php
5+
6+
/* Basic auto-implementation */
7+
class Test {
8+
public function __invoke(): string {
9+
return "foo";
10+
}
11+
}
12+
13+
var_dump(new Test instanceof Invokable);
14+
var_dump((new ReflectionClass(Test::class))->getInterfaceNames());
15+
16+
/* Inheritance: child inherits Invokable from parent */
17+
class Child extends Test {}
18+
19+
var_dump(new Child instanceof Invokable);
20+
var_dump((new ReflectionClass(Child::class))->getInterfaceNames());
21+
22+
/* Child overrides __invoke */
23+
class ChildOverride extends Test {
24+
public function __invoke(): string {
25+
return "bar";
26+
}
27+
}
28+
29+
var_dump(new ChildOverride instanceof Invokable);
30+
31+
/* Arbitrary signature: different params and return type */
32+
class Adder {
33+
public function __invoke(int $a, int $b): int {
34+
return $a + $b;
35+
}
36+
}
37+
38+
var_dump(new Adder instanceof Invokable);
39+
40+
/* No params, no return type */
41+
class NoSig {
42+
public function __invoke() {}
43+
}
44+
45+
var_dump(new NoSig instanceof Invokable);
46+
47+
/* Explicit + implicit: class has __invoke and writes implements Invokable */
48+
class ExplicitAndImplicit implements Invokable {
49+
public function __invoke(): void {}
50+
}
51+
52+
var_dump(new ExplicitAndImplicit instanceof Invokable);
53+
/* Should appear only once in the interface list */
54+
var_dump((new ReflectionClass(ExplicitAndImplicit::class))->getInterfaceNames());
55+
56+
?>
57+
--EXPECT--
58+
bool(true)
59+
array(1) {
60+
[0]=>
61+
string(9) "Invokable"
62+
}
63+
bool(true)
64+
array(1) {
65+
[0]=>
66+
string(9) "Invokable"
67+
}
68+
bool(true)
69+
bool(true)
70+
bool(true)
71+
bool(true)
72+
array(1) {
73+
[0]=>
74+
string(9) "Invokable"
75+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
Invokable is covariant to callable in return types
3+
--FILE--
4+
<?php
5+
6+
class Base {
7+
public function getHandler(): callable {
8+
return fn() => 1;
9+
}
10+
}
11+
12+
/* Invokable is a valid covariant return type for callable */
13+
class Child extends Base {
14+
public function getHandler(): Invokable {
15+
return new class {
16+
public function __invoke(): int {
17+
return 2;
18+
}
19+
};
20+
}
21+
}
22+
23+
$c = new Child();
24+
$handler = $c->getHandler();
25+
var_dump($handler instanceof Invokable);
26+
var_dump($handler());
27+
28+
/* Concrete class with __invoke is also covariant to callable */
29+
class Handler {
30+
public function __invoke(): int {
31+
return 3;
32+
}
33+
}
34+
35+
class Child2 extends Base {
36+
public function getHandler(): Handler {
37+
return new Handler();
38+
}
39+
}
40+
41+
$c2 = new Child2();
42+
$handler2 = $c2->getHandler();
43+
var_dump($handler2 instanceof Invokable);
44+
var_dump($handler2());
45+
46+
/* Closure is covariant to callable via Invokable */
47+
class Child3 extends Base {
48+
public function getHandler(): Closure {
49+
return fn() => 4;
50+
}
51+
}
52+
53+
$c3 = new Child3();
54+
$handler3 = $c3->getHandler();
55+
var_dump($handler3 instanceof Invokable);
56+
var_dump($handler3());
57+
58+
?>
59+
--EXPECT--
60+
bool(true)
61+
int(2)
62+
bool(true)
63+
int(3)
64+
bool(true)
65+
int(4)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Closure implements Invokable
3+
--FILE--
4+
<?php
5+
6+
/* Arrow function */
7+
$fn = fn() => 1;
8+
var_dump($fn instanceof Invokable);
9+
10+
/* Anonymous function */
11+
$fn2 = function(int $x): int { return $x * 2; };
12+
var_dump($fn2 instanceof Invokable);
13+
14+
/* Closure::fromCallable */
15+
$fn3 = Closure::fromCallable('strlen');
16+
var_dump($fn3 instanceof Invokable);
17+
18+
/* First-class callable syntax */
19+
$fn4 = strlen(...);
20+
var_dump($fn4 instanceof Invokable);
21+
22+
/* Reflection confirms Closure implements Invokable */
23+
var_dump(in_array('Invokable', (new ReflectionClass(Closure::class))->getInterfaceNames()));
24+
25+
?>
26+
--EXPECT--
27+
bool(true)
28+
bool(true)
29+
bool(true)
30+
bool(true)
31+
bool(true)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Enums with __invoke() auto-implement Invokable
3+
--FILE--
4+
<?php
5+
6+
/* Enum with __invoke auto-implements Invokable */
7+
enum Color {
8+
case Red;
9+
case Blue;
10+
public function __invoke(): string { return $this->name; }
11+
}
12+
13+
var_dump(Color::Red instanceof Invokable);
14+
var_dump((Color::Red)());
15+
16+
/* Enum explicitly implementing Invokable */
17+
enum Direction implements Invokable {
18+
case Up;
19+
case Down;
20+
public function __invoke(): int { return $this === self::Up ? 1 : -1; }
21+
}
22+
23+
var_dump(Direction::Up instanceof Invokable);
24+
var_dump((Direction::Up)());
25+
26+
?>
27+
--EXPECT--
28+
bool(true)
29+
string(3) "Red"
30+
bool(true)
31+
int(1)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Enum explicitly implementing Invokable without __invoke() causes fatal error
3+
--FILE--
4+
<?php
5+
6+
enum Color implements Invokable {
7+
case Red;
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Enum Color must have an __invoke() method to implement Invokable in %s on line %d
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
Explicit implements Invokable without __invoke() causes fatal error
3+
--FILE--
4+
<?php
5+
6+
class Bad implements Invokable {}
7+
8+
?>
9+
--EXPECTF--
10+
Fatal error: Class Bad must have an __invoke() method to implement Invokable in %s on line %d
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
Interface extending Invokable
3+
--FILE--
4+
<?php
5+
6+
interface MyInvokable extends Invokable {}
7+
8+
/* Class implementing MyInvokable must have __invoke */
9+
class Good implements MyInvokable {
10+
public function __invoke(): void {}
11+
}
12+
13+
var_dump(new Good() instanceof Invokable);
14+
var_dump(new Good() instanceof MyInvokable);
15+
16+
var_dump((new ReflectionClass(Good::class))->getInterfaceNames());
17+
18+
?>
19+
--EXPECT--
20+
bool(true)
21+
bool(true)
22+
array(2) {
23+
[0]=>
24+
string(11) "MyInvokable"
25+
[1]=>
26+
string(9) "Invokable"
27+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
Invokable in intersection types
3+
--FILE--
4+
<?php
5+
6+
interface Loggable {
7+
public function getLog(): string;
8+
}
9+
10+
class LoggingHandler implements Loggable {
11+
public function __invoke(): string {
12+
return "result";
13+
}
14+
public function getLog(): string {
15+
return "log entry";
16+
}
17+
}
18+
19+
function process(Invokable&Loggable $handler): void {
20+
echo $handler() . "\n";
21+
echo $handler->getLog() . "\n";
22+
}
23+
24+
process(new LoggingHandler());
25+
26+
/* Object that is Invokable but not Loggable should be rejected */
27+
class PlainHandler {
28+
public function __invoke(): string {
29+
return "plain";
30+
}
31+
}
32+
33+
try {
34+
process(new PlainHandler());
35+
} catch (TypeError $e) {
36+
echo "TypeError caught\n";
37+
}
38+
39+
?>
40+
--EXPECT--
41+
result
42+
log entry
43+
TypeError caught

0 commit comments

Comments
 (0)