Skip to content

Add Invokable interface#21574

Open
aldemeery wants to merge 1 commit intophp:masterfrom
aldemeery:invokable-interface
Open

Add Invokable interface#21574
aldemeery wants to merge 1 commit intophp:masterfrom
aldemeery:invokable-interface

Conversation

@aldemeery
Copy link
Copy Markdown

TLDR;

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.

Background

This has been a known gap for a while. The 2008 RFC (https://wiki.php.net/rfc/invokable) never landed, and the discussion resurfaced in PR #15492 when @ondrejmirtes asked about extending Closure's callable covariance to all __invoke() objects. @Girgias's reply at the time was direct: "we'd need a Stringable equivalent for __invoke()". The same sentiment was echoed by her and @Crell on the PHP Foundation Discourse in November 2024. The idea had community endorsement but no implementation.

This PR is that implementation.


What this adds

An Invokable marker interface, auto-implemented for any class that defines __invoke(), following the Stringable pattern precisely. The auto-implementation works in three phases: at compilation for user-space classes, at trait binding for __invoke() inherited from traits, and at internal class registration. Closure explicitly declares implements Invokable in its stub. This is necessary because its __invoke() is generated per-instance and never appears in the function table.

Invokable also satisfies covariant return type checks against callable. Rather than adding Closure as a separate special case alongside this, Closure's existing covariance was unified under Invokable: it satisfies the check because it implements the interface, not through a parallel path.

A side fix included here: __invoke was the only major magic method without a cached pointer in zend_class_entry. This adds zend_function *__invoke to the struct, consistent with every other magic method.


On the marker interface design

@iluuu1994 raised this in a related discussion: the interface could be a marker that the engine implements but that users cannot implement manually, since without a declared signature it's arguably meaningless. I think there's a better middle ground.

The reason __invoke() can't be declared in the interface is LSP. Unlike __toString(): string, there is no signature compatible with all valid __invoke() implementations. But that doesn't mean explicit implementation is pointless. This PR allows users to write implements Invokable explicitly and enforces that they actually have __invoke(), with a fatal error if they don't. Abstract classes and interfaces are exempt, deferring enforcement to concrete subclasses.

This gives explicit implements Invokable real meaning: it's self-documenting, statically analysable, and engine-enforced. More importantly, it enables typed callable contracts through interface extension:

interface RequestHandler extends Invokable {
    public function __invoke(Request $req): Response;
}

Here Invokable is the base, and RequestHandler pins down the signature. This is the pattern that makes the interface genuinely useful beyond instanceof checks, something neither pure duck typing nor a non-implementable marker would allow.


What this unlocks

// Property types
class Pipeline {
    public function __construct(private Invokable $handler) {}
}

// Intersection types
function process(Invokable&Loggable $handler): void {}

// Covariant return types
class Child extends Base {
    public function getHandler(): Invokable { ... } // parent returns callable
}

Backward compatibility

None broken. Classes with __invoke() gain the interface silently, same as Stringable in 8.0.


Tests

15 new test files: auto-implementation, inheritance, traits, Closure, explicit implementation, enforcement, property types, callable covariance, type declarations, interface extension, abstract classes, anonymous classes, intersection types, enums, and visibility edge cases.


RFC

I'm working through the RFC submission process and will post to internals shortly. Submitting the implementation early to give reviewers something concrete to discuss.

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.
@aldemeery aldemeery force-pushed the invokable-interface branch from 44e530d to 6c84a12 Compare March 29, 2026 21:06
@ondrejmirtes
Copy link
Copy Markdown
Contributor

This is basically callable&object. If the PHP type system allowed this intersection (PHPStan’s does) then we would not need Invokable 😊

@aldemeery
Copy link
Copy Markdown
Author

This is basically callable&object. If the PHP type system allowed this intersection (PHPStan’s does) then we would not need Invokable 😊

That's a fair point.

But even then, callable can't appear in property types regardless of intersections...that's a fundamental constraint, not a type system gap. public callable&object $handler would still be invalid. Or am I missing something?

@iluuu1994
Copy link
Copy Markdown
Member

callable is disallowed in property types due to visibility constraints. I.e. we can't guarantee the value is callable in all contexts. This would not apply to __invoke, which must be public. So I think this constraint could be ignored when combined with object or some other named type, though I have not verified.

@aldemeery
Copy link
Copy Markdown
Author

callable is disallowed in property types due to visibility constraints

Ooh, TIL!

I think there are still a couple of things an interface gives you that callable&object can't, though:

  1. instanceof checks: Invokable gives you a real type to check against.
  2. Extensibility: interface RequestHandler extends Invokable lets you build typed callable contracts with fixed signatures, which you can't do with a type system construct.

@Girgias
Copy link
Copy Markdown
Member

Girgias commented Mar 29, 2026

I don't really think this is a good approach as an interface would constraint the signature of the __invoke() magic method. And I'm not sure what the actual benefit of an Invokable over the callable type provides. After 6 years of having Stringable I think us adding this was somewhat of a mistake and didn't really solve any problems, it just created new ones.

This new interface suffers the same exact problem that callable has, in that it doesn't tell you how you're meant to call the variable, or what the call would return. And fixing this requires function types, not a marker interface.

Moreover, recent features in PHP make it extremely easy to grab a Closure and return it, or pass it to a function parameter or property.

And if the "solution" about signatures and LSP is having a marker interface that doesn't enforce the signature of __invoke(), then we are just messing around with the language semantics for no good reason.

It really shouldn't matter how one obtains the callable/Closure.

@aldemeery
Copy link
Copy Markdown
Author

I don't really think this is a good approach as an interface would constraint the signature of the __invoke() magic method. And I'm not sure what the actual benefit of an Invokable over the callable type provides. After 6 years of having Stringable I think us adding this was somewhat of a mistake and didn't really solve any problems, it just created new ones.

This new interface suffers the same exact problem that callable has, in that it doesn't tell you how you're meant to call the variable, or what the call would return. And fixing this requires function types, not a marker interface.

Moreover, recent features in PHP make it extremely easy to grab a Closure and return it, or pass it to a function parameter or property.

And if the "solution" about signatures and LSP is having a marker interface that doesn't enforce the signature of __invoke(), then we are just messing around with the language semantics for no good reason.

It really shouldn't matter how one obtains the callable/Closure.

Really appreciate the thoughtful feedback, and please correct me if I'm reasoning about this wrong.

Your point about function types being the real long-term solution resonates with me...that would be a far more powerful answer to the signature problem, and I wouldn't argue against it.

Where I'm a little unsure is whether the signature problem is what this PR is actually being evaluated against. As far as I can tell, neither Closure nor callable tell you the signature of what you're calling either, and yet both are genuinely useful types.

The "you don't know how to call it" limitation exists across all three (callable, Closure, and Invokable), and function types would be the right fix for all three equally.

What this PR is trying to do is much narrower than that. It's not trying to solve the signature problem...it's trying to fill the gap between Closure and callable. A proper OOP type for invokable objects that you can use in property types, instanceof checks, and type hierarchies. That's the only claim it makes.

If that gap isn't considered worth filling, that's a completely fair position and I'd genuinely like to understand why.

I just want to make sure the disagreement is about that specific question, rather than holding this to a standard that Closure and callable themselves don't meet.

@Girgias
Copy link
Copy Markdown
Member

Girgias commented Mar 30, 2026

Yes, I don't think it is a gap worth filling, similar to how I feel that Stringable (and more broadly strict_types) was a mistake to be introduced.

It is extremely easy to create a Closure since we have the First Class Callable some_function_or_method(...) syntax. Returning a Closure and typing it as such is therefore extremely easy.

For inputs, it shouldn't matter what sort of "callable" you are provided, especially if you are going to call it immediately.

The only reason for Stringable to exists is because strict_types exists, and when enabled prevents objects with a __toString() method to be passed to string types. This causes nonsensical design choices of "should I mark my parameters as string|Stringable or not" when it should just always be string and let the engine do the type juggling.

The proposal of adding an Invokable interfaces reproduces this exact same mistake. Why should you care that the callable is an object with an invoke method? Why can it not be an array or be a string? You as the consumer of such an argument shouldn't care how it is represented. You shouldn't even be trying to call $f->__invoke().

The reason why callable cannot be used in property types currently is because of visibility issues, as Ilija has already said (see https://3v4l.org/hCpiG for an example), but primarily because of partially supported callables that have been deprecated in PHP 8.2. Because those partially supported callables don't just depend on where they are created but also where they are used.

When thoses partially supported callables are removed in PHP 9, it seems very feasible to allow callable to be used as a property type by effectively converting any "legacy" (array, string, object with __invoke methods) callables into a Closure object during the type check, thus removing any scope visibility behaviour from callables created within methods. (As an aside I firmly believe this behaviour would reduce the engine complexity around callables)

I also don't think that a magic interface that does effectively nothing is good language design, and that this is trying to fix a "problem" in a way that is going to cause more problems down the line rather than wait for the proper solution of fixing the callable type. Similar to the introduction of Stringable, where the proper solution IMHO is to unify PHP's typing modes.

Moreover, there will be a technical BC break in that classes that currently don't implement any interface and do not use traits but have an __invoke() method will no longer be early bindable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants