-
Notifications
You must be signed in to change notification settings - Fork 11.6k
Description
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
framework/src/Illuminate/Database/Eloquent/Concerns/PreventsCircularRecursion.php
Lines 25 to 46 in a2c66ad
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:
- Arrange a mocked user and use it for authentication.
- Make an HTTP request.
- 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