PHP RFC: Sealed Classes


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.


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.

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

namespace Psl\Result {
  sealed interface ResultInterface permits Success, Failure { ... }
  final class Success implements ResultInterface { ... }
  final class Failure implements ResultInterface { ... }
  function wrap(callable $callback): ResultInterface { ... }
  function unwrap(ResultInterface $result): mixed
    return match($result::class) {
      Result\Success::class => $result->value(),
      Result\Failure::class => throw $result->error(),
    }; // no need for default, it's not possible.
namespace App {
  use Psl\Result;
  // Fatal error: Class App\Maybe cannot implement sealed interface Psl\Result\ResultInterface.
  final class Maybe implements Result\ResultInterface {}

Similarly, a trait that is sealed can only be used by the classes named in the `permits` clause.

This is an example taken from the Symfony Cache component

namespace Symfony\Component\Cache\Traits {
  use Symfony\Component\Cache\Adapter\FilesystemAdapter;
  use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter;
  use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
  sealed trait FilesystemCommonTrait permits FilesystemTrait, PhpFilesAdapter { ... }
  sealed trait FilesystemTrait permits FilesystemAdapter, FilesystemTagAwareAdapter {
   use FilesystemCommonTrait; // ok
namespace Symfony\Component\Cache\Adapter {
   use Symfony\Component\Cache\Traits\FilesystemTrait;
   final class FilesystemAdapter {
     use FilesystemTrait; // ok
   final class FilesystemTagAwareAdapter {
     use FilesystemTrait; // ok
namespace App\Cache {
    use Symfony\Component\Cache\Traits\FilesystemTrait;
    // Error: Class App\Cache\MyFilesystemCache may not use sealed trait (Symfony\Component\Cache\Traits\FilesystemTrait)
    final class MyFilesystemAdapter {
      use FilesystemTrait;
    // Error: Trait App\Cache\MyFilesystemTrait may not use sealed trait (Symfony\Component\Cache\Traits\FilesystemTrait)
    trait MyFilesystemTrait {
      use FilesystemTrait;


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 {}

Backward Incompatible Changes

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

Proposed PHP Version(s)

PHP 8.1

RFC Impact

To Opcache


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 sealed 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.


rfc/sealed_classes.txt · Last modified: 2021/04/26 10:24 by azjezz