- 后端
- 企业应用
【免费下载链接】snipe-it
A free open source IT asset/license management system
本篇指南聚焦 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_company | FMCS 下,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)。
七、从规则到校验触发:一条完整的调用链
理解了注册约定后,把整条链路串起来,便于在实际开发中定位问题:
- 注册:
ValidationServiceProvider::boot()里Validator::extend('xxx', ...)将规则名注册进 Laravel 验证器。 - 声明:模型
$rules(如 Asset.php、Category.php)或表单请求类(app/Http/Requests)中以字符串规则形式引用,如unique_undeleted:assets,asset_tag;带参数的规则依赖 UniqueUndeletedTrait 等 Trait 自动补齐表名与 ID。 - 触发:控制器调用模型校验或
$request->validate()时,Laravel 按规则字符串执行闭包;$validator->getData()让规则能够读取同表单的其他字段(如parent_must_be_top_level读取主键、fmcs_location读取company_id)。 - 消息:校验失败时,
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
相关推荐
ES6-learning:类和面向对象编程的完整指南
ES6 learning:类和面向对象编程的完整指南 欢迎来到ES6 learning项目的终极教程!🎯 今天我们将深入探讨JavaScript ES6中最激
design_patterns_in_typescript服务发现机制:观察者模式与中介者模式的实践
design_patterns_in_typescript服务发现机制:观察者模式与中介者模式的实践 你是否在开发分布式系统时遇到过服务节点动态上下线难以追踪的
示例工程Laravel-Excel 服务提供者解析:ExcelServiceProvider 注册流程
Laravel Excel 服务提供者解析:ExcelServiceProvider 注册流程 服务提供者的核心作用 在 Laravel 框架中,服务提供者(S
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考