I have a class that looks like this. I will also paste it below for reference:
<?php
trait T1
{
abstract protected function _doStuff();
}
trait T2
{
protected function _doStuff()
{
echo "Doing stuff in trait
";
}
}
class C
{
use T1 {
_doStuff as _traitDoStuff;
}
use T2 {
_doStuff as _traitDoStuff;
}
protected function _doStuff()
{
echo "Doing stuff in class
";
$this->_traitDoStuff();
}
}
Here's what's happening here:
T1::_doStuff()
is used, and aliased as _traitDoStuff()
. As per PHP docs, this does not rename the method, but only adds an alias. So at this point, both _doStuff()
and _traitDoStuff()
exist as abstract protected methods.T2::_doStuff()
is used, and aliased as _traitDoStuff()
. As per PHP docs, due to precedence, both are overridden by methods of T2
. So at this point, T1::_doStuff()
no longer exists. Even if it would, it would be implemented by T2::_doStuff()
.C
implements _doStuff()
, which calls _traitDoStuff()
. At this point, regardless of which implementation of _doStuff()
is used, it is obvious that this method is implemented, so the contract defined by T1::_doStuff()
is satisfied, or doesn't exist.And yet, when I run this, it gives the following error:
Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (C::_doStuff)
As can be seen from 3v4l, this manifests everywhere from PHP 5.4 to 7.2, which kinda hints that this is not an early trait bug. Can somebody please explain this to me?
Apparently, I just forgot to specify the method that I am aliasing, i.e. T1::_doStuff as _traitDoStuff
.
Something like that maybe ?
<?php
trait T1
{
abstract protected function _doStuff();
}
trait T2
{
protected function _doStuff()
{
echo "Doing stuff in trait
";
}
}
class C
{
use T1 {
T1::_doStuff as _traitDoStuff;
}
use T2 {
T2::_doStuff as _traitDoStuff;
}
protected function _doStuff()
{
echo "Doing stuff in class
";
$this->_traitDoStuff();
}
}
The lack of class-scope resolution operators (T1::
and T2::
) were masking a deeper issue. Consider these simpler cases:
trait A {
abstract public function foo();
}
class B {
use A; // works
public function foo() {}
}
class C {
use A { A::foo as traitFoo; } // works, provided this:
public function traitFoo() {} // <-- is present
public function foo() {}
}
class D {
use A { A::foo as traitFoo; } // does not work by itself
public function foo() {}
}
What actually happens: aliasing an abstract method introduces another abstract method in your class. The PHP.net engine error message is hugely misleading:
Fatal error: Class D contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (D::foo)
But the HHVM engine is far more informative:
Fatal error: Uncaught Error: Class D contains abstract method (traitFoo) and must therefore be declared abstract or implement the remaining methods
The Horizontal Reuse (aka Trait) RFC does not explicitly discuss this case, so this is arguably a bug. Feel free to report it at bugs.php.net.
So why did adding the class resolution operator fix it?
When you added the class-scope resolution operators, which contained:
use T2 { T2::_doStuff as _traitDoStuff; }
you were satisfying the "phantom" abstract protected function _traitDoStuff
introduced by:
use T1 { T1::_doStuff as _traitDoStuff; }
If you had removed the aliasing, like use T2;
or use T2 { T2::_doStuff as _anotherMethod; }
you would see the same crash.