[Bug] Trait method overriding an inherited parent method is silently dropped in AOT
Environment
- TypePHP: v0.6.8
- PHP: 8.4.24 NTS (phpx 2.6.11)
- OS: Linux x86_64, GCC
Description
When a trait method overrides a method inherited from the parent class (i.e. the class itself does not declare the method, the trait provides it, and the parent has a method with the same name), the AOT-compiled class loses the trait method and falls back to the parent implementation. Zend executes the trait method, as PHP semantics require (trait methods take precedence over inherited methods).
This is a behavioral divergence between Zend and AOT for the same source code.
Related fixes #56 (redeclaration override rules) and #67 (cross-instance method scope) do not cover this scenario — verified on v0.6.8 which contains both.
Minimal reproduction
Verified on v0.6.8 (repro project compiles and runs; output differs from Zend):
<?php
// repro.php
class BaseModel
{
public function delete(): string
{
return $this->performDelete();
}
protected function performDelete(): string
{
return 'base (hard delete)';
}
}
trait SoftDeletes
{
protected function performDelete(): string
{
return 'trait (soft delete)';
}
}
class Article extends BaseModel
{
use SoftDeletes;
}
function main(): void
{
var_dump((new Article())->delete());
}
Expected (Zend, and PHP semantics)
string(18) "trait (soft delete)"
Per PHP precedence rules: class's own methods > trait methods > inherited parent methods.
Actual (AOT)
string(18) "base (hard delete)"
The trait method SoftDeletes::performDelete() is silently lost during compilation; the call dispatches to the parent BaseModel::performDelete().
Real-world impact
This pattern is pervasive in the Laravel/Hyperf ecosystem. Concrete case from a production Hyperf 3.2 project:
Hyperf\Database\Model\SoftDeletes::performDeleteOnModel() overrides Model::performDeleteOnModel()
- Under AOT,
$model->delete() / Model::destroy() performs a physical DELETE instead of setting deleted_at — soft delete silently degrades to hard delete, causing actual data loss.
Current workaround (explicit override forwarding to a trait alias):
class Article extends BaseModel
{
use SoftDeletes {
performDelete as protected softDeletePerformDelete;
}
protected function performDelete(): string
{
return $this->softDeletePerformDelete();
}
}
This works but should not be necessary.
[Bug] Trait method overriding an inherited parent method is silently dropped in AOT
Environment
Description
When a trait method overrides a method inherited from the parent class (i.e. the class itself does not declare the method, the trait provides it, and the parent has a method with the same name), the AOT-compiled class loses the trait method and falls back to the parent implementation. Zend executes the trait method, as PHP semantics require (trait methods take precedence over inherited methods).
This is a behavioral divergence between Zend and AOT for the same source code.
Related fixes #56 (redeclaration override rules) and #67 (cross-instance method scope) do not cover this scenario — verified on v0.6.8 which contains both.
Minimal reproduction
Verified on v0.6.8 (repro project compiles and runs; output differs from Zend):
Expected (Zend, and PHP semantics)
Per PHP precedence rules: class's own methods > trait methods > inherited parent methods.
Actual (AOT)
The trait method
SoftDeletes::performDelete()is silently lost during compilation; the call dispatches to the parentBaseModel::performDelete().Real-world impact
This pattern is pervasive in the Laravel/Hyperf ecosystem. Concrete case from a production Hyperf 3.2 project:
Hyperf\Database\Model\SoftDeletes::performDeleteOnModel()overridesModel::performDeleteOnModel()$model->delete()/Model::destroy()performs a physical DELETE instead of settingdeleted_at— soft delete silently degrades to hard delete, causing actual data loss.Current workaround (explicit override forwarding to a trait alias):
This works but should not be necessary.