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
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
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 https://3v4l.org/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] https://github.com/php/php-src/pull/15492
[2] https://github.com/php/php-src/issues/18161
[3] https://wiki.php.net/rfc/deprecate_partially_supported_callables
[4] https://github.com/Girgias/unify-typing-modes-rfc
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 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
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