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!
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
Hi!
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
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]
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
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]
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?
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]
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:
https://github.com/php-fig/event-dispatcher-util/blob/master/src/ParameterDeriverTrait.php
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
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