资讯动态

Spree 6.0 移除 PermittedAttributes:用模型级 `additional_permitted_attributes` 钩子重构属性白名单

发布时间:2026/9/14 8:23:17 来源:尧图企业网站定制
Spree 6.0 移除 PermittedAttributes用模型级additional_permitted_attributes钩子重构属性白名单【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree导读Spree::PermittedAttributes是 Spree 长期存在的一个全局可变属性白名单注册表66 个属性列表本是为已被移除的 Rails Admin 与 Storefront 设计的扩展入口。Spree 6.0 将其彻底删除无弃用桥接并引入一个位于Spree::Base上的class_attribute :additional_permitted_attributes模型级扩展钩子让扩展在初始化器中以一行代码向指定模型追加可写属性同时保持 API v3 控制器内联声明的扁平参数约定。读完本文你将掌握该机制被删除的完整理由、新钩子的语义与源码实现、ResourceController的并集union接线方式、以及从旧注册表迁移的完整 10 步路径。背景为什么删除一个已经快死掉的注册表Spree::PermittedAttributes是一个全局可变的白名单共维护 66 个属性列表最初的设计目标是让宿主应用host app与扩展从初始化器里追加可写属性而无需装饰decorate控制器。但它的存在前提在 Spree 6.0 已经瓦解消费端消失该注册表是为 Rails Admin 与 Storefront 构建的而这两者在 6.0 中均已移除API v3 自带约定v3 控制器在自身内部内联声明允许属性采用扁平参数flat-params约定见 6.0-admin-api.md商家数据改由 Custom Fields 承载商户新增的数据字段由 Custom Fields 提供且自 5.6 起支持排序与过滤见 6.0-store-scoped-custom-field-definitions.md。从代码实际使用情况看该注册表已接近死亡66 个键中只有 5 个还在代码库中被按名称读取而建立在它之上的组合辅助模块Spree::Core::ControllerHelpers::StrongParameters提供的 6 个组合 helper如permitted_checkout_attributes、permitted_product_attributes没有任何调用方。注册表的三个消费路径全部被移除5 处显式 Store API 读取wishlists、wishlist_items、carts/payment_sessions、carts/items、customer/payment_setup_sessions另有Spree::Carts::AddItem通过展平line_item_attributes来过滤传入的 options 键。ResourceController#permitted_attributes中的名称推断回退inference fallback通过模型名经public_send推导属性键。但几乎所有到达该回退的 Admin 控制器都是只读的真正有写动作的四个控制器customers/addresses、orders、admin_users、stock_transfers都自己定义了内联 permit从不触碰它。ControllerHelpers::StrongParameters委托全部 66 个键并定义组合 helper零调用方其组合 helper 描述的嵌套属性载荷形态bill_address_attributes、payments_attributes正是 v3 以扁平参数规则明确拒绝的。注册表的失败模式三处重复声明注册表还有一个被证实过的失效模式同一属性必须在多个互不关联的位置各声明一次例如变体variant属性需要在三处登记漏掉任何一处写路径都会静默丢弃该属性——详见 6.0-duties-and-custom-fees.md 中记录的三处three-places问题。结论很明确一个并非真正单一事实来源single source of truth的全局列表比没有列表更糟。新钩子Spree::Base.additional_permitted_attributes替代方案是在Spree::Base上定义一个class_attribute默认值为空数组# spree/core/app/models/spree/base.rb # Extra writable attributes contributed by extensions, appended to the v3 # controller allowlist for this resource. Core attributes belong in the # controllers own list — this exists so an extension that adds a column can # make it writable without decorating a controller: # # Spree::Product.additional_permitted_attributes [:brand_id] # # Entries are params.permit fragments: bare symbols, or hashes for # collections and nested structures ({ region_ids: [] }). Append with # rather than assigning, so extensions dont clobber each other. class_attribute :additional_permitted_attributes, instance_writer: false, default: [].freeze对应的实际源码位于 spree/core/app/models/spree/base.rb。扩展在初始化器中声明与旧注册表的喂入位置完全一致# config/initializers/spree.rb Spree::Product.additional_permitted_attributes [:brand_id]无需装饰器、无需注册步骤。使用class_attribute带来的关键语义按类存储 继承向Spree::Product追加不会影响Spree::Variant或Spree::BaseSTI 子类可在类体中直接赋值核心 STI 子类通过self.additional_permitted_attributes [product_ids: []]声明自己的值不会扰动父类条目是params.permit片段裸 Symbol 表示标量属性Hash 表示集合与嵌套结构如{ region_ids: [] }。必须永远不要计划文档与升级指南都反复强调追加一律使用绝不用——直接赋值会覆盖另一个扩展已经追加的内容。此外用原地修改会抛出FrozenError默认值是冻结的共享数组default: [].freeze原地改动会把你的属性泄漏到每一个其他模型上。Controller 接线两级拆分完成并集ResourceController源码见 spree/api/app/controllers/spree/api/v3/resource_controller.rb采用了子类声明自己的列表、基类追加扩展贡献的两级拆分def permitted_params normalize_params(params.permit(*permitted_attributes)) end # Resource list extension contributions. Subclasses override # resource_permitted_attributes, never this. def permitted_attributes resource_permitted_attributes model_additional_permitted_attributes end def resource_permitted_attributes raise NotImplementedError, Subclass must implement resource_permitted_attributes or permitted_params end def model_additional_permitted_attributes return [] unless respond_to?(:model_class, true) model model_class return [] unless model.respond_to?(:additional_permitted_attributes) Array(model.additional_permitted_attributes) rescue NotImplementedError [] end三条使用守则覆盖permitted_attributes会静默丢失扩展属性该方法是并集本身正确的覆盖点是resource_permitted_attributes返回纯列表。没有机制强制这一点因此被明确写入了CLAUDE.md供开发者注意不要为了拿到并集而把控制器卷入normalize_params直接 splat 就能获得同样结果且不改动参数语义。resolve_prefixed_ids会递归进入嵌套 Hash把匹配/\A[a-z]_[a-zA-Z0-9]\z/的任何*_id键值解码——这会破坏网关preferences与商户metadata这类不透明值。计划文档给出的例子Stripe 的webhook_endpoint_id值为we_1MqJ8bLkdIwHu7ixIhqHQfbo一旦被解码会变成2345514981514645561106。携带网关preferences或商户metadata的控制器绝不能被卷入规范化model_additional_permitted_attributes中的两个守卫都是承重的load-bearing控制器未必定义model_classrespond_to?检查加NotImplementedErrorrescue且Spree.base_class可被宿主覆盖因此该钩子并不保证一定从Spree::Base继承下来。直接覆盖permitted_params的控制器对于完全绕过上述路径、自行覆盖permitted_params的控制器需要把扩展属性 splat 进自己的params.permit调用——这是一行式改动不改变该控制器既有的规范化行为def permitted_params params.permit(*model_additional_permitted_attributes, :name, :active) end两个凭据类端点被刻意排除在外api_keys与invitations——它们的参数属于授权面authorization surface不是可扩展的资源数据。四个既有规则基类的收敛Spree::PromotionRule、Spree::PromotionAction、Spree::DeliveryMethodRule与Spree::CommissionRule此前各自定义了内容完全相同的additional_permitted_attributes默认值。一旦Spree::Base承载该钩子这四份冗余定义即被删除由继承默认值接管其 STI 子类中的覆盖保持不变。但需要注意消费这些规则类的控制器走的是另一套机制。PromotionsController#subclassed_collection_attributes与配送方式规则等价物会遍历一个子类注册表跨所有已注册类型做并集——因为控制器写的是 STI 基类无法预知请求针对哪个子类。新的ResourceController并集只询问单一model_class。两者可以共存注册表遍历面向多态写入模型钩子面向具体资源。这些控制器中冗余的respond_to?守卫可随本次改动一并移除。源码中的 STI 子类赋值示例从仓库实际代码可见各 STI 子类在类体中直接赋值促销规则类如 promotion/rules/product.rb、user.rb、category.rbself.additional_permitted_attributes [product_ids: []]/[customer_ids: []]/[category_ids: []]促销动作类如 promotion/actions/create_adjustment.rbself.additional_permitted_attributes [calculator: [:type, { preferences: {} }]]配送方式规则类delivery_method_rules/excluded_products_rule.rbself.additional_permitted_attributes [product_ids: []]。这就是计划文档所述把既有的八处方法式覆盖统一转换为赋值让一个机制同时覆盖核心声明与扩展追加的落地形态。旧用途的替代关系总览旧用途6.0 替代方案商家新增数据字段Custom Fields5.6 起可排序/可过滤扩展为核心模型增加数据库列模型上的additional_permitted_attributes扩展新增 STI 规则/动作类型additional_permitted_attributes 注册表注册不变核心属性列表控制器内联params.permit不变完整迁移路径10 步新增钩子在Spree::Base上定义additional_permitted_attributes删除四个规则基类上的冗余副本。接入并集并移除推断回退将并集接进ParamsNormalizer#normalize_params把ResourceController#permitted_attributes中的推断回退替换为NotImplementedError。跑一遍 API 测试套件任何此前静默依赖推断的写控制器现在都会响亮失败——这正是目的所在。实际中恰好有一个案例admin/tax_categories_controller拥有完整 CRUD 却无自己的白名单现在显式声明[:name, :tax_code, :description, :is_default]。内联 5 处 Store API 读取wishlists、wishlist_items、carts/payment_sessions、carts/items、customer/payment_setup_sessions。每个列表都很短逐字移入控制器即可。处理Carts::AddItem它读取line_item_attributes以过滤传入的 options 键需要一个不属于控制器的存放处。落地为Spree::LineItem上的常量由 workflow 与carts/items_controller共同读取。实现中已确认该列表现为Spree::LineItem::WRITABLE_ATTRIBUTESworkflow 通过::Spree::LineItem.additional_permitted_attributes.flat_map { |a| a.is_a?(Hash) ? a.keys : a }读取见 spree/core/app/workflows/spree/carts/add_item.rb。值得一提的细节旧代码执行line_item_attributes.flatten会把字面量{ metadata: {} }哈希也当作 options 键——毫无意义只是靠后续的delete_if丢弃 nil 才幸存下来新常量只保留真正起作用的三个标量键。转换跳过normalize_params的params.permit调用点这些调用点默认退出并集需要显式转换才能让扩展属性抵达已经走规范化的控制器无论列表如何构建都无需改动。删除ControllerHelpers::StrongParameters连同它在两个基类控制器中的include、它的require_dependency以及spree/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb测试文件。删除spree/core/lib/spree/permitted_attributes.rb及其所有require。更新文档重写 docs/developer/customization/api.mdx 的Permitted Attributes一节更新docs/developer/tutorial/extending-models.mdx两处与docs/developer/tutorial/api.mdx中的brand_id示例。升级指南在 docs/developer/upgrades/5.6-to-6.0.mdx 中新增破坏性变更条目覆盖常量删除、helper 模块删除与additional_permitted_attributes替代并附 before/after 示例。测试覆盖ResourceController的并集行为——扩展声明的属性可写、未声明的属性被丢弃并确认当permitted_attributes与permitted_params都未定义时抛出NotImplementedError。升级实操grep 定位、三桶归类与持久化验证升级指南提供了可直接落地的迁移操作。第 1 步找出所有调用点。常量与 helper 方法都已消失遗漏的引用会在代码首次运行时以NameError或NoMethodError响亮暴露但不一定在启动时grep -rn PermittedAttributes app config lib grep -rnE permitted_[[:alnum:]_]_attributes app config lib第二个模式刻意放宽helper 模块曾按注册表键各生成一个permitted_*_attributes方法因此有数十个之多。第 2 步判断每个属性归属哪一类。绝大多数落入三个桶只有最后一类才需要新钩子你原来添加的是什么6.0 中放到哪里商户管理的字段文本、数字、下拉Custom Fields——无需代码且可过滤/排序你注册的 STI 类型的配置促销规则、配送方式规则等对应子类上的additional_permitted_attributes与原来一致你的扩展为核心模型新增的真实数据库列模型上的additional_permitted_attributes第 3 步把声明指向模型。声明仍留在初始化器中只是接收者从全局注册表换成模型本身# Before Spree::PermittedAttributes.product_attributes :brand_id # After Spree::Product.additional_permitted_attributes [:brand_id]使用而非会因默认值冻结而抛出FrozenError。只声明属于自己的属性——重复声明控制器已允许的键如metadata、prices不会扩大权限强参数对同一键保留最后一个过滤器你的声明反而会覆盖控制器自己的。第 4 步修复你自己的控制器。如果你继承了 Spree v3 资源控制器并依赖模型名推断属性列表现在必须显式声明class BrandsController Spree::Api::V3::Admin::ResourceController protected def model_class Spree::Brand end # Before: no such method — the base class inferred brand_attributes # from the model name. Now you say what you accept. def resource_permitted_attributes [:name, :slug, :description] end end既不声明resource_permitted_attributes也不声明permitted_params会在首次写入时抛出NotImplementedError——在测试套件中就能暴露而不是静默放行一份过期列表。第 5 步验证写入真的落库。声明从未抵达控制器时会静默失败——强参数丢弃未允许的键请求仍返回 200列保持旧值。要断言的是保存后的记录而非响应状态码patch /api/v3/admin/products/#{product.prefixed_id}, params: { brand_id: brand.id }, headers: headers expect(product.reload.brand_id).to eq(brand.id)仓库的 resource_controller_spec.rb 正是以这种方式验证并集临时向Spree::TaxCategory.additional_permitted_attributes追加:brand_id断言扩展声明属性可写测试结束后恢复原值。边界与约束这个钩子的适用范围计划文档明确划定了该钩子的边界只面向扩展新增的列核心属性留在控制器的内联params.permit中。核心模型绝不能用这个钩子声明自己的基础属性——那等于逐个类地重新制造一个注册表Custom Fields 仍是商家数据的第一推荐路径该钩子被定位为扩展新增真实数据库列时的回退方案而非默认的扩展故事新 v3 控制器声明permitted_attributes纯列表而非覆盖permitted_params除非控制器确实需要定制构建器——覆盖permitted_params即退出扩展并集不要依赖模型名推断回退每个写控制器必须显式陈述自己的列表正在基于PermittedAttributes编写扩展的应立即转向additional_permitted_attributes。参考资料docs/plans/6.0-admin-api.md——v3 扁平参数约定docs/plans/6.0-duties-and-custom-fees.md——促成放弃全局列表的三处声明失败案例docs/plans/6.0-delivery-method-rules.md 与 docs/plans/6.0-extendable-validations.md——additional_permitted_attributes先例与装饰器仍是加法路径的立场被移除代码spree/core/lib/spree/permitted_attributes.rb、spree/core/lib/spree/core/controller_helpers/strong_parameters.rb6.0 起不再存在于仓库中【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价