Skip to content

\Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion::withoutRecursion() method crashes on mocked models #52727

Closed
@sanfair

Description

@sanfair

Laravel Version

11.22.0

PHP Version

8.3.8

Database Driver & Version

No response

Description

Method \Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion::withoutRecursion() will crash if it is invoked from eval.

Method definition

protected function withoutRecursion($callback, $default = null)
{
$trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
$onceable = Onceable::tryFromTrace($trace, $callback);
$stack = static::getRecursiveCallStack($this);
if (array_key_exists($onceable->hash, $stack)) {
return is_callable($stack[$onceable->hash])
? static::setRecursiveCallValue($this, $onceable->hash, call_user_func($stack[$onceable->hash]))
: $stack[$onceable->hash];
}
try {
static::setRecursiveCallValue($this, $onceable->hash, $default);
return call_user_func($onceable->callable);
} finally {
static::clearRecursiveCallValue($this, $onceable->hash);
}
}

That happens because \Illuminate\Support\Onceable::tryFromTrace() will return null when the second frame in the trace stack is eval.

A more appropriate example is when the \Illuminate\Database\Eloquent\Model::toArray() method is called on a model mocked by mockery/mockery.


Real-world example.

Stack:

  • Laravel v11.22.0
  • Inertia
  • Pest/PHPUnit

Our feature test:

  1. Arrange a mocked user and use it for authentication.
  2. Make an HTTP request.
  3. Assert
    • that a method on the model is called (in our case, one of Laravel cashier's methods).
    • that the server responds with an inertia component.

The exception happens when Ineria middleware tries to pass the mocked user model to the front end.


Steps To Reproduce

I prepared two Pest tests that demonstrate this bug:

test('that `Model::withoutRecursion()` method can be called using eval', function () {
    $model = new class extends \Illuminate\Database\Eloquent\Model
    {
        public function toArray(): array
        {
            // Added to prevent IDE to complain about "Undefined variable '$value'" on line 25.
            $result = [];

            eval(<<<'EVAL'

            $result = $this->withoutRecursion(
                fn () => array_merge($this->attributesToArray(), $this->relationsToArray()),
                fn () => $this->attributesToArray(),
            );

            EVAL
            );

            return $result;
        }
    };

    expect($model->toArray())->toEqual([]);
});

test('that `Model::toArray()` method work on mocked models', function () {
    $model = Mockery::mock(\App\Models\User::class)->makePartial();

    expect($model->toArray())->toEqual([]);
});

Repo: https://github.com/sanfair/laravel-prevent-circular-recursion-bug
Commit with tests: sanfair/laravel-prevent-circular-recursion-bug@89b173c

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions