matteoc99/laravel-preference

Laravel 包,旨在以简单和可扩展的方式存储和管理用户设置/偏好

v2.1.2 2024-05-05 15:45 UTC

README

Latest Version on Packagist Total Downloads Tests codecov

此 Laravel 包旨在以简单和可扩展的方式存储和管理用户设置/偏好。

目录

功能

  • 类型安全的类型转换
  • 验证与授权
  • 可扩展的(创建你自己的验证规则和类型转换)
  • 枚举支持
  • 自定义 API 路由
    • 与 GUI 或后端功能一起使用偏好

路线图

  • 事件系统 -> #13

  • API 响应自定义 -> #14

  • QoL 辅助函数

  • 缓存

  • Blade 指令

  • 欢迎更多建议。(查看 贡献

安装

您可以通过 composer 安装此包

composer require matteoc99/laravel-preference

重要

考虑安装 graham-campbell/security-core:^4.0 以利用 XSS 清理。有关更多信息,请参阅 安全

配置

您可以使用以下命令发布配置文件

php artisan vendor:publish --tag="laravel-preference-config"
  'db' => [
        'connection' => null, //string: the connection name to use 
        'preferences_table_name'      => 'preferences',
        'user_preferences_table_name' => 'user_preferences',
    ],
    'xss_cleaning' => true, // clean user input for cross site scripting attacks
    'routes' => [
        'enabled'     => false, // set true to register routes, more on that later
        'middlewares' => [
            'auth', // general middleware
            'user'=> 'verified', // optional, scoped middleware
            'user.general'=> 'verified' // optional, scoped & grouped middleware
        ],
        'prefix' => 'preferences', 
        'groups'      => [
            //enum class list of preferences
            'general'=>General::class
        ],
        'scopes'=> [
           // as many preferenceable models as you want
            'user' => \Illuminate\Auth\Authenticatable::class
        ]
    ]

注意

如果需要,请在运行迁移之前考虑更改基础表名称

使用以下命令运行迁移

php artisan migrate

使用方法

概念

每个偏好至少有一个名称和一个类型转换器。名称存储在一个或多个枚举中,并是该偏好的唯一标识符

对于额外的验证,您可以添加您自定义的规则对象。

对于额外的安全性,您可以添加策略

定义你的偏好

将它们组织在一个或多个 字符串支持的 枚举中。

注意

尽管它不需要是字符串支持的,但它对开发者来说更加友好。尤其是当通过 API 交互时

每个枚举都会进行范围限制,不会与其他相同情况的枚举冲突

例如:

use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;

enum Preferences :string implements PreferenceGroup
{
    case LANGUAGE="language";
    case QUALITY="quality";
    case CONFIG="configuration";
}

enum General :string implements PreferenceGroup
{
    case LANGUAGE="language"; 
    case THEME="theme";
}

创建偏好

单模式

use Matteoc99\LaravelPreference\Enums\Cast;

public function up(): void
{
    PreferenceBuilder::init(Preferences::LANGUAGE)
        ->withDefaultValue("en")
        ->withRule(new InRule("en", "it", "de"))
        ->create();
        
   
    // Or
    PreferenceBuilder::init(Preferences::LANGUAGE)->create()
    // different enums with the same value do not conflict
    PreferenceBuilder::init(General::LANGUAGE)->create()
    
    // update
    PreferenceBuilder::init(Preferences::LANGUAGE)
        ->withRule(new InRule("en", "it", "de"))
        ->updateOrCreate()

    // or with casting
    PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
        ->withDefaultValue(Language::EN)
        ->create()

    // nullable support
    PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
        ->withDefaultValue(null)
        ->nullable()
        ->create()

}

public function down(): void
{
    PreferenceBuilder::delete(Preferences::LANGUAGE);
}

批量模式

use Illuminate\Database\Migrations\Migration;use Matteoc99\LaravelPreference\Enums\Cast;use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;use Matteoc99\LaravelPreference\Rules\InRule;

return new class extends Migration {


    public function up(): void
    {

        PreferenceBuilder::initBulk($this->preferences(),
        true // nullable for the whole Bulk
        );
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        PreferenceBuilder::deleteBulk($this->preferences());
    }

    /**
     * Reverse the migrations.
     */
    public function preferences(): array
    {
       return [
            ['name' => Preferences::LANGUAGE, 'cast' => Cast::STRING, 'default_value' => 'en', 'rule' => new InRule("en", "it", "de")],
            ['name' => Preferences::THEME, 'cast' => Cast::STRING, 'default_value' => 'light'],
            ['name' => Preferences::CONFIGURATION, 'cast' => Cast::ARRAY],
            ['name' => Preferences::CONFIGURATION, 
                'nullable' => true // or nullable for only one configuration
            ],
            // or an array of initialized single-mode builders
            PreferenceBuilder::init(Preferences::LANGUAGE)->withRule(new InRule("en", "it", "de")), 
            PreferenceBuilder::init(Preferences::THEME)->withRule(new InRule("light", "dark")) 
            //mixing both in one array is also possible
       ];
    }
};

偏好构建

检查构建偏好的所有可用方法

可用方法

此表包括构建偏好时所有可用功能的完整列表。

可用辅助函数

可选地,将默认值作为第二个参数传递

        // quickly build a nullable Array preference
        PreferenceBuilder::buildArray(VideoPreferences::CONFIG);

        PreferenceBuilder::buildString(VideoPreferences::LANGUAGE);

与偏好协同工作

需要两件事

  • HasPreferences 特性以访问辅助函数
  • PreferenceableModel 接口以访问实现
    • 特别是 isUserAuthorized

isUserAuthorized

守卫函数,用于验证当前登录(如果有的话)的用户是否可以访问此模型 签名

  • $user 登录用户
  • PolicyAction 枚举:用户想要执行的操作索引/获取/更新/删除

注意

这只是关于授权的最基本内容。
有关更精细的授权检查,请参阅 策略

示例实现

use Matteoc99\LaravelPreference\Contracts\PreferenceableModel;
use Matteoc99\LaravelPreference\Enums\PolicyAction;
use Matteoc99\LaravelPreference\Traits\HasPreferences;

class User extends \Illuminate\Foundation\Auth\User implements PreferenceableModel
{
    use HasPreferences;

    protected $fillable = ['email'];

    public function isUserAuthorized(?Authenticatable $user, PolicyAction $action): bool
    {
        return $user?->id == $this->id ;
    }
}

示例

    $user->setPreference(Preferences::LANGUAGE,"de");
    $user->getPreference(Preferences::LANGUAGE); // 'de' as string

    $user->setPreference(Preferences::LANGUAGE,"fr"); 
    // ValidationException because of the rule: ->withRule(new InRule("en","it","de"))
    $user->setPreference(Preferences::LANGUAGE,2); 
    // ValidationException because of the cast: Cast::STRING

    $user->removePreference(Preferences::LANGUAGE); 
    $user->getPreference(Preferences::LANGUAGE); // 'en' as string
    
    // get all of type Preferences,
    $user->getPreferences(Preferences::class)
    // or of type general
    $user->getPreferences(General::class)
    //or all
    $user->getPreferences(): Collection of UserPreferences


    // removes all preferences set for tht user
    $user->removeAllPreferences();
    

类型转换

在创建偏好时设置类型转换

注意

类型转换主要有三个任务

  • 基本验证
  • 数据库的转换
  • 准备API响应

示例

PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)

可用的类型转换

自定义类型转换器

实现CastableEnum

重要

自定义转换器需要是基于字符串的枚举

示例

use Illuminate\Contracts\Validation\ValidationRule;
use Matteoc99\LaravelPreference\Contracts\CastableEnum;

enum MyCast: string implements CastableEnum
{
    case TIMEZONE = 'tz';
 
    public function validation(): ValidationRule|array|string|null
    {
        return match ($this) {
            self::TIMEZONE => 'timezone:all',
        };
    }

    public function castFromString(string $value): mixed
    {
        return match ($this) {
            self::TIMEZONE => $value,
        
        };
    }
    public function castToString(mixed $value): string
    {
        return match ($this) {
            self::TIMEZONE => (string)$value,
        };
    }
    
   public function castToDto(mixed $value): array
    {
        return ['value' => $value];
    } 
}

 PreferenceBuilder::init(Preferences::TIMEZONE, MyCast::TIMEZONE)->create();

规则

额外的验证,可能比提供的转换更复杂

添加规则

     PreferenceBuilder::init(General::VOLUME, Cast::INT)
        ->withRule(new LowerThanRule(5))
        ->updateOrCreate()


    PreferenceBuilder::initBulk([
        'name' => General::VOLUME,
        'cast' => Cast::INT
        'rule' => new LowerThanRule(5)
     ]);

可用的规则

自定义规则

实现Laravel的ValidationRule

示例

class MyRule implements ValidationRule
{

    protected array $data;

    public function __construct(...$data)
    {
        $this->data = $data;
    }

    public function message()
    {
        return sprintf("Wrong Timezone, one of: %s expected", implode(", ",$this->data));
    }
    
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if(!Str::startsWith($value, $this->data)){
            $fail($this->message());
        }
    }
}

 PreferenceBuilder::init("timezone",MyCast::TIMEZONE)
            ->withRule(new MyRule("Europe","Asia"))

策略

每个偏好都可以有一个策略,如果isUserAuthorized不足以满足你的用例

创建策略

实现PreferencePolicy和合同定义的4个方法

添加策略

    PreferenceBuilder::init(Preferences::LANGUAGE)
        ->withPolicy(new MyPolicy())
        ->updateOrCreate()


    PreferenceBuilder::initBulk([
        'name' => Preferences::LANGUAGE,
        'policy' => new MyPolicy()
     ]);

路由

默认关闭,在配置中启用

警告

(当前)限制:无法通过API设置对象转换

解剖学

'Scope': PreferenceableModel模型
'Group': PreferenceGroup枚举

然后路由被转换成

可以通过路由名称:{prefix}.{scope}.{group}.{index/get/update/delete}访问

URI参数

scope_id:范围的唯一标识符(例如,用户的ID)。
preference:特定偏好枚举的值(例如,General::LANGUAGE->value)。
group:将组名映射到相应的枚举类。请参阅下面的配置
scope:将范围名称映射到相应的Eloquent模型。请参阅下面的配置

配置示例

 'routes' => [
        'enabled'     => true, 
        'middlewares' => [
            'auth',
            'user'=> 'verified'
        ],
        'prefix' => 'custom_prefix', 
        'groups'      => [
            'general'=>\Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General::class
            'video'=>\Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences::class
        ],
        'scopes'=> [
            'user' => \Matteoc99\LaravelPreference\Tests\TestSubjects\Models\User::class
        ]
    ]

将产生以下路由名称

  • custom_prefix.user.general.index
  • custom_prefix.user.general.get
  • custom_prefix.user.general.update
  • custom_prefix.user.general.delete
  • custom_prefix.user.video.index
  • custom_prefix.user.video.get
  • custom_prefix.user.video.update
  • custom_prefix.user.video.delete

动作

注意

示例使用范围user和组general

索引

GET

更新

枚举修补

当创建你的枚举偏好时,添加包含可能枚举的setAllowedClasses以重建值

注意

如果多个枚举之间存在共享的多个情况,则取第一个匹配项

然后,发送值时会有所不同

  • BackedEnum:发送值或情况
  • UnitEnum:发送情况

示例

enum Theme
{
    case LIGHT;
    case DARK;
}
curl -X PATCH 'https://your.domain/custom_prefix/user/{scope_id}/general/{preference}' \
    -d '{"value": "DARK"}'

删除

中间件

在配置文件中设置全局或上下文特定的中间件

'middlewares' => [
'web', // required for Auth::user() and policies
'auth', //no key => general middleware which gets applied to all routes
'user'=> 'verified', //  scoped middleware only for user routes should you have other preferencable models
'user.general'=> 'verified' // scoped & grouped middleware only for a specific model + enum
],

注意

已知问题:没有web中间件,你将无法通过Auth外观访问用户,因为它是通过中间件设置的。正在寻找替代方案

安全

XSS清理仅在对用户可见的API调用上执行。可以通过配置禁用,如果不要求的话:user_preference.xss_cleaning

直接通过setPreference设置偏好时,假定已执行了此清理步骤(如果需要的话)。

考虑安装Security-Core以使用此功能

从 v1 升级

  • 在你的偏好枚举中实现PreferenceGroup
  • 在所有希望使用偏好的模型中实现 PreferenceableModel
  • HasValidation 切换到 ValidationRule
  • 特性行为签名变更:组别已被移除,名称现在需要 PreferenceGroup
  • Builder:移除了设置组别,名称现在期望一个 PreferenceGroup 枚举
  • DataRule 已被移除,添加一个构造函数以获取您自己的定制参数
  • 数据库序列化不兼容性将需要您重新运行偏好迁移
    • 单模式:请确保使用 updateOrCreate,例如 PreferenceBuilder::init(VideoPreferences::QUALITY)->updateOrCreate();
    • 批量模式:像往常一样初始化 bulk,因为它与 upsert 一起工作

测试

composer 测试

composer 覆盖率

在本地测试管道

查看 act,通过 gh 安装它

然后运行:composer pipeline

贡献

有关详细信息,请参阅 贡献指南

安全漏洞

请审查 我们的安全策略 了解如何报告安全漏洞。

鸣谢

许可证

MIT 许可证 (MIT)。请检查 许可文件 以获取更多信息。

支持目标