[PHP-DEV] RFC proposal: Invokable interface

Hi all,

I’d like to propose an Invokable interface, the Stringable equivalent for __invoke().

The idea has come up a few times over the years (most recently in the PR #15492 discussion, where Gina suggested this exact approach) but never had a concrete implementation.

I’ve put one together: https://github.com/php/php-src/pull/21574

It follows the Stringable pattern: auto-implemented for any class defining __invoke(), explicitly implementable with enforcement, and covariant to callable in return type checks.

I’m working on a formal RFC and would love feedback before posting it.

I’d also like to request wiki karma to create the RFC page — my wiki username is aldemeery.

Thanks,

Osama Aldemeery

Hi!

On 30/03/2026 00:42, Osama Aldemeery wrote:

It follows the Stringable pattern: auto-implemented for any class defining __invoke(), explicitly implementable with enforcement, and covariant to callable in return type checks.

Stringable is just a normal interface that can be used directly as any other regular interface, it's just also applied magically.

Invokable cannot be a regular interface because the signature for __invoke is not fixed, so it's a "weird" interface with magic behavior like Throwable and Iterable. What is common about these weird interfaces is that they can't be directly implemented by user. I don't feel like breaking this rule is a good idea for something with no clear benefits.

Second thing is that magic methods are not supposed to be used directly, so having __invoke is just an implementation detail, the consumer code should not check for that, that's a code smell.

Anton

Anton,

Thank you for taking the time to respond.
This is really helpful feedback, and I genuinely think the distinction you’re drawing is an important one to get right.

I think the parallel with Stringable holds more closely than it might seem.
If we put them side by side:

  • Both are automatically implemented by the engine for classes that define the corresponding magic method.

  • Both can be explicitly implemented by users.

  • Both enforce the presence of a magic method. Stringable enforces __toString(), Invokable enforces __invoke().

  • The argument that magic methods are just implementation details holds true for both.
    The only difference is that __toString() has a fixed signature, so Stringable can enforce it through a normal interface declaration. __invoke() doesn’t have a fixed signature, so Invokable uses an enforcement handler instead.
    Different mechanism, same contract. The difference is caused by the variable signature, not by any fundamental difference in what the interface represents.

The comparison to Throwable is interesting, but as far as I understand (and please correct me if I am wrong), Throwable restricts direct user implementation by design to keep the exception hierarchy clean and predictable.

That said, I could be wrong about any of this, and in which case I would genuinely appreciate you correcting my reasoning.

Regards,

Osama Aldemeery

On Mon, Mar 30, 2026 at 12:52 AM Anton Smirnov <sandfox@sandfox.me> wrote:

Hi!

On 30/03/2026 00:42, Osama Aldemeery wrote:

It follows the Stringable pattern: auto-implemented for any class
defining __invoke(), explicitly implementable with enforcement, and
covariant to callable in return type checks.

Stringable is just a normal interface that can be used directly as any
other regular interface, it’s just also applied magically.

Invokable cannot be a regular interface because the signature for
__invoke is not fixed, so it’s a “weird” interface with magic behavior
like Throwable and Iterable. What is common about these weird interfaces
is that they can’t be directly implemented by user. I don’t feel like
breaking this rule is a good idea for something with no clear benefits.

Second thing is that magic methods are not supposed to be used directly,
so having __invoke is just an implementation detail, the consumer code
should not check for that, that’s a code smell.

Anton

On 30 March 2026 01:32:24 BST, Osama Aldemeery <aldemeery@gmail.com> wrote:

The only difference is that __toString() has a fixed signature, so
Stringable can enforce it through a normal interface declaration.
__invoke() doesn't
have a fixed signature, so Invokable uses an enforcement handler instead.
Different mechanism, same contract. The difference is caused by the
variable signature, not by any fundamental difference in what the interface
represents.

I think that is a very fundamental difference.

Given $foo instanceof Stringable, the user knows they can write (string)$foo and the object will do something - that's already a pretty weak contract, in my eyes, but it is a contract.

Given $foo instanceof Invokable, the user knows even less. They know they can invoke the object somehow, but there's a fair chance that $foo() will fail because of mandatory parameters.

Can you give an example where this very loose contract would be useful?

Regards,

Rowan Tommins
[IMSoP]

On Mon, Mar 30, 2026, at 09:37, Rowan Tommins [IMSoP] wrote:

On 30 March 2026 01:32:24 BST, Osama Aldemeery <aldemeery@gmail.com> wrote:

The only difference is that __toString() has a fixed signature, so
Stringable can enforce it through a normal interface declaration.
__invoke() doesn’t
have a fixed signature, so Invokable uses an enforcement handler instead.
Different mechanism, same contract. The difference is caused by the
variable signature, not by any fundamental difference in what the interface
represents.

I think that is a very fundamental difference.

Given $foo instanceof Stringable, the user knows they can write (string)$foo and the object will do something - that’s already a pretty weak contract, in my eyes, but it is a contract.

Given $foo instanceof Invokable, the user knows even less. They know they can invoke the object somehow, but there’s a fair chance that $foo() will fail because of mandatory parameters.

Can you give an example where this very loose contract would be useful?

Regards,

Rowan Tommins
[IMSoP]

I’d prefer if the interface forced an empty argument set. You can add arguments so long as they’re optional. But generally speaking, there is no need for arguments on an invokable class – that is what constructors and properties are for.

— Rob

On 30 March 2026 08:58:03 BST, Rob Landers <rob@bottled.codes> wrote:

I'd prefer if the interface forced an empty argument set. You can add arguments so long as they're optional. But generally speaking, there is no need for arguments on an invokable class -- that is what constructors and properties are for.

An invokable class can be used for all the same things as any other callable - I've seen them used for event handlers, middleware, comparison for sorting, and so on.

A type check for "callable with no arguments and mixed/unspecified return" would be a simple special case of the often-discussed "callable with specified signature"; but I don't think that's what's proposed here.

Regards,

Rowan Tommins
[IMSoP]

On Monday 30 March 2026 02:32:24 (+02:00), Osama Aldemeery wrote:

> > That said, I could be wrong about any of this, and in which case I would
> genuinely appreciate you correcting my reasoning.

Before that you wrote earlier that this has come up multiple times before. As far as I understand the idea, has it been explored with an interface ordinarily declared in PHP scripts, and if so, what were the outcomes?

And thinking, stringable before PHP's exclusive internal use came out of PHP scripts, namely framework scripts from Symfony land IIRC. If you draw the relation to that interface, was there a recent review if/how it went? Would be interesting as we now should have more data.

What I can say for sure is that stringable was an annoyance after it went internal as now everything with a __toString() method had to be declared anonymous or with it's own interface exported upfront. No big deal if you find an internal class you can extend from and re-use it, but was not always pretty and it's not possible to extend from Closure and apart from that I'm not creative enough right know to remember another one with __invoke(). (might be a non-issue with anonymous classes, so merely food for thought, not reasoning against or for it here)

So how sharp is the axe before we start with the woodwork? (from your point of view)

-- hakre

On Monday 30 March 2026 10:37:49 (+02:00), Rowan Tommins [IMSoP] wrote:

> On 30 March 2026 08:58:03 BST, Rob Landers <rob@bottled.codes> wrote:
> >I'd prefer if the interface forced an empty argument set. You can add arguments so long as they're optional. But generally speaking, there is no need for arguments on an invokable class -- that is what constructors and properties are for.
> > An invokable class can be used for all the same things as any other callable - I've seen them used for event handlers, middleware, comparison for sorting, and so on.

When you spotted those, were they also with such a generic invocable interface as in this idea in their interface hierarchy?

On 30 March 2026 10:51:22 BST, Hans Krentel <hanskrentel@yahoo.de> wrote:

When you spotted those, were they also with such a generic invocable interface as in this idea in their interface hierarchy?

If they had any interfaces at all, I would expect them to be specific to the use case. As I said earlier, I can't see how you would use an interface that asserted just the existence of the method without any of its signature.

Regards,

Rowan Tommins
[IMSoP]

On Mon, Mar 30, 2026, at 6:21 AM, Rowan Tommins [IMSoP] wrote:

On 30 March 2026 10:51:22 BST, Hans Krentel <hanskrentel@yahoo.de> wrote:

When you spotted those, were they also with such a generic invocable interface as in this idea in their interface hierarchy?

If they had any interfaces at all, I would expect them to be specific
to the use case. As I said earlier, I can't see how you would use an
interface that asserted just the existence of the method without any of
its signature.

Regards,

Rowan Tommins
[IMSoP]

This has sort of come up in designing the compose operator that I've been working on, on-and-off. I'd want to allow Closure + Closure to return a closure, but also allow Invokable + Closure, Closure + Invokable, and Invokable + Invokable to return a closure. But right now there is no obvious way (to me at least) to detect an Invokable object and treat it accordingly. So that's where it might be useful to me, at least.

However, that's an engine level check, not one that would make sense in user-space. In user space, I'm not entirely sure where you would want this and not *also* want a use-case-specific interface.

I've done quite a bit of dynamic dispatch work lately, and is_callable() and method_exists('__invoke') have sufficed for what I need.

As an older example:

I'm sure someone will argue it could be simplified with more modern assumptions, but for the sake of this thread, how would an Invokable interface make that code cleaner?

--Larry Garfield

On Sunday, 29 March 2026 at 22:45, Osama Aldemeery <aldemeery@gmail.com> wrote:

Hi all,

I'd like to propose an Invokable interface, the Stringable equivalent for __invoke().

The idea has come up a few times over the years (most recently in the PR #15492 discussion, where Gina suggested this exact approach) but never had a concrete implementation.

I've put one together: Add `Invokable` interface by aldemeery · Pull Request #21574 · php/php-src · GitHub

It follows the Stringable pattern: auto-implemented for any class defining __invoke(), explicitly implementable with enforcement, and covariant to callable in return type checks.

I'm working on a formal RFC and would love feedback before posting it.
I'd also like to request wiki karma to create the RFC page — my wiki username is aldemeery

I already replied on the PR, but I am very much *not* in favour of this proposal.
My comment in PR #15492 [1] specifically says that we don't have such an interface, and I don't want to add duck typing as this goes against PHP's nominal typing system.
The discussion around such an interface happened in PR #18161 [2].
However, Tim noted that we cannot have an interface the defines the signature of __invoke().
And then Ilija suggested a marker interface similar to Throwabale.

In the year since this discussion has happened I've had time to think and the reason I never even attempted to move this forward is that this is, IMHO, repeating the same mistake of `Stringable`.

The only reason for `Stringable` to exists is that `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.
One shouldn't care that the callable is an object with an invoke method? Why could it not be a callable array or be a callable string?
As the consumer of such an argument the representation of a callable shouldn't matter.

The main argument seems to be that the callable type cannot be used on property types.
There are 2 reasons why this is the case.
The one most people know is that string/array callables is because there are scope visibility implications.
(See Online PHP editor | output for hCpiG for an example.)
However, the **primary** reason is because of partially supported callables that have been deprecated in PHP 8.2. [3]
Those partially supported callables don't just depend on *where* they are created but also where they are *used*.

When those partially supported callables are removed in PHP 9, it seems very feasible to allow `callable` to be used as a property type.
The mechanism to do so would be to *effectively* convert any "legacy" (array, string, object with __invoke methods) callables into a `Closure` object during the type check.
This would remove any scope visibility issues from callables created within methods.
(As an aside I firmly believe this behaviour would reduce the engine complexity around callables)

Moreover, I don't believe 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.
Similarly to how I feel about the introduction of `Stringable`, where the proper solution IMHO is to unify PHP's typing modes. [4]

Finally, with all the syntactic improvement in PHP, creating a Closure object to go deal with the current limitations of the callable type feels like a non-issue.

Best regards,
Gina P. Banyard
[1] Zend: Make Closure covariant to callable by Girgias · Pull Request #15492 · php/php-src · GitHub
[2] in return type class with __invoke method is not a callable · Issue #18161 · php/php-src · GitHub
[3] PHP: rfc:deprecate_partially_supported_callables
[4] GitHub - Girgias/unify-typing-modes-rfc: PHP RFC: Unify PHP's typing modes · GitHub

I honestly don’t feel there’s a need for this, as we already have the callable type. It shouldn’t matter if the callable is an object defining __invoke(), a function, a closure, or a first class callable. If you need to restrict to an object that’s invokable, you can do that already via (object&callable). Further, PHPStan, Psalm, PHPCS, and php-cs-fixer all understand annotations around the callable type already to further define the allowed signature (e.g., number and types of arguments, return value). This is a solved problem already.

Further, creating an interface for this is hugely difficult, if not impossible, due to the return value associated with the interface, as implementations will inevitably conflict with the Liskov Substitution Principle. If we say “void”, you cannot declare an implementation with a return value. If you define mixed, you cannot have a void or never return.

···

Matthew Weier O’Phinney
mweierophinney@gmail.com
https://mwop.net/
he/him

On Thursday 02 April 2026 16:23:03 (+02:00), Matthew Weier O'Phinney wrote:

> If you define mixed, you cannot have a void or never return.

We can't speak for Liskov (only she can), but as far as stable PHP is concerned never is actually fine, and should also stand a common interpretation of LSP, as never is the bottom type and henceforth a specialization of mixed. (Some may go for void, too, but that is far more abstract and does not work conceptually in PHP if I'm not mistaken.)

Furthermore, the PHP runtime also guarantees that any function can never return, not only by the never return type.

So it's at least irrelevant to implement a new throwable via an invocable mixed interface or never, the invocation in the runtime guarantees the higher contract to the return:

     <?php
          interface invocable {
         public function __invoke(): mixed;
     }
          $o = new class implements invocable {
         public function __invoke(): never {}
     };
          $o();

Interface violation normally yields a compile error, not a type error, nowadays.

So if you think that is an error, the discussion should have brought something to the table.

-- hakre