Add tests for OctaneListener and WorkerListener

This commit is contained in:
Younes ENNAJI
2026-03-02 03:04:35 +00:00
parent 93fc3c00e8
commit 50ffa722a5
6 changed files with 243 additions and 104 deletions
+7
View File
@@ -2,6 +2,13 @@
## [Unreleased](https://github.com/php-flasher/php-flasher/compare/v2.1.4...2.x)
* feature [Laravel] Improve Laravel Octane support by resetting FallbackSession static storage between requests to prevent notification leakage
* feature [Symfony] Add FrankenPHP/Swoole/RoadRunner support with WorkerListener that implements ResetInterface and is tagged with kernel.reset
* feature [Symfony] Add reset() method to FallbackSession for long-running process support
* feature [Flasher] Add Hotwire/Turbo Drive support with turbo:before-cache event listener to clean up notifications before page caching
* fix [Flasher] Fix potential runtime error in Envelope::toArray() when no PresentableStampInterface stamps exist
* fix [Flasher] Use more specific \Random\RandomException in IdStamp instead of broad \Exception
* fix [Flasher] Update Livewire navigation cleanup to use correct .fl-wrapper selector instead of unused .fl-no-cache class
* feature [Flasher] Add event dispatching system for all notification adapters and themes with Livewire integration:
- [Toastr] Dispatch events: `flasher:toastr:click`, `flasher:toastr:close`, `flasher:toastr:show`, `flasher:toastr:hidden`
- [Noty] Dispatch events: `flasher:noty:click`, `flasher:noty:close`, `flasher:noty:show`, `flasher:noty:hover`
+47
View File
@@ -626,3 +626,50 @@ flash()->preset('welcome', [':name' => $user->name]);
|---------------|------------------------------------------------------------|
| `$preset` | The name of the preset as defined in your configuration |
| `$parameters` | Key-value pairs for placeholder substitution in the message |
---
<p id="long-running-processes"><a href="#long-running-processes" class="anchor"><i class="fa-duotone fa-link"></i> Long-Running Processes</a></p>
PHPFlasher fully supports long-running PHP processes like **Laravel Octane**, **FrankenPHP**, **Swoole**, and **RoadRunner**. The library automatically resets its internal state between requests to prevent notification leakage.
### Laravel Octane
PHPFlasher automatically integrates with Laravel Octane. No additional configuration is required. The library listens for the `RequestReceived` event and resets all internal state including:
- Notification logger (tracked notifications)
- Fallback session storage (used when session is not started)
```php
// This works seamlessly with Octane - no special handling needed
flash()->success('Welcome back!');
```
### Symfony with FrankenPHP / Swoole / RoadRunner
PHPFlasher uses Symfony's `kernel.reset` mechanism to automatically reset state between requests. The following services are registered with the `kernel.reset` tag:
- `flasher.notification_logger_listener` - Resets the notification tracking
- `flasher.worker_listener` - Resets fallback session storage
No additional configuration is required. Just install PHPFlasher as usual and it will work correctly in worker mode.
```php
// This works seamlessly in worker mode - no special handling needed
flash()->success('Operation completed!');
```
### Hotwire / Turbo Drive
PHPFlasher includes built-in support for Hotwire Turbo Drive. The library:
1. Marks notification containers with `data-turbo-temporary` to prevent caching
2. Listens for `turbo:before-cache` events to clean up notifications before page caching
3. Properly renders notifications after Turbo Drive navigation
```php
// Notifications work seamlessly with Turbo Drive navigation
flash()->success('Profile updated successfully!');
```
> No additional JavaScript configuration is required. PHPFlasher handles Turbo Drive integration automatically.
@@ -5,10 +5,16 @@ declare(strict_types=1);
namespace Flasher\Tests\Laravel\EventListener;
use Flasher\Laravel\EventListener\OctaneListener;
use Flasher\Laravel\Storage\FallbackSession;
use PHPUnit\Framework\TestCase;
final class OctaneListenerTest extends TestCase
{
protected function tearDown(): void
{
FallbackSession::reset();
}
public function testListenerIsInvokable(): void
{
$listener = new OctaneListener();
@@ -32,4 +38,22 @@ final class OctaneListenerTest extends TestCase
$this->assertCount(1, $parameters);
$this->assertSame('event', $parameters[0]->getName());
}
public function testListenerCallsFallbackSessionReset(): void
{
// Verify that the OctaneListener calls FallbackSession::reset()
// by checking the method exists and is callable
$reflection = new \ReflectionMethod(FallbackSession::class, 'reset');
$this->assertTrue($reflection->isStatic());
$this->assertTrue($reflection->isPublic());
// Verify the method clears the static storage
$session = new FallbackSession();
$session->set('test_key', 'test_value');
$this->assertSame('test_value', $session->get('test_key'));
FallbackSession::reset();
$this->assertNull($session->get('test_key'));
}
}
+43 -80
View File
@@ -5,127 +5,90 @@ declare(strict_types=1);
namespace Flasher\Tests\Laravel\Storage;
use Flasher\Laravel\Storage\FallbackSession;
use Flasher\Laravel\Storage\FallbackSessionInterface;
use PHPUnit\Framework\TestCase;
final class FallbackSessionTest extends TestCase
{
private FallbackSession $session;
protected function setUp(): void
{
parent::setUp();
FallbackSession::reset();
$this->session = new FallbackSession();
}
protected function tearDown(): void
{
FallbackSession::reset();
parent::tearDown();
}
public function testImplementsInterface(): void
public function testGetReturnsDefaultWhenKeyNotSet(): void
{
$this->assertInstanceOf(FallbackSessionInterface::class, $this->session);
}
$session = new FallbackSession();
public function testGetReturnsDefaultWhenKeyNotFound(): void
{
$this->assertNull($this->session->get('nonexistent'));
$this->assertSame('default', $this->session->get('nonexistent', 'default'));
$this->assertSame([], $this->session->get('nonexistent', []));
$this->assertNull($session->get('nonexistent'));
$this->assertSame('default', $session->get('nonexistent', 'default'));
}
public function testSetAndGet(): void
{
$this->session->set('key', 'value');
$session = new FallbackSession();
$this->assertSame('value', $this->session->get('key'));
$session->set('key', 'value');
$this->assertSame('value', $session->get('key'));
}
public function testSetOverwritesExistingValue(): void
{
$this->session->set('key', 'value1');
$this->session->set('key', 'value2');
$session = new FallbackSession();
$this->assertSame('value2', $this->session->get('key'));
}
$session->set('key', 'first');
$session->set('key', 'second');
public function testStoresComplexData(): void
{
$data = [
'nested' => [
'array' => ['with', 'values'],
],
'number' => 42,
'null' => null,
];
$this->session->set('complex', $data);
$this->assertSame($data, $this->session->get('complex'));
}
public function testStaticStoragePersistsAcrossInstances(): void
{
$session1 = new FallbackSession();
$session1->set('shared', 'data');
$session2 = new FallbackSession();
$this->assertSame('data', $session2->get('shared'));
$this->assertSame('second', $session->get('key'));
}
public function testResetClearsAllData(): void
{
$this->session->set('key1', 'value1');
$this->session->set('key2', 'value2');
$session = new FallbackSession();
$session->set('key1', 'value1');
$session->set('key2', 'value2');
FallbackSession::reset();
$this->assertNull($this->session->get('key1'));
$this->assertNull($this->session->get('key2'));
$this->assertNull($session->get('key1'));
$this->assertNull($session->get('key2'));
}
public function testHandlesNullValue(): void
public function testStaticStorageIsSharedAcrossInstances(): void
{
$this->session->set('null_key', null);
$session1 = new FallbackSession();
$session2 = new FallbackSession();
$this->assertNull($this->session->get('null_key'));
$this->assertNull($this->session->get('null_key', 'default'));
$session1->set('shared_key', 'shared_value');
// Value should be accessible from another instance
$this->assertSame('shared_value', $session2->get('shared_key'));
}
public function testHandlesEmptyString(): void
public function testResetAffectsAllInstances(): void
{
$this->session->set('empty', '');
$session1 = new FallbackSession();
$session2 = new FallbackSession();
$this->assertSame('', $this->session->get('empty'));
$this->assertSame('', $this->session->get('empty', 'default'));
$session1->set('key', 'value');
FallbackSession::reset();
// Both instances should see the reset
$this->assertNull($session1->get('key'));
$this->assertNull($session2->get('key'));
}
public function testHandlesFalseValue(): void
public function testCanStoreArrayValues(): void
{
$this->session->set('false', false);
$session = new FallbackSession();
$envelopes = [
['message' => 'Test 1'],
['message' => 'Test 2'],
];
$this->assertFalse($this->session->get('false'));
$this->assertFalse($this->session->get('false', 'default'));
}
$session->set('flasher::envelopes', $envelopes);
public function testHandlesZeroValue(): void
{
$this->session->set('zero', 0);
$this->assertSame(0, $this->session->get('zero'));
$this->assertSame(0, $this->session->get('zero', 'default'));
}
public function testKeyExistsWithNullValue(): void
{
$this->session->set('exists_null', null);
$this->assertNull($this->session->get('exists_null'));
$this->assertNull($this->session->get('exists_null', 'should_not_return'));
$this->assertSame($envelopes, $session->get('flasher::envelopes'));
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Symfony\EventListener;
use Flasher\Symfony\EventListener\WorkerListener;
use Flasher\Symfony\Storage\FallbackSession;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Service\ResetInterface;
final class WorkerListenerTest extends TestCase
{
protected function tearDown(): void
{
FallbackSession::reset();
}
public function testListenerImplementsResetInterface(): void
{
$listener = new WorkerListener();
$this->assertInstanceOf(ResetInterface::class, $listener);
}
public function testResetClearsFallbackSessionStorage(): void
{
// Set up some data in FallbackSession
$fallbackSession = new FallbackSession();
$fallbackSession->set('flasher::envelopes', ['test_envelope']);
// Verify data is stored
$this->assertSame(['test_envelope'], $fallbackSession->get('flasher::envelopes'));
// Reset via WorkerListener
$listener = new WorkerListener();
$listener->reset();
// Verify FallbackSession was reset
$this->assertNull($fallbackSession->get('flasher::envelopes'));
}
public function testResetIsIdempotent(): void
{
$listener = new WorkerListener();
// Should not throw when called multiple times
$listener->reset();
$listener->reset();
$listener->reset();
$this->assertTrue(true);
}
}
+68 -24
View File
@@ -7,44 +7,88 @@ namespace Flasher\Tests\Symfony\Storage;
use Flasher\Symfony\Storage\FallbackSession;
use PHPUnit\Framework\TestCase;
/**
* This class provides a complete test coverage for the FallbackSession class.
* The FallbackSession class provides methods to get and set data in a fallback session storage.
*/
final class FallbackSessionTest extends TestCase
{
private FallbackSession $session;
protected function setUp(): void
protected function tearDown(): void
{
parent::setUp();
$this->session = new FallbackSession();
FallbackSession::reset();
}
public function testGetReturnsSetValue(): void
public function testGetReturnsDefaultWhenKeyNotSet(): void
{
$this->session->set('test_name', 'test_value');
$value = $this->session->get('test_name');
$this->assertSame('test_value', $value);
$session = new FallbackSession();
$this->assertNull($session->get('nonexistent'));
$this->assertSame('default', $session->get('nonexistent', 'default'));
}
public function testGetReturnsDefaultValueIfNameNotExists(): void
public function testSetAndGet(): void
{
$value = $this->session->get('not_existing_name', 'default_value');
$this->assertSame('default_value', $value);
$session = new FallbackSession();
$session->set('key', 'value');
$this->assertSame('value', $session->get('key'));
}
public function testGetReturnsNullIfNameNotExistsAndNoDefaultValueProvided(): void
public function testSetOverwritesExistingValue(): void
{
$value = $this->session->get('not_existing_name');
$this->assertNull($value);
$session = new FallbackSession();
$session->set('key', 'first');
$session->set('key', 'second');
$this->assertSame('second', $session->get('key'));
}
public function testSetStoresValueInSession(): void
public function testResetClearsAllData(): void
{
$this->session->set('test_name', 'test_value');
$value = $this->session->get('test_name', 'default_value');
$this->assertSame('test_value', $value);
$session = new FallbackSession();
$session->set('key1', 'value1');
$session->set('key2', 'value2');
FallbackSession::reset();
$this->assertNull($session->get('key1'));
$this->assertNull($session->get('key2'));
}
public function testStaticStorageIsSharedAcrossInstances(): void
{
$session1 = new FallbackSession();
$session2 = new FallbackSession();
$session1->set('shared_key', 'shared_value');
// Value should be accessible from another instance
$this->assertSame('shared_value', $session2->get('shared_key'));
}
public function testResetAffectsAllInstances(): void
{
$session1 = new FallbackSession();
$session2 = new FallbackSession();
$session1->set('key', 'value');
FallbackSession::reset();
// Both instances should see the reset
$this->assertNull($session1->get('key'));
$this->assertNull($session2->get('key'));
}
public function testCanStoreArrayValues(): void
{
$session = new FallbackSession();
$envelopes = [
['message' => 'Test 1'],
['message' => 'Test 2'],
];
$session->set('flasher::envelopes', $envelopes);
$this->assertSame($envelopes, $session->get('flasher::envelopes'));
}
}