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.

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(Result\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;
    }
}

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

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

References

rfc/sealed_classes.1619379865.txt.gz · Last modified: 2021/04/25 19:44 by azjezz