Sealed Class View as Markdown

Declined
Authors
Saif Eddin Gmati azjezz@protonmail.com, Joe Watkins krakjoe@php.net
Date
2021-04-24
Target
PHP 8.2
Source
wiki.php.net/rfc/sealed_classes

Votes

Accept sealed classes RFC? closed
Yes
16
No
11
27 voters
asgrim Yes 2022-03-17 14:59
ashnazg Yes 2022-03-27 15:16
beberlei No 2022-03-17 20:32
brzuchal Yes 2022-03-17 08:10
crell Yes 2022-03-28 20:19
danack Yes 2022-03-23 17:54
derick Yes 2022-03-21 10:31
diegopires Yes 2022-03-17 14:40
galvao No 2022-03-24 06:58
heiglandreas Yes 2022-03-24 18:58
irker Yes 2022-03-17 13:49
kalle No 2022-03-17 20:01
kocsismate No 2022-03-17 13:26
mcmic No 2022-03-30 14:45
nicolasgrekas No 2022-03-17 10:21
ocramius Yes 2022-03-17 10:45
patrickallaert No 2022-03-18 11:24
pollita No 2022-03-18 17:58
ramsey Yes 2022-03-18 23:29
santiagolizardo Yes 2022-03-25 17:04
sergey Yes 2022-03-22 09:58
stas No 2022-03-17 22:32
svpernova09 Yes 2022-03-22 15:33
thekid Yes 2022-03-31 16:02
theodorejb No 2022-03-17 12:28
weierophinney Yes 2022-03-17 11:09
zimt No 2022-03-19 17:31
Which syntax option do you prefer? closed
`sealed` + `permits`
12
`permits` only
15
`for`
0
27 voters
asgrim `sealed` + `permits` 2022-03-17 14:57
ashnazg `sealed` + `permits` 2022-03-27 15:16
brzuchal `sealed` + `permits` 2022-03-17 08:10
bwoebi `permits` only 2022-03-17 10:18
crell `sealed` + `permits` 2022-03-28 20:20
cschneid `permits` only 2022-03-17 13:53
danack `permits` only 2022-03-23 17:54
derick `sealed` + `permits` 2022-03-21 10:31
diegopires `permits` only 2022-03-17 14:40
galvao `permits` only 2022-03-24 06:59
heiglandreas `sealed` + `permits` 2022-03-24 18:59
ilutov `sealed` + `permits` 2022-03-17 09:04
irker `permits` only 2022-03-17 13:49
kalle `permits` only 2022-03-17 20:02
marandall `permits` only 2022-03-17 20:56
mcmic `sealed` + `permits` 2022-03-30 14:45
nicolasgrekas `permits` only 2022-03-17 10:40
ocramius `sealed` + `permits` 2022-03-17 10:46
petk `permits` only 2022-03-28 15:27
pollita `permits` only 2022-03-18 17:58
ramsey `sealed` + `permits` 2022-03-18 23:31
santiagolizardo `sealed` + `permits` 2022-03-25 17:04
sergey `permits` only 2022-03-22 09:58
svpernova09 `permits` only 2022-03-22 15:33
thekid `permits` only 2022-03-31 16:02
theodorejb `permits` only 2022-03-17 12:28
weierophinney `sealed` + `permits` 2022-03-17 11:11

Discussions on the mailing list

[RFC][Draft] Sealed Classes
by Saif Eddin Gmati · 73 messages
[RFC][Draft] Sealed Classes
by Saif Eddin Gmati · 2 messages
[RFC] [Under Discussion] Sealed Classes
by Saif Eddin Gmati · 4 messages
[RFC] [VOTE] Sealed Classes
by Saif Eddin Gmati · 6 messages

RFC text

Copied from wiki.php.net, last synchronized Oct 13, 2026 11:52 UTC

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 `DateTimeInterface` ), this RFC is proposing to make this kind of functionally possible to user land.

Proposal

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

A class that is sealed can be extended directly only by the classes named in the attribute value list. Similarly, an interface, or a trait that is sealed can be implemented, or used directly only by the classes named in the permit clause value list. Classes named in the permit clause value list can themselves be extended arbitrarily unless they are final or also sealed. In this way, sealing provides a single-level restraint on inheritance.

sealed interface Option permits Some, None { ... }
 
interface Some extends Option { ... } // ok
interface None extends Option { ... } // ok
 
interface Maybe extends Option { ... } // Fatal error: Interface Maybe cannot extend sealed interface Option.

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 { ... }
 
  class Success implements ResultInterface { ... }
  class Failure implements ResultInterface { ... }
 
  function wrap(callable $callback): ResultInterface { ... }
 
  function unwrap(ResultInterface $result): mixed
  {    
    return match(true) {
      $result instanceof Success => $result->value(),
      $result instanceof Failure => 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 PSL Channel component

namespace Psl\Channel\Internal {
  use Psl\Channel\ReceiverInterface;
 
  sealed trait ChannelSideTrait permits BoundedReceiver, BoundedSender, UnboundedReceiver, UnboundedSender { ... }
 
  // OK
  final class BoundedReceiver implements ReceiverInterface
  {
    use ChannelSideTrait;
 
    ...
  }
 
  // OK
  final class UnboundedReceiver implements ReceiverInterface
  {
    use ChannelSideTrait;
 
    ...
  }
}
 
namespace App\Channel {
    use Psl\Channel\Internal\ChannelSideTrait;
 
    // Fatal error: Class App\Channel\MyReceiver may not use sealed trait Psl\Channel\Internal\ChannelSideTrait.
    final class MyReceiver {
      use ChannelSideTrait;
 
      ...
    }
 
    // Fatal error: Trait App\Channel\MyReceiver may not use sealed trait Psl\Channel\Internal\ChannelSideTrait.
    trait MyChannelSideTrait {
      use ChannelSideTrait;
 
      ...
    }
}

When a sealed class, trait, or an interface permits a specific interface, any class can use it as long as that interface exists in it's inheritance tree, as follows:

This also applies when sealing to a specific class, or a trait.

interface Foo {}
 
sealed interface Bar permits Foo {}
 
class FooImpl implements Foo {}
 
class Baz extends FooImpl {}
 
// `BarImpl` is allowed to implement `Bar` because it extends `Baz`, which
// extends `FooImpl`, which implement `Foo`, making `BarImpl` an instance of `Foo`.
class BarImpl extends Baz implements Bar {}

Sealed classes with no `permits` clause, or with an empty `permits` clause will result in a parse error, as follows:

// Parse error: syntax error, unexpected '{', expecting permits (T_PERMITS) in ...
sealed class Bar { }
 
// Parse error: syntax error, unexpected '{', expecting identifier (T_STRING) or namespace (T_NAMESPACE) or \\ (T_NS_SEPARATOR) in ...
sealed class Bar permits { }

Why not composite type aliases

Some have suggested that the use of composite type aliases could solve the same problem, such as:

<?php
 
final class Success { ... }
final class Failure { ... }
 
type Result = Success | Failure;
 
function foo(): Result { ... }
 
$result = foo();
if ($result instanceof Success) {
  // $result is Success
} else {
  // $result is Failure
}

However, a type alias of N types, is not the same as a sealed class that permits N sub-types, as sealing offers 2 major differences.

1. shared functionality

Sealed classes feature allow you to implement functionalities in the parent class, such as:

<?php

sealed abstract class Result permits Success, Failure
{
  public function then(Closure $success, Closure $failure): Result
  {
      try {
        $result = $this instanceof Success ? $success($this->value) : $failure($this->throwable);

        return new Success($result);
      } catch(Throwable $e) {
        return new Failure($e);
      }
  }

  public function catch(Closure $failure): Result
  {
    return $this->then(fn($value) => $value, $failure);
  }

  public function map(Closure $success): Result
  {
    return $this->then(
      $success,
      fn($exception) => throw $exception
    );
  }
}

final class Success extends Result {
  public function __construct(
    public readonly mixed $value,
  ) {}
}

final class Failure extends Result {
  public function __construct(
    public readonly Throwable $throwable,
  ) {}
}

2. the N+1 type.

Unlike a type alias, a sealed class is by itself a type.

Considering the following code which uses type aliases:

final class B {}
final class C {}
 
type A = B|C;

When you have a function defined as:

function consumer(A $instance): void
{
  echo $instance::class;
}

The output can only be either `“B”` or `“C”`.

However, considering the following code which uses sealed classes feature:

sealed class A permits B, C {}
final class B extends A {}
final class C extends A {}

The output of `consumer` could be either `“A”`, `“B”`, or `“C”`, as `A` is a non-abstract class, it is possible to do `consumer(new A())`.

Syntax

Some people might be against introducing a new keywords 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 {}

FAQ's

Wouldn't a sealed class without permits clauses be considered final?

No, a sealed class will always have a `permits` clauses, if a sealed class is defined without a `permits` clauses, it's considered a compile error.

Would PHP check if permitted classes exists when loading a sealed class?

No, when loading a sealed class, PHP would treat just like any other class, and store the permitted types list to check against later when another type tries to inherit from it.

What if the permitted types don't actually extend the sealed type

Example:

sealed interface A permits B {}
 
class B {}

This code would not produce any errors, as another type ( e.g: `C` ) could exist, in which it inherits from both `B`, and `A`, therefor, an instance of `A&B` could still exist.

What if the permitted types don't actually extend the sealed type, and are final

Example:

sealed interface A permits B {}
 
final class B {}

In this case, we would end up with an interface 'A', but with no possible instance of it, however, due to the behavior stated above of only checking permitted types on inheritance and not when loading the sealed type, this is allowed, and is considered a small inconvenience.

Backward Incompatible Changes

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

Proposed PHP Version(s)

PHP 8.2

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.

Vote

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

Voting started on 2022-03-17 and will end on 2022-03-31.

Vote: Accept sealed classes RFC? — Yes: 16 · No: 11 (closed)

A Second vote for the preferred syntax choice.

Vote: Which syntax option do you prefer? — `sealed` + `permits`: 12 · `permits` only: 15 · `for`: 0 (closed)

Patches and Tests

Prototype patch using `for` syntax: https://github.com/php/php-src/compare/master...azjezz:sealed-classes

References

Changelog

  • 1.1: added comparison to composite types.
  • 1.2: added FAQ's section.