Trait constructors / property initialization

php.internals

Andreas Hennings

4 years ago
Hello list, I want to bring up a topic that has bothered me whenever I use traits. I don't have a solution proposal for it, yet, unfortunately. I was going to comment in the other thread about traits, but it seems better suited for a new discussion. ---------- Traits allow to share code between classes of different inheritance chains. They can be used instead of composition, or they can be used to help with composition - e.g. a trait can expose functionality from a specific injected dependency. ---------- When using base classes, we can follow a convention to always call the parent constructor. We can even make the properties in the base class private, to fully encapsulate them. But: When using properties in traits, how can I make sure that they are properly initialized in the class constructor? Also, what if I want to provide an init method with specific logic to set that property? How can I make sure that method will be called in the constructor? I found that Psalm can detect "PropertyNotSetInConstructor", which is also applied to properties from traits. But this is not as straightforward as calling a parent constructor. Can and should we provide a language-level solution for this? Or should this be left to static analysis tools and IDEs? Cheers, Andreas

Andreas Hennings

4 years ago
On Thu, 13 Jan 2022 at 15:36, Andreas Hennings <andreas@dqxtech.net> wrote:
> > Hello list, > I want to bring up a topic that has bothered me whenever I use traits. > I don't have a solution proposal for it, yet, unfortunately. > I was going to comment in the other thread about traits, but it seems > better suited for a new discussion. > > ---------- > > Traits allow to share code between classes of different inheritance chains. > They can be used instead of composition, or they can be used to help > with composition - e.g. a trait can expose functionality from a > specific injected dependency. > > ---------- > > When using base classes, we can follow a convention to always call the > parent constructor. > We can even make the properties in the base class private, to fully > encapsulate them. > > But: > When using properties in traits, how can I make sure that they are > properly initialized in the class constructor?
I think I have a solution: abstract properties in traits! The idea would be: - traits can have abstract properties that are private, protected or public. - non-abstract classes cannot have abstract properties. - class properties override trait properties, with some compatibility requirements. - non-abstract protected or public class properties from the parent class also override trait properties. - (optional) abstract classes can have abstract properties that are protected or public. - (optional) abstract class properties from a parent class are overridden by the trait property, but with compatibility checks. This implies that abstract trait properties _must_ be redeclared in the class that uses the trait, interface X {} interface XHaving { public function getX(): X; } trait T { abstract private X $x; public function getX(): X {return $this->x;} } class C implements XHaving { use T; public function __construct( private X $x, ) {} } class D implements XHaving { use T; // Error, must redeclare abstract property T::$x. } The benefit: Properties are initialized in the same file where they are declared. I don't know if we need aliasing for properties, perhaps we should first go without that. I did find a discussion about abstract properties in externals.io, but this was for interfaces and classes, not for traits. https://externals.io/message/64126#66682
> > Also, what if I want to provide an init method with specific logic to > set that property? How can I make sure that method will be called in > the constructor?
This part would not be solved by the abstract properties. But I think that's ok.