diff --git a/resources/views/partials/alerts.blade.php b/resources/views/partials/alerts.blade.php index d34eb8874..3b466dc08 100755 --- a/resources/views/partials/alerts.blade.php +++ b/resources/views/partials/alerts.blade.php @@ -1,8 +1,8 @@ -@if($error = session()->get('error')) +@if($error = \Dcat\Admin\Support\SessionMessage::tryFrom(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')) @@ -16,26 +16,26 @@ @endif @endif -@if($success = session()->get('success')) +@if($success = \Dcat\Admin\Support\SessionMessage::tryFrom(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')) +@if($info = \Dcat\Admin\Support\SessionMessage::tryFrom(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')) +@if($warning = \Dcat\Admin\Support\SessionMessage::tryFrom(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 +@endif 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..a30881b11 100644 --- a/resources/views/partials/toastr.blade.php +++ b/resources/views/partials/toastr.blade.php @@ -1,9 +1,3 @@ -@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', [])); - @endphp - -@endif \ No newline at end of file +@if($toastr = \Dcat\Admin\Support\SessionMessage::tryFrom(Session::get('dcat-admin-toastr'))) + +@endif 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..e6a3f29e0 --- /dev/null +++ b/src/Support/SessionMessage.php @@ -0,0 +1,93 @@ +with() 序列化后变为数组的问题 + * 2. session.serialization = json 时,protected 属性不会被 json_encode 包含的问题 + */ +final class SessionMessage implements \JsonSerializable +{ + private const JSON_CLASS_KEY = '__dcat_class'; + + private const JSON_CLASS_VALUE = self::class; + + private const TOASTR_TYPES = ['success', 'error', 'warning', 'info']; + + public function __construct( + protected string $title = '', + protected string $message = '', + protected array $options = [], + ) { + } + + public static function make(string $title, string $message = '', array $options = []): self + { + return new self($title, $message, $options); + } + + 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; + } + + 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): ?self + { + if ($value instanceof self) { + return $value; + } + + if ( + is_array($value) + && ($value[self::JSON_CLASS_KEY] ?? null) === self::JSON_CLASS_VALUE + ) { + return new self( + is_string($value['title'] ?? null) ? $value['title'] : '', + is_string($value['message'] ?? null) ? $value['message'] : '', + is_array($value['options'] ?? null) ? $value['options'] : [], + ); + } + + return null; + } +} diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 15b686789..6e5a2da2b 100755 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -2,10 +2,10 @@ 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 +298,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 +306,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 +353,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 +361,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); } diff --git a/tests/Unit/SessionMessageTest.php b/tests/Unit/SessionMessageTest.php new file mode 100644 index 000000000..066a04aac --- /dev/null +++ b/tests/Unit/SessionMessageTest.php @@ -0,0 +1,192 @@ + 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()); + } + + 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