
本文介绍如何在 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。
✅ 实现步骤概览
-
前置:定义页面扩展(Extension)并存储图标标识
首先需在页面模板或 Admin UI 中为页面添加一个受控图标选择字段(例如使用 select 类型,选项为预定义图标名如 “home”, “blog”, “contact”)。该字段值应写入 DocumentExtension 的 navigation.icon 路径(如通过 Sulu 扩展文档 实现自定义 Tab 和表单映射)。 -
核心:注册 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)); } }
- 注册服务并启用监听器
在 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 伪元素处理,真正实现「可控、可维护、可扩展」的导航图标体系。