Optional Interfaces View as Markdown

Declined
Authors
Juris Evertovskis, juris@glaive.pro
Date
2024-12-28
Version
1.0
Source
wiki.php.net/rfc/optional-interfaces

Vote

Add the optional interfaces feature? closed
Yes
15
No
19
34 voters
alcaeus Yes 2025-03-20 07:50
asgrim No 2025-03-15 06:49
ashnazg Yes 2025-03-26 15:07
brzuchal Yes 2025-03-14 17:50
bukka Yes 2025-03-14 09:49
bwoebi Yes 2025-03-14 14:46
cpriest Yes 2025-03-14 15:38
crell Yes 2025-03-15 16:41
cschneid No 2025-03-26 09:26
daniels Yes 2025-03-28 11:45
derick No 2025-03-17 11:31
dharman No 2025-03-15 16:08
fabpot Yes 2025-03-18 15:36
galvao No 2025-03-16 18:39
girgias No 2025-03-14 22:43
heiglandreas Yes 2025-03-14 08:38
jhdxr No 2025-03-25 17:04
josh No 2025-03-18 19:20
kalle No 2025-03-14 08:14
kocsismate No 2025-03-21 10:13
levim No 2025-03-26 18:03
malferov No 2025-03-23 05:08
nicolasgrekas Yes 2025-03-14 08:18
ocramius No 2025-03-14 11:20
pierrick No 2025-03-27 15:09
pollita No 2025-03-24 17:35
ramsey Yes 2025-03-24 15:43
santiagolizardo No 2025-03-24 21:15
sebastian No 2025-03-15 09:32
seld Yes 2025-03-17 09:25
sergey Yes 2025-03-14 09:45
theodorejb No 2025-03-19 01:09
timwolla No 2025-03-17 19:07
weierophinney Yes 2025-03-15 16:41

Discussions on the mailing list

[RFC] [Discussion] Optional interfaces
by Juris Evertovskis · 14 messages
[VOTE] Optional Interfaces
by Juris Evertovskis · 12 messages

RFC text

Copied from wiki.php.net, last synchronized Oct 13, 2026 11:52 UTC

Introduction

PHP's implements keyword allows classes to declare that they implement one or more interfaces. This mechanism is strict: the specified interfaces must exist at runtime. While this strictness is usually beneficial, it can create challenges when developing libraries that need to interoperate with other optional libraries or different versions of libraries and when creating projects that need to be compatible with multiple PHP versions and usable with or without some PHP extensions.

In such scenarios developers resort to workarounds like

These solutions introduce complexity and a maintenance burden. The conditional declarations may sometimes confuse IDEs and static analysis tools that pick up the multiple definitions. If an optional interface is mocked by a dummy, the implementing class contains no indication of that and misleads the reader into thinking that an actual interface with that name is defined in a meaningful way.

Proposal

By prefixing an interface name with ?, a class can optionally implement an interface. If the interface exists, the class will implement it; otherwise, the declaration will be ignored without causing an error.

This allows the developer to clearly announce that the class is interoperable with an interface, but does not require the interface per se.

Example 1: Implementing an interface if it exists

namespace MyNamespace;
 
use ExternalNamespace\TheInterface;
 
class MyClass implements ?TheInterface
{
    // ...
}

If \ExternalNamespace\TheInterface exists, MyClass will implement it. It will behave as if the class was declared as class MyClass implements TheInterface.

If it doesn't exist, MyClass will work as if it was declared without implements ?TheInterface.

Example 2: Optional interfaces in a list of interfaces

Optional interfaces can appear on an interface list together with required interfaces in any order. The ? applies only to the name directly following the token.

class MyClass1 implements A, B, ?C, ?D {}
class MyClass2 implements ?C, ?D, A, B {}
class MyClass3 implements A, ?C, ?D, B {}
class MyClass4 implements ?C, A, B, ?D {}

All of these classes implement and require interfaces A and B while the interfaces C and D will only be implemented if they exist.

Example 3: Extending optional interfaces

An interface can also be optional when it's in an extends list of another interface. The list of extandable interface names works just like the implements list for classes.

interface MyInterface extends RequiredInterface, ?OptionalInterface {}

Dynamically defined interfaces

The presence of optional interfaces is evaluated when the class is defined.

// This class does not implement OptionalInterface as it doesn't exist.
class OldClass implements ?OptionalInterface {}
$oldClass = new OldClass;
 
eval('interface OptionalInterface {}');
 
// This class implements OptionalInterface as it does exist now
class NewClass implements ?OptionalInterface {}
 
// The instance of OldClass is unaffected
class_implements($oldclass); // []
 
// The definition of OldClass is not affected either
// New instances will still NOT implement the interface that didn't exist when the class was defined
$oldClass2 = new OldClass;
class_implements($oldClass2)); // []

Interface implementation

The presence of ? makes interface “optional” only in the sense that it is skipped if the interface does not exist. It does not make the implementation rules softer or “optional” in any other sense.

If an optional interface exists, the implementation is checked as it is with non-optional interfaces. This code would still be invalid:

interface Iface
{
    public function someMethod();
}
 
class NaughtyClass implements ?Iface {}
 
// Fatal error: Class NaughtyClass contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Iface::someMethod) in ...

If a method is marked as an Override, it must override or implement something in an interface that is actually implemented.

interface ExistingInterface
{
    public function method();
}
 
class TestClass implements ?ExistingInterface, ?NonExistantInterface
{
    #[\Override]
    public function method() {} // This is fine as it implements an interface method
 
    #[\Override]
    public function other() {} // This is still not allowed
}
 
// Fatal error: TestClass::other() has #[\Override] attribute, but no matching parent method exists in ...

By using #[\Override] while only specifying optional interfaces you are enforcing that at least one of those interfaces must be present.

class Signable implements ?NewLib\SignableInterface, ?OldVersion_SignableInterface
{
    #[\Override]
    public function getDoc(): string
    {
        return $this->doc;
    }
}

Type checks

A class only conforms to types that are actually implemented.

interface ExistingInterface {}
 
class TestClass implements ?ExistingInterface, ?NonExistantInterface {}
 
function f1(ExistingInterface $x) { echo "F1"; }
function f2(NonExistantInterface $x) { echo "F2"; }
 
$c = new TestClass;
 
f1($c); // F1
f2($c); // Fatal error: Uncaught TypeError: f2(): Argument #1 ($x) must be of type NonExistantInterface, TestClass given, called in ...

Example 4: Use case — compatibility with an optional library

Consider a package that allows one to build DB expressions with an OOP syntax. On it's own it could be used via PDO like this:

$expression = new ColumnExpression('url')->unaccent()->append(new StringExpression('/path'));
 
$statement = $pdo->prepare("
    update links
    set url = $expression
    where id = ?
");
 
// merge bindings with other bindings
$statement->execute([...$expression->bindings, 123]);

But to use these expressions in Laravel's Eloquent, we would have to implement their Expression interface. But we don't want to Laravel (or illuminate/database) as a dependency of our package because it makes perfect sense without it or with any other ORM. We have to find a workaround.

One of the possibilities observed in the wild is to extract the actual implementation of our expression class to a BaseExpressionImplementation trait and do this:

use Illuminate\Contracts\Database\Query\Expression as LaravelExpression;
 
if (interface_exists(LaravelExpression::class)) {
    /**
     * Class description
     */
    class BaseExpression implements \Stringable, LaravelExpression
    {
        use BaseExpressionImplementation;
    }
} else {
    /**
     * Class description
     */
    class BaseExpression implements \Stringable
    {
        use BaseExpressionImplementation;
    }
}

However by utilizing optional interfaces one can create this class in a more clear and straightforward way:

use Illuminate\Contracts\Database\Query\Expression as LaravelExpression;
 
/**
 * Class description
 */
class BaseExpression implements \Stringable, ?LaravelExpression
{
    // The actual implementation
}

Example 5: Use case — compatibility with multiple versions

Consider you're working on the perfect collections' package. You support all the array access, traversing, jsonization and so on. But suddenly here comes PHP 8.14 and it introduces a couple of new interfaces. And your audience really wants you to support the features of Xmlable and Yamlable.

What do you do? Should you drop the PHP 8.13 support and add the interfaces? Maintain two versions? Do some kind of conditional class definition like in Example 4? If your library had some kind of booting process, you could define dummy Xmlable and Yamlable if they don't exist, but the users just create the instances of your collection and that's it.

With optional interfaces you can add optional support to the optional features:

class Collection implements ArrayAccess, Iterator, Serializable, Countable, JsonSerializable, Enumable, ?Xmlable, ?Yamlable
{
    // the implementation here
}

Backward Incompatible Changes

None.

Classes that do not use the ? token in the interface list will function as before. Before this RFC the ? token in interface lists is not valid, therefore a runnable code that would be affected does not exist.

Proposed PHP Version(s)

Next PHP 8.x.

RFC Impact

To Opcache

Opcache will store the interface names along with their optionality information. This means that a cached class will implement an optional interface once it exists even if the interface didn't exist during the request when the class was cached. The opposite is also true — if an optional interface is no longer loaded during the next request, a cached class will stop implementing it.

To Reflection API

This RFC does not add the optionality information in the Reflection API. Reflection will only list the interfaces that are actually implemented.

interface IfaceA {}
 
class C implements ?IfaceA, ?IfaceB {}
 
$reflection = new ReflectionClass('C');
$reflection->getInterfaceNames(); // ['IfaceA']

To Autoloading

The presence of the ? token in the interface list does not affect autoloading. If an optional interface is not loaded, the autoloader will be invoked. An optional interface is only skipped if it still doesn't exist after the autoloading.

Proposed Voting Choices

Yes or no vote. 2/3 required to pass.

Voting started on 2025-03-14 and will close on 2025-03-29 00:00:00 UTC.

Vote: Add the optional interfaces feature? — Yes: 15 · No: 19 (closed)

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References

Rejected Features

Keep this updated with features that were discussed on the mail lists.