From ac2e1eeadee13d02b252be07f9aac05a9a4b81c8 Mon Sep 17 00:00:00 2001 From: TingSong-Syu Date: Tue, 22 Jul 2025 10:08:38 +0800 Subject: [PATCH] Add exception context preservation in Log Context Repository - Use WeakMap to associate exceptions with their scope context - Capture context state when exceptions are thrown within scopes - Add for() method to retrieve context for specific throwables - Include comprehensive test coverage for exception context handling --- src/Illuminate/Log/Context/Repository.php | 22 ++++++++ tests/Log/ContextTest.php | 66 +++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/Illuminate/Log/Context/Repository.php b/src/Illuminate/Log/Context/Repository.php index a221409186cb..0c15defef492 100644 --- a/src/Illuminate/Log/Context/Repository.php +++ b/src/Illuminate/Log/Context/Repository.php @@ -14,6 +14,7 @@ use Illuminate\Support\Traits\Macroable; use RuntimeException; use Throwable; +use WeakMap; class Repository { @@ -47,12 +48,18 @@ class Repository */ protected static $handleUnserializeExceptionsUsing; + /** + * @var WeakMap + */ + protected static $forThrowable; + /** * Create a new Context instance. */ public function __construct(Dispatcher $events) { $this->events = $events; + self::$forThrowable ??= new WeakMap(); } /** @@ -557,12 +564,27 @@ public function scope(callable $callback, array $data = [], array $hidden = []) try { return $callback(); + } catch (Throwable $e) { + self::$forThrowable[$e] ??= (new static($this->events)) + ->add($this->all()) + ->addHidden($this->allHidden()); + + throw $e; } finally { $this->data = $dataBefore; $this->hidden = $hiddenBefore; } } + /** + * @param Throwable $e + * @return static + */ + public function for(Throwable $e) + { + return self::$forThrowable[$e] ?? $this; + } + /** * Determine if the repository is empty. * diff --git a/tests/Log/ContextTest.php b/tests/Log/ContextTest.php index 23d54e0d6951..2e036a4c0bce 100644 --- a/tests/Log/ContextTest.php +++ b/tests/Log/ContextTest.php @@ -698,6 +698,72 @@ public function test_it_remembers_a_hidden_value() Context::rememberHidden('foo', $closure); $this->assertSame(1, $closureRunCount); } + + public function test_it_can_restore_scope_context_for_throwable() + { + Context::add('foo', 'bar'); + Context::addHidden('hello', 'world'); + + try { + Context::scope(function () { + Context::add('foo', 'bar2'); + Context::addHidden('hello', 'world3'); + + throw new RuntimeException('Test exception'); + }); + } catch (RuntimeException $e) { + $context = Context::for($e); + $this->assertSame('bar2', $context->get('foo')); + $this->assertSame('world3', $context->getHidden('hello')); + } + } + + public function test_it_can_fallback_to_global_context_for_none_scoped_throwable() + { + Context::add('foo', 'bar'); + Context::addHidden('hello', 'world'); + + try { + Context::scope(function () { + Context::add('foo', 'bar2'); + Context::addHidden('hello', 'world3'); + }); + + throw new RuntimeException('Test exception'); + } catch (RuntimeException $e) { + $context = Context::for($e); + $this->assertSame('bar', $context->get('foo')); + $this->assertSame('world', $context->getHidden('hello')); + } + } + + public function test_it_can_restore_innermost_scope_context_for_nested_throwable() + { + Context::add('foo', 'bar'); + Context::addHidden('hello', 'world'); + + try { + Context::scope(function () { + Context::add('foo', 'bar2'); + Context::addHidden('hello', 'world2'); + + Context::scope(function () { + Context::add('foo', 'bar3'); + Context::addHidden('hello', 'world3'); + + throw new RuntimeException('Nested exception'); + }); + }); + } catch (RuntimeException $e) { + $context = Context::for($e); + $this->assertSame('bar3', $context->get('foo')); + $this->assertSame('world3', $context->getHidden('hello')); + } + + // Verify global context is restored + $this->assertSame('bar', Context::get('foo')); + $this->assertSame('world', Context::getHidden('hello')); + } } enum Suit