rfc:constants_in_traits

This is an old revision of the document!


PHP RFC: Constants in Traits

Introduction

Traits [1] are used for horizontal code reuse across classes, and currently allow the definition of methods and properties, but not constants. This means that it is not possible to define invariants expected by a trait in the trait itself. So currently, workarounds are required in some cases, such as defining constants in its composing class or an interface implemented by the composing class.

This RFC proposes to allow defining constants in traits in the same manner that is currently possible for properties, with the following use cases in mind.

  • Defining invariants used internally by methods in traits
  • Defining constants as a public API

Defining invariants used internally by methods in traits

The main use case is the definition of invariants to be used by methods in traits. Currently, we can write codes like the following.

trait FooTrait {
    public function doFoo(int $value): void {
        if ($value > self::MAX_VALUE) {
            throw new \Exception('out of range');
        }
    }
}
 
class FooClass {
    private const MAX_VALUE = 42;
    use FooTrait;
}

This code requires that the constant definition used by the method in the trait be provided in the composing class. It can be said that the implementation detail of the trait is leaked to the composing class. It would be better if definitions of constants needed to make a trait work could be provided by the trait itself.

Defining constants as a public API

One common use case for traits is to provide a default implementation that conforms to a specific interface.

intertface FooInterface {
    public const FLAG_1 = 1;
    public function doFoo(int $flags): void;
}
 
trait FooTrait {
    public function doFoo(int $flags): void {
        if ($flags & self::FLAG_1) {
            echo 'Got flag 1';
        }
    }
}
 
class FooClass implements FooInterface {
    use FooTrait;
}

Since the constant internally used by the trait in the above code is also part of the public API, it's natural to have the definition of this constant on the interface side. However, there is currently no way on the trait side to require that the composing class implements a specific interface. Such a feature should be discussed in a separate proposal. Aside from that, if traits could have the same constant definitions of the interface and guarantee that they are compatible, it would improve the completeness of the trait as a module, as it enables the “standalone” use of the trait without the specific interface.

Proposal

This RFC proposes to allow defining constants in traits. Trait constants can be defined in the same way as class constants, and are flattened into the definition of the composing class in the same way as property and method definitions in traits.

trait Foo {
    public const FLAG_1 = 1;
    public const FLAG_2 = 2;
 
    public function doFoo(int $flags): void {
        if ($flags & self::FLAG_1) {
            echo 'Got flag 1';
        }
        if ($flags & self::FLAG_2) {
            echo 'Got flag 2';
        }
    }
}

Prohibit direct access through a trait name

Trait constants cannot be accessed through the name of the trait in a form like TraitName::CONSTANT. This is in line with the deprecation of accessing static members of traits directly [2]. Trait constants must be accessed through the composing class. That is, they must be accessed through the composing class name, or its descendant class name, self, static, parent, or its instance.

trait T {
    public const CONSTANT = 42;
 
    public function doSomething(): void {
        // Fatal Error
        echo T::CONSTANT;
 
        // OK
        echo self::CONSTANT;
        echo static::CONSTANT;
        echo $this::CONSTANT;
    }
}
 
class Base {
    use Foo;
}
 
class Child extends Base {
    public function doSomething(): void {
        // OK
        echo parent::CONSTANT;
    }
}
 
// OK
echo Base::CONSTANT;
echo Child::CONSTANT;
echo (new Base)::CONSTANT;
$child = new Child;
echo $child::CONSTANT;
 
// Fatal Error
echo T::CONSTANT;

Compatibility restrictions similar to properties

Trait constants have the same compatibility restrictions as properties of traits. That is, if a trait constant of a given name appears in multiple places, such as in a composing class or in another trait in the composing class, they are only compatible if they have the same visibility and value; otherwise, they are treated as conflicts. If the definitions of the trait constants conflict, it triggers a fatal error.

trait T1 {
    public const CONSTANT = 42;
}
 
// OK
class C1 {
    use T1;
    public const CONSTANT = 42;
}
 
// Fatal Error
class C2 {
    use T1;
    public const CONSTANT = 43;
}
 
// Fatal Error
class C3 {
    use T1;
    protected const CONSTANT = 42;
}
 
interface I {
    public const CONSTANT = 43;
}
 
// Fatal Error
class C4 implements I {
    use T1;
}
 
class Base {
    public const CONSTANT = 43;
}
 
// Fatal Error
class Derived extends Base {
    use T1;
}
 
// Fatal Error
trait T2 {
    use T1;
    public const CONSTANT = 43;
}

Unlike properties, trait constants can be declared as final as with class constants, and the finality of trait constants is also used for this compatibility check.

trait T {
    public final const CONSTANT = 42;
}
 
// OK
class C1 {
    use T;
    public final const CONSTANT = 42;
}
 
// Fatal Error
class C2 {
    use T;
    public const CONSTANT = 42;
}

As in the case of trait properties, and unlike trait methods, no as or insteadof conflict resolution is provided. Also, changing visibility in the composing class by as is not supported.

trait T1 {
    public const CONSTANT = 42;
}
trait T2 {
    public const CONSTANT = 43;
}
 
// Fatal Error
class C1 {
    use T1 { CONSTANT as ALIAS; }
}
 
// Fatal Error
class C2 {
    use T1, T2 {
        T1::CONSTANT insteadof T2;
        T1::CONSTANT as ALIAS;
    }
}
 
// Fatal Error
class C3 {
    use T1 {
        CONSTANT as private;
    }
}

Backward Incompatible Changes

There are no backwards-incompatible changes in this RFC.

Proposed PHP Version(s)

PHP 8.2

Discussions

Why were constants left out of traits initially

It's an old story and no one remembers why, but it was probably simply overlooked as there was barely any mention of constants in traits in the old ML discussions [3][4][5][6][7][8].

The response from the original author of the trait RFC[9] is quoted below [10].

Hm. This isn’t something that I remember coming up specifically back then. If it had been discussed in more detail, I’d probably have included it in the RFC. So, my working assumption is: it wasn’t something I really thought about.

Why not provide conflict resolution like methods

While trait methods can use aliases with as and selection with insteadof to resolve conflicts, there is not much benefit in allowing this to be used in constants in traits. Consider the following example.

trait ByteHandler {
    private const BIT_MASK = 0b1111_1111;
 
    public function truncateToByte(int $i): int {
        return $i & BIT_MASK;
    }
}
 
trait WordHandler {
    private const BIT_MASK = 0b1111_1111_1111_1111;
 
    public function truncateToWord(int $i): int {
        return $i & BIT_MASK;
    }
}
 
class C {
    use ByteHandler, WordHandler {
        ByteHandler::BIT_MASK insteadof WordHandler;
        WordHandler::BIT_MASK as WORD_BIT_MASK;
    }
}

Creating an alias for a constant in a trait does not rewrite the method implementation in the trait to refer to that alias. Traits with constant definitions selected by the user can continue to refer to the correct invariants expected by the trait. However, for other traits the selected definition will be wrong invariants of the same name. truncateToWord() in this case will truncate the value with an 8-bit bitmask, which will cause the WordHandler's expected invariants to be compromised, and this is clearly a bug.

While the main use case of trait methods is to be invoked from outside of the trait, the main use case of a trait constant is to serve as a member that is referenced by methods in the trait. Therefore, the same way of conflict resolution as for methods is not very useful.

Currently, PHP has held off on better conflict resolution for trait properties for the last decade, and simply marks multiple incompatible definitions as errors, as an obvious sign of a mistake. One idea to address this issue is to introduce new visibility “trait local” that are only accessible within a given trait [11]. This is beyond the scope of this proposal and would require a separate RFC.

Constants in PHP can hold state through object constants and are more similar to properties than methods. Both constants and properties should have the same style of conflict resolution. Therefore, for now, this RFC only proposes that trait constants have the same restrictions as properties.

Why are those compatibility checks performed on properties in the first place

It's basically a way to deal with state conflicts in the diamond problem.

If we were to allow some “overrides” for trait properties, for example, we might have to decide which definitions would “win out” at each location.

There can be more than one policy for handling state conflicts, and PHP has implemented one restricted approach for now and has not yet addressed another policy after that.

It should be noted that in the original trait paper, traits have only behavior and no state, thereby avoiding the state conflict problem in diamond inheritance. In the original trait paper, it is assumed that the state is provided on the composing class side and accessed from traits through accessors [12]. This pure approach guides too much boilerplate in creating and using traits.

Historically, there have been two typical approaches to state conflicts in the diamond problem: one is to merge the state of the common ancestor, and the other is to have an independent state for each common ancestor in separate “paths” and provide a way to select one. Since different use cases require one or the other, programming languages sometimes have features that allow programmers to use these two methods selectively, such as virtual inheritance in C++.

Where having a state becomes tricky is when the diamond problem occurs. If there are no conflicts, it does not matter if a trait has state. And even if there is a conflict, if the programming language defaults to either merge or having an independent state, that default will work fine for half of the use cases.

PHP strikes a balance in this problem, allowing traits to define properties, and choosing to deal with conflicts by merging states, and also marking any conflicting definitions with different visibility or default values as an error, as a sign of an unintended name conflict [13].

Why not introduce visibility changes like methods

Trait methods can also change visibility on the composing class side via the as keyword.

trait T {
    public function method(): void {}
}
 
class C {
    use T {
        method as public;
    }
}

Simply because it is not currently available for properties, this RFC doesn't propose to allow similar visibility changes for trait constants.

A survey of the 1149 packages on packagist shows that at least 222 locations in 26 packages use this feature for trait methods.

We refrain from judging whether this is large or small, but there are probably cases where it is more convenient for classes to be able to decide which members to expose.

We do not preclude the future introduction of this feature, but for the sake of simplicity, we do not include it in this RFC.

In case it is to be introduced, it would be necessary to allow the same visibility change on the property side for consistency, and to provide a way to distinguish between a constant and a method with the same name in the trait, while maintaining consistency with the compatibility check behavior.

Comparison to other languages

Hack

Hack gives priority to the definition of the trait used first when multiple trait constants conflict. If there is a conflict with a parent class, the parent class definition takes precedence, and if there is a conflict with an interface definition, it results in a type error. [14]

Proposed Voting Choices

A 2/3 majority is needed for this RFC to pass. Voting will start on 5. July 2022 and end on 19. July 2022

Patches and Tests

References

  • [1] The explanation of traits in the PHP manual
  • [2] PHP RFC: Deprecations for PHP 8.1 | Accessing static members on traits
  • [3] marc.info | search results of “trait+const” in the archive of php-internals
  • [4] marc.info | search results of “trait+constant” in the archive of php-internals
  • [5] marc.info | search results of “trait+constants” in the archive of php-internals
  • [6] marc.info | search results of “traits+const” in the archive of php-internals
  • [7] marc.info | search results of “traits+constant” in the archive of php-internals
  • [8] marc.info | search results of “traits+constants” in the archive of php-internals
  • [9] Request for Comments: Horizontal Reuse for PHP
  • [10] externals.io | [RFC] [Under Discussion] Constants in traits | The current discussion about constants in traits
  • [11] externals.io | How to build a real Trait thing without exclusion and renaming | The future introduction of trait-local was considered and put on hold in the initial discussion
  • [12] Traits: A mechanism for fine-grained reuse | The original trait paper
  • [13] externals.io | Traits and Properties | Discussion of trait properties leading to the current form
  • [14] HHVM and Hack Documentation | Traits And Interfaces: Using A Trait | Resolution of naming conflicts
  • https://externals.io/message/110741 The initial discussion about constants in traits
rfc/constants_in_traits.1656521473.txt.gz · Last modified: 2022/06/29 16:51 by sji