拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Snipe-IT 服务提供者开发指南:在 Provider 中注册验证规则与模型观察者

Snipe-IT 服务提供者开发指南:在 Provider 中注册验证规则与模型观察者
  • 后端
  • 企业应用

【免费下载链接】snipe-it

A free open source IT asset/license management system

项目地址:https://gitcode.com/GitHub_Trending/sn/snipe-it
点击查看免费下载

本篇指南聚焦 Snipe-IT(开源 IT 资产/授权管理系统)中app/Providers/**目录的开发约定,详细讲解如何通过ValidationServiceProvider注册命名验证规则,以及如何在AppServiceProvider::boot()中挂载模型观察者。读完本文,你将掌握 Snipe-IT 自定义验证规则的完整链路(Provider 注册 → 模型$rules引用 → 控制器/表单校验触发),并理解观察者注册方式与项目约定的边界(为什么禁止在模型上使用#[ObservedBy]属性、为什么通用规则不能放进app/Rules)。

一、app/Providers目录的职责划分

app/Providers存放 Laravel 应用级服务提供者。在 Snipe-IT 中,这些 Provider 各自承担明确的引导职责,本文涉及的两种核心模式分别由两个 Provider 承载:

Provider职责
ValidationServiceProvider注册所有命名验证规则(Validator::extend()/extendImplicit()闭包)
AppServiceProvider在boot()中挂载模型观察者、配置分页器与视图组件等

目录下的其他 Provider(如 AuthServiceProvider、EventServiceProvider、RouteServiceProvider 等)分别负责权限策略绑定、事件监听注册与路由加载,本文不展开。

二、命名验证规则统一注册在 ValidationServiceProvider

Snipe-IT 的约定是:新增的命名验证规则一律以Validator::extend()(或extendImplicit())闭包的形式,写在 ValidationServiceProvider.php 的boot()方法中,随后即可在模型或表单请求的$rules数组里以字符串形式直接引用该规则名。

2.1 基本注册模式

规则闭包接收 Laravel Validator 传入的标准四个参数:

  • $attribute:被校验的字段名
  • $value:被校验字段的当前值
  • $parameters:规则字符串中冒号后以逗号分隔的参数数组
  • $validator:当前的 Validator 实例(可通过$validator->getData()读取同一次请求中的其他字段)

以email_array为例,它校验逗号分隔的收件人列表是否全部是合法邮箱:

Validator::extend('email_array', function ($attribute, $value, $parameters, $validator) { $value = str_replace(' ', '', $value); $array = explode(',', $value); $email_to_validate = []; foreach ($array as $email) { $email_to_validate['alert_email'][] = $email; } $rules = ['alert_email.*' => 'email']; $messages = [ 'alert_email.*' => trans('validation.custom.email_array'), ]; $validator = Validator::make($email_to_validate, $rules, $messages); return $validator->passes(); });

可以看到,闭包内部把用户输入拆分为数组后,用内置的email规则逐项校验,并把错误消息映射到validation.custom.email_array翻译键。

2.2 什么时候用extendImplicit():fmcs_company的启发

Laravel 默认会跳过字段为空(null/缺失)时的"显式"扩展规则,因此如果某条规则必须在空值提交时也要触发,就必须用Validator::extendImplicit()注册,这和内置的required、filled、accepted使用隐式规则是同一原因。

源码中fmcs_company就是典型例子——它的职责正是"严格多公司模式(FMCS)下,有公司归属的非超级管理员必须选择一个公司,不允许提交空值":

Validator::extendImplicit('fmcs_company', function ($attribute, $value, $parameters, $validator) { $settings = Setting::getSettings(); if (! $settings->full_multiple_companies_support) { return true; } if ((bool) $settings->null_company_is_floater) { return true; } if (! empty($value)) { return true; } if (! auth()->check()) { return true; } $actor = auth()->user(); if ($actor->isSuperUser()) { return true; } if (! $actor->companies()->exists()) { return true; } return false; });

它的通过条件覆盖了完整场景:FMCS 关闭、允许"浮动"空公司、表单已填值、CLI/Seeder 等无认证上下文、超级管理员、无公司归属用户(在空"伪公司"命名空间合法操作)。规则同时在Validator::replacer('fmcs_company', ...)中把:attribute替换为公司的翻译文案。

2.3 用Validator::replacer()美化错误消息

当需要把规则参数嵌入翻译后的错误消息时,可以像two_column_unique_undeleted那样注册一个replacer:

Validator::replacer('two_column_unique_undeleted', function ($message, $attribute, $rule, $parameters) { $message = str_replace(':table1', $parameters[0], $message); $message = str_replace(':table2', $parameters[2], $message); // Change underscores to spaces for a friendlier display $message = str_replace('_', ' ', $message); return $message; });

三、常用内置命名规则与参数速查

下面按源码中 ValidationServiceProvider.php 的实际实现,整理项目中最常用、可直接复用的命名规则及其参数约定。

3.1 软删除场景下的唯一性规则

Snipe-IT 大量使用 Laravel 软删除(deleted_at)。内置unique规则无法区分"已删除记录"与"现存记录",会导致重复数据无法重新创建,因此项目提供了一整套配套规则:

规则名参数语义源码注释
unique_undeleted:table,field表名、字段名仅在与现存(未删除)记录比较时保证唯一见$rules中大量使用,如'asset_tag' => 'unique_undeleted:assets,asset_tag'
exists_undeleted:table,column表名、列名(默认id)校验值指向的行的确存在且未被软删除,用于用户输入 ID 的查找(如 checkout 目标)用法exists_undeleted:users,id
unique_undeleted_in_scope:col1,col2,...作用域列名列表在兄弟节点范围内唯一,支持树形表与 FMCS 公司内唯一;NULL 按标准 SQL 语义自成"桶"模型侧只需写'name' => 'unique_undeleted_in_scope:parent_id,company_id'
two_column_unique_undeleted:other_col另一列名两列组合唯一(参数自动由 Trait 补齐表名与 ID)见下节

unique_undeleted内部还含有一个资产序列号的专门分支:当校验的是assets.serial且系统设置unique_serial != '1'时直接放行(Asset.php 中'serial' => 'nullable|string|unique_undeleted:assets,serial')。

模型中的实际用法(来自源码):

  • Asset.php:'asset_tag' => ['required', 'min:1', 'max:255', 'unique_undeleted:assets,asset_tag', 'not_array']
  • Category.php:'name' => 'required|min:1|max:255|two_column_unique_undeleted:category_type'
  • Company.php:'name' => 'required|max:255|unique_undeleted_in_scope:parent_id'

two_column_unique_undeleted的参数补全依赖 UniqueUndeletedTrait 与 TwoColumnUniqueUndeletedTrait,它们会把表名和当前记录 ID 自动前置到规则字符串中,所以模型侧只需声明需要参与唯一约束的业务列。

3.2 树形结构 / 父子关系规则

Location.parent_id、Company.parent_id这类自引用字段需要防止环与深度失控,项目按场景拆成多条规则,各自给出可读的错误消息:

规则名语义
non_circular:table,pk[,depth]沿父链向上追溯,禁止任一祖先等于自身主键;depth默认 50,防止无限循环
parent_must_be_top_level:table,pk被选父节点本身必须是顶级节点(其parent_id为 NULL),防止层级超过 1 层
must_have_no_children:table,pk仅更新时生效:如果当前行已有子节点,则不允许给它再指定父节点
parent_within_scope校验调用方有权限把行迁移到新父节点下(按公司作用域比对,防止越权改parent_id扩大作用域)
parent_matches_location_companyFMCS 下,Location 的父级必须与子级属于同一公司(阻断 GHSA-jmrm-535m-cx3c 类跨租户写入)

Location.php 的完整组合示例:

'parent_id' => 'nullable|exists:locations,id|non_circular:locations,id|parent_matches_location_company',

Company.php 的完整组合示例:

'parent_id' => 'nullable|integer|exists:companies,id|parent_must_be_top_level:companies,id|must_have_no_children:companies,id|parent_within_scope',

3.3 FMCS(多公司支持)相关规则

多公司严格模式(full_multiple_companies_support)下,各业务模型(Accessory、Asset、Component、Consumable、Location 等)的company_id/location_id普遍链式声明了:

'company_id' => 'integer|nullable|exists:companies,id|fmcs_company', 'location_id' => 'exists:locations,id|nullable|fmcs_location',
  • fmcs_company:严格模式下强制有公司归属的用户提交非空公司(隐式规则,见 2.2)。
  • fmcs_location:当scope_locations_fmcs开启时,校验被引用的 Location 的有效公司归属(沿父链追溯effectiveFmcsCompanyId())必须落在请求提交的company_id/company_ids作用域内;无公司上下文时放行。
  • 注意源码中这两条规则都通过withoutGlobalScopes()绕过CompanyableScope查找真实记录——否则作用域会把外部租户的记录隐藏成"不存在",反而放行了跨租户写入。

3.4 自定义字段与表单校验规则

Snipe-IT 允许管理员自定义字段并配置正则与选项,Provider 中也提供了配套校验:

  • valid_regex:校验自定义字段里配置的"regex:..." 字符串在preg_match下不会抛异常(仅验证可编译,不验证语义)。
  • checkboxes/radio_buttons:校验提交的多选值/单选值确实存在于 CustomField 定义的选项列表中;兼容逗号分隔字符串的旧式提交。
  • not_array:拒绝数组值(如 Asset.php 中model_id、asset_tag上使用)。

3.5 用户安全相关规则

  • cant_manage_self:禁止用户把自己的id设为manager_id(自己当自己的上级)。
  • disallow_same_pwd_as_user_fields:密码不得与username、email、first_name、last_name相同。
  • letters/numbers/case_diff/symbols:密码复杂度检测,分别校验字母、数字、大小写混排与符号(使用 Unicode 属性正则\pL、\pN等)。

3.6 其他

  • is_unique_across_company_and_location:跨company_id+location_id双作用域唯一,目前用于 Department.php('name' => 'required|string|max:255|is_unique_across_company_and_location:departments,name')。
  • 错误消息通常挂在lang/*/validation.php的custom键下,便于多语言覆盖。

四、app/Rules的边界:只放加密自定义字段规则对象

项目明确规定:app/Rules目录是加密自定义字段规则对象的专属领地,禁止把通用规则对象放进去。

查看 app/Rules 目录可见其成员具有明显的一致性——AlphaEncrypted、BooleanEncrypted、CssColor、DateEncrypted、EmailEncrypted、IPEncrypted、IPv4Encrypted、IPv6Encrypted、MacEncrypted、NumericEncrypted、RegexEncrypted、UrlEncrypted、ValidJson、AllowedUploadExtension、ExternalUrl等,它们都服务于加密存储的自定义字段的"加密后校验"需求。

这里的取舍逻辑很清晰:

  • 通用、可复用的业务规则(唯一性、树形结构、FMCS 作用域等)→ 以闭包形式集中在ValidationServiceProvider,字符串规则名在模型$rules中即写即用,无需实例化对象。
  • 依赖具体加密算法/字段类型的规则(加密字段的值必须能解密、格式正确)→ 实现为独立的 Rule 类对象(Rule::class或new XxxEncrypted()形式使用),放在app/Rules。

顺带一提,UniqueUndeleted.php 也位于该目录,它是加密字段默认值场景下对unique_undeleted规则的对象化封装,与 Trait 提供的字符串形式互补。

五、观察者注册:统一放在 AppServiceProvider::boot()

项目约定:模型观察者通过Model::observe(ModelObserver::class)在 AppServiceProvider.php 的boot()方法中显式挂载,禁止在模型类上使用 PHP 8 的#[ObservedBy]属性。

当前源码中的完整注册块(AppServiceProvider.php):

Accessory::observe(AccessoryObserver::class); Asset::observe(AssetObserver::class); AssetModel::observe(AssetModelObserver::class); Component::observe(ComponentObserver::class); Consumable::observe(ConsumableObserver::class); License::observe(LicenseObserver::class); Location::observe(LocationObserver::class); Maintenance::observe(MaintenanceObserver::class); Setting::observe(SettingObserver::class); User::observe(UserObserver::class);

对应的观察者实现位于 app/Observers(如 AssetObserver.php、UserObserver.php),各自监听模型的created/updated/deleting等事件,用于维持库存快照、日志记录、关联校验等副作用。

统一在 Provider 挂载的优势在于:所有观察者注册点集中可见,升级/排查时只需检查一个文件;同时避免#[ObservedBy]属性与代码库中广泛存在的动态模型扩展、软删除作用域交互时产生意外行为。

六、AppServiceProvider 的其余引导工作(同文件内的常见约定)

虽然观察者挂载是boot()的核心,但同文件中还包含几项 Snipe-IT 部署时常被问到的引导逻辑,一并说明以便理解该文件的完整上下文:

  • 强制 HTTPS:当APP_URL以https://开头或设置了APP_FORCE_TLS时调用$url->forceScheme('https');若未设置APP_ALLOW_INSECURE_HOSTS,则用URL::forceRootUrl()固定根 URL,防止 Host 头伪造,配置异常时输出APP_URL相关的错误日志(AppServiceProvider.php)。
  • 分页器:Paginator::useBootstrap(),使用 Bootstrap 风格分页。
  • 视图组件:为layouts.default和partials.impersonation-banner绑定 SidebarComposer 与 ImpersonationBannerComposer。
  • Schema 默认长度:Schema::defaultStringLength(191),兼容 MySQL 旧索引长度限制。
  • Http 客户端宏:为HttpClientResponse注册throwIfNotJson()宏,当同步适配器请求返回非 JSON 内容(如误指向供应商 Web 控制台的 HTML 页面)时抛出 SyncAdapterVendorException,避免"Sync complete. 0 hosts, 0 errors"式的静默失败。
  • register() 条件注册:本地环境注册 Telescope;生产环境且配置了 Rollbar token 时注册 RollbarServiceProvider;并单例绑定自定义 SCIM 配置 SnipeSCIMConfig 覆盖默认 SCIM 行为(AppServiceProvider.php)。

七、从规则到校验触发:一条完整的调用链

理解了注册约定后,把整条链路串起来,便于在实际开发中定位问题:

  1. 注册:ValidationServiceProvider::boot()里Validator::extend('xxx', ...)将规则名注册进 Laravel 验证器。
  2. 声明:模型$rules(如 Asset.php、Category.php)或表单请求类(app/Http/Requests)中以字符串规则形式引用,如unique_undeleted:assets,asset_tag;带参数的规则依赖 UniqueUndeletedTrait 等 Trait 自动补齐表名与 ID。
  3. 触发:控制器调用模型校验或$request->validate()时,Laravel 按规则字符串执行闭包;$validator->getData()让规则能够读取同表单的其他字段(如parent_must_be_top_level读取主键、fmcs_location读取company_id)。
  4. 消息:校验失败时,Validator::replacer()负责把参数渲染进validation.custom.*翻译字符串,用户看到可读的错误提示。

八、开发约定速查清单

  • 新命名验证规则 →Validator::extend()/extendImplicit()闭包,写入 ValidationServiceProvider.php 的boot()。
  • 需要空值也触发校验的规则(如"必须选公司")→ 用extendImplicit(),并在闭包内自行处理各项放行条件。
  • 参数型规则(表名、字段名、作用域列)→ 用冒号 + 逗号分隔传入,必要时配Validator::replacer()美化消息。
  • 通用规则对象 → 不要放进app/Rules,那里只放加密自定义字段相关 Rule 类。
  • 观察者 → 在 AppServiceProvider.php 的boot()用Model::observe(Observer::class)显式注册,不要使用#[ObservedBy]属性。
  • 多公司/作用域相关的规则在实现中大量使用withoutGlobalScopes()绕过作用域查找真实记录,编写类似规则时务必注意这一点,否则会形成"作用域把外部记录藏起来 → 校验误放行"的安全漏洞。
  • 后端
  • 企业应用

【免费下载链接】snipe-it

A free open source IT asset/license management system

项目地址:https://gitcode.com/GitHub_Trending/sn/snipe-it
点击查看免费下载
上一篇:强力突破语言障碍:Screen Translator让屏幕文字翻译变得如此简单
下一篇:百度网盘直链解析工具深度解析:技术架构与高效下载实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表