rfc:sealed_classes

This is an old revision of the document!


PHP RFC: Sealed Classes

Introduction

The purpose of inheritance is code reuse, for when you have a class that shares common functionality, and you want others to be able to extend it and make use of this functionality in their own class.

However, when you have a class in your code base that shares some implementation detail between 2 or more other objects, your only protection against others making use of this class is to add `@internal` annotation, which doesn't offer any runtime guarantee that no one is extending this object.

Internally, PHP has the `Throwable` interface, which defines common functionality between `Error` and `Exception` and is implemented by both, however, end users are not allowed to implement `Throwable`.

Currently PHP has a special case for `Throwable`, and what this RFC is proposing is to make this kind of functionally possible to end users as well, so that `Throwable` is not a spacial case anymore.

Proposal

Support for sealed classes is added through a new modifier `sealed`, and a new `permits` clause that takes place after `extends`, and `implements`.

sealed class Shape permits Circle, Square, Rectangle {}
 
final class Circle extends Shape {} // ok
final class Square extends Shape {} // ok
final class Rectangle extends Shape {} // ok
 
final class Triangle extends Shape {} // Fatal error: Class Triangle cannot extend sealed class Shape.

Similarly, an interface that is sealed can be implemented directly only by the classes named in the `permits` clause.

namespace Foo {
  sealed interface ExceptionInterface permits RuntimeException, InvalidArgumentException {}
 
  final class BarException extends \Exception implements ExceptionInterface {}
  final class BazException extends \Exception implements ExceptionInterface {}
}
 
namespace Qux {
  use Foo\ExceptionInterface;
 
  // Fatal error: Class Qux\QuuxException cannot implement sealed class Foo\ExceptionInterface.
  final class QuuxException extends \Exception implements ExceptionInterface {}
}
 
namespace Corge {
  use Foo\ExceptionInterface;
  use Foo\BarException;
  use Foo\BazException;
 
  try {
    grault();
  } catch(ExceptionInterface $e) {
    echo match($e::class) {
      BarException::class => 'bar exception',
      BazException::class => 'baz exception',
    }; // no need for default case, it's not possible.
  }
}

Syntax

Some people might be against introducing a new keyword into the language, which will lead to `sealed` and `permits` not being a valid class names anymore, therefor, a second vote will take place to decide which syntax should be used.

The available options are the following:

1. using `sealed`+`permits`:

sealed class Foo permits Bar, Baz {}
 
sealed interface Qux permits Quux, Quuz {}
 
sealed trait Corge permits Grault, Garply {}

2. using `permits` only:

class Foo permits Bar, Baz {}
 
interface Qux permits Quux, Quuz {}
 
trait Corge permits Grault, Garply {}

3. using pre-reserved `for` keyword:

class Foo for Bar, Baz {}
 
interface Qux for Quux, Quuz {}
 
trait Corge for Grault, Garply {}

4. using `Sealed` attribute

#[Sealed(Bar::class, Baz::class)]
class Foo {}
 
#[Sealed(Quux::class, Quuz::class)]
interface Qux {}
 
#[Sealed(Grault::class, Garply::class)]
trait Corge {}

Backward Incompatible Changes

`sealed` and `permits` become reserved keywords in PHP 8.1

Proposed PHP Version(s)

PHP 8.1

RFC Impact

To Opcache

TBD

To Reflection

The following additions will be made to expose the new flag via reflection:

  • New constant ReflectionClass::IS_SEALED to expose the bit flag used for locked classes
  • The return value of ReflectionClass::getModifiers() will have this bit set if the class being reflected is sealed
  • Reflection::getModifierNames() will include the string “sealed” if this bit is set
  • A new ReflectionClass::isSealed() method will allow directly checking if a class is sealed
  • A new ReflectionClass::getPermittedClasses() method will return the list of class names allowed in the `permits` clause.

Proposed Voting Choices

As this is a language change, a 2/3 majority is required.

Patches and Tests

Links to any external patches and tests go here.

If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed.

Make it clear if the patch is intended to be the final patch, or is just a prototype.

For changes affecting the core language, you should also provide a patch for the language specification.

References

rfc/sealed_classes.1619298812.txt.gz · Last modified: 2021/04/24 21:13 by azjezz