From c8df97fe7f8b1b3ed49df493024f59216bce3598 Mon Sep 17 00:00:00 2001 From: Shine Date: Fri, 22 May 2026 15:33:46 +0800 Subject: [PATCH 1/7] fix: resolve Laravel 13 session MessageBag serialization issue In Laravel 13, MessageBag objects stored via redirect()->with() are serialized to arrays, causing "Call to a member function get() on array" errors in blade templates. Introduce SessionMessage value object as a clean replacement, providing getTitle(), getMessage(), and getOptions() methods. Closes #24 --- resources/views/partials/alerts.blade.php | 16 +++--- resources/views/partials/exception.blade.php | 11 ++-- resources/views/partials/toastr.blade.php | 6 +-- src/Http/Controllers/ScaffoldController.php | 12 ++--- src/Support/SessionMessage.php | 56 ++++++++++++++++++++ src/Support/helpers.php | 11 ++-- 6 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 src/Support/SessionMessage.php diff --git a/resources/views/partials/alerts.blade.php b/resources/views/partials/alerts.blade.php index d34eb8874..cd769bd73 100755 --- a/resources/views/partials/alerts.blade.php +++ b/resources/views/partials/alerts.blade.php @@ -1,8 +1,8 @@ @if($error = session()->get('error'))
-

 {{ \Illuminate\Support\Arr::get($error->get('title'), 0) }}

-

{!! \Illuminate\Support\Arr::get($error->get('message'), 0) !!}

+

 {{ $error->getTitle() }}

+

{!! $error->getMessage() !!}

@elseif ($errors = session()->get('errors')) @if ($errors->hasBag('error')) @@ -19,23 +19,23 @@ @if($success = session()->get('success'))
-

 {{ \Illuminate\Support\Arr::get($success->get('title'), 0) }}

-

{!! \Illuminate\Support\Arr::get($success->get('message'), 0) !!}

+

 {{ $success->getTitle() }}

+

{!! $success->getMessage() !!}

@endif @if($info = session()->get('info'))
-

 {{ \Illuminate\Support\Arr::get($info->get('title'), 0) }}

-

{!! \Illuminate\Support\Arr::get($info->get('message'), 0) !!}

+

 {{ $info->getTitle() }}

+

{!! $info->getMessage() !!}

@endif @if($warning = session()->get('warning'))
-

 {{ \Illuminate\Support\Arr::get($warning->get('title'), 0) }}

-

{!! \Illuminate\Support\Arr::get($warning->get('message'), 0) !!}

+

 {{ $warning->getTitle() }}

+

{!! $warning->getMessage() !!}

@endif \ No newline at end of file diff --git a/resources/views/partials/exception.blade.php b/resources/views/partials/exception.blade.php index fb3069f03..004903303 100755 --- a/resources/views/partials/exception.blade.php +++ b/resources/views/partials/exception.blade.php @@ -1,11 +1,16 @@ @if(isset($errors) && $errors->hasBag('exception')) - getBag('exception'); ?> + @php + $error = $errors->getBag('exception'); + $errorType = $error->first('type'); + $errorFile = $error->first('file'); + $errorLine = $error->first('line'); + @endphp

- {{ class_basename($error->get('type')[0]) }} - In {{ basename($error->get('file')[0]) }} line {{ $error->get('line')[0] }} : + {{ class_basename($errorType) }} + In {{ basename($errorFile) }} line {{ $errorLine }} :

  {!! $error->first('message') !!}

diff --git a/resources/views/partials/toastr.blade.php b/resources/views/partials/toastr.blade.php index e9512face..ac6a2b83c 100644 --- a/resources/views/partials/toastr.blade.php +++ b/resources/views/partials/toastr.blade.php @@ -1,9 +1,9 @@ @if(Session::has('dcat-admin-toastr')) @php $toastr = Session::get('dcat-admin-toastr'); - $type = $toastr->get('type')[0] ?? 'success'; - $message = $toastr->get('message')[0] ?? ''; - $options = admin_javascript_json($toastr->get('options', [])); + $type = $toastr->getTitle(); + $message = $toastr->getMessage(); + $options = admin_javascript_json($toastr->getOptions()); @endphp @endif \ No newline at end of file diff --git a/src/Http/Controllers/ScaffoldController.php b/src/Http/Controllers/ScaffoldController.php index 312affa31..c4a97cce6 100644 --- a/src/Http/Controllers/ScaffoldController.php +++ b/src/Http/Controllers/ScaffoldController.php @@ -11,13 +11,13 @@ use Dcat\Admin\Scaffold\ModelCreator; use Dcat\Admin\Scaffold\RepositoryCreator; use Dcat\Admin\Support\Helper; +use Dcat\Admin\Support\SessionMessage; use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\URL; -use Illuminate\Support\MessageBag; use Illuminate\Support\Str; class ScaffoldController extends Controller @@ -275,10 +275,7 @@ protected function getDatabaseColumns($db = null, $tb = null) protected function backWithException(\Exception $exception) { - $error = new MessageBag([ - 'title' => 'Error', - 'message' => $exception->getMessage(), - ]); + $error = SessionMessage::make('Error', $exception->getMessage()); return redirect()->refresh()->withInput()->with(compact('error')); } @@ -293,10 +290,7 @@ protected function backWithSuccess($paths, $message) $messages[] = "
$message"; - $success = new MessageBag([ - 'title' => 'Success', - 'message' => implode('
', $messages), - ]); + $success = SessionMessage::make('Success', implode('
', $messages)); return redirect()->refresh()->with(compact('success')); } diff --git a/src/Support/SessionMessage.php b/src/Support/SessionMessage.php new file mode 100644 index 000000000..935a513e0 --- /dev/null +++ b/src/Support/SessionMessage.php @@ -0,0 +1,56 @@ +with() 序列化后变为数组的问题 + */ +class SessionMessage +{ + public function __construct( + protected string $title = '', + protected string $message = '', + protected array $options = [], + ) { + } + + public static function make(string $title, string $message = '', array $options = []): static + { + return new static($title, $message, $options); + } + + public static function fromArray(array $data): static + { + return new static( + title: (string) (\Illuminate\Support\Arr::first((array) ($data['title'] ?? '')) ?? ''), + message: (string) (\Illuminate\Support\Arr::first((array) ($data['message'] ?? '')) ?? ''), + options: (array) ($data['options'] ?? []), + ); + } + + public function getTitle(): string + { + return $this->title; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getOptions(): array + { + return $this->options; + } + + public function toArray(): array + { + return [ + 'title' => $this->title, + 'message' => $this->message, + 'options' => $this->options, + ]; + } +} diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 15b686789..8c591fdba 100755 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -2,10 +2,9 @@ use Dcat\Admin\Admin; use Dcat\Admin\Support\Helper; +use Dcat\Admin\Support\SessionMessage; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Renderable; -use Illuminate\Http\Request; -use Illuminate\Support\MessageBag; use Symfony\Component\HttpFoundation\Response; if (! function_exists('admin_setting')) { @@ -298,7 +297,7 @@ function admin_base_path($path = '') if (! function_exists('admin_toastr')) { /** - * Flash a toastr message bag to session. + * Flash a toastr message to session. * * @param string $message * @param string $type @@ -306,7 +305,7 @@ function admin_base_path($path = '') */ function admin_toastr($message = '', $type = 'success', $options = []) { - $toastr = new MessageBag(get_defined_vars()); + $toastr = SessionMessage::make($type, $message, $options); session()->flash('dcat-admin-toastr', $toastr); } @@ -353,7 +352,7 @@ function admin_warning($title, $message = '') if (! function_exists('admin_info')) { /** - * Flash a message bag to session. + * Flash a message to session. * * @param string $title * @param string $message @@ -361,7 +360,7 @@ function admin_warning($title, $message = '') */ function admin_info($title, $message = '', $type = 'info') { - $message = new MessageBag(get_defined_vars()); + $message = SessionMessage::make($title, $message); session()->flash($type, $message); } From cecbbe03230aebe417cbc853f4cfc9435f9c9ae9 Mon Sep 17 00:00:00 2001 From: Shine Date: Tue, 26 May 2026 22:19:06 +0800 Subject: [PATCH 2/7] refactor: remove dead code from SessionMessage Remove unused fromArray() and toArray() methods. SessionMessage intentionally does not implement Arrayable to prevent Laravel 13 from auto-converting it to an array via redirect()->with(). Having toArray() posed a theoretical risk if future Laravel versions detect the method instead of the interface. --- src/Support/SessionMessage.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Support/SessionMessage.php b/src/Support/SessionMessage.php index 935a513e0..86c23fed3 100644 --- a/src/Support/SessionMessage.php +++ b/src/Support/SessionMessage.php @@ -21,15 +21,6 @@ public static function make(string $title, string $message = '', array $options return new static($title, $message, $options); } - public static function fromArray(array $data): static - { - return new static( - title: (string) (\Illuminate\Support\Arr::first((array) ($data['title'] ?? '')) ?? ''), - message: (string) (\Illuminate\Support\Arr::first((array) ($data['message'] ?? '')) ?? ''), - options: (array) ($data['options'] ?? []), - ); - } - public function getTitle(): string { return $this->title; @@ -45,12 +36,4 @@ public function getOptions(): array return $this->options; } - public function toArray(): array - { - return [ - 'title' => $this->title, - 'message' => $this->message, - 'options' => $this->options, - ]; - } } From ce862334aab937636af0773517e469b07e78aab5 Mon Sep 17 00:00:00 2001 From: Shine Date: Sun, 31 May 2026 17:45:04 +0800 Subject: [PATCH 3/7] fix: support JSON session serialization for SessionMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When session.serialization=json (Laravel 13 default for security hardening), PHP objects with protected properties are encoded as empty JSON objects {}, making instanceof checks fail after deserialization. - Implement JsonSerializable on SessionMessage to encode all fields with a type discriminator key (__dcat_class), ensuring data survives JSON round-trip - Add tryFrom() static method to reconstruct from either a SessionMessage instance (PHP serialization) or a tagged array (JSON serialization) - Update alerts.blade.php and toastr.blade.php to use tryFrom() instead of instanceof, making them compatible with both serialization modes Compatible with PHP 8.1–8.5 and Laravel 10–13. --- resources/views/partials/alerts.blade.php | 10 ++--- resources/views/partials/toastr.blade.php | 17 ++++---- src/Support/SessionMessage.php | 53 +++++++++++++++++++++-- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/resources/views/partials/alerts.blade.php b/resources/views/partials/alerts.blade.php index cd769bd73..3b466dc08 100755 --- a/resources/views/partials/alerts.blade.php +++ b/resources/views/partials/alerts.blade.php @@ -1,4 +1,4 @@ -@if($error = session()->get('error')) +@if($error = \Dcat\Admin\Support\SessionMessage::tryFrom(session()->get('error')))

 {{ $error->getTitle() }}

@@ -16,7 +16,7 @@ @endif @endif -@if($success = session()->get('success')) +@if($success = \Dcat\Admin\Support\SessionMessage::tryFrom(session()->get('success')))

 {{ $success->getTitle() }}

@@ -24,7 +24,7 @@
@endif -@if($info = session()->get('info')) +@if($info = \Dcat\Admin\Support\SessionMessage::tryFrom(session()->get('info')))

 {{ $info->getTitle() }}

@@ -32,10 +32,10 @@
@endif -@if($warning = session()->get('warning')) +@if($warning = \Dcat\Admin\Support\SessionMessage::tryFrom(session()->get('warning')))

 {{ $warning->getTitle() }}

{!! $warning->getMessage() !!}

-@endif \ No newline at end of file +@endif diff --git a/resources/views/partials/toastr.blade.php b/resources/views/partials/toastr.blade.php index ac6a2b83c..9fbcd930b 100644 --- a/resources/views/partials/toastr.blade.php +++ b/resources/views/partials/toastr.blade.php @@ -1,9 +1,8 @@ -@if(Session::has('dcat-admin-toastr')) - @php - $toastr = Session::get('dcat-admin-toastr'); - $type = $toastr->getTitle(); - $message = $toastr->getMessage(); - $options = admin_javascript_json($toastr->getOptions()); - @endphp - -@endif \ No newline at end of file +@if($toastr = \Dcat\Admin\Support\SessionMessage::tryFrom(Session::get('dcat-admin-toastr'))) + @php + $type = $toastr->getTitle(); + $message = $toastr->getMessage(); + $options = admin_javascript_json($toastr->getOptions()); + @endphp + +@endif diff --git a/src/Support/SessionMessage.php b/src/Support/SessionMessage.php index 86c23fed3..7e52ffb4b 100644 --- a/src/Support/SessionMessage.php +++ b/src/Support/SessionMessage.php @@ -3,12 +3,18 @@ namespace Dcat\Admin\Support; /** - * Session 消息对象(兼容 Laravel 10-13). + * Session 消息对象(兼容 Laravel 10-13,兼容 PHP session.serialization = json/php). * - * 解决 Laravel 13 中 MessageBag 通过 redirect()->with() 序列化后变为数组的问题 + * 解决两类问题: + * 1. Laravel 13 中 MessageBag 通过 redirect()->with() 序列化后变为数组的问题 + * 2. session.serialization = json 时,protected 属性不会被 json_encode 包含的问题 */ -class SessionMessage +class SessionMessage implements \JsonSerializable { + private const JSON_CLASS_KEY = '__dcat_class'; + + private const JSON_CLASS_VALUE = self::class; + public function __construct( protected string $title = '', protected string $message = '', @@ -36,4 +42,45 @@ public function getOptions(): array return $this->options; } + /** + * 支持 JSON session 序列化(session.serialization = json). + * + * 将对象编码为包含类型标识符的数组,确保 protected 属性不丢失。 + */ + public function jsonSerialize(): mixed + { + return [ + self::JSON_CLASS_KEY => self::JSON_CLASS_VALUE, + 'title' => $this->title, + 'message' => $this->message, + 'options' => $this->options, + ]; + } + + /** + * 从 session 中读取的值尝试构造实例. + * + * 兼容两种情况: + * - PHP 序列化(session.serialization = php):值已经是 SessionMessage 对象 + * - JSON 序列化(session.serialization = json):值是带类型标识的数组 + */ + public static function tryFrom(mixed $value): ?static + { + if ($value instanceof static) { + return $value; + } + + if ( + is_array($value) + && ($value[self::JSON_CLASS_KEY] ?? null) === self::JSON_CLASS_VALUE + ) { + return new static( + $value['title'] ?? '', + $value['message'] ?? '', + $value['options'] ?? [], + ); + } + + return null; + } } From 7e04f4567f6d976757d4c81e9431ce9a479cbc2a Mon Sep 17 00:00:00 2001 From: Shine Date: Sun, 31 May 2026 21:38:12 +0800 Subject: [PATCH 4/7] fix: use json_encode for toastr message to prevent XSS Replace unescaped string interpolation in JS with json_encode() to safely escape single quotes, backslashes and control characters. Also remove the intermediate @php block by inlining the method calls. --- resources/views/partials/toastr.blade.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/resources/views/partials/toastr.blade.php b/resources/views/partials/toastr.blade.php index 9fbcd930b..620e8626d 100644 --- a/resources/views/partials/toastr.blade.php +++ b/resources/views/partials/toastr.blade.php @@ -1,8 +1,3 @@ @if($toastr = \Dcat\Admin\Support\SessionMessage::tryFrom(Session::get('dcat-admin-toastr'))) - @php - $type = $toastr->getTitle(); - $message = $toastr->getMessage(); - $options = admin_javascript_json($toastr->getOptions()); - @endphp - + @endif From 9f54ccc9b1776157ea80a61c0e03acaafe23b212 Mon Sep 17 00:00:00 2001 From: Shine Date: Sun, 31 May 2026 21:46:47 +0800 Subject: [PATCH 5/7] feat: add toastr type whitelist and SessionMessage unit tests - Add getToastrType() to SessionMessage with whitelist validation (['success','error','warning','info']), falls back to 'info' for unknown values to prevent arbitrary JS method injection - Update toastr.blade.php to use getToastrType() instead of getTitle() - Add 21 unit tests covering make/getters, toastr whitelist, jsonSerialize roundtrip, and tryFrom (PHP + JSON serialization paths) --- resources/views/partials/toastr.blade.php | 2 +- src/Support/SessionMessage.php | 7 + tests/Unit/SessionMessageTest.php | 162 ++++++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/SessionMessageTest.php diff --git a/resources/views/partials/toastr.blade.php b/resources/views/partials/toastr.blade.php index 620e8626d..a30881b11 100644 --- a/resources/views/partials/toastr.blade.php +++ b/resources/views/partials/toastr.blade.php @@ -1,3 +1,3 @@ @if($toastr = \Dcat\Admin\Support\SessionMessage::tryFrom(Session::get('dcat-admin-toastr'))) - + @endif diff --git a/src/Support/SessionMessage.php b/src/Support/SessionMessage.php index 7e52ffb4b..852968074 100644 --- a/src/Support/SessionMessage.php +++ b/src/Support/SessionMessage.php @@ -15,6 +15,8 @@ class SessionMessage implements \JsonSerializable private const JSON_CLASS_VALUE = self::class; + private const TOASTR_TYPES = ['success', 'error', 'warning', 'info']; + public function __construct( protected string $title = '', protected string $message = '', @@ -32,6 +34,11 @@ public function getTitle(): string return $this->title; } + public function getToastrType(): string + { + return in_array($this->title, self::TOASTR_TYPES, true) ? $this->title : 'info'; + } + public function getMessage(): string { return $this->message; diff --git a/tests/Unit/SessionMessageTest.php b/tests/Unit/SessionMessageTest.php new file mode 100644 index 000000000..c4f2053e1 --- /dev/null +++ b/tests/Unit/SessionMessageTest.php @@ -0,0 +1,162 @@ + 3000]); + + $this->assertSame('success', $msg->getTitle()); + $this->assertSame('hello', $msg->getMessage()); + $this->assertSame(['timeOut' => 3000], $msg->getOptions()); + } + + public function test_make_defaults_message_and_options(): void + { + $msg = SessionMessage::make('info'); + + $this->assertSame('', $msg->getMessage()); + $this->assertSame([], $msg->getOptions()); + } + + // ----------------------------------------------------------------------- + // getToastrType — whitelist + // ----------------------------------------------------------------------- + + #[\PHPUnit\Framework\Attributes\DataProvider('validToastrTypes')] + public function test_get_toastr_type_returns_valid_type(string $type): void + { + $this->assertSame($type, SessionMessage::make($type)->getToastrType()); + } + + public static function validToastrTypes(): array + { + return [ + ['success'], + ['error'], + ['warning'], + ['info'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('invalidToastrTypes')] + public function test_get_toastr_type_falls_back_to_info_for_invalid_type(string $type): void + { + $this->assertSame('info', SessionMessage::make($type)->getToastrType()); + } + + public static function invalidToastrTypes(): array + { + return [ + [''], + ['alert'], + ['SUCCESS'], + [''], + ['error; DROP TABLE'], + ]; + } + + // ----------------------------------------------------------------------- + // jsonSerialize (JSON session 序列化) + // ----------------------------------------------------------------------- + + public function test_json_serialize_includes_class_key_and_all_fields(): void + { + $msg = SessionMessage::make('error', 'something went wrong', ['closeButton' => true]); + $data = $msg->jsonSerialize(); + + $this->assertSame('Dcat\Admin\Support\SessionMessage', $data['__dcat_class']); + $this->assertSame('error', $data['title']); + $this->assertSame('something went wrong', $data['message']); + $this->assertSame(['closeButton' => true], $data['options']); + } + + public function test_json_encode_roundtrip(): void + { + $msg = SessionMessage::make('warning', 'be careful', ['positionClass' => 'toast-top-right']); + $decoded = json_decode(json_encode($msg), true); + + $this->assertSame('warning', $decoded['title']); + $this->assertSame('be careful', $decoded['message']); + $this->assertSame(['positionClass' => 'toast-top-right'], $decoded['options']); + } + + // ----------------------------------------------------------------------- + // tryFrom — PHP 序列化路径 + // ----------------------------------------------------------------------- + + public function test_try_from_accepts_existing_instance(): void + { + $original = SessionMessage::make('success', 'done'); + $result = SessionMessage::tryFrom($original); + + $this->assertSame($original, $result); + } + + public function test_try_from_returns_null_for_plain_object(): void + { + $this->assertNull(SessionMessage::tryFrom(new \stdClass())); + } + + public function test_try_from_returns_null_for_null(): void + { + $this->assertNull(SessionMessage::tryFrom(null)); + } + + public function test_try_from_returns_null_for_string(): void + { + $this->assertNull(SessionMessage::tryFrom('success')); + } + + // ----------------------------------------------------------------------- + // tryFrom — JSON 序列化路径 + // ----------------------------------------------------------------------- + + public function test_try_from_reconstructs_from_json_array(): void + { + $original = SessionMessage::make('info', 'hello', ['timeOut' => 5000]); + $array = json_decode(json_encode($original), true); + + $restored = SessionMessage::tryFrom($array); + + $this->assertNotNull($restored); + $this->assertSame('info', $restored->getTitle()); + $this->assertSame('hello', $restored->getMessage()); + $this->assertSame(['timeOut' => 5000], $restored->getOptions()); + } + + public function test_try_from_returns_null_for_array_without_class_key(): void + { + $this->assertNull(SessionMessage::tryFrom(['title' => 'info', 'message' => 'hi'])); + } + + public function test_try_from_returns_null_for_array_with_wrong_class_key(): void + { + $this->assertNull(SessionMessage::tryFrom([ + '__dcat_class' => 'Some\Other\Class', + 'title' => 'info', + 'message' => 'hi', + ])); + } + + public function test_try_from_tolerates_missing_fields_in_json_array(): void + { + $restored = SessionMessage::tryFrom([ + '__dcat_class' => 'Dcat\Admin\Support\SessionMessage', + ]); + + $this->assertNotNull($restored); + $this->assertSame('', $restored->getTitle()); + $this->assertSame('', $restored->getMessage()); + $this->assertSame([], $restored->getOptions()); + } +} \ No newline at end of file From 69f0c3025da5b952f3f630bd31fb51e8c0b35da0 Mon Sep 17 00:00:00 2001 From: Shine Date: Sun, 31 May 2026 21:50:45 +0800 Subject: [PATCH 6/7] fix: add type guards in SessionMessage::tryFrom() for tampered session data Validate title/message as string and options as array when reconstructing from a JSON-serialized array, preventing unexpected types from reaching the view layer. Add two unit tests covering the new guards. --- src/Support/SessionMessage.php | 6 +++--- tests/Unit/SessionMessageTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/Support/SessionMessage.php b/src/Support/SessionMessage.php index 852968074..5aaeb12b0 100644 --- a/src/Support/SessionMessage.php +++ b/src/Support/SessionMessage.php @@ -82,9 +82,9 @@ public static function tryFrom(mixed $value): ?static && ($value[self::JSON_CLASS_KEY] ?? null) === self::JSON_CLASS_VALUE ) { return new static( - $value['title'] ?? '', - $value['message'] ?? '', - $value['options'] ?? [], + is_string($value['title'] ?? null) ? $value['title'] : '', + is_string($value['message'] ?? null) ? $value['message'] : '', + is_array($value['options'] ?? null) ? $value['options'] : [], ); } diff --git a/tests/Unit/SessionMessageTest.php b/tests/Unit/SessionMessageTest.php index c4f2053e1..066a04aac 100644 --- a/tests/Unit/SessionMessageTest.php +++ b/tests/Unit/SessionMessageTest.php @@ -159,4 +159,34 @@ public function test_try_from_tolerates_missing_fields_in_json_array(): void $this->assertSame('', $restored->getMessage()); $this->assertSame([], $restored->getOptions()); } + + public function test_try_from_ignores_non_string_title_and_message(): void + { + $restored = SessionMessage::tryFrom([ + '__dcat_class' => 'Dcat\Admin\Support\SessionMessage', + 'title' => ['injected'], + 'message' => 42, + 'options' => ['timeOut' => 3000], + ]); + + $this->assertNotNull($restored); + $this->assertSame('', $restored->getTitle()); + $this->assertSame('', $restored->getMessage()); + $this->assertSame(['timeOut' => 3000], $restored->getOptions()); + } + + public function test_try_from_ignores_non_array_options(): void + { + $restored = SessionMessage::tryFrom([ + '__dcat_class' => 'Dcat\Admin\Support\SessionMessage', + 'title' => 'info', + 'message' => 'hello', + 'options' => 'not-an-array', + ]); + + $this->assertNotNull($restored); + $this->assertSame('info', $restored->getTitle()); + $this->assertSame('hello', $restored->getMessage()); + $this->assertSame([], $restored->getOptions()); + } } \ No newline at end of file From e2ae02819227912ebc9adbddd44fdff9632357c7 Mon Sep 17 00:00:00 2001 From: Shine Date: Sun, 31 May 2026 22:11:40 +0800 Subject: [PATCH 7/7] fix: resolve PHPStan errors in SessionMessage and helpers - Declare SessionMessage as final (value object, no subclassing needed), replace new static()/static return types with new self()/self to fix 'Unsafe usage of new static()' errors - Restore missing 'use Illuminate\Http\Request' in helpers.php that was accidentally removed during refactor, fixing class.notFound on admin_redirect() --- src/Support/SessionMessage.php | 12 ++++++------ src/Support/helpers.php | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Support/SessionMessage.php b/src/Support/SessionMessage.php index 5aaeb12b0..e6a3f29e0 100644 --- a/src/Support/SessionMessage.php +++ b/src/Support/SessionMessage.php @@ -9,7 +9,7 @@ * 1. Laravel 13 中 MessageBag 通过 redirect()->with() 序列化后变为数组的问题 * 2. session.serialization = json 时,protected 属性不会被 json_encode 包含的问题 */ -class SessionMessage implements \JsonSerializable +final class SessionMessage implements \JsonSerializable { private const JSON_CLASS_KEY = '__dcat_class'; @@ -24,9 +24,9 @@ public function __construct( ) { } - public static function make(string $title, string $message = '', array $options = []): static + public static function make(string $title, string $message = '', array $options = []): self { - return new static($title, $message, $options); + return new self($title, $message, $options); } public function getTitle(): string @@ -71,9 +71,9 @@ public function jsonSerialize(): mixed * - PHP 序列化(session.serialization = php):值已经是 SessionMessage 对象 * - JSON 序列化(session.serialization = json):值是带类型标识的数组 */ - public static function tryFrom(mixed $value): ?static + public static function tryFrom(mixed $value): ?self { - if ($value instanceof static) { + if ($value instanceof self) { return $value; } @@ -81,7 +81,7 @@ public static function tryFrom(mixed $value): ?static is_array($value) && ($value[self::JSON_CLASS_KEY] ?? null) === self::JSON_CLASS_VALUE ) { - return new static( + return new self( is_string($value['title'] ?? null) ? $value['title'] : '', is_string($value['message'] ?? null) ? $value['message'] : '', is_array($value['options'] ?? null) ? $value['options'] : [], diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 8c591fdba..6e5a2da2b 100755 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -5,6 +5,7 @@ use Dcat\Admin\Support\SessionMessage; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Renderable; +use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; if (! function_exists('admin_setting')) {