Sulu CMS 中为导航项动态注入页面图标字段的完整实现方案

1次阅读

Sulu CMS 中为导航项动态注入页面图标字段的完整实现方案

本文介绍如何在 Sulu cms 中为导航 API(sulu_headless.api.navigation)响应动态添加 navigationIcon 字段,使每个导航项携带预设图标配置,规避媒体库自由上传限制,同时无需修改核心数据结构

本文介绍如何在 sulu cms 中为导航 api(`sulu_headless.api.navigation`)响应动态添加 `navigationicon` 字段,使每个导航项携带预设图标配置,规避媒体库自由上传限制,同时无需修改核心数据结构。

在 Sulu CMS 中,为页面导航项(Navigation Items)统一展示图标是常见需求,但官方未提供开箱即用的“受限图标选择器”字段类型。用户若通过 excerpt 或 single_media_selection 实现,将面临两大痛点:一是无法限定媒体选择范围(如仅允许系统图标集),二是所选媒体 ID 无法直接序列化到 Headless API 的导航响应中——因为 SuluHeadlessBundle 默认仅暴露基础文档字段,不包含自定义扩展数据(如页面扩展中的图标配置)。

本方案采用事件驱动 + 响应增强策略,在不侵入核心逻辑、不修改数据库 Schema、不依赖前端 hack 的前提下,精准补全导航 json 响应。其核心思想是:利用 symfony 的 kernel.response 事件监听 Headless 导航接口返回前的原始响应体,解析 JSON,根据每个导航项的 uuid 反查对应页面文档,从中提取已存于 DocumentExtension 中的图标配置,并注入新字段 navigationIcon。

✅ 实现步骤概览

  1. 前置:定义页面扩展(Extension)并存储图标标识
    首先需在页面模板或 Admin UI 中为页面添加一个受控图标选择字段(例如使用 select 类型,选项为预定义图标名如 “home”, “blog”, “contact”)。该字段值应写入 DocumentExtension 的 navigation.icon 路径(如通过 Sulu 扩展文档 实现自定义 Tab 和表单映射)。

  2. 核心:注册 kernel.response 监听器
    创建监听器类,仅对 sulu_headless.api.navigation 路由生效,避免全局性能损耗:

<?php declare(strict_types=1);  namespace AppEventListener;  use SuluComponentDocumentManagerDocumentManagerInterface; use SuluComponentDocumentManagerExceptionDocumentManagerException; use SymfonyComponentHttpKernelEventResponseEvent;  class NavigationListener {     public function __construct(private DocumentManagerInterface $documentManager)     {     }      public function onKernelResponse(ResponseEvent $event): void     {         $request = $event->getRequest();         // 仅拦截 Headless 导航 API 响应         if ($request->attributes->get('_route') !== 'sulu_headless.api.navigation') {             return;         }          $content = $event->getResponse()->getContent();         $responseObj = json_decode($content, true);          // 确保结构存在且非空         if (!isset($responseObj['_embedded']['items']) || !is_array($responseObj['_embedded']['items'])) {             return;         }          // 遍历每个导航项,注入图标字段         foreach ($responseObj['_embedded']['items'] as &$item) {             if (!isset($item['uuid'])) {                 continue;             }              try {                 $doc = $this->documentManager->find($item['uuid']);                 $extensions = $doc->getExtensionsData()->toArray();                  // 提取 navigation.icon,若存在则注入 navigationIcon 字段                 if (isset($extensions['navigation']['icon'])) {                     $item['navigationIcon'] = $extensions['navigation']['icon'];                 }             } catch (DocumentManagerException $e) {                 // 日志记录异常(生产环境建议使用 PsrLogLoggerInterface)                 error_log(sprintf('Failed to load document %s: %s', $item['uuid'], $e->getMessage()));                 continue;             }         }          // 重写响应内容(保留原始 HTTP 状态码与 Header)         $event->getResponse()->setContent(json_encode($responseObj, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));     } }
  1. 注册服务并启用监听器
    在 config/services.yaml 中声明服务并绑定事件:
# config/services.yaml services:     AppEventListenerNavigationListener:         tags:             - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }

⚠️ 注意事项

  • 性能考量:该监听器会在每次导航请求时触发 N 次文档查询(N = 导航项数量)。建议对高频导航(如顶部主菜单)启用缓存(如 Doctrine 缓存或 redis),或结合 SuluComponentContentCompatStructureInterface::getExtensions() 的轻量访问方式进一步优化。
  • 扩展健壮性:示例中已加入 try/catch 和空值检查,实际部署前请补充日志与监控;若图标需渲染为媒体 URL,可在监听器中调用 MediaManager 解析 media_id 并生成 url 字段。
  • 未来兼容性:Sulu 官方已在规划 HeadlessBundle 对 DocumentExtension 的原生支持(见答案中提及的长期方向),当前方案可平滑过渡至未来版本。

最终,前端调用 /api/navigations/main?flat=true 时,即可在每个导航项中获取标准化的图标标识:

{   "id": "a1b2c3d4-...",   "title": "首页",   "url": "/",   "navigationIcon": "home" }

你可据此在 React/Vue 组件中通过 iconMap[navigationIcon] 渲染 SVG 图标,或交由 CSS ::before 伪元素处理,真正实现「可控、可维护、可扩展」的导航图标体系。

text=ZqhQzanResources