Fix Laravel 13 JSON session serialization compatibility

This commit is contained in:
Younes ENNAJI
2026-03-28 01:52:00 +01:00
parent c4f1b059a3
commit f372dcf70e
2 changed files with 150 additions and 4 deletions
+19 -3
View File
@@ -25,10 +25,26 @@ final readonly class SessionBag implements BagInterface
{ {
$session = $this->getSession(); $session = $this->getSession();
/** @var Envelope[] $envelopes */
$envelopes = $session->get(self::ENVELOPES_NAMESPACE, []); $envelopes = $session->get(self::ENVELOPES_NAMESPACE, []);
return $envelopes; if (!\is_array($envelopes)) {
return [];
}
$result = [];
foreach ($envelopes as $envelope) {
if ($envelope instanceof Envelope) {
$result[] = $envelope;
} elseif (\is_string($envelope)) {
$unserialized = @unserialize($envelope);
if ($unserialized instanceof Envelope) {
$result[] = $unserialized;
}
}
// Arrays and invalid data silently skipped (graceful degradation)
}
return $result;
} }
public function set(array $envelopes): void public function set(array $envelopes): void
@@ -41,7 +57,7 @@ final readonly class SessionBag implements BagInterface
return; return;
} }
$session->put(self::ENVELOPES_NAMESPACE, $envelopes); $session->put(self::ENVELOPES_NAMESPACE, array_map(serialize(...), $envelopes));
} }
private function getSession(): Session|FallbackSessionInterface private function getSession(): Session|FallbackSessionInterface
+131 -1
View File
@@ -9,6 +9,8 @@ use Flasher\Laravel\Storage\FallbackSessionInterface;
use Flasher\Laravel\Storage\SessionBag; use Flasher\Laravel\Storage\SessionBag;
use Flasher\Prime\Notification\Envelope; use Flasher\Prime\Notification\Envelope;
use Flasher\Prime\Notification\Notification; use Flasher\Prime\Notification\Notification;
use Flasher\Prime\Stamp\DelayStamp;
use Flasher\Prime\Stamp\HopsStamp;
use Flasher\Prime\Stamp\IdStamp; use Flasher\Prime\Stamp\IdStamp;
use Flasher\Tests\Laravel\TestCase; use Flasher\Tests\Laravel\TestCase;
use Illuminate\Session\SessionManager; use Illuminate\Session\SessionManager;
@@ -65,7 +67,7 @@ final class SessionBagTest extends TestCase
$sessionMock = \Mockery::mock(Store::class); $sessionMock = \Mockery::mock(Store::class);
$sessionMock->allows()->isStarted()->andReturns(true); $sessionMock->allows()->isStarted()->andReturns(true);
$sessionMock->allows()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($envelopes); $sessionMock->allows()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($envelopes);
$sessionMock->expects()->put(SessionBag::ENVELOPES_NAMESPACE, $envelopes); $sessionMock->expects()->put(SessionBag::ENVELOPES_NAMESPACE, array_map(serialize(...), $envelopes));
$this->sessionManagerMock->allows()->driver()->andReturns($sessionMock); $this->sessionManagerMock->allows()->driver()->andReturns($sessionMock);
@@ -193,4 +195,132 @@ final class SessionBagTest extends TestCase
$this->assertSame($envelopes2, $this->sessionBag->get()); $this->assertSame($envelopes2, $this->sessionBag->get());
} }
public function testEnvelopeSurvivesJsonSerializationRoundTrip(): void
{
$notification = new Notification();
$notification->setType('success');
$notification->setMessage('Operation completed');
$original = new Envelope($notification, [
new IdStamp('json-test-id'),
new HopsStamp(2),
new DelayStamp(500),
]);
$sessionMock = \Mockery::mock(Store::class);
$sessionMock->allows()->isStarted()->andReturns(true);
$captured = null;
$sessionMock->expects('put')->with(SessionBag::ENVELOPES_NAMESPACE, \Mockery::on(function ($value) use (&$captured) {
$captured = $value;
return true;
}));
$this->sessionManagerMock->allows()->driver()->andReturns($sessionMock);
$this->sessionBag->set([$original]);
$jsonEncoded = json_encode($captured, \JSON_THROW_ON_ERROR);
$jsonDecoded = json_decode($jsonEncoded, true, 512, \JSON_THROW_ON_ERROR);
$sessionMock->allows()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($jsonDecoded);
$restored = $this->sessionBag->get();
$this->assertCount(1, $restored);
$this->assertInstanceOf(Envelope::class, $restored[0]);
$this->assertSame('success', $restored[0]->getType());
$this->assertSame('Operation completed', $restored[0]->getMessage());
$this->assertSame('json-test-id', $restored[0]->get(IdStamp::class)->getId());
$this->assertSame(2, $restored[0]->get(HopsStamp::class)->getAmount());
$this->assertSame(500, $restored[0]->get(DelayStamp::class)->getDelay());
}
public function testGetHandlesEnvelopeObjectsForBackwardCompatibility(): void
{
$envelopes = [
new Envelope(new Notification(), new IdStamp('legacy')),
];
$envelopes[0]->setType('info');
$envelopes[0]->setMessage('Old session');
$sessionMock = \Mockery::mock(Store::class);
$sessionMock->expects()->isStarted()->andReturns(true);
$sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($envelopes);
$this->sessionManagerMock->expects()->driver()->andReturns($sessionMock);
$result = $this->sessionBag->get();
$this->assertSame($envelopes, $result);
}
public function testGetSkipsCorruptedArrayData(): void
{
$sessionMock = \Mockery::mock(Store::class);
$sessionMock->expects()->isStarted()->andReturns(true);
$sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns([
['title' => '', 'message' => '', 'type' => 'success', 'options' => [], 'metadata' => ['id' => 'corrupted']],
]);
$this->sessionManagerMock->expects()->driver()->andReturns($sessionMock);
$result = $this->sessionBag->get();
$this->assertSame([], $result);
}
public function testGetSkipsInvalidSerializedStrings(): void
{
$sessionMock = \Mockery::mock(Store::class);
$sessionMock->expects()->isStarted()->andReturns(true);
$sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns([
'invalid-not-serialized-data',
'O:8:"stdClass":0:{}', // Valid serialized object but wrong type
serialize(new Envelope(new Notification(), new IdStamp('valid'))),
]);
$this->sessionManagerMock->expects()->driver()->andReturns($sessionMock);
$result = $this->sessionBag->get();
$this->assertCount(1, $result);
$this->assertInstanceOf(Envelope::class, $result[0]);
$this->assertSame('valid', $result[0]->get(IdStamp::class)->getId());
}
public function testPreservesAllStampsAcrossRoundTrip(): void
{
$notification = new Notification();
$notification->setType('warning');
$notification->setMessage('Multi-stamp');
$original = new Envelope($notification, [
new IdStamp('stamp-test'),
new HopsStamp(3),
new DelayStamp(1000),
]);
$serialized = serialize($original);
$jsonEncoded = json_encode([$serialized], \JSON_THROW_ON_ERROR);
$jsonDecoded = json_decode($jsonEncoded, true, 512, \JSON_THROW_ON_ERROR);
$sessionMock = \Mockery::mock(Store::class);
$sessionMock->expects()->isStarted()->andReturns(true);
$sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($jsonDecoded);
$this->sessionManagerMock->expects()->driver()->andReturns($sessionMock);
$restored = $this->sessionBag->get();
$this->assertCount(1, $restored);
$this->assertInstanceOf(Envelope::class, $restored[0]);
$this->assertSame('warning', $restored[0]->getType());
$this->assertSame('Multi-stamp', $restored[0]->getMessage());
$this->assertSame('stamp-test', $restored[0]->get(IdStamp::class)->getId());
$this->assertSame(3, $restored[0]->get(HopsStamp::class)->getAmount());
$this->assertSame(1000, $restored[0]->get(DelayStamp::class)->getDelay());
}
} }