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.
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.
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
.
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.
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 {}
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)); // []
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; } }
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 ...
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 }
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 }
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.
Next PHP 8.x.
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.
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']
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.
Yes or no vote. 2/3 required to pass.
After the project is implemented, this section should contain
Keep this updated with features that were discussed on the mail lists.