Skip to content

PHP 8.5 bootTraits() wrong boot order #59612

@justlunix

Description

@justlunix

Laravel Version

12.56.0

PHP Version

8.5.2

Database Driver & Version

No response

Description

Notice

While we have only tested this with Laravel 12, this is most likely also an issue with Laravel 13. The framework code has not been changed in-between afaik.

Issue

With PHP 8.5 there have been fixes concerning the order on how trait methods are loaded.

See php/php-src#15753 and php/php-src#16198 for reference.

This results in methods appearing through traits outputting in a different order than before when using the reflection api. Before, parent trait methods appeared first with children following. Now it is the other way around.

This causes issues with the bootTraits method in the Illuminate/Database/Eloquent/Model class as it cycles through (new ReflectionClass($class))->getMethods().

As an example, we use spatie/laravel-model-states. This package has a trait HasStates, which executes $this->getCasts() internally on initializeHasStates(). The casts come from the Illuminate/Database/Eloquent/Concerns/HasAttributes and its initializeHasAttributes().

class MyModel extends Model {
    use HasStates;
}

In PHP 8.4, the (new ReflectionClass($class))->getMethods() first returned the method from HasAttributes, then HasStates. -> they were executed in the correct order

In PHP 8.5, it returns children first: first HasStates, then HasAttributes -> This will break because HasStates relies on HasAttributes to be executed first.

Possible Fix

The order in the output of class_uses_recursive has not changed.

This is PHP 8.5:

Image

It should be possible to instead loop over this instead of the reflection methods to keep the same order as before.

// \Illuminate\Database\Eloquent\Model::bootTraits
$ref = (new ReflectionClass($class));

foreach (class_uses_recursive($class) as $trait) {
    $conventionalBootMethod = 'boot'.class_basename($trait);
    $conventionalInitMethod = 'initialize'.class_basename($trait);

    if ($ref->hasMethod($conventionalBootMethod) &&
        ($method = $ref->getMethod($conventionalBootMethod)) &&
        ! in_array($method->getName(), $booted) &&
        $method->isStatic() &&
        $method->getAttributes(Boot::class) !== []) {
        $method->invoke(null);

        $booted[] = $method->getName();
    }

    if ($ref->hasMethod($conventionalInitMethod) &&
        ($method = $ref->getMethod($conventionalInitMethod)) ||
        $method->getAttributes(Initialize::class) !== []) {
        static::$traitInitializers[$class][] = $method->getName();
    }
}

Steps To Reproduce

It is basic PHP functionality.

trait T1
{
    public function t1_1() {}
    public function t1_2() {}
}

trait T2
{
    public function t2() {}
}

class A
{
    use T1;
}

class B extends A
{
    use T2;
}

var_dump((new ReflectionClass(new B))->getMethods());

You can run this code in PHP 8.4:

Image

And in PHP 8.5:

Image

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions