diff --git a/CHANGELOG.md b/CHANGELOG.md index ea380119..17ba8eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - [Notyf] Dispatch events: `flasher:notyf:click`, `flasher:notyf:dismiss` - [Themes] Dispatch events: `flasher:theme:click` (generic) and `flasher:theme:{name}:click` (specific) - [Laravel] Add LivewireListener classes for all adapters and themes to enable Livewire event handling +* fix [Flasher] Fix FilterCriteria uninitialized property error when constructed with empty array +* fix [Flasher] Fix null comparison issues in PriorityCriteria, HopsCriteria, and DelayCriteria that relied on PHP's implicit null-to-0 coercion ## [v2.1.3](https://github.com/php-flasher/php-flasher/compare/v2.1.2...v2.1.3) - 2025-01-25 diff --git a/src/Prime/Storage/Filter/Criteria/DelayCriteria.php b/src/Prime/Storage/Filter/Criteria/DelayCriteria.php index 5aa7b88d..55a596dd 100644 --- a/src/Prime/Storage/Filter/Criteria/DelayCriteria.php +++ b/src/Prime/Storage/Filter/Criteria/DelayCriteria.php @@ -46,14 +46,9 @@ final readonly class DelayCriteria implements CriteriaInterface $delay = $stamp->getDelay(); - if (null === $this->maxDelay) { - return $delay >= $this->minDelay; - } + $meetsMin = null === $this->minDelay || $delay >= $this->minDelay; + $meetsMax = null === $this->maxDelay || $delay <= $this->maxDelay; - if ($delay <= $this->maxDelay) { - return $delay >= $this->minDelay; - } - - return false; + return $meetsMin && $meetsMax; } } diff --git a/src/Prime/Storage/Filter/Criteria/FilterCriteria.php b/src/Prime/Storage/Filter/Criteria/FilterCriteria.php index 79cb3b2f..9d2125bf 100644 --- a/src/Prime/Storage/Filter/Criteria/FilterCriteria.php +++ b/src/Prime/Storage/Filter/Criteria/FilterCriteria.php @@ -11,7 +11,7 @@ final class FilterCriteria implements CriteriaInterface /** * @var \Closure[] */ - private array $callbacks; + private array $callbacks = []; /** * @throws \InvalidArgumentException diff --git a/src/Prime/Storage/Filter/Criteria/HopsCriteria.php b/src/Prime/Storage/Filter/Criteria/HopsCriteria.php index 5efa2f26..f07152f7 100644 --- a/src/Prime/Storage/Filter/Criteria/HopsCriteria.php +++ b/src/Prime/Storage/Filter/Criteria/HopsCriteria.php @@ -11,9 +11,9 @@ final readonly class HopsCriteria implements CriteriaInterface { use RangeExtractor; - private readonly ?int $minAmount; + private ?int $minAmount; - private readonly ?int $maxAmount; + private ?int $maxAmount; /** * @throws \InvalidArgumentException @@ -44,14 +44,11 @@ final readonly class HopsCriteria implements CriteriaInterface return false; } - if (null === $this->maxAmount) { - return $stamp->getAmount() >= $this->minAmount; - } + $amount = $stamp->getAmount(); - if ($stamp->getAmount() <= $this->maxAmount) { - return $stamp->getAmount() >= $this->minAmount; - } + $meetsMin = null === $this->minAmount || $amount >= $this->minAmount; + $meetsMax = null === $this->maxAmount || $amount <= $this->maxAmount; - return false; + return $meetsMin && $meetsMax; } } diff --git a/src/Prime/Storage/Filter/Criteria/PriorityCriteria.php b/src/Prime/Storage/Filter/Criteria/PriorityCriteria.php index 18345b8a..0944cb6c 100644 --- a/src/Prime/Storage/Filter/Criteria/PriorityCriteria.php +++ b/src/Prime/Storage/Filter/Criteria/PriorityCriteria.php @@ -46,14 +46,9 @@ final readonly class PriorityCriteria implements CriteriaInterface $priority = $stamp->getPriority(); - if (null === $this->maxPriority) { - return $priority >= $this->minPriority; - } + $meetsMin = null === $this->minPriority || $priority >= $this->minPriority; + $meetsMax = null === $this->maxPriority || $priority <= $this->maxPriority; - if ($priority <= $this->maxPriority) { - return $priority >= $this->minPriority; - } - - return false; + return $meetsMin && $meetsMax; } } diff --git a/tests/Prime/Storage/Filter/Criteria/DelayCriteriaTest.php b/tests/Prime/Storage/Filter/Criteria/DelayCriteriaTest.php index 088ae109..b8dc65af 100644 --- a/tests/Prime/Storage/Filter/Criteria/DelayCriteriaTest.php +++ b/tests/Prime/Storage/Filter/Criteria/DelayCriteriaTest.php @@ -248,4 +248,52 @@ final class DelayCriteriaTest extends TestCase $this->assertCount(3, $result); } + + public function testMatchWithOnlyMaxAndNullMin(): void + { + // This test verifies explicit null handling for min + // Previously relied on PHP's implicit null-to-0 coercion + $envelope = new Envelope(new Notification(), [new DelayStamp(3)]); + + $criteria = new DelayCriteria(['max' => 5]); + + // With min=null and max=5, delay=3 should match + $this->assertTrue($criteria->match($envelope)); + } + + public function testMatchWithOnlyMaxRejectsHigherDelay(): void + { + $envelope = new Envelope(new Notification(), [new DelayStamp(10)]); + + $criteria = new DelayCriteria(['max' => 5]); + + // With min=null and max=5, delay=10 should NOT match + $this->assertFalse($criteria->match($envelope)); + } + + public function testMatchWithBothNullMinAndMax(): void + { + // When both min and max are null, all envelopes with delay stamp should match + $envelope = new Envelope(new Notification(), [new DelayStamp(100)]); + + $criteria = new DelayCriteria([]); + + $this->assertTrue($criteria->match($envelope)); + } + + public function testApplyWithNullMinAndMaxMatchesAll(): void + { + $envelopes = [ + new Envelope(new Notification(), [new DelayStamp(0)]), + new Envelope(new Notification(), [new DelayStamp(50)]), + new Envelope(new Notification(), [new DelayStamp(100)]), + ]; + + $criteria = new DelayCriteria([]); + + $result = $criteria->apply($envelopes); + + // All envelopes with delay stamp should match when no constraints + $this->assertCount(3, $result); + } } diff --git a/tests/Prime/Storage/Filter/Criteria/FilterCriteriaTest.php b/tests/Prime/Storage/Filter/Criteria/FilterCriteriaTest.php index 299dcb0b..0d7ecf23 100644 --- a/tests/Prime/Storage/Filter/Criteria/FilterCriteriaTest.php +++ b/tests/Prime/Storage/Filter/Criteria/FilterCriteriaTest.php @@ -166,4 +166,32 @@ final class FilterCriteriaTest extends TestCase $this->assertSame(['first', 'second'], $order); $this->assertCount(2, $result); } + + public function testConstructorWithEmptyArrayDoesNotThrow(): void + { + // This test verifies the fix for uninitialized $callbacks property + // Previously, new FilterCriteria([]) would leave $callbacks uninitialized + // causing "must not be accessed before initialization" error on apply() + $criteria = new FilterCriteria([]); + + $envelopes = [new Envelope(new Notification())]; + $result = $criteria->apply($envelopes); + + // With no callbacks, envelopes should pass through unchanged + $this->assertSame($envelopes, $result); + } + + public function testApplyWithEmptyCallbacksReturnsEnvelopesUnchanged(): void + { + $criteria = new FilterCriteria([]); + + $notification = new Notification(); + $notification->setMessage('test'); + $envelopes = [new Envelope($notification)]; + + $result = $criteria->apply($envelopes); + + $this->assertCount(1, $result); + $this->assertSame('test', $result[0]->getMessage()); + } } diff --git a/tests/Prime/Storage/Filter/Criteria/HopsCriteriaTest.php b/tests/Prime/Storage/Filter/Criteria/HopsCriteriaTest.php index 25d0fe5d..fc46593c 100644 --- a/tests/Prime/Storage/Filter/Criteria/HopsCriteriaTest.php +++ b/tests/Prime/Storage/Filter/Criteria/HopsCriteriaTest.php @@ -229,4 +229,52 @@ final class HopsCriteriaTest extends TestCase $this->assertCount(1, $result); } + + public function testMatchWithOnlyMaxAndNullMin(): void + { + // This test verifies explicit null handling for min + // Previously relied on PHP's implicit null-to-0 coercion + $envelope = new Envelope(new Notification(), [new HopsStamp(2)]); + + $criteria = new HopsCriteria(['max' => 5]); + + // With min=null and max=5, hops=2 should match + $this->assertTrue($criteria->match($envelope)); + } + + public function testMatchWithOnlyMaxRejectsHigherHops(): void + { + $envelope = new Envelope(new Notification(), [new HopsStamp(10)]); + + $criteria = new HopsCriteria(['max' => 5]); + + // With min=null and max=5, hops=10 should NOT match + $this->assertFalse($criteria->match($envelope)); + } + + public function testMatchWithBothNullMinAndMax(): void + { + // When both min and max are null, all envelopes with hops stamp should match + $envelope = new Envelope(new Notification(), [new HopsStamp(100)]); + + $criteria = new HopsCriteria([]); + + $this->assertTrue($criteria->match($envelope)); + } + + public function testApplyWithNullMinAndMaxMatchesAll(): void + { + $envelopes = [ + new Envelope(new Notification(), [new HopsStamp(1)]), + new Envelope(new Notification(), [new HopsStamp(50)]), + new Envelope(new Notification(), [new HopsStamp(100)]), + ]; + + $criteria = new HopsCriteria([]); + + $result = $criteria->apply($envelopes); + + // All envelopes with hops stamp should match when no constraints + $this->assertCount(3, $result); + } } diff --git a/tests/Prime/Storage/Filter/Criteria/PriorityCriteriaTest.php b/tests/Prime/Storage/Filter/Criteria/PriorityCriteriaTest.php index ebae681c..5c0607e4 100644 --- a/tests/Prime/Storage/Filter/Criteria/PriorityCriteriaTest.php +++ b/tests/Prime/Storage/Filter/Criteria/PriorityCriteriaTest.php @@ -245,4 +245,52 @@ final class PriorityCriteriaTest extends TestCase $this->assertCount(1, $result); } + + public function testMatchWithOnlyMaxAndNullMin(): void + { + // This test verifies explicit null handling for min + // Previously relied on PHP's implicit null-to-0 coercion + $envelope = new Envelope(new Notification(), [new PriorityStamp(3)]); + + $criteria = new PriorityCriteria(['max' => 5]); + + // With min=null and max=5, priority 3 should match + $this->assertTrue($criteria->match($envelope)); + } + + public function testMatchWithOnlyMaxRejectsHigherPriority(): void + { + $envelope = new Envelope(new Notification(), [new PriorityStamp(10)]); + + $criteria = new PriorityCriteria(['max' => 5]); + + // With min=null and max=5, priority 10 should NOT match + $this->assertFalse($criteria->match($envelope)); + } + + public function testMatchWithBothNullMinAndMax(): void + { + // When both min and max are null, all envelopes with priority stamp should match + $envelope = new Envelope(new Notification(), [new PriorityStamp(100)]); + + $criteria = new PriorityCriteria([]); + + $this->assertTrue($criteria->match($envelope)); + } + + public function testApplyWithNullMinAndMaxMatchesAll(): void + { + $envelopes = [ + new Envelope(new Notification(), [new PriorityStamp(-100)]), + new Envelope(new Notification(), [new PriorityStamp(0)]), + new Envelope(new Notification(), [new PriorityStamp(100)]), + ]; + + $criteria = new PriorityCriteria([]); + + $result = $criteria->apply($envelopes); + + // All envelopes with priority stamp should match when no constraints + $this->assertCount(3, $result); + } }