-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[11.x] Handle circular references in model serialization (#52461)
* [11.x] Added failing test for serializing circular relations If a circular relationship is set up between two models using `setRelation()` (or similar methods) then calling `$model->relationsToArray()` will call `toArray()` on each related model, which will in turn call `relationsToArray()`. In an instance where one of the related models is an object that has already had `toArray()` called further up the stack, it will infinitely recurse down and result in a stack overflow. The same issue exists with `getQueueableRelations()`, `push()`, and potentially other methods. This adds tests which will fail if one of the known potentially problematic methods gets into a recursive loop. * [11.x] Added PreventsCircularRecursion This adds a trait for Eloquent which can be used to prevent recursively serializing circular references. * [11.x] Changed the name to `withoutRecursion()`, accept a callable default * formatting * [11.x] Delay calling a "default" callback until the last possible second * [11.x] Added additional tests for "callable" defaults --------- Co-authored-by: Taylor Otwell <[email protected]>
- Loading branch information
1 parent
8a4a52e
commit 2835e2b
Showing
4 changed files
with
580 additions
and
31 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
src/Illuminate/Database/Eloquent/Concerns/PreventsCircularRecursion.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<?php | ||
|
||
namespace Illuminate\Database\Eloquent\Concerns; | ||
|
||
use Illuminate\Support\Arr; | ||
use Illuminate\Support\Onceable; | ||
use WeakMap; | ||
|
||
trait PreventsCircularRecursion | ||
{ | ||
/** | ||
* The cache of objects processed to prevent infinite recursion. | ||
* | ||
* @var WeakMap<static, array<string, mixed>> | ||
*/ | ||
protected static $recursionCache; | ||
|
||
/** | ||
* Prevent a method from being called multiple times on the same object within the same call stack. | ||
* | ||
* @param callable $callback | ||
* @param mixed $default | ||
* @return mixed | ||
*/ | ||
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); | ||
} | ||
} | ||
|
||
/** | ||
* Remove an entry from the recursion cache for an object. | ||
* | ||
* @param object $object | ||
* @param string $hash | ||
*/ | ||
protected static function clearRecursiveCallValue($object, string $hash) | ||
{ | ||
if ($stack = Arr::except(static::getRecursiveCallStack($object), $hash)) { | ||
static::getRecursionCache()->offsetSet($object, $stack); | ||
} elseif (static::getRecursionCache()->offsetExists($object)) { | ||
static::getRecursionCache()->offsetUnset($object); | ||
} | ||
} | ||
|
||
/** | ||
* Get the stack of methods being called recursively for the current object. | ||
* | ||
* @param object $object | ||
* @return array | ||
*/ | ||
protected static function getRecursiveCallStack($object): array | ||
{ | ||
return static::getRecursionCache()->offsetExists($object) | ||
? static::getRecursionCache()->offsetGet($object) | ||
: []; | ||
} | ||
|
||
/** | ||
* Get the current recursion cache being used by the model. | ||
* | ||
* @return WeakMap | ||
*/ | ||
protected static function getRecursionCache() | ||
{ | ||
return static::$recursionCache ??= new WeakMap(); | ||
} | ||
|
||
/** | ||
* Set a value in the recursion cache for the given object and method. | ||
* | ||
* @param object $object | ||
* @param string $hash | ||
* @param mixed $value | ||
* @return mixed | ||
*/ | ||
protected static function setRecursiveCallValue($object, string $hash, $value) | ||
{ | ||
static::getRecursionCache()->offsetSet( | ||
$object, | ||
tap(static::getRecursiveCallStack($object), fn (&$stack) => $stack[$hash] = $value), | ||
); | ||
|
||
return static::getRecursiveCallStack($object)[$hash]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.