Conversation
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.
44e530d to
6c84a12
Compare
|
This is basically |
That's a fair point. But even then, |
|
|
Ooh, TIL! I think there are still a couple of things an interface gives you that
|
|
I don't really think this is a good approach as an interface would constraint the signature of the This new interface suffers the same exact problem that Moreover, recent features in PHP make it extremely easy to grab a And if the "solution" about signatures and LSP is having a marker interface that doesn't enforce the signature of 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 The "you don't know how to call it" limitation exists across all three ( 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 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 |
|
Yes, I don't think it is a gap worth filling, similar to how I feel that It is extremely easy to create a 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 The proposal of adding an The reason why When thoses partially supported callables are removed in PHP 9, it seems very feasible to allow 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 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 |
TLDR;
Analogous to the
Stringableinterface,Invokableis automatically implemented for classes defining__invoke(), but can also be explicitly implemented.This addresses a gap in PHP's type system:
callableis too broad (accepts strings, arrays) and cannot be used as a property type.Closureis too narrow (excludes objects with__invoke()).Invokableprovides a proper OOP type for invokable objects, enabling property types, intersection types, and instanceof checks.Key behaviors:
__invoke()signatures.Stringableexactly).Invokablerequires an__invoke()method, or a fatal error is raised.callablein return type checks.ce->__invokefield added tozend_class_entryfor 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
Invokablemarker interface, auto-implemented for any class that defines__invoke(), following theStringablepattern 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.Closureexplicitly declaresimplements Invokablein its stub. This is necessary because its__invoke()is generated per-instance and never appears in the function table.Invokablealso satisfies covariant return type checks againstcallable. Rather than adding Closure as a separate special case alongside this, Closure's existing covariance was unified underInvokable: it satisfies the check because it implements the interface, not through a parallel path.A side fix included here:
__invokewas the only major magic method without a cached pointer inzend_class_entry. This addszend_function *__invoketo 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 writeimplements Invokableexplicitly 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 Invokablereal meaning: it's self-documenting, statically analysable, and engine-enforced. More importantly, it enables typed callable contracts through interface extension:Here
Invokableis the base, andRequestHandlerpins down the signature. This is the pattern that makes the interface genuinely useful beyondinstanceofchecks, something neither pure duck typing nor a non-implementable marker would allow.What this unlocks
Backward compatibility
None broken. Classes with
__invoke()gain the interface silently, same asStringablein 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.