rfc:sealed_classes

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:sealed_classes [2021/04/25 07:03] – add sealed traits example azjezzrfc:sealed_classes [2022/04/01 08:21] (current) – closed vote 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 <azjezz@protonmail.com>, Joe Watkins +  * Author: Saif Eddin Gmati <azjezz@protonmail.com>, Joe Watkins <krakjoe@php.net> 
-  * Status: Draft+  * Status: Decline 
 +  * Target Version: PHP 8.2
   * First Published at: https://wiki.php.net/rfc/sealed_classes   * First Published at: https://wiki.php.net/rfc/sealed_classes
  
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`, 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.+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 ===== ===== 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, 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.
  
 <code php> <code php>
-sealed class Shape permits CircleSquare, Rectangle {}+sealed interface Option permits SomeNone ... }
  
-final class Circle extends Shape {} // ok +interface Some extends Option ... } // ok 
-final class Square extends Shape {} // ok +interface None extends Option ... } // ok
-final class Rectangle extends Shape {} // ok+
  
-final class Triangle extends Shape {} // Fatal error: Class Triangle cannot extend sealed class Shape.+interface Maybe extends Option ... } // Fatal error: Interface Maybe cannot extend sealed interface Option.
 </code> </code>
  
Line 36: Line 41:
   sealed interface ResultInterface permits Success, Failure { ... }   sealed interface ResultInterface permits Success, Failure { ... }
  
-  final class Success implements ResultInterface { ... } +  class Success implements ResultInterface { ... } 
-  final class Failure implements ResultInterface { ... } +  class Failure implements ResultInterface { ... } 
-  +
   function wrap(callable $callback): ResultInterface { ... }   function wrap(callable $callback): ResultInterface { ... }
      
-  function unwrap(Result\ResultInterface $result): mixed+  function unwrap(ResultInterface $result): mixed
   {       {    
-    return match($result::class) { +    return match(true) { 
-      Result\Success::class => $result->value(), +      $result instanceof Success => $result->value(), 
-      Result\Failure::class => throw $result->error(),+      $result instanceof Failure => throw $result->error(),
     }; // no need for default, it's not possible.     }; // no need for default, it's not possible.
   }   }
Line 62: Line 67:
  
 <blockquote> <blockquote>
-This is an example taken from the [[https://github.com/symfony/symfony/blob/bb1e1e58aea5318e96d1c22cc8a91668ed7baaaa/src/Symfony/Component/Cache|Symfony Cache component]]+This is an example taken from the [[https://github.com/azjezz/psl/blob/9f8657cb37be971862f2de013cbd16469f48e53e/src/Psl/Channel/Internal/ChannelSideTrait.php|PSL Channel component]]
 </blockquote> </blockquote>
  
 <code php> <code php>
-namespace Symfony\Component\Cache\Traits +namespace Psl\Channel\Internal 
-  use Symfony\Component\Cache\Adapter\FilesystemAdapter; +  use Psl\Channel\ReceiverInterface;
-  use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter; +
-  use Symfony\Component\Cache\Adapter\PhpFilesAdapter;+
      
-  sealed trait FilesystemCommonTrait permits FilesystemTraitPhpFilesAdapter { ... } +  sealed trait ChannelSideTrait permits BoundedReceiverBoundedSender, UnboundedReceiverUnboundedSender { ... }
-  sealed trait FilesystemTrait permits FilesystemAdapterFilesystemTagAwareAdapter { +
-   use FilesystemCommonTrait; // ok +
-   ... +
-  } +
-}+
  
-namespace Symfony\Component\Cache\Adapter +  // OK 
-   use Symfony\Component\Cache\Traits\FilesystemTrait;+  final class BoundedReceiver implements ReceiverInterface 
 +  
 +    use ChannelSideTrait;
  
-   final class FilesystemAdapter { +    ... 
-     use FilesystemTrait; // ok +  }
-     ... +
-   }+
  
-   final class FilesystemTagAwareAdapter +  // OK 
-     use FilesystemTrait// ok +  final class UnboundedReceiver implements ReceiverInterface 
-     ... +  
-   }+    use ChannelSideTrait
 + 
 +    ... 
 +  }
 } }
  
-namespace App\Cache +namespace App\Channel 
-    use Symfony\Component\Cache\Traits\FilesystemTrait;+    use Psl\Channel\Internal\ChannelSideTrait;
        
-    // Error: Class App\Cache\MyFilesystemCache may not use sealed trait (Symfony\Component\Cache\Traits\FilesystemTrait) +    // Fatal error: Class App\Channel\MyReceiver may not use sealed trait Psl\Channel\Internal\ChannelSideTrait. 
-    final class MyFilesystemAdapter +    final class MyReceiver 
-      use FilesystemTrait;+      use ChannelSideTrait; 
 +       
 +      ...
     }     }
  
-    // Error: Trait App\Cache\MyFilesystemTrait may not use sealed trait (Symfony\Component\Cache\Traits\FilesystemTrait) +    // Fatal error: Trait App\Channel\MyReceiver may not use sealed trait Psl\Channel\Internal\ChannelSideTrait. 
-    trait MyFilesystemTrait +    trait MyChannelSideTrait 
-      use FilesystemTrait;+      use ChannelSideTrait; 
 +       
 +      ...
     }     }
 } }
 </code> </code>
 +
 +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:
 +
 +
 +<blockquote>
 +This also applies when sealing to a specific class, or a trait.
 +</blockquote>
 +
 +<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 {}
 +</code>
 +
 +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 '{', 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 { }
 +</code>
 +
 +==== 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
 +}
 +</code>
 +
 +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:
 +
 +<code>
 +<?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,
 +  ) {}
 +}
 +</code>
 +
 +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;
 +</code>
 +
 +When you have a function defined as:
 +
 +<code php>
 +function consumer(A $instance): void
 +{
 +  echo $instance::class;
 +}
 +</code>
 +
 +The output can only be either `"B"` or `"C"`.
 +
 +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 {}
 +</code>
 +
 +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 ===== ===== Syntax =====
  
-Some people might be against introducing a new keyword into the language, which will lead to `sealed` and `permits`+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. not being a valid class names anymore, therefor, a second vote will take place to decide which syntax should be used.
  
Line 145: Line 286:
  
  
-4. using `Sealedattribute+===== FAQ's ===== 
 + 
 +== Wouldn't a sealed class without permits clauses be considered final? == 
 + 
 +No, a sealed class will always have a `permitsclauses, 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 {}
  
-#[Sealed(Bar::class, Baz::class)] +class {} 
-class Foo {}+</code>
  
-#[Sealed(Quux::classQuuz::class)] +This code would not produce any errors, as another type e.g`C` ) could existin which it inherits from both `B`, and `A`, therefor, an instance of `A&B` could still exist. 
-interface Qux {}+ 
 +== What if the permitted types don't actually extend the sealed type, and are final == 
 + 
 +Example
 + 
 +<code php> 
 +sealed interface A permits B {}
  
-#[Sealed(Grault::class, Garply::class)] +final class {}
-trait Corge {}+
 </code> </code>
  
 +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 ===== ===== 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 179: 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::IS_SEALED to expose the bit flag used for locked classes+  * 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   * 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   * Reflection::getModifierNames() will include the string "sealed" if this bit is set
Line 186: 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 and Tests ===== +Voting started on 2022-03-17 and will end on 2022-03-31.
-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="Accept sealed classes RFC?" auth="azjezz" voteType="single" closed="true"> 
 +   * Yes 
 +   * No 
 +</doodle>
  
-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, you should also provide a patch for the language specification.+<doodle title="Which syntax option do you prefer?" auth="azjezz" voteType="single" closed="true"> 
 +   * `sealed` + `permits` 
 +   * `permits` only 
 +   * `for
 +</doodle>
  
 +===== Patches and Tests =====
 +
 +Prototype patch using `for` syntax: https://github.com/php/php-src/compare/master...azjezz:sealed-classes
  
 ===== References ===== ===== References =====
Line 206: Line 378:
   * [[https://kotlinlang.org/docs/sealed-classes.html|Sealed classes in Kotlin]]   * [[https://kotlinlang.org/docs/sealed-classes.html|Sealed classes in Kotlin]]
  
 +===== Changelog =====
 +
 +
 +  * 1.1: added comparison to composite types.
 +  * 1.2: added FAQ's section.
  
rfc/sealed_classes.1619334204.txt.gz · Last modified: 2021/04/25 07:03 by azjezz