Skip to content

Instantly share code, notes, and snippets.

@azjezz
Created April 23, 2024 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save azjezz/7da4970c68f7dd48f3ad392ccfa27588 to your computer and use it in GitHub Desktop.
Save azjezz/7da4970c68f7dd48f3ad392ccfa27588 to your computer and use it in GitHub Desktop.

PHP RFC: Revised Sealed Classes and Interfaces

Introduction

This RFC proposes a refined approach to sealed classes and interfaces in PHP, enabling more robust type safety and encapsulation in object-oriented design. The concept of sealing restricts which classes or interfaces can extend or implement marked classes and interfaces, empowering library and framework developers to define closed hierarchies of types with controlled subclassing or implementation. Technical Justification for Sealed Types

Sealed types are vital in ensuring that specific parts of a software system adhere to a predefined structure, limiting the potential for unintended usage or modifications that could lead to fragile code or security issues. They facilitate clear design contracts and can significantly enhance the maintainability and predictability of code, especially in large-scale applications where components must interact in well-defined ways.

Specifics of Sealing in PHP:

  • Enhanced Control Over Inheritance: Sealed types restrict extensions to a specified set of types, allowing library designers to maintain strict control over how their APIs are extended and preventing misuse in client code.
  • Encapsulation and Safety: By limiting inheritance, sealed classes protect critical functionality encapsulated within a class or interface, reducing the risk of breaking changes and ensuring consistent behavior. Performance Optimization: Sealed types can potentially enable optimizations by allowing engines like Zend to make assumptions about the class hierarchy, reducing the overhead of dynamic checks during runtime.

Although PHP internally uses a form of sealing for critical system interfaces like Throwable, this capability is not directly available in userland PHP. This RFC proposes to introduce sealed classes and interfaces to PHP userland, allowing developers to define clear and robust architectural boundaries within their applications.

Proposal

This RFC proposes the introduction of the sealed keyword in PHP, coupled with a permits clause. A sealed class or interface can only be extended or implemented by the classes or interfaces explicitly listed in the permits clause. This explicit declaration ensures that only designated subclasses or implementors can extend or use the sealed type, providing a controlled form of inheritance that can prevent misuse and foster more maintainable code bases.

Syntax and Semantics

The syntax for declaring sealed types is proposed to be explicit to maintain clarity and avoid ambiguity:

  1. Definition: A sealed keyword is introduced for classes and interfaces. When a class or interface is declared as sealed, it must specify which classes or interfaces are allowed to extend or implement it using a permits clause.
  2. Permits Clause: The permits clause specifies a list of allowed subclasses or implementers.
sealed interface Throwable permits Exception, Error { /* ... */ }

sealed abstract class Result permits Success, Failure { /* ... */ }

Unlike the previous RFC, this RFC only presents the syntax that uses sealed and permits keywords directly, as it is deemed the most explicit and clear.

Despite reserving two new keywords (sealed and permits), the clarity this syntax provides is considered essential for the proper implementation of the feature.

Why Not Composite Type Aliases

While composite type aliases offer a convenient way to group multiple types under a single alias, they do not provide the same level of functionality as sealed classes. This distinction is crucial for understanding why sealed classes are being proposed in PHP instead of, or potentially in addition to, composite type aliases. Below, we explore two significant differences that demonstrate why sealed classes are preferable for certain programming scenarios.

Shared Functionality

One of the primary advantages of sealed classes over type aliases is the ability to define shared functionalities within the parent class, which all permitted subclasses are required to implement or can inherit directly. This feature is absent in type aliases, where each type operates independently of the others under the alias.

Example of shared functionality using sealed classes:

sealed abstract class Result permits Success, Failure {
    // Common method that leverages the type's guaranteed properties
    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);
        }
    }

    // Additional shared functionalities that utilize the sealed class structure
    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 readonly class Success extends Result {
    public function __construct(
        public mixed $value,
    ) {}
}

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

In this example, Result defines methods that both Success and Failure subclasses can use, which simplifies handling of results across different parts of the application while maintaining type safety and clear inheritance.

Type Identity and Polymorphism

Unlike type aliases, a sealed class itself is a type. This characteristic allows functions to treat the sealed class as a distinct entity from its subclasses, adding an additional layer of flexibility and type safety.

Comparison example using type aliases and sealed classes:

Type Aliases Example:

final class B {}
final class C {}

type A = B | C;

function consumer(A $instance): void {
    echo $instance::class; // Outputs either "B" or "C"
}

Sealed Classes Example:

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

function consumer(A $instance): void {
    echo $instance::class; // Outputs "A", "B", or "C"
}

In the sealed class example, consumer might output "A", "B", or "C", demonstrating that instances of A can be treated distinctly from its subclasses. This is not possible with type aliases, where the alias merely references its constituent types without forming a new type.

These distinctions underscore the added benefits of sealed classes in scenarios where shared functionality and the creation of a new, distinct type are necessary. Type aliases, while useful for certain cases, do not provide the same level of control or capability, justifying the need for sealed classes in PHP.

Arguments in Support of the Revised Sealed Classes RFC

  1. Clear Intent and API Stability

The revised RFC for sealed classes explicitly supports the clear declaration of API intent, allowing class authors to define which types may extend or implement their classes. This isn't just about restricting flexibility; it's about ensuring that extensions and implementations are thought through as part of the design, not as afterthoughts. Contrary to the assertion that existing mechanisms like attributes suffice for this purpose, these are often not enforced by the PHP runtime and rely heavily on external tools for enforcement. Sealed classes provide a guarantee enforced by the language itself, ensuring that any consumer of the API adheres to the intended design constraints.

  1. Encapsulation and Maintenance

The introduction of sealed classes enhances encapsulation by limiting inheritance to a known set of subclasses. This controlled environment helps prevent the fragile base class problem where changes to the base class can inadvertently affect derived classes. By defining explicit extension points, developers can make changes to the base class with a clear understanding of how these changes will propagate. This clarity reduces maintenance overhead and increases the robustness of software, as changes are less likely to introduce bugs in derived classes.

  1. Improved Design Decisions

Sealed classes encourage better design decisions by making inheritance a deliberate choice rather than a default option. In many programming scenarios, composition should be favored over inheritance, and sealed classes promote this approach by forcing developers to consider whether subclassing is appropriate. For cases where inheritance is justified, sealed classes ensure that it occurs within a controlled and predictable framework.

  1. Performance Considerations

Although the current RFC does not claim performance improvements from sealed classes, theoretical optimizations could be enabled in the future. For example, knowing the complete set of subclasses could allow the PHP engine to optimize method calls and type checks. While these benefits are speculative at this stage, they present a potential area for future enhancements in PHP's performance.

  1. Total Functions and Exhaustiveness

Sealed classes enable the definition of total functions that can operate exhaustively over all possible subclasses. This is particularly useful in switch statements or visitor patterns where handling all possible subclasses explicitly can lead to more robust and easier-to-understand code. This level of exhaustiveness is not achievable through annotations or attributes, as they do not enforce structure at the language level.

  1. Broader Language Consistency

Many modern programming languages include features similar to sealed classes (e.g., Kotlin and Java). By incorporating sealed classes, PHP aligns itself more closely with common object-oriented programming practices, making it easier for developers who use multiple languages to apply their knowledge across different environments.

FAQs

Why introduce sealed classes in PHP?

Sealed classes allow developers to define a controlled environment where only specific classes can inherit from another class or implement an interface. This can be crucial for framework developers or libraries that need to ensure certain components are used in a predefined way without allowing external modifications.

Are the names in the permits clause loaded immediately?

No, PHP does not load the names specified in the permits clause when the sealed class is defined. They are only checked for compatibility when a class attempts to extend or implement the sealed type.

What if the permitted types do not actually extend or implement the sealed type?

In PHP, a permitted type listed in a permits clause does not necessarily need to extend the sealed class or interface at the time of declaration.

For example, if sealed interface A permits B {} and class B {} exists, PHP does not produce an error simply because B does not immediately implement A. A third type, say C, could be introduced where class C extends B implements A {}, thus forming a valid inheritance chain where C becomes an instance of both A and B.

What if the permitted types don't actually extend the sealed type and are declared as final?

Consider a scenario where sealed interface A permits B {} and final class B {} is declared. In this case, even though B is final and cannot be extended, PHP allows this configuration without raising an error during the loading of A or B. The absence of concrete implementations extending B that also implement A may lead to a practical limitation in using A as intended. However, this is considered a minor inconvenience rather than a critical issue, as it arises from specific and somewhat unusual architectural decisions. The system only enforces type compatibility checks during inheritance or implementation, not at the time of declaration.

This approach provides flexibility and minimizes initial load-time errors, allowing developers to structure their type hierarchies with greater freedom.

Why is there only one syntax proposed?

The proposal advocates for a single, explicit syntax to avoid potential confusion and to maintain semantic clarity. The explicit use of sealed and permits keywords was chosen to clearly communicate the restrictions imposed by sealed classes and interfaces, reflecting the intent of the feature directly in the code.

Backward Incompatible Changes

sealed and permits become reserved keywords in PHP 8.5

Proposed PHP Version(s)

8.5

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.

Patches and Tests

Prototype: https://github.com/azjezz/php-src/tree/sealed_classes_v2

References

Changelog

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment