rfc:sealed_classes
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionLast revisionBoth sides next revision | ||
rfc:sealed_classes [2021/04/24 21:13] – /s/per/pre/ azjezz | rfc:sealed_classes [2022/03/17 12:53] – azjezz | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Sealed Classes ====== | ====== PHP RFC: Sealed Classes ====== | ||
* Date: 2021-04-24 | * Date: 2021-04-24 | ||
- | * Author: Saif Eddin Gmati < | + | * Author: Saif Eddin Gmati < |
- | * Status: | + | * Status: |
+ | * Target Version: PHP 8.2 | ||
* First Published at: https:// | * First Published at: https:// | ||
Line 14: | Line 15: | ||
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`. | 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`, | + | Currently PHP has a special case for `Throwable` |
===== Proposal ===== | ===== Proposal ===== | ||
Support for sealed classes is added through a new modifier `sealed`, and a new `permits` clause that takes place after `extends`, and `implements`. | 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, | ||
+ | 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. | ||
<code php> | <code php> | ||
- | sealed | + | sealed |
- | final class Circle | + | interface Some extends |
- | final class Square | + | interface None extends |
- | final class Rectangle extends Shape {} // ok | + | |
- | final class Triangle | + | interface Maybe extends |
</ | </ | ||
- | Similarly, an interface that is sealed can be implemented directly only by the classes named in the `permits` clause. | + | An interface that is sealed can be implemented directly only by the classes named in the `permits` clause. |
<code php> | <code php> | ||
- | namespace | + | namespace |
- | sealed interface | + | sealed interface |
+ | |||
+ | 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-> | ||
+ | $result instanceof Failure => throw $result-> | ||
+ | }; // no need for default, it's not possible. | ||
+ | | ||
- | final class BarException extends \Exception implements ExceptionInterface {} | ||
- | final class BazException extends \Exception implements ExceptionInterface {} | ||
} | } | ||
- | namespace | + | namespace |
- | use Foo\ExceptionInterface; | + | use Psl\Result; |
- | // Fatal error: Class Qux\QuuxException | + | // Fatal error: Class App\Maybe cannot implement sealed |
- | final class QuuxException extends \Exception | + | final class Maybe implements |
} | } | ||
+ | </ | ||
- | namespace Corge { | + | Similarly, a trait that is sealed can only be used by the classes named in the `permits` clause. |
- | use Foo\ExceptionInterface; | + | |
- | use Foo\BarException; | + | |
- | use Foo\BazException; | + | |
- | try { | + | < |
- | | + | This is an example taken from the [[https:// |
- | | + | </ |
- | echo match($e::class) { | + | |
- | | + | <code php> |
- | | + | namespace Psl\Channel\Internal |
- | }; // no need for default case, it's not possible. | + | use Psl\Channel\ReceiverInterface; |
+ | | ||
+ | sealed trait ChannelSideTrait permits BoundedReceiver, | ||
+ | |||
+ | // OK | ||
+ | final class BoundedReceiver implements ReceiverInterface | ||
+ | | ||
+ | use ChannelSideTrait; | ||
+ | |||
+ | | ||
+ | | ||
+ | |||
+ | | ||
+ | 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. | ||
+ | </ | ||
+ | |||
+ | <code php> | ||
+ | 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: | ||
+ | |||
+ | <code php> | ||
+ | // Parse error: syntax error, unexpected ' | ||
+ | sealed class Bar { } | ||
+ | |||
+ | // Parse error: syntax error, unexpected ' | ||
+ | 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: | ||
+ | |||
+ | <code php> | ||
+ | <?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-> | ||
+ | |||
+ | return new Success($result); | ||
+ | } catch(Throwable $e) { | ||
+ | return new Failure($e); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public function catch(Closure $failure): Result | ||
+ | { | ||
+ | return $this-> | ||
+ | } | ||
+ | |||
+ | public function map(Closure $success): Result | ||
+ | { | ||
+ | return $this-> | ||
+ | $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: | ||
+ | |||
+ | <code php> | ||
+ | final class B {} | ||
+ | final class C {} | ||
+ | |||
+ | type A = B|C; | ||
+ | </ | ||
+ | |||
+ | When you have a function defined as: | ||
+ | |||
+ | <code php> | ||
+ | function consumer(A $instance): void | ||
+ | { | ||
+ | echo $instance:: | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The output can only be either `" | ||
+ | |||
+ | However, considering the following code which uses sealed classes feature: | ||
+ | |||
+ | <code php> | ||
+ | sealed class A permits B, C {} | ||
+ | final class B extends A {} | ||
+ | final class C extends A {} | ||
+ | </ | ||
+ | |||
+ | The output of `consumer` could be either `" | ||
===== Syntax ===== | ===== Syntax ===== | ||
- | Some people might be against introducing a new keyword | + | Some people might be against introducing a new keywords |
not being a valid class names anymore, therefor, a second vote will take place to decide which syntax should be used. | not being a valid class names anymore, therefor, a second vote will take place to decide which syntax should be used. | ||
Line 101: | Line 286: | ||
- | 4. using `Sealed` attribute | + | ===== FAQ's ===== |
+ | |||
+ | == Wouldn' | ||
+ | |||
+ | 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: | ||
<code php> | <code php> | ||
+ | sealed interface A permits B {} | ||
- | # | + | class B {} |
- | class Foo {} | + | </ |
- | #[Sealed(Quux::class, Quuz::class)] | + | 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. |
- | interface | + | |
+ | == What if the permitted types don't actually extend the sealed type, and are final == | ||
+ | |||
+ | Example: | ||
+ | |||
+ | <code php> | ||
+ | sealed | ||
- | # | + | final class B {} |
- | trait Corge {} | + | |
</ | </ | ||
+ | In this case, we would end up with an interface ' | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
- | `sealed` and `permits` become reserved keywords in PHP 8.1 | + | `sealed` and `permits` become reserved keywords in PHP 8.2 |
===== Proposed PHP Version(s) ===== | ===== Proposed PHP Version(s) ===== | ||
- | PHP 8.1 | + | PHP 8.2 |
===== RFC Impact ===== | ===== RFC Impact ===== | ||
Line 135: | Line 342: | ||
The following additions will be made to expose the new flag via reflection: | The following additions will be made to expose the new flag via reflection: | ||
- | * New constant ReflectionClass:: | + | * New constant ReflectionClass:: |
* The return value of ReflectionClass:: | * The return value of ReflectionClass:: | ||
* Reflection:: | * Reflection:: | ||
Line 142: | Line 349: | ||
- | ===== Proposed Voting Choices | + | ===== Vote ===== |
As this is a language change, a 2/3 majority is required. | As this is a language change, a 2/3 majority is required. | ||
- | ===== Patches | + | Voting started on 2022-03-17 |
- | 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. | + | <doodle title=" |
+ | * Yes | ||
+ | * No | ||
+ | </ | ||
- | Make it clear if the patch is intended to be the final patch, or is just a prototype. | + | A Second vote for the preferred syntax choice. |
- | For changes affecting the core language, | + | <doodle title=" |
+ | * `sealed` + `permits` | ||
+ | * `permits` only | ||
+ | * `for` | ||
+ | </ | ||
+ | ===== Patches and Tests ===== | ||
+ | |||
+ | Prototype patch using `for` syntax: https:// | ||
===== References ===== | ===== References ===== | ||
Line 162: | Line 378: | ||
* [[https:// | * [[https:// | ||
+ | ===== Changelog ===== | ||
+ | |||
+ | |||
+ | * 1.1: added comparison to composite types. | ||
+ | * 1.2: added FAQ's section. | ||
rfc/sealed_classes.txt · Last modified: 2022/04/01 08:21 by azjezz