# RFC: Optional Interfaces

- Status: Declined
- Authors: Juris Evertovskis, juris@glaive.pro
- Date: 2024-12-28
- Version: 1.0
- Original: https://wiki.php.net/rfc/optional-interfaces
- Page: https://externals.io/rfc/optional-interfaces
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Add the optional interfaces feature? (closed)

Yes: 15 · No: 19

Voters: alcaeus (Yes), asgrim (No), ashnazg (Yes), brzuchal (Yes), bukka (Yes), bwoebi (Yes), cpriest (Yes), crell (Yes), cschneid (No), daniels (Yes), derick (No), dharman (No), fabpot (Yes), galvao (No), girgias (No), heiglandreas (Yes), jhdxr (No), josh (No), kalle (No), kocsismate (No), levim (No), malferov (No), nicolasgrekas (Yes), ocramius (No), pierrick (No), pollita (No), ramsey (Yes), santiagolizardo (No), sebastian (No), seld (Yes), sergey (Yes), theodorejb (No), timwolla (No), weierophinney (Yes)

## Discussions on the mailing list

- [\[RFC\] \[Discussion\] Optional interfaces](https://externals.io/message/126185.md) — started on 2024-12-28, 14 messages
- [\[VOTE\] Optional Interfaces](https://externals.io/message/126747.md) — started on 2025-03-14, 12 messages

---

## 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

- class declarations inside a conditional control structure ([Symfony](https://github.com/symfony/symfony/blob/95c43e38811a159c82112a13e631ee7121b83e54/src/Symfony/Component/Cache/Exception/CacheException.php#L17), [Laravel](https://github.com/laravel/framework/blob/a30619310dd739c22f561579c3de07f33d109612/src/Illuminate/Testing/ParallelRunner.php#L7), [League](https://github.com/thephpleague/oauth2-server-bundle/blob/3f88e38cd9e6b992119e5e415bf2603d47da2f6f/src/DependencyInjection/Security/OAuth2Factory.php#L11));
- dummy interfaces ([WordPress](https://github.com/WordPress/WordPress-Coding-Standards/blob/9f9726a01a886b3dcced5327390470a56314aa56/WordPress/Tests/NamingConventions/PrefixAllGlobalsUnitTest.1.inc#L525)) which get clumsier in namespaces ([a StackOverflow answer](https://stackoverflow.com/a/57996889/2182900));
- aliasing other interfaces ([Carbon](https://github.com/briannesbitt/Carbon/blob/a9000603ea337c8df16cc41f8b6be95a65f4d0f5/src/Carbon/Traits/Localization.php#L27));
- creating separate bridge libraries.

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

```php
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.

```php
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.

```php
interface MyInterface extends RequiredInterface, ?OptionalInterface {}
```

### Dynamically defined interfaces

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

```php
// 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:

```php
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.

```php
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.

```php
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.

```php
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:

```php
$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:

```php
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:

```php
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:

```php
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.

```php
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

- Implementation (WIP): <https://github.com/php/php-src/pull/17288>

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

- Discussion: <https://externals.io/message/126185>

## Rejected Features

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