From f61b7d97fae9ddf9db6b8c0516c73219a4805b3f Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 20 Mar 2026 01:37:37 +0000 Subject: [PATCH 1/2] Replace firstWhere --- src/Models/StoredWorkflow.php | 16 ++++++- tests/Unit/Models/StoredWorkflowTest.php | 54 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index fd8d0a3..f9fd5e4 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -146,7 +146,9 @@ public function findLogByIndex(int $index, bool $fresh = false): ?StoredWorkflow if ($this->relationLoaded('logs')) { /** @var Collection $logs */ $logs = $this->getRelation('logs'); - return $logs->firstWhere('index', $index); + return $logs->first( + static fn (StoredWorkflowLog $log): bool => self::modelHasIndex($log, $index) + ); } return $this->logs() @@ -197,7 +199,9 @@ public function findTimerByIndex(int $index): ?StoredWorkflowTimer if ($this->relationLoaded('timers')) { /** @var Collection $timers */ $timers = $this->getRelation('timers'); - return $timers->firstWhere('index', $index); + return $timers->first( + static fn (StoredWorkflowTimer $timer): bool => self::modelHasIndex($timer, $index) + ); } return $this->timers() @@ -234,6 +238,14 @@ public function orderedSignals(): Collection ->get(); } + private static function modelHasIndex(Model $model, int $index): bool + { + // Use raw attributes so loaded relations never fall back to Eloquent's magic relation lookup. + $attributes = $model->getAttributes(); + + return array_key_exists('index', $attributes) && (int) $attributes['index'] === $index; + } + public function exceptions(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(config('workflows.stored_workflow_exception_model', StoredWorkflowException::class)) diff --git a/tests/Unit/Models/StoredWorkflowTest.php b/tests/Unit/Models/StoredWorkflowTest.php index ec6e533..b07371a 100644 --- a/tests/Unit/Models/StoredWorkflowTest.php +++ b/tests/Unit/Models/StoredWorkflowTest.php @@ -323,6 +323,33 @@ public function testFindLogByIndexUsesLoadedLogsRelation(): void $this->assertCount(0, DB::getQueryLog()); } + public function testFindLogByIndexDoesNotUseFirstWhereForLoadedLogsRelation(): void + { + $workflow = StoredWorkflow::create([ + 'class' => 'TestWorkflow', + 'status' => 'running', + ]); + + $log = $workflow->logs() + ->create([ + 'index' => 0, + 'now' => now(), + 'class' => 'test', + ]); + + $workflow->setRelation('logs', new class([$log]) extends \Illuminate\Database\Eloquent\Collection { + public function firstWhere($key, $operator = null, $value = null) + { + throw new \BadMethodCallException('Loaded log lookup should not rely on firstWhere.'); + } + }); + + $foundLog = $workflow->findLogByIndex(0); + + $this->assertNotNull($foundLog); + $this->assertSame($log->id, $foundLog->id); + } + public function testCreateLogSyncsLoadedLogsRelation(): void { $workflow = StoredWorkflow::create([ @@ -372,6 +399,33 @@ public function testFindTimerByIndexUsesLoadedTimersRelation(): void $this->assertCount(0, DB::getQueryLog()); } + public function testFindTimerByIndexDoesNotUseFirstWhereForLoadedTimersRelation(): void + { + $workflow = StoredWorkflow::create([ + 'class' => 'TestWorkflow', + 'status' => 'running', + ]); + + $timer = $workflow->timers() + ->create([ + 'index' => 3, + 'stop_at' => now() + ->addSecond(), + ]); + + $workflow->setRelation('timers', new class([$timer]) extends \Illuminate\Database\Eloquent\Collection { + public function firstWhere($key, $operator = null, $value = null) + { + throw new \BadMethodCallException('Loaded timer lookup should not rely on firstWhere.'); + } + }); + + $foundTimer = $workflow->findTimerByIndex(3); + + $this->assertNotNull($foundTimer); + $this->assertSame($timer->id, $foundTimer->id); + } + public function testFindTimerByIndexQueriesWhenTimersRelationIsNotLoaded(): void { $workflow = StoredWorkflow::create([ From d9d19d9e14e5a7b089ddd3c529067ab46236352c Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 20 Mar 2026 01:44:38 +0000 Subject: [PATCH 2/2] Cleanup --- src/Models/StoredWorkflow.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Models/StoredWorkflow.php b/src/Models/StoredWorkflow.php index f9fd5e4..a79f24a 100644 --- a/src/Models/StoredWorkflow.php +++ b/src/Models/StoredWorkflow.php @@ -146,9 +146,7 @@ public function findLogByIndex(int $index, bool $fresh = false): ?StoredWorkflow if ($this->relationLoaded('logs')) { /** @var Collection $logs */ $logs = $this->getRelation('logs'); - return $logs->first( - static fn (StoredWorkflowLog $log): bool => self::modelHasIndex($log, $index) - ); + return $logs->first(static fn (StoredWorkflowLog $log): bool => self::modelHasIndex($log, $index)); } return $this->logs() @@ -238,14 +236,6 @@ public function orderedSignals(): Collection ->get(); } - private static function modelHasIndex(Model $model, int $index): bool - { - // Use raw attributes so loaded relations never fall back to Eloquent's magic relation lookup. - $attributes = $model->getAttributes(); - - return array_key_exists('index', $attributes) && (int) $attributes['index'] === $index; - } - public function exceptions(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(config('workflows.stored_workflow_exception_model', StoredWorkflowException::class)) @@ -346,4 +336,12 @@ protected function recursivePrune(self $workflow): void $workflow->delete(); } } + + private static function modelHasIndex(Model $model, int $index): bool + { + // Use raw attributes so loaded relations never fall back to Eloquent's magic relation lookup. + $attributes = $model->getAttributes(); + + return array_key_exists('index', $attributes) && (int) $attributes['index'] === $index; + } }