[RFC] [Discussion] Optional interfaces

php.internals

Juris Evertovskis

1 year ago
Hi, I would like to propose a new syntax that let's you implement an interface only if it exists. `class MyClass extends ?OptionalInterface {}` If the `OptionalInterface` exists, the class implements it. If no such interface can be found, it is ignored. https://wiki.php.net/rfc/optional-interfaces The need to declare compatibility with a certain interface without requiring the interface itself comes up when you're writing code that should work with various versions of other libraries or PHP itself and when a library provides interoperability with some other library or a PHP extension, but does not require it. Although this would mainly be used by library developers, the consumers of those libraries would also benefit as this syntax would clearly communicate the intention of optional interoperability. IDEs and static analysis tools could also have easier time with `?OptionalInterface` instead of class definitions inside an `if (interface_exists(.)) { . } else {.}`. I've not entirely sure about the naming as an "optional interface" feels like an oxymoron. I've also considered names like non-required interfaces, conditional interfaces, optional/conditional implementation or even soft interfaces/soft implementation. Please let me know if the naming itself bothers you. Previous discussion: https://externals.io/message/125967 WIP implementation: https://github.com/php/php-src/pull/17288 Looking forward to your feedback and suggestions! Juris

Larry Garfield

1 year ago
On Sat, Dec 28, 2024, at 2:56 PM, Juris Evertovskis wrote:
> Hi, > > I would like to propose a new syntax that let’s you implement an > interface only if it exists. > > `class MyClass extends ?OptionalInterface {}` > > If the `OptionalInterface` exists, the class implements it. If no such > interface can be found, it is ignored. > > https://wiki.php.net/rfc/optional-interfaces > > The need to declare compatibility with a certain interface without > requiring the interface itself comes up when you’re writing code that > should work with various versions of other libraries or PHP itself and > when a library provides interoperability with some other library or a > PHP extension, but does not require it. > > Although this would mainly be used by library developers, the consumers > of those libraries would also benefit as this syntax would clearly > communicate the intention of optional interoperability. IDEs and static > analysis tools could also have easier time with `?OptionalInterface` > instead of class definitions inside an `if (interface_exists(…)) { … } > else {…}`. > > I’ve not entirely sure about the naming as an “optional interface” > feels like an oxymoron. I’ve also considered names like non-required > interfaces, conditional interfaces, optional/conditional implementation > or even soft interfaces/soft implementation. Please let me know if the > naming itself bothers you. > > Previous discussion: https://externals.io/message/125967 > WIP implementation: https://github.com/php/php-src/pull/17288 > > Looking forward to your feedback and suggestions! > Juris
Going by the design/RFC, this seems reasonable to me, and I can see the use for it. My only concern would be if the implementation makes anything more complex for the engine or compiler that could cause future optimization problems, but I'm not qualified to answer that. It may help the RFC to include some non-trivial real-world-ish examples, in addition to the A and B stuff. (You can probably steal some from the projects linked to.) --Larry Garfield

Alexandru Pătrănescu

1 year ago
On Sat, Dec 28, 2024, 22:58 Juris Evertovskis <juris@glaive.pro> wrote:
> Hi, > > > > I would like to propose a new syntax that let’s you implement an interface > only if it exists. > > > > `class MyClass extends ?OptionalInterface {}` > > > > If the `OptionalInterface` exists, the class implements it. If no such > interface can be found, it is ignored. > > > > https://wiki.php.net/rfc/optional-interfaces > > > Hi Juris,
I think the proposal looks good but I some some points to be discussed: From what I understand, the autoloading mechanism would be triggered for the optional interface, but it would not be an error if it would not succeed. I think we should explicitly spell this out in the RFC, to make it clear that autoloading will be attempted. If an interface was not available at class declaration time, the function class_implements (on both class and object) will return false going forward, as if the class definition did not contain the interface at all? Similarly, if at runtime the object would be passed to a function/method that has the parameter type of that interface, would it fail saying that the object is not an instance of that interface? Note: autoloading is not triggered when just using an interface/class name in a parameter type or as return type. I think completely erasing the interface sounds good, I just think it should be explicitly spelled out in the RFC, and all the implications that follows. If a second class will implement the same optional interface, will it trigger autoloading again? I think it should. If in the meantime, between defining two such classes, the interface would be defined (using a hack or some sort), would it cause problems? I think that given the current existing workarounds this is a valid concern. To avoid problems, it would be wise to completely erase the interface from first class definition when it was not available, even if the second class might have it. -- Alex

Niels Dossche

1 year ago
On 28/12/2024 21:56, Juris Evertovskis wrote:
> Hi, > >   > > I would like to propose a new syntax that let’s you implement an interface only if it exists. > >   > > `class MyClass extends ?OptionalInterface {}` > >   > > If the `OptionalInterface` exists, the class implements it. If no such interface can be found, it is ignored. > >   > > https://wiki.php.net/rfc/optional-interfaces <https://wiki.php.net/rfc/optional-interfaces> > >   > > The need to declare compatibility with a certain interface without requiring the interface itself comes up when you’re writing code that should work with various versions of other libraries or PHP itself and when a library provides interoperability with some other library or a PHP extension, but does not require it. > >   > > Although this would mainly be used by library developers, the consumers of those libraries would also benefit as this syntax would clearly communicate the intention of optional interoperability. IDEs and static analysis tools could also have easier time with `?OptionalInterface` instead of class definitions inside an `if (interface_exists(…)) { … } else {…}`. > >   > > I’ve not entirely sure about the naming as an “optional interface” feels like an oxymoron. I’ve also considered names like non-required interfaces, conditional interfaces, optional/conditional implementation or even soft interfaces/soft implementation. Please let me know if the naming itself bothers you. > >   > > Previous discussion: https://externals.io/message/125967 <https://externals.io/message/125967> > > WIP implementation: https://github.com/php/php-src/pull/17288 <https://github.com/php/php-src/pull/17288> > >   > > Looking forward to your feedback and suggestions! > > Juris >
Hi I've left some remarks on your PR to help you further with the opcache stuff. This should fix the tests and answer some questions. I also left a concern regarding the caching, and in particular I'm thinking about the inheritance cache. I'll copy it here: Suppose this scenario: 1. You do a request and an optional interface is not available, this class is cached without that interface. 2. A future request comes in, and reuses the cache, but since this may be via a different code path it's possible that the interface is now available. Yet because the class was cached, this won't implement the interface now even though it's available at this point in time. I'm not entirely sure if this is a real problem but it should be checked. I also don't know what assumptions in the engine this may break. Kind regards Niels

Juris Evertovskis

1 year ago
Hey, Thanks for all the feedback! The RFC is now expanded to explicitly address the concerns on how the optional interfaces interact with type checks, eval'd interfaces, opcache, autoloading, reflection and the Override attribute. Did Did I miss something? Please let me know if anything is still unclear or requires other improvement. https://wiki.php.net/rfc/optional-interfaces I've also polished the implementation and added tests for all the concerns mentioned above. https://github.com/php/php-src/pull/17288 However the implementation is not final as I was not able (yet) to solve how my implementation interacts with the Stringable interface. BR, Juris

Ilija Tovilo

1 year ago
Hi Juris On Sun, Feb 9, 2025 at 10:16 PM Juris Evertovskis <juris@glaive.pro> wrote:
> > https://wiki.php.net/rfc/optional-interfaces
Thanks for your efforts. I've reviewed the patch, it seems there are still a few issues and it would be great if we could solve them before this goes into voting. A few thoughts on the RFC itself: You mention `#[Override]` in the RFC and how it will work on optional interfaces that are available at runtime (the `TestClass::method()` example). Given the interface is implemented as optional, it is to be expected that the interface will go away under certain configurations. This will break, because the method is no longer overridden. Hence, it might be best to also disallow `#[Override]` on optional interface methods, to signal the breaking for the missing interface scenario.
> A class only conforms to types that are actually implemented.
Just as a note, this also has some semantic implications. Notably `$object instanceof OptionalInterface` will evaluate to `false` without errors when the interface goes missing. To catch this issue with static analysis, the project will need to be analysed without the interface being available, or they will need to gain new rules that catch such expressions that can lead to inconsistent results. Also, when I read the subject "optional interfaces", what came to mind was "an interface that may or may not be implemented", i.e. similar to Objective-C's optional protocol methods [1]. I'm not sure if the naming can be improved. Overall, I'm not particularly convinced this is a big problem that needs solving, but I'll leave that up to framework designers to decide. Ilija [1] https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html#//apple_ref/doc/uid/TP40011210-CH11-SW6

Juris Evertovskis

1 year ago
Hi all, It's almost awkward to bother the list with this humble RFC amidst so many exciting proposals :) With help from Ilija and Niels the implementation is now working and tested, so the RFC seems getting closer to the voting phase. Ilija raised a point about optional interfaces not being a reliable "overridable" for `#[Override]`. I can certainly understand that concern. At the same time I think there could be value in being able to say "although all overridables are optional, this method must override something, so at least one of them must be present". This would be applicable when a package provides compatibility with multiple tools (or versions of a tool), but still requires at least one of them to be present. Besides I think that using `#[Override]` with an optional interface would be a very deliberate step so such consequences would be not only expected, but intended. I've added an example illustrating this to the RFC. Does this justification make sense or does it sound like a horrible design? Would it be better suited for a secondary vote? I haven't yet investigated how complex it would be to let Overrides ignore optional interfaces as non-existant. Does anyone have strong feelings on this or any other concerns? BR, Juris

Juris Evertovskis

1 year ago
Hey, Just a headsup that I plan to start the vote in a couple of days. Please let me know if there are concerns that might affect your stance on this proposal. https://wiki.php.net/rfc/optional-interfaces BR, Juris

Gina P. Banyard

1 year ago
On Tuesday, 11 March 2025 at 16:37, Juris Evertovskis <juris@glaive.pro> wrote: > Hey, > > Just a headsup that I plan to start the vote in a couple of days. Please let me know if there are concerns that might affect your stance on this proposal. > > https://wiki.php.net/rfc/optional-interfaces I will be voting against this. I understand why you might want this, but this seems to be a very niche requirement and messing with type level guarantees is not something I want to see. The RFCs implies that any invalid code will be caught at compile time, but this is not true. If the optional dependency is never present, you can violate the interface in whatever way you want, which you might not be doing on purpose. Moreover, this also does not help if an optional dependency changes the requirements of their interface and is present, so you would still need to do those "hacky" workarounds. (e.g. if a dependency changes how they define the interface in a major release) The fact an interface can become "available" at a later stage and classes defined prior to it being available having an optional dependency on it not implementing it is bound to be weird and unintuitive. And ultimately, this does not really solve the problem of supporting optional dependencies, or using your object in ways that are compatible with another dependency. Because it still relies on a library/framework/whatever maintainer to add, possibly infinite, optional interfaces for any new optional dependency which might want to interact with instances of your classes. As such I would prefer something which is more similar to type classes ("add type information outside of your class declaration"). Or even explicit runtime interface implementation which could for example look like: $obj implements OptionalDependency; someFunctionFromOptionalDependency($obj); which gives control to the *user* where and how class instance may interact with dependencies that are outside of your control, and that you don't even need to care of. Best regards, Gina P. Banyard >

Bob Weinand

1 year ago
Hey Gina, On 12.3.2025 06:16:15, Gina P. Banyard wrote:
> On Tuesday, 11 March 2025 at 16:37, Juris Evertovskis > <juris@glaive.pro> wrote: >> >> Hey, >> >> Just a headsup that I plan to start the vote in a couple of days. >> Please let me know if there are concerns that might affect your >> stance on this proposal. >> >> https://wiki.php.net/rfc/optional-interfaces >> > > I will be voting against this. > > I understand why you might want this, but this seems to be a very > niche requirement and messing with type level guarantees is not > something I want to see. > The RFCs implies that any invalid code will be caught at compile time, > but this is not true.
I'd assume you'd require-dev all your dependencies and see the issues at compile time.
> If the optional dependency is never present, you can violate the > interface in whatever way you want, which you might not be doing on > purpose. > Moreover, this also does not help if an optional dependency changes > the requirements of their interface and is present, so you would still > need to do those "hacky" workarounds. > (e.g. if a dependency changes how they define the interface in a major > release)
That's what version requirements are for in composer. Similarly, if a major release is released, you just implement the methods defined on the interface for all major versions you support. This does not need the hacky workarounds in any way.
> > The fact an interface can become "available" at a later stage and > classes defined prior to it being available having an optional > dependency on it not implementing it is bound to be weird and unintuitive.
This is not more or less intuitive than the current workarounds you'll have to do with "if (class_exists('interfacename'))".
> > And ultimately, this does not  really solve the problem of supporting > optional dependencies, or using your object in ways that are > compatible with another dependency. > Because it still relies on a library/framework/whatever maintainer to > add, possibly infinite, optional interfaces for any new optional > dependency which might want to interact with instances of your classes.
It does not and it does not need to. That's a wholly different feature (e.g. "when class X is declared, auto-attach trait Y and implement Z") or similar. But that doesn't preclude me from wanting to support various external libraries out-of-the box without manual interaction of the users. Which is what this RFC solves.
> As such I would prefer something which is more similar to type classes > ("add type information outside of your class declaration"). > Or even explicit runtime interface implementation which could for > example look like: > >     $obj implements OptionalDependency; >     someFunctionFromOptionalDependency($obj); > > which gives control to the *user* where and how class instance may > interact with dependencies that are outside of your control, and that > you don't even need to care of. > > Best regards, > > Gina P. Banyard
Bob

Joerg Sowa

1 year ago
Hello Juris, There is some uncertainty for me on how this approach would work with namespaces. Let's get this example: https://3v4l.org/bI1Rj I would expect to get the error message that I forgot to insert `use Stringable;`. But with your idea everything is fine. I don't like such ambiguity and I worry it may bring some confusion to developers. Kind regards, Jorg On Sat, Dec 28, 2024 at 9:57 PM Juris Evertovskis <juris@glaive.pro> wrote:

Viktor Khramov

1 year ago
Hey Juris. I have a question about this feature. At the example blow interface A
> { > public function x(Foo $foo); > } > > interface B > { > public function x(Bar $foo); > } > > class Test implements ?A, ?B > { > } >
what would happen if both interfaces existed? пт, 14 мар. 2025 г. в 17:08, Jorg Sowa <jorg.sowa@gmail.com>:

Juris Evertovskis

1 year ago
On 2025-03-14 16:24, Viktor Khramov wrote:
> Hey Juris. > > I have a question about this feature. > > At the example blow > >> interface A >> { >> public function x(Foo $foo); >> } >> >> interface B >> { >> public function x(Bar $foo); >> } >> >> class Test implements ?A, ?B >> { >> } > > what would happen if both interfaces existed?
Hi Viktor, If `A` exists then `?A` is equivalent to `A`. In your example Test will implement both A and B. If Test::x() will be incompatible with either A::x() or B::x(), you will end up with an error. BR, Juris

Juris Evertovskis

1 year ago
On 2025-03-14 16:04, Jorg Sowa wrote:
> Hello Juris, There is some uncertainty for me on how this approach > would work with namespaces. > > Let's get this example: > https://3v4l.org/bI1Rj > > I would expect to get the error message that I forgot to insert `use > Stringable;`. > But with your idea everything is fine. I don't like such ambiguity and > I worry it may bring some confusion to developers. > > Kind regards, Jorg
Hi Jorg, You are correct. We can't really distinguish whether an interface is absent because the dependency is absent or because the name was mistyped. In practice I would not expect this to be a big issue. Adding a ?OptionalInterface would be a deliberate action when your objects needs to pass a specific type check. In case of a typo the type check will fail and the object will be rejected by the consumer. I admit that it's not as nice as a compile-time error, but my point is that the typo would still be easy to notice as it would break the whole purpose of that interface implementation. BR, Juris