rfc:optional-interfaces

PHP RFC: Optional Interfaces

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.

Add the optional interfaces feature?
Real name Yes No
Final result: 0 0
This poll has been 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.

rfc/optional-interfaces.txt · Last modified: 2025/03/02 19:56 by tontonsb