rfc:marking_overriden_methods

PHP RFC: Marking overridden methods (#[\Override])

Introduction

When implementing an interface or inheriting from another class PHP performs various checks to ensure that implemented methods are compatible with the constraints imposed by the interface or parent class. However there is one thing it cannot check: Intent.

PHP verifies that the signature of implemented methods is compatible with a given interface or an overridden method from a parent class but it cannot check whether a method is actually intended to implement the interface method or override the parent method or not and thus cannot assist the developer by detecting mistakes.

The same is true for the human reader. While the human reader is able to determine intent by carefully looking the the code and possibly the VCS history, it would certainly be simpler if the intention by the original author would be explicitly expressed in a way that ensures that the information stays up to date.

The following examples showcase possible situations where being able to express if a method is intended to override another method or implement an interface would make it easier to debug a mistake, to refactor and to clean up existing code. Another possible use case is to easily detect a possibly breaking change in a parent class that was provided by a library without needing to read the changelog in detail or missing some item in the list of changes. The examples are intended to match real-world use-cases to be more descriptive and thus include references to existing libraries or frameworks. By virtue of being an example they might not be fully functional or might not match best practices in a given ecosystem, though.

Examples

Using traits for default implementations of an interface:

<?php
 
interface Formatter {
  public function format(string $input): string;
  public function isSupported(string $input): bool;
}
 
trait DefaultFormatter {
  public function format(string $input): string
  {
    return $input;
  }
 
  public function isSupported(string $Input): bool
  {
    return true;
  }
}
 
final class LengthRestrictedFormatter {
  use DefaultFormatter;
 
  public function __construct(private int $maxLength) {}
 
  /* The name of the method was misremembered as isValid() instead of
   * isSupported(), but the default implementation in the trait will
   * satisfy the interface, leading to erroneous behavior.
   */
  public function isValid(string $input): bool
  {
    return strlen($input) < $this->maxLength;
  }
}

Inheriting from a class and intentionally overriding a method:

<?php
 
namespace MyApp\Tests;
 
use PHPUnit\Framework\TestCase;
 
final class MyTest extends TestCase
{
    protected bool $myProp;
 
    /* A typo was introduced in setUp() and this method will never be called,
     * as it is protected in a final class that does not reference it.
     */
    protected function setUpp(): void
    {
        $this->myProp = true;
    }
 
    public function testItWorks(): void
    {
        $this->assertTrue($this->myProp);
    }
}

Implementing an interface that later deprecates and removes a method:

<?php
 
interface StringValidator {
  public function validate(string $input): bool;
}
 
final class NonEmptyValidator implements StringValidator {
  public function validate(string $input): bool
  {
    return $input !== '';
  }
 
  /* Was this method part of a previous version of the interface and is
   * no longer required or is it a specific feature of NonEmptyValidator?
   * The name indicates that it likely was part of some interface, but
   * we can't be sure.
   */
  public function getIdentifierForErrorMessage(): string
  {
    return 'string_must_not_be_empty';
  }
}

Inheriting from a class that later adds a new method.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Http;
 
class RssFeed extends Model {
  /* Laravel 5.4 added the refresh() method to Eloquent, but we already
   * have a custom method with the same name and signature that does
   * something entirely different.
   */
  public function refresh()
  {
    $this->message = Http::get($this->url);
    $this->save();
  }
}

Proposal

To be able to express the intent in code, a new #[\Override] attribute shall be added. If this attribute is added to a method, the engine shall validate that a method with the same name exists in a parent class or any of the implemented interfaces. If no such method exists a compile time error shall be emitted.

  • In both the “LengthFormatter” and the “MyTest” examples, applying the attribute on the method in question would have detected the mistake and emitted an error.
  • For the “NonEmptyValidator” example the error message would indicate that the interface changed and that the method can safely be removed, because the public API of the class is meant to directly mirror the interface’s API.
  • For the “RssFeed” example the attribute would not directly be able to prevent the mistake. However an IDE or static analysis tool might emit a diagnosis to indicate that adding the attribute would now be possible after upgrading the framework might have been able to highlight the new method in the parent class. By emitting such a diagnosis, the possibly breaking change due to developers expecting the refresh() method to behave in a certain way would be implicitly detectable.

Semantics

The rule of thumb is: If changing the method signature would result in the Fatal error: Declaration of X must be compatible with Y error message, the #[\Override] attribute is satisfied and does not emit an error.

  • Public and protected methods of a parent class or implemented interface satisfy #[\Override].
    • Abstract methods satisfy #[\Override].
    • Static methods behave as instance methods.
  • Private methods of a parent class do not satisfy #[\Override], because they are no part of the externally visible API.
  • __construct() of a parent class do not satisfy #[\Override], because it's not part of the API of an already-constructed object.
  • The attribute is ignored on traits, but:
    • Abstract methods in a used trait satisfy #[\Override].
    • Regular methods in a used trait that are “shadowed” by a method in the class using the trait do not satisfy #[\Override].
    • Methods from a used trait behave as if the method definition was copied and pasted into the target class. Specifically the #[\Override] attribute on a trait method requires the existence of a matching method in a parent class or implemented interface.
  • #[\Override] works as expected on enums and anonymous classes.
  • #[\Override] works as expected on interface. A matching method needs to exist in a parent interface.

Why an attribute and not a keyword?

This RFC proposes an attribute instead of a keyword, because contrary to other modifiers (e.g. visibility) that are part of the method signature, the attribute does not affect behavior or compatibility for users that further extend a given class and neither does it affect users that call the method. It is purely an assistance to the author of a given class.

Furthermore using an attribute improves backwards compatibility, because no parser changes are required. The attribute can be added to codebases that need to support older PHP versions and existing analysis tools and IDEs will be able to make sense of the code, even if they do not understand the semantics of the attribute.

Precedent in other programming languages

Static Analysis Tools and IDEs

Once the attribute is implemented, static analysis tools and IDEs should add a diagnosis that encourages the developer to add the attribute whenever possible. By doing so, the developer would be able to detect when a method is overridden by accident, as the tool would suggest adding the attribute where the developer did not expect it to be suggested.

It goes without saying that the reverse is also true: Static analysis tools and IDEs should flag whenever the attribute is used where it would result in a Fatal Error when executing the code.

Properties

While overriding properties has similar implications as with methods, they are not part of this proposal.

As of now properties may not be part of an interface and thus only properties of a parent class can be overridden. The type of properties is enforced to be invariant and properties do not have behavior attached. A property can only ever be overridden by a compatible property with possibly added attributes.

These characteristics of properties imply that enforcing a specific property behavior is hard in the first place. Having the #[\Override] attribute would not provide an actual benefit. A parent class could introduce a property with a matching name and type, but a different purpose. However the #[\Override] attribute cannot protect against this, as it does not enforce anything about the absence of a parent property.

Examples

Valid examples

class P {
    protected function p(): void {}
}
 
class C extends P {
    #[\Override]
    public function p(): void {}
}
class Foo implements IteratorAggregate
{
    #[\Override]
    public function getIterator(): Traversable
    {
        yield from [];
    }
}
trait T {
    #[\Override]
    public function t(): void {}
}
trait T {
    #[\Override]
    public function i(): void {}
}
 
interface I {
    public function i(): void;
}
 
class Foo implements I {
    use T;
}
interface I {
    public function i();
}
 
interface II extends I {
    #[\Override]
    public function i();
}
 
class P {
    public function p1() {}
    public function p2() {}
    public function p3() {}
    public function p4() {}
}
 
class PP extends P {
    #[\Override]
    public function p1() {}
    public function p2() {}
    #[\Override]
    public function p3() {}
}
 
class C extends PP implements I {
    #[\Override]
    public function i() {}
    #[\Override]
    public function p1() {}
    #[\Override]
    public function p2() {}
    public function p3() {}
    #[\Override]
    public function p4() {}
    public function c() {}
}

Invalid examples

class C
{
    #[\Override]
    public function c(): void {} // Fatal error: C::c() has #[\Override] attribute, but no matching parent method exists
}
interface I {
    public function i(): void;
}
 
class P {
    #[\Override]
    public function i(): void {} // Fatal error: P::i() has #[\Override] attribute, but no matching parent method exists
}
 
class C extends P implements I {}
trait T {
    #[\Override]
    public function t(): void {}
}
 
class Foo {
    use T; // Fatal error: Foo::t() has #[\Override] attribute, but no matching parent method exists
}
class P {
    private function p(): void {}
}
 
class C extends P {
    #[\Override]
    public function p(): void {} // Fatal error: C::p() has #[\Override] attribute, but no matching parent method exists
}
trait T {
    public function t(): void {}
}
 
class C {
    use T;
 
    #[\Override]
    public function t(): void {} // Fatal error: C::t() has #[\Override] attribute, but no matching parent method exists
}
interface I {
    #[\Override]
    public function i(): void; // Fatal error: I::i() has #[\Override] attribute, but no matching parent method exists
}

Backward Incompatible Changes

Override can no longer be used as a class name in the global namespace. A GitHub search for “class Override ” language:php symbol:override revealed a total of 94 matches in source code. The majority of the matches are namespaced, but there are some occurrences in the global namespace.

Proposed PHP Version(s)

Next minor (8.3).

RFC Impact

To SAPIs

None.

To Existing Extensions

Extensions should possibly add the attribute to their methods where appropriate.

To Opcache

None.

New Constants

None.

php.ini Defaults

None.

Open Issues

n/a

Unaffected PHP Functionality

Any functionality that is not related to objects and classes is unaffected. Classes that do not leverage inheritance of implement interfaces are unaffected. The entire functionality is opt-in, so existing code is also unaffected.

Future Scope

Proposed Voting Choices

Implement the #[\Override] attribute as described?
Real name Yes No
alcaeus (alcaeus)  
ashnazg (ashnazg)  
brzuchal (brzuchal)  
crell (crell)  
derick (derick)  
ericmann (ericmann)  
galvao (galvao)  
girgias (girgias)  
jacques (jacques)  
kalle (kalle)  
kocsismate (kocsismate)  
mcmic (mcmic)  
mfonda (mfonda)  
nicolasgrekas (nicolasgrekas)  
nielsdos (nielsdos)  
ocramius (ocramius)  
petk (petk)  
pierrick (pierrick)  
ramsey (ramsey)  
sergey (sergey)  
svpernova09 (svpernova09)  
theodorejb (theodorejb)  
timwolla (timwolla)  
Final result: 22 1
This poll has been closed.

Patches and Tests

Implementation

References

Rejected Features

rfc/marking_overriden_methods.txt · Last modified: 2023/06/29 18:24 by timwolla