rfc:traits-with-interfaces

PHP RFC: Traits with interfaces

Introduction

Allow traits to implement interfaces. Classes that insert the trait would then implement the interface, as though it was declared on the class.

Proposals

This RFC proposes two language changes to PHP’s traits. The second proposal requires the first.

Proposal 1: Traits implement interfaces

Traits provide horizontal reuse of methods: a class that uses a trait mixes in the implementation of a set of methods. Interfaces provide a promise of a class's implementation: a class that implements an interface is guaranteed to provide a set of methods.

These concepts fit together well. The set of methods provided by a trait may match the set of methods guaranteed by an interface. While the programmer’s intends that the trait provide an implementation of the complete interface, PHP cannot enforce this intention in code.

This first proposal is that a trait be permitted to declare that it implements an interface. Having the trait declare that it implements an interface makes the relationship between the interface (specification) and trait (implementation) explicit.

The trait must implement each of the methods from all the interfaces it implements. Failure to do so will be a fatal error. The method declarations must be compatible with the interface. Some or all of the trait’s implementing methods may be abstract, with the class including the trait providing the method implementation (similar to an abstract class that implements an interface).

Concretely, Proposal 1 makes this code be valid and functional:

<?php
 
interface I {
    function foo();
}
 
trait T implements I {
    function foo() {
    }
}

See “Examples” below for additional sample code.

If a class inserts a trait that implements an interface, the class may or may not declare that interface. Semantically, this proposal makes the relationship between the trait and interface more explicit, but does not change how classes behave that use such a trait.

This pattern -- where a trait provides a standard implementation of an interface -- exists in the wild. See “References” for links.

This change does not introduce any new keywords. The syntax change is limited to the trait declaration. See “Patches and Tests” for a proposed patch for the language specification.

Proposal 2: Propagating interfaces from traits to classes

This proposal depends on Proposal 1, above.

A trait that implements an interface provides methods that fulfill the interface’s contract. When a class inserts that trait, the class now fulfills the interface, but the class must explicitly specify that it implements the interface. This second proposal is that any class, by inserting a trait that implements an interface, would implicitly be declared to implement that interface. The class need not repeat the interface declaration.

Concretely, Proposal 2 makes this code be valid and functional:

<?php
 
interface I {
    function foo();
}
 
trait T implements I {
    function foo() {
    }
}
 
class C {
    use T;
}
 
print_r(class_implements(C::class));
// Array
// (
//     [I => I]
// )

Classes that insert the trait may override any members (as with existing traits) and continue to implement the interface. Other classes need not be aware that a class implements an interface via a trait or directly, in the same way they need not be aware if an interface is inherited by a class.

This change reduces the overhead of implementing an interface via a trait, because a class will only need to insert the trait, not also explicitly declare the interface.

Other languages that implement languages features like PHP’s traits allow for interface specifications like this RFC. See “References” for links.

Examples

Example #1: Trait implementing interface, and providing additional methods

Relies on Proposal 1

<?php
 
interface Logger {
    function error($message);
    function info($message);
}
 
trait FileLogger implements Logger {
    abstract function logToFile($message);
 
    function error($message) {
        $this->logToFile("ERROR: $message");
    }
 
    function info($message) {
        $this->logToFile("INFO: $message");
    }
}

Example #2: Trait that does not implement all required methods

Relies on Proposal 1

<?php
 
interface Logger {
    function error($message);
    function info($message);
}
 
// Fatal error: Trait ErrorLogger contains 1 abstract method and must implement the remaining methods (Logger::info)
trait ErrorLogger implements Logger {
    function error($message) {
        print $message;
    }
}

Example #3: Trait that implements part of the interface via an abstract method

Relies on Proposal 1

<?php
 
interface Logger {
    function error($message);
    function info($message);
}
 
trait ErrorLogger implements Logger {
    function error($message) {
        print $message;
    }
 
    abstract function info($message);
}

Example #4: Class implementing interface via trait

Relies on Proposals 1 and 2

<?php
 
interface Logger {
    function error($message);
    function info($message);
}
 
trait FileLogger implements Logger {
    abstract function logToFile($message);
 
    function error($message) {
        $this->logToFile("ERROR: $message");
    }
 
    function info($message) {
        $this->logToFile("INFO: $message");
    }
}
 
class Widget {
    use FileLogger;
 
    function logToFile($message) {
        // ...
    }
}
 
// Prints Array( [Logger] => Logger )
print_r(class_implements(Widget::class));

Example #5: Method from trait is renamed, so interface is no longer satisfied

Relies on Proposals 1 and 2

See “Open Issues” below for a discussion of this example.

<?php
 
trait VarsToJson implements JsonSerializable {
    public function jsonSerialize() {
        return get_object_vars($this);
    }
}
 
// Fatal error: Access level to VarsToJson::jsonSerialize() must be public (as in class JsonSerializable)
class Widget {
    use VarsToJson {
        jsonSerialize as private;
    }
}

Backward Incompatible Changes

None. All existing traits will continue to work.

Proposed PHP Version(s)

Next PHP 7.x, currently PHP 7.1.

RFC Impact

To SAPIs

None.

To Existing Extensions

Any extension that is aware of PHP’s AST will need to be updated to handle the change to trait declarations.

To Opcache

None expected. TBD once a draft implementation is complete.

New Constants

No new constants.

php.ini Defaults

No new settings.

Open Issues

Proposal 2

Given a trait that implements an interface, what happens when a class that uses that trait renames one of the methods required by the interface? Example 5 shows this as a fatal error: the class no longer fulfills the interface declared by the trait, which is invalid.

This is not the only valid behavior in this case. Alternatively, the interface declaration could be dropped from the class, leaving the class with the trait’s methods but not the interface.

This issue does not apply to Proposal 1, as it only affects the case where interface declarations propagate from trait to class.

Unaffected PHP Functionality

This does not impact interfaces or class inheritance, nor how classes include traits.

Trait conflict resolution is unchanged, as adding interfaces to a class is always additive: if multiple superclasses or traits specify the same interface, the class will simply implement it once.

This change does not affect the runtime semantics of traits. A class that implements an interface via a trait is indistinguishable from a class that implements it directly (or via inheritance).

Future Scope

Nothing yet.

Proposed Voting Choices

The two proposals will be voted separately and concurrently. Both will require a 2/3 majority. Vote date TBD.

If Proposal 1 fails to pass, Proposal 2 is moot and also fails.

Patches and Tests

Implementation

TBD

References

Existing PHP codebases that would benefit from this change

Trait-like forms with interfaces in other languages

Rejected Features

Nothing yet.

rfc/traits-with-interfaces.txt · Last modified: 2017/09/22 13:28 by 127.0.0.1