Skip to content

[Bug] Trait method overriding an inherited parent method is silently dropped in AOT #75

Description

@xiaoyin199

[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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions