Compare commits

...

4 Commits

Author SHA1 Message Date
Younes ENNAJI 58123f259c Release v2.5.1 2026-03-28 02:08:46 +01:00
Younes ENNAJI d427132437 Upgrade Laravel demo to Laravel 13 for JSON session testing
- Update PHP requirement to ^8.3
- Upgrade laravel/framework to ^13.0 and laravel/tinker to ^3.0
- Upgrade pestphp/pest to ^4.0 and pest-plugin-laravel to ^4.0
- Update spatie/laravel-csp to ^3.23 and barryvdh/laravel-debugbar to ^4.1
- Add session serialization config option (defaults to 'json')
- Add FlasherSessionTest to verify flasher works with JSON sessions
2026-03-28 02:05:14 +01:00
Younes ENNAJI f372dcf70e Fix Laravel 13 JSON session serialization compatibility 2026-03-28 01:52:00 +01:00
Younes ENNAJI c4f1b059a3 upgrade dependencies 2026-03-28 01:50:03 +01:00
32 changed files with 1719 additions and 1323 deletions
+8 -2
View File
@@ -1,8 +1,14 @@
# CHANGELOG for 2.x
## [Unreleased](https://github.com/php-flasher/php-flasher/compare/v2.2.0...2.x)
## [Unreleased](https://github.com/php-flasher/php-flasher/compare/v2.5.1...2.x)
## [v2.5.0](https://github.com/php-flasher/php-flasher/compare/v2.1.4...v2.2.0) - 2026-03-07
## [v2.5.1](https://github.com/php-flasher/php-flasher/compare/v2.5.0...v2.5.1) - 2026-03-28
### Fixed
* fix [Laravel] Fix Laravel 13 JSON session serialization compatibility - StorageManager now handles both array and object formats returned by session
## [v2.5.0](https://github.com/php-flasher/php-flasher/compare/v2.1.4...v2.5.0) - 2026-03-07
### Added
Generated
+11 -11
View File
@@ -1535,16 +1535,16 @@
},
{
"name": "league/commonmark",
"version": "2.7.0",
"version": "2.8.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405"
"reference": "59fb075d2101740c337c7216e3f32b36c204218b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405",
"reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b",
"reference": "59fb075d2101740c337c7216e3f32b36c204218b",
"shasum": ""
},
"require": {
@@ -1569,11 +1569,11 @@
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0"
"vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
@@ -1581,7 +1581,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.8-dev"
"dev-main": "2.9-dev"
}
},
"autoload": {
@@ -1638,7 +1638,7 @@
"type": "tidelift"
}
],
"time": "2025-05-05T12:20:28+00:00"
"time": "2026-03-19T13:16:38+00:00"
},
{
"name": "league/config",
@@ -7268,7 +7268,7 @@
{
"name": "Younes ENNAJI",
"email": "younes.ennaji.pro@gmail.com",
"homepage": "https://www.linkedin.com/in/younes--ennaji/",
"homepage": "https://www.linkedin.com/in/yoeunes/",
"role": "Developer"
}
],
+1 -1
View File
@@ -1 +1 @@
8.2
8.3
+7 -7
View File
@@ -14,25 +14,25 @@
}
],
"require": {
"php": "^8.2",
"php": "^8.3",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
"livewire/livewire": "^3.6",
"php-flasher/php-flasher": "@dev",
"spatie/laravel-csp": "^2.10",
"spatie/laravel-csp": "^3.23",
"spatie/laravel-ray": "^1.40"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-debugbar": "^4.1",
"fakerphp/faker": "^1.24",
"larastan/larastan": "^3.2",
"laravel/pint": "^1.21",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.7",
"pestphp/pest": "^3.7",
"pestphp/pest-plugin-laravel": "^3.1",
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-laravel": "^4.0",
"spatie/laravel-ignition": "^2.9",
"spatie/ray": "^1.41"
},
+1160 -1012
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -49,6 +49,19 @@ return [
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This option determines how session data is serialized before being stored.
| The "json" driver is more secure and performant but cannot serialize
| PHP objects. The "php" driver uses PHP's native serialization.
|
*/
'serialization' => env('SESSION_SERIALIZATION', 'json'),
/*
|--------------------------------------------------------------------------
| Session File Location
@@ -0,0 +1,41 @@
<?php
it('stores and retrieves flash notifications with json serialization', function () {
// Trigger a flash notification
$this->get('/');
flash()->success('Test notification', ['title' => 'Success']);
// Make another request to retrieve from session
$response = $this->get('/');
$response->assertStatus(200);
});
it('handles multiple flash notifications across requests', function () {
flash()->success('First message');
flash()->error('Second message');
flash()->warning('Third message');
$response = $this->get('/');
$response->assertStatus(200);
});
it('preserves notification properties through json session roundtrip', function () {
flash()
->options(['position' => 'top-right', 'timeout' => 5000])
->success('Test message');
$response = $this->get('/');
$response->assertStatus(200);
});
it('works with different notification types', function () {
flash()->success('Success message');
flash()->error('Error message');
flash()->warning('Warning message');
flash()->info('Info message');
$response = $this->get('/');
$response->assertStatus(200);
});
+1 -1
View File
@@ -1 +1 @@
8.2
8.4
+284 -242
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -56,7 +56,7 @@
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^28.0.9",
"@rollup/plugin-eslint": "^9.2.0",
@@ -64,15 +64,15 @@
"@rollup/plugin-strip": "^3.0.4",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"@types/node": "^22.19.13",
"@types/node": "^22.19.15",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"all-contributors-cli": "^6.26.1",
"autoprefixer": "^10.4.27",
"browserslist": "^4.28.1",
"cross-env": "^7.0.3",
"cssnano": "^7.1.2",
"cssnano-preset-advanced": "^7.0.10",
"cssnano": "^7.1.3",
"cssnano-preset-advanced": "^7.0.11",
"eslint": "^8.57.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.2",
@@ -80,18 +80,18 @@
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.5",
"postcss": "^8.5.6",
"postcss-discard-comments": "^7.0.5",
"postcss": "^8.5.8",
"postcss-discard-comments": "^7.0.6",
"punycode": "^2.3.1",
"rimraf": "^6.1.3",
"rollup": "^4.59.0",
"rollup": "^4.60.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-filesize": "^10.0.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-progress": "^1.1.2",
"sass": "^1.97.3",
"sass": "^1.98.0",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
+19 -3
View File
@@ -25,10 +25,26 @@ final readonly class SessionBag implements BagInterface
{
$session = $this->getSession();
/** @var Envelope[] $envelopes */
$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
@@ -41,7 +57,7 @@ final readonly class SessionBag implements BagInterface
return;
}
$session->put(self::ENVELOPES_NAMESPACE, $envelopes);
$session->put(self::ENVELOPES_NAMESPACE, array_map(serialize(...), $envelopes));
}
private function getSession(): Session|FallbackSessionInterface
+1 -1
View File
@@ -29,7 +29,7 @@
"require": {
"php": ">=8.2",
"illuminate/support": "^11.0|^12.0|^13.0",
"php-flasher/flasher": "^2.5.0"
"php-flasher/flasher": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -28,8 +28,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-noty": "^2.5.0"
"php-flasher/flasher-laravel": "^2.5.1",
"php-flasher/flasher-noty": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-noty",
"version": "2.5.0",
"version": "2.5.1",
"type": "module",
"license": "MIT",
"main": "dist/flasher-noty.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.5.0",
"@flasher/flasher": "^2.5.1",
"noty": "^3.2.0-beta-deprecated"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.5.0"
"php-flasher/flasher": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -28,8 +28,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-noty": "^2.5.0",
"php-flasher/flasher-symfony": "^2.5.0"
"php-flasher/flasher-noty": "^2.5.1",
"php-flasher/flasher-symfony": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-notyf": "^2.5.0"
"php-flasher/flasher-laravel": "^2.5.1",
"php-flasher/flasher-notyf": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-notyf",
"version": "2.5.0",
"version": "2.5.1",
"type": "module",
"license": "MIT",
"main": "dist/flasher-notyf.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.5.0",
"@flasher/flasher": "^2.5.1",
"notyf": "^3.10.0"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.5.0"
"php-flasher/flasher": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-notyf": "^2.5.0",
"php-flasher/flasher-symfony": "^2.5.0"
"php-flasher/flasher-notyf": "^2.5.1",
"php-flasher/flasher-symfony": "^2.5.1"
},
"autoload": {
"psr-4": {
+1 -1
View File
@@ -18,7 +18,7 @@ final readonly class Flasher implements FlasherInterface
{
use ForwardsCalls;
public const VERSION = '2.5.0';
public const VERSION = '2.5.1';
public function __construct(
private string $default,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher",
"version": "2.5.0",
"version": "2.5.1",
"type": "module",
"license": "MIT",
"main": "dist/flasher.cjs.js",
+2 -2
View File
@@ -30,8 +30,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-sweetalert": "^2.5.0"
"php-flasher/flasher-laravel": "^2.5.1",
"php-flasher/flasher-sweetalert": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-sweetalert",
"version": "2.5.0",
"version": "2.5.1",
"type": "module",
"license": "MIT",
"main": "dist/flasher-sweetalert.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.5.0",
"@flasher/flasher": "^2.5.1",
"sweetalert2": "^11.6.13"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.5.0"
"php-flasher/flasher": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -30,8 +30,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-sweetalert": "^2.5.0",
"php-flasher/flasher-symfony": "^2.5.0"
"php-flasher/flasher-sweetalert": "^2.5.1",
"php-flasher/flasher-symfony": "^2.5.1"
},
"autoload": {
"psr-4": {
+1 -1
View File
@@ -28,7 +28,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.5.0",
"php-flasher/flasher": "^2.5.1",
"symfony/config": "^7.0|^8.0",
"symfony/console": "^7.0|^8.0",
"symfony/dependency-injection": "^7.0|^8.0",
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-toastr": "^2.5.0"
"php-flasher/flasher-laravel": "^2.5.1",
"php-flasher/flasher-toastr": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-toastr",
"version": "2.5.0",
"version": "2.5.1",
"type": "module",
"license": "MIT",
"main": "dist/flasher-toastr.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.5.0",
"@flasher/flasher": "^2.5.1",
"toastr": "^2.1.4"
},
"devDependencies": {
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.5.0"
"php-flasher/flasher": "^2.5.1"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-symfony": "^2.5.0",
"php-flasher/flasher-toastr": "^2.5.0"
"php-flasher/flasher-symfony": "^2.5.1",
"php-flasher/flasher-toastr": "^2.5.1"
},
"autoload": {
"psr-4": {
+131 -1
View File
@@ -9,6 +9,8 @@ use Flasher\Laravel\Storage\FallbackSessionInterface;
use Flasher\Laravel\Storage\SessionBag;
use Flasher\Prime\Notification\Envelope;
use Flasher\Prime\Notification\Notification;
use Flasher\Prime\Stamp\DelayStamp;
use Flasher\Prime\Stamp\HopsStamp;
use Flasher\Prime\Stamp\IdStamp;
use Flasher\Tests\Laravel\TestCase;
use Illuminate\Session\SessionManager;
@@ -65,7 +67,7 @@ final class SessionBagTest extends TestCase
$sessionMock = \Mockery::mock(Store::class);
$sessionMock->allows()->isStarted()->andReturns(true);
$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);
@@ -193,4 +195,132 @@ final class SessionBagTest extends TestCase
$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());
}
}