编程

使用 spatie/laravel-data 保存 Laravel 应用的设置

886 2023-11-16 17:48:00

很长一段时间以来,我一直在努力寻找一种在我的 Laravel 应用中为用户、团队或任何其他模型存储设置的好方法。在过去的10年里,我使用了不同的方法来解决这个问题。

要么在相应的数据库表格中每个设置添加特定的字段列(比如,在 user 表中添加 timezone 或 date_format 字段);或者在 user 表中添加通用的设置 JSON 字段,来存储设置。

每个方法都有其优点和缺点。如果需要通过特定设置查询(比如,查询哪个用户有自定义日期格式如 YYYY-MM-DD),添加单独的列是不错的方案。另一方面,仅为一个小的设置添加一个新字段可能会过剩,尤其是当表中有数百万或数十亿行时。

将设置字段与核心表分离可以更好地提高数据库性能。数据库不必一直加载所有设置字段,这在你需要数据库中的用户或项目列表时非常有用。

添加宽泛的设置字段是最简单的方式,不过可能带来性能问题,并且使行中出现不同结构。如果预期的设置不存在于用户中,应用如何响应?你是否不得不在每个地方检查设置的 JSON 键是否存在(比如,$user->settings?->my_custom_setting)?

我的方案是,使用 spatie/laravel-data

spatie/laravel-data

该 laravel-data 包的主要用例是,在 Laravel 项目中创建强类型数据对象。以下是这样的数据对象的示例:

use Spatie\LaravelData\Data;

class SongData extends Data
{
    public function __construct(
        public string $title,
        public Artist $artist,
    ) {
    }
}

该包也支持 Eloquent 转型,这意味着数据对象可以保存到数据端,并且当检索时转换会强类型数据实例。

强类型和 Eloquent 转型结合启发我使用该包保存应用设置。

示例

这是一个 UserSettings 对象的用例。

<?php

namespace App\Data;

use App\Domain\Support\Enums\ThemeApperance;
use Spatie\LaravelData\Data;

class UserSettings extends Data
{
    public function __construct(
        public string $timezone = 'UTC',
        public string $locale = 'en',
        public string $date_format = 'YYYY-MM-DD',
        public ThemeApperance $apperance = ThemeApperance::AUTO,
    ) {
        //
    }
}

在这些设置中,存储看 timezone、locale 及偏好的日期格式,以及用户的主题外观。

创建完添加 settings 字段到 users 表的迁移后,更新 User 模型,使 settings 转型为 UserSettings 实例。

use App\Data\UserSettings;

/**
 * The attributes that should be cast.
 *
 * @var array<string, string>
 */
protected $casts = [
    'settings' => UserSettings::class . ':default',
];

现在,多亏了 laravel-data,无论何时 $user->settings 总能获取 UserSettings 实例中所有的属性。

如果用户在两年前登录,且该应用不支持 $date_format,用户中的这个值将回退到我在 UserSettings  类中声明的默认值。 

下次用户更新设置时,过期的数据库状态将得到更新。不再支持的设置也将被移除。

如果你不想等待用户更新设置,你可以创建 Artisan 命令来帮你更新。

use App\Data\UserSettings;
use App\Models\User;

Artisan::command('app:update-user-settings', function () {
    // Get all Users and update their settings
    User::query()
        ->each(function (User $user) {
            // Update settings to the newest format
            $user->settings = UserSettings::from($user->settings);
            $user->save();
        });
});

该命令循环迭代数据库中的所有用户,并使用一个新版本更新 settings 字段。用户的当前设置(比如,他们选择的 $date_formate)将被迁移。

为什么这样

起初,我提到的添加一个宽泛的 settings 字段,如果没有一个结构化的方式来存储设置,可能不是一个好主意。如果应用的不同部分中添加了新的键到设置中,可能很快就变得混乱了。而使用 laravel-data,该问题得以解决。设置只有一个来源。

你可以使用类型提示和枚举让设置强类型。你甚至可以创建嵌套结构的设置。

假设 UserSettings 类包含 UserGeneralSettingsUserNotificationSettingsUserApperanceSettings

<?php

namespace App\Data;

use App\Domain\Support\Enums\ThemeApperance;
use Spatie\LaravelData\Data;

class UserSettings extends Data
{
    public function __construct(
        public UserGeneralSettings $general,
        public UserNotificationSettings $notification,
        public UserApperanceSettings $apperance,
    ) {
        //
    }
}

需要记住的是,对特定设置的查询可能带来性能问题,或许应该避免。

如果应用通常需要查询选择特定的 date_formate 的用户,最好将该设置提升为单独的字段。这可能会让数据库的工作及索引更为容易。

结论

展望未来,我将在所有需要设置概念的新应用和现有应用中使用这种方法。

我相信使用 spatie/laravel 数据和 Eloquent 类型转换比只将您的设置放入通用的 $settings 数组要好。我鼓励你在下一个项目中尝试一下。

你喜欢这种方法吗?还是觉得这不是个好主意呢?欢迎留言。