Sylius API 中基于当前渠道(Channel)提供可用语言列表的架构决策与实现解析 电商后端API网关【免费下载链接】SyliusHeadless open-source eCommerce platform on top of PHP/Symfony/API Platform项目地址https://gitcode.com/gh_mirrors/sy/Sylius点击查看免费下载导读本文围绕 Sylius 项目中的架构决策记录ADRadr/2022_03_15_api_providing_locales_available_in_active_channel.md完整梳理如何让商城Shop用户在 API 中只能看到当前活跃渠道Active Channel所启用的语言Locales这一问题的技术选型过程与最终实现方案。通过本文读者将理解为什么 Sylius 选择Doctrine Collection Extension而非 Data Provider掌握ChannelBasedExtension查询扩展的完整工作原理、服务注册方式、API 端点配置以及配套的测试用例与响应格式可直接用于理解和扩展 Sylius Headless API 的语言管理能力。背景为什么需要渠道可用语言过滤在多渠道Multi-channel电商架构中一个 Sylius 实例可以同时运营多个渠道每个渠道拥有自己独立的语言集合Locales。从业务角度看顾客只应看到当前所访问渠道中启用的语言——例如波兰渠道不应向顾客展示未启用的de_DE语言否则顾客可能切换到该渠道不支持的界面语言影响购物体验与订单数据一致性。Sylius 的 API基于 API Platform将语言Locale作为资源暴露给商城前端。若不加以限制GET /api/v2/shop/locales将返回系统中全部语言这显然与渠道内可用语言的业务约束冲突。该 ADR 于 2022-03-15 记录并接受其历史可以追溯到 2021-07-05 的早期 ADR adr/2021_07_05_api_providing_locales_available_in_active_channel.md两者共同记录了完整的技术决策演化过程。架构决策Doctrine Collection Extension vs Data Provider决策背景与两种候选方案该问题在 2021 年 7 月的原始 ADR 中首次提出当时的结论是采用Data Provider方案该方案后来被标记为 rejected。而在 2022 年 3 月的 ADR 中决策被推翻最终选择了Doctrine Collection Extension方案。两种方案的核心权衡如下方案优点缺点Doctrine Collection Extension与现有修改响应内容的实现方式一致可与分页Pagination等其他 API 扩展协同工作语言Locale与渠道Channel之间不存在直接关联关系导致查询扩展实现复杂Data Provider实现简单早期资源已有类似做法使用 Data Provider 会绕过分页等额外的 Doctrine 扩展丢失 API Platform 的通用能力决策结果与理由最终决策采用 Doctrine Collection Extension。其核心理由是该方案与当前项目修改响应内容的整体实现思路保持一致且不会绕过分页等 Doctrine 扩展机制。由于 Data Provider 会完全接管数据获取流程API Platform 内置的分页、过滤等扩展将不再生效而查询扩展Query Collection Extension是在 SQL 层面注入过滤条件因此能够与分页、排序等扩展无缝叠加。值得注意的是原始 ADR 中使用 Data Provider的理由之一是渠道下语言数量不会太多缺少分页不是大问题而 2022 年 3 月的 ADR 则从架构一致性角度推翻了这一权衡认为即便实现更复杂保持与既有扩展机制的一致性更重要。最终实现ChannelBasedExtension查询扩展源码结构最终实现落地在 src/Sylius/Bundle/ApiBundle/Doctrine/ORM/QueryExtension/Shop/Locale/ChannelBasedExtension.php这是一个实现了 API PlatformQueryCollectionExtensionInterface的查询扩展类namespace Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Locale; use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection; use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; use Sylius\Component\Locale\Model\LocaleInterface; use Webmozart\Assert\Assert; final readonly class ChannelBasedExtension implements QueryCollectionExtensionInterface { public function __construct(private SectionProviderInterface $sectionProvider) { } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation null, array $context [], ): void { if (!is_a($resourceClass, LocaleInterface::class, true)) { return; } if (!$this-sectionProvider-getSection() instanceof ShopApiSection) { return; } Assert::keyExists($context, ContextKeys::CHANNEL); $channel $context[ContextKeys::CHANNEL]; if ($channel-getLocales()-count() 0) { $localesParameterName $queryNameGenerator-generateParameterName(locales); $rootAlias $queryBuilder-getRootAliases()[0]; $queryBuilder -andWhere(sprintf(%s.id in (:%s), $rootAlias, $localesParameterName)) -setParameter($localesParameterName, $channel-getLocales()) ; } } }关键逻辑逐步拆解资源类型守卫is_a($resourceClass, LocaleInterface::class, true)确保扩展只作用于Locale资源对其他资源的集合查询不做任何干预对应测试test_does_not_apply_conditions_to_collection_for_unsupported_resource。商城分区守卫通过SectionProviderInterface获取当前请求所属的分区Section只有ShopApiSection商城 API才应用过滤。这意味着管理后台Admin API不受影响管理员仍可查看全部语言对应测试test_does_not_apply_conditions_for_non_shop_api_section。这一设计保证了扩展对 Admin 端完全透明。渠道上下文断言使用Webmozart\Assert\Assert::keyExists断言上下文中存在渠道对象。上下文键定义于 src/Sylius/Bundle/ApiBundle/Serializer/ContextKeys.php 中的ContextKeys::CHANNEL值为sylius_api_channel。如果商城请求上下文中缺少渠道将抛出InvalidArgumentException对应测试test_throws_an_exception_if_context_has_no_channel。空渠道语言集合的优雅降级当$channel-getLocales()-count() 0时不添加任何 SQL 条件即不限制返回结果。这一行为意味着未配置任何语言的渠道商城端仍能看到全部语言可参考 tests/Api/Shop/LocalesTest.php 中it_gets_locales使用channel_without_locales.yaml夹具断言返回全部语言的测试。SQL 注入当渠道配置了语言时扩展生成形如o.id in (:locales)的 DQL 条件并绑定渠道的语言集合作为参数。参数名通过QueryNameGeneratorInterface::generateParameterName(locales)动态生成避免与查询中其他同名参数冲突——这正是能与分页等扩展协同工作的机制保障对应测试test_applies_conditions_for_shop_api_section。服务注册如何让 API Platform 识别该扩展扩展类本身不会自动生效必须注册为带api_platform.doctrine.orm.query_extension.collection标签的服务。在 src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.php 中可以看到$services -set(sylius_api.doctrine.orm.query_extension.shop.locale.channel_based, LocaleChannelBasedExtension::class) -args([service(sylius.section_resolver.uri_based)]) -tag(api_platform.doctrine.orm.query_extension.collection) ;这里注入的是sylius.section_resolver.uri_based服务基于 URI 的分区解析器由 Sylius 的 Section Resolver 机制提供。该服务负责根据请求 URI 判断当前请求属于商城分区还是管理后台分区正是第 2 步守卫逻辑的数据来源。同文件中还可以看到大量同族扩展例如sylius_api.doctrine.orm.query_extension.shop.currency.channel_based、sylius_api.doctrine.orm.query_extension.shop.country.channel_based、sylius_api.doctrine.orm.query_extension.shop.taxon.channel_based等说明按渠道过滤集合是 Sylius Shop API 中的通用模式Locale 只是其中之一。资源与端点配置商城端 Locale 资源定义在 src/Sylius/Bundle/ApiBundle/Resources/config/api_platform/resources/shop/Locale.xmlresource class%sylius.model.locale.class% operations operation namesylius_api_shop_locale_get_collection classApiPlatform\Metadata\GetCollection uriTemplate/shop/locales paginationEnabledfalse normalizationContext values value namegroups values valuesylius:shop:locale:index/value /values /values /values /normalizationContext /operation operation namesylius_api_shop_locale_get classApiPlatform\Metadata\Get uriTemplate/shop/locales/{code} normalizationContext values value namegroups values valuesylius:shop:locale:show/value /values /values /values /normalizationContext /operation /operations /resource值得注意的细节集合操作/shop/locales显式设置了paginationEnabledfalse。这看似与 ADR 强调的保留分页能力相矛盾但恰好印证了 ADR 中的权衡逻辑——虽然扩展机制具备与分页协同的能力但语言列表本身数量有限因此 Sylius 在端点层面选择关闭分页换取更简洁的响应。这体现了机制能力与端点策略两个层面的独立决策。单个资源操作/shop/locales/{code}不受渠道过滤影响查询扩展只作用于集合查询applyToCollection便于商城端按代码获取具体语言。单元测试行为契约的验证测试位于 src/Sylius/Bundle/ApiBundle/tests/Doctrine/ORM/QueryExtension/Shop/Locale/ChannelBasedExtensionTest.php用四个用例完整锁定了上述行为test_does_not_apply_conditions_to_collection_for_unsupported_resource非 Locale 资源时扩展不产生任何查询修改连getSection都不会被调用。test_does_not_apply_conditions_for_non_shop_api_sectionAdmin 分区时跳过过滤。test_applies_conditions_for_shop_api_sectionShop 分区且渠道有语言时生成o.id in (:locales)并绑定参数参数名由QueryNameGeneratorInterface生成此处为locales。test_throws_an_exception_if_context_has_no_channelShop 分区但上下文缺失渠道时抛出InvalidArgumentException。这四个测试从不适用场景适用场景异常路径三个维度定义了扩展的行为契约是理解该扩展语义的最佳切入点。集成验证Shop API 的真实响应仓库中的集成测试 tests/Api/Shop/LocalesTest.php 提供了端到端的行为验证#[Test] public function it_gets_only_locales_from_current_channel(): void { $this-loadFixturesFromFiles([locale.yaml, channel/channel.yaml]); $this-requestGet(uri: /api/v2/shop/locales, headers: self::CONTENT_TYPE_HEADER); $this-assertResponse($this-client-getResponse(), shop/locale/get_locales_from_channel_response); }对应的夹具 tests/Api/DataFixtures/ORM/channel/channel.yaml 中渠道配置了locales: [locale_en, locale_pl]因此期望响应 tests/Api/Responses/shop/locale/get_locales_from_channel_response.json 只包含en_US与pl_PL两种语言{ context: /api/v2/contexts/Locale, id: /api/v2/shop/locales, type: hydra:Collection, hydra:member: [ { id: /api/v2/shop/locales/en_US, type: Locale, code: en_US, name: English (United States) }, { id: /api/v2/shop/locales/pl_PL, type: Locale, code: pl_PL, name: Polish (Poland) } ], hydra:totalItems: 2 }对照测试方法it_gets_locales使用不含语言的渠道夹具channel_without_locales.yaml则断言返回全部语言——这与扩展中count() 0时不加条件的分支逻辑完全对应验证了空渠道的降级行为。与其他渠道过滤扩展的对比通用模式确认从 extensions.php 的注册清单可以观察到Sylius 对商城 API 的渠道隔离采用了统一的扩展实现模式shop.country.channel_based国家仅 collectionshop.taxon.channel_based分类仅 collectionshop.currency.channel_based货币collection itemshop.exchange_rate.channel_based汇率collection itemshop.shipping_method.channel_based配送方式collection itemshop.payment_method.channel_based支付方式collection itemshop.locale.channel_based语言仅 collection这一对比说明Locale 扩展选择仅作用于集合查询是因为单个语言资源/shop/locales/{code}无需按渠道过滤而货币、配送方式等资源因单品也存在渠道差异扩展同时注册了 collection 与 item 两个标签。理解这一模式有助于在扩展 Sylius API 时遵循既有约定新增按渠道隔离的资源时优先考虑实现QueryCollectionExtensionInterface/QueryExtensionInterface并注册到 extensions.php而非另起炉灶使用 Data Provider。结论与实践要点机制一致性优先于实现简便性该 ADR 的最终结论Doctrine Collection Extension是对早期 Data Provider 方案记录于 adr/2021_07_05_api_providing_locales_available_in_active_channel.md已标记 rejected的正式推翻理由是与既有 API 扩展体系保持一致、不丢失分页等能力。查询扩展是 SQL 层面的条件注入ChannelBasedExtension通过o.id in (:locales)直接修改 DQL因此能与分页、排序等 API Platform 扩展无缝叠加而 Data Provider 会绕过整个扩展管线。分区Section隔离是安全边界扩展通过SectionProviderInterface严格区分 Shop API 与 Admin API商城端受限、管理端不受影响体现了面向顾客的 API 收紧权限、面向管理的 API 保持完整的设计意图。空集合的语义约定渠道未配置语言时扩展不施加过滤返回全部语言这一行为已由集成测试锁定属于有意设计的降级策略。端点层面的分页策略独立决策虽然扩展支持与分页协同/shop/locales端点仍显式关闭分页paginationEnabledfalse因为语言列表规模有限简化响应反而更优。对于需要自定义商城语言行为的开发者建议从 ChannelBasedExtension.php 及其单元测试入手通过调整 DQL 条件或注册新的查询扩展在保持 API Platform 扩展机制完整性的前提下定制过滤逻辑。赞分享电商后端API网关【免费下载链接】SyliusHeadless open-source eCommerce platform on top of PHP/Symfony/API Platform项目地址https://gitcode.com/gh_mirrors/sy/Sylius点击查看免费下载相关推荐Sylius API 按渠道过滤可用 Locale从 Data Provider 到 Doctrine Collection Extension 的架构演进与源码实现Sylius API 按渠道过滤可用 Locale从 Data Provider 到 Doctrine Collection Extension 的架构演进与电商后端API网关在 Novu 中为聊天渠道新增 Channel Connect Button基于 novu/js 与 novu/react 的三种架构实践指南在 Novu 中为聊天渠道新增 Channel Connect Button基于 novu/js 与 novu/react 的三种架构实践指南 导读 本指后端消息路由前端通信AI AgentOpenCV 4.4 G-API 全览图执行模型、内核后端、异构推理与流式流水线实战指南opencv_contribOpenCV 4.4 G API 全览图执行模型、内核后端、异构推理与流式流水线实战指南opencv_contrib G APIGraph API是电商后端API网关上一篇Netron远程查看模型通过URL打开任意模型文件打造团队共享的可视化页面下一篇三步解锁WeMod高级功能Wand-Enhancer完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考