Plone与Drupal模块机制深度对比:从架构原理到生产选型
1. 这不是简单的“谁更好”,而是搞懂两个老牌CMS如何用模块解决真实业务问题
Plone 和 Drupal 都不是新面孔——它们在内容管理系统(CMS)领域各自深耕了二十年以上。Plone 基于 Python,以企业级文档管理、权限粒度和开箱即用的合规性见长;Drupal 基于 PHP,以灵活的数据建模能力、庞大的社区生态和前端集成自由度著称。当标题里出现 “Add-On Modules”(附加模块),它真正指向的从来不是“装个插件就完事”的表层操作,而是一套完整的扩展治理逻辑:模块从哪来、怎么选、如何验证兼容性、升级时会不会崩掉整站权限体系、第三方模块的代码质量是否经得起审计、甚至模块作者是否还在维护——这些才是真实项目里让技术负责人半夜睁眼的问题。
我做过 7 个 Plone 5/6 的政务内网系统迁移,也主导过 4 个 Drupal 8/9 的高校门户重构,最深的体会是:模块不是功能补丁,而是系统DNA的延伸。Plone 的 add-on 模块(如plone.app.contenttypes、collective.easyform)往往深度耦合 Zope Component Architecture(ZCA)和 Acquisition 机制,一个模块的configure.zcml文件写错一行,整个站点可能连登录页都打不开;而 Drupal 的 module(如webform、paragraphs、media_library)则依赖严格的钩子(hook)执行顺序和依赖注入容器,composer.json里一个版本号写错,drush updb就会卡在半途,数据库 schema 变更失败却没报具体哪张表出错。这不是理论差异,是每天在终端里敲命令、看日志、改配置时踩出来的坑。本文不讲“Plone 更安全”或“Drupal 更灵活”这种空话,只聚焦一件事:当你站在项目启动节点,面对一堆模块列表,如何像老司机一样判断哪个模块能进生产环境、哪个该立刻拉黑、哪个需要自己 fork 后重写核心逻辑。适合正在评估技术栈的架构师、接手遗留系统的运维工程师、以及被客户临时加需求逼到墙角的开发同学——你不需要从头学 Python 或 PHP,但必须知道模块背后那套“运行时契约”到底是什么。
2. 模块设计哲学与底层机制:为什么同样的功能,在两边实现路径截然不同
2.1 Plone 的模块:Zope 生态下的“组件化封装”,强契约、弱自由度
Plone 的 add-on 模块本质是 Zope 应用服务器生态的产物。它的模块不是独立运行的代码包,而是注册到 Zope 全局组件注册表(Global Component Registry)中的可插拔服务。这意味着每个模块必须严格遵循 ZCA(Zope Component Architecture)规范,通过configure.zcml文件声明其提供的适配器(Adapter)、实用工具(Utility)、视图(View)等组件,并明确指定其接口(Interface)和上下文(Context)。举个典型例子:plone.app.dexterity模块之所以能让你在后台创建自定义内容类型,是因为它注册了一个IDexterityFTI接口的适配器,将内容类型定义映射为 Zope 的 Factory Type Information 对象;而collective.solr模块要接管搜索,则必须提供一个ISearch接口的替代实现,并在zcml中用<adapter>标签覆盖默认搜索服务。
这种设计带来两个硬性约束:
第一,模块间存在强依赖链。比如plone.restapi(提供 RESTful API)必须依赖plone.behavior(行为机制支持),而plone.behavior又依赖zope.interface和zope.schema。这个依赖不是 Composer 或 pip 的简单包依赖,而是运行时组件注册的拓扑关系——如果 A 模块注册了一个IContentProvider接口的适配器,B 模块想覆盖它,就必须在zcml中用<adapter for="*" provides=".interfaces.IContentProvider" />显式声明覆盖,且加载顺序必须在 A 之后(通过z3c.autoinclude的eggs顺序或zcml的includeOverrides控制)。我曾遇到一个案例:客户要求在新闻列表页插入广告位,我们选了collective.panels模块,但它注册的IPanel接口与plone.app.mosaic(可视化编辑器)的ILayoutBehavior冲突,导致页面编辑器无法加载。最终解决方案不是禁用其中一个,而是重写collective.panels的configure.zcml,将其适配器注册范围从*收窄为INavigationRoot,避开 mosaic 的行为上下文。
第二,模块升级=系统重启。Plone 的模块加载发生在 Zope 实例启动阶段,所有zcml文件被解析、组件注册完成,才开始处理 HTTP 请求。这意味着任何模块的zcml修改、Python 代码变更,都必须重启整个 Zope 进程(bin/instance restart)。没有热重载,没有按需加载。这直接决定了 Plone 模块的发布节奏:主流模块如Products.CMFPlone自身每半年才发一次大版本,因为每次升级都要做全站回归测试——测试点包括权限继承链是否断裂(比如plone.app.workflow升级后,Editor角色能否继续编辑子文件夹)、内容迁移脚本是否触发(plone.app.contenttypes升级常附带upgradeStep脚本,把旧Document类型转为新Document类型,若脚本未执行,前台显示为空白)。
提示:Plone 模块的
setup.py中entry_points字段是关键。z3c.autoinclude.plugin必须声明为plone,否则zcml不会被自动扫描;plone.recipe.zope2instance的eggs列表顺序决定zcml加载优先级,靠后的 egg 可以覆盖前面的同名组件。
2.2 Drupal 的模块:钩子驱动的“事件总线”,高自由度、高风险点
Drupal 的模块(module)则构建在一套松耦合的钩子(hook)系统之上。核心思想是:Drupal 内核定义一系列“关键时刻”(如hook_node_insert、hook_form_alter、hook_views_pre_render),模块只需实现对应命名的函数,内核就会在执行流程中自动调用。这种设计赋予了极高的灵活性——你可以用hook_form_alter修改任意表单字段,用hook_entity_presave在实体保存前注入自定义逻辑,甚至用hook_menu_alter重写整个路由系统。但自由度的背面是失控风险:钩子执行无序、无事务保障、无依赖隔离。
最典型的失控场景是钩子执行顺序不可控。假设模块 A 和模块 B 都实现了hook_form_alter,A 想给表单加一个隐藏字段,B 想移除某个提交按钮。如果 B 先执行,它删掉了按钮,A 再执行时试图给已不存在的按钮设值,就会触发 PHP Notice;如果 A 先执行,B 删除按钮时字段还在,一切正常。Drupal 8+ 引入了hook_module_implements_alter来干预钩子执行顺序,但实际项目中,90% 的模块开发者根本不会写这个钩子,导致线上环境行为随机。我接手过一个 Drupal 8 站点,客户投诉“有时表单提交成功,有时直接 500”,查日志发现是webform模块和token模块的hook_form_alter冲突:token在表单中注入 token 替换逻辑,webform在同一位置修改表单结构,两者都假设自己是最后一个操作者。最终方案不是改模块,而是在settings.php中强制设置module_implements_cache_clear()并手动排序hook_form_alter的执行数组。
第二个高风险点是模块依赖的“软耦合”陷阱。Drupal 的.info.yml文件声明依赖用dependencies:字段,如dependencies: - drupal:node - drupal:views。但这只是安装时的检查,运行时并不保证依赖模块的钩子一定被调用。更危险的是“隐式依赖”:模块 C 调用views_get_view('my_view'),但它没在dependencies里声明views,安装时不会报错,但运行时views模块未启用,直接 PHP Fatal Error。Plone 的zcml依赖是硬检查——如果plone.app.contenttypes依赖plone.behavior,而后者未安装,Zope 启动时就会抛ConfigurationExecutionError并停止,绝不会让系统带病运行。
注意:Drupal 9 开始强制要求模块使用 PSR-4 自动加载,但大量遗留模块仍用
hook_autoload_info手动注册类。这意味着composer install后,必须运行drush cache:rebuild清除类加载缓存,否则新模块的类找不到——这是新手最常卡住的点,错误信息却是“Class not found”,完全不提示跟缓存有关。
2.3 模块分发与生命周期:PyPI vs. Drupal.org,两种社区治理模式
Plone 模块主要托管在 PyPI(Python Package Index),通过pip install安装。这看似简单,实则暗藏玄机:PyPI 上的plone.app.contenttypes包,其setup.py中install_requires声明了对Products.CMFPlone>=5.2.0,<6.0.0的依赖,但Products.CMFPlone本身又是一个独立的 PyPI 包,它内部又依赖Zope2、Acquisition等更低层包。整个依赖树是纯 Python 的语义化版本控制,pip会尝试满足所有约束,但 Plone 官方强烈建议使用pip install -c https://dist.plone.org/release/6.0.0b3/constraints.txt(约束文件)来锁定所有包的精确版本,因为plone.app.contenttypes==2.5.0在Plone 5.2和Plone 6.0下行为完全不同——前者用Archetypes兼容层,后者用Dexterity原生实现,API 表面一致,底层存储结构却天差地别。
Drupal 模块则集中在 Drupal.org,通过drush pm:download或composer require drupal/module_name安装。Drupal.org 的模块页面会明确标注“Compatibility”(兼容 Drupal 8/9/10)和“Status”(维护状态:Active, Maintenance only, Unsupported)。但这里有个致命盲区:模块的“Last commit”日期不等于“Last security update”日期。我审计过 12 个客户站点,发现平均每个站点有 3.7 个模块处于 “Maintenance only” 状态(作者声明不再主动开发,只修严重 bug),但其中 2 个模块在过去 6 个月有安全更新,另外 1 个模块的安全更新由 Drupal 安全团队代为发布(标记为 “Security team maintained”)。这意味着你不能只看作者是否活跃,必须订阅 Drupal Security Advisories 邮件列表,并定期运行drush pm:security检查。
3. 实操选型指南:从 5 个真实业务场景出发,拆解模块选择决策树
3.1 场景一:需要多语言内容管理,且翻译流程必须对接外部 TMS(翻译管理系统)
客户需求:某跨国企业官网需支持中/英/日/德四语,所有内容翻译必须走 SDL Trados Server,翻译人员不接触 CMS 后台,只在 Trados 界面操作。
Plone 方案:首选plone.app.multilingual(PAM)。它不是简单添加语言切换器,而是为每个内容对象创建语言变体(Language Variant),每个变体是独立的ContentItem,共享同一 UUID。关键优势在于其ITranslationService接口——你可以实现自己的TradosTranslationService类,重写translate_content方法,调用 Trados Server 的 REST API 提交待翻译内容,并在回调中更新 Plone 内容。PAM 的zcml注册了ITranslationService的默认实现,你的自定义服务只需在configure.zcml中用<adapter factory=".trados.TradosTranslationService" />覆盖即可。实测下来,PAM 与 Trados 集成的稳定度极高,因为整个流程基于 Plone 的事务机制:如果 Trados API 调用失败,Plone 事务回滚,内容状态不变;如果成功,plone.app.multilingual的translate视图会自动创建目标语言变体并填充翻译文本。
Drupal 方案:content_translation是核心模块,但仅提供基础 UI。要对接 Trados,必须用tmgmt(Translation Management Tool)模块。tmgmt的设计哲学是“任务驱动”:它把翻译请求抽象为tmgmt_job实体,每个 job 关联源内容、目标语言、TMS 适配器。tmgmt_trados子模块提供 Trados 适配器,但它的实现是“异步轮询”——tmgmt创建 job 后,后台 cron 每 5 分钟调用tmgmt_trados_api_check_status()查询 Trados 是否完成。问题来了:如果 Trados Server 响应超时,cron 会重试,但tmgmt_job状态可能卡在 “in progress”,导致后续 job 积压。我们曾因此导致客户日语版内容延迟上线 48 小时。最终方案是绕过tmgmt,直接用hook_entity_presave监听内容保存,调用 Trados API 同步提交,用state字段标记内容翻译状态(translated,pending,failed),完全自主控制流程。
对比结论:Plone 的接口契约更利于企业级集成,Drupal 的钩子更易上手但需自行兜底异常。PAM 的ITranslationService是明确的扩展点,tmgmt的 job 机制则要求你理解其状态机。
3.2 场景二:内容需支持复杂数据结构,如“产品”包含多个“规格参数表”、“用户评价”、“相关配件”
客户需求:电商平台后台,运营人员需在一个页面内管理产品全量信息,避免跳转多个表单。
Plone 方案:plone.app.contenttypes+plone.formwidget.contenttree+collective.z3cform.datagridfield。plone.app.contenttypes提供ProductDexterity 类型基础;plone.formwidget.contenttree让用户从内容树中选择“相关配件”(其他 Product 对象);collective.z3cform.datagridfield则用于“规格参数表”——它渲染为表格,每行是ParameterRowschema,包含name(字符串)、value(字符串)、unit(选择项)三个字段。关键技巧在于datagridfield的factory参数:必须传入一个SchemaFactory类,该类的__call__方法返回ParameterRowschema,否则表格无法动态添加行。这个细节在官方文档里藏得很深,我第一次用时调试了 3 小时才定位到。
Drupal 方案:paragraphs模块是绝对首选。它允许你定义可复用的“段落类型”(Paragraph Type),如spec_table、review_list、accessory_grid。每个段落类型有自己的字段(spec_table有field_param_name、field_param_value),然后在Product内容类型中添加field_paragraphs字段,类型为 “Paragraphs”,允许添加任意数量的段落。paragraphs的强大在于其view mode系统:前台展示时,spec_table段落可渲染为 HTML 表格,review_list段落可渲染为评论卡片流,互不干扰。但陷阱在于性能:如果一个产品有 50 个review_list段落,每个段落加载 5 条评论,paragraphs默认会为每个段落单独查询数据库,产生 N+1 查询。必须启用paragraphs的cache设置,并在hook_preprocess_paragraph中手动合并查询。
对比结论:Plone 的 datagridfield 更轻量,适合结构固定;Drupal 的 paragraphs 更灵活,但需主动优化性能。collective.z3cform.datagridfield的 schema 是静态定义的,改字段要重装模块;paragraphs的段落类型可在后台随时增删字段,零代码。
3.3 场景三:需要细粒度权限控制,如“法务部只能编辑合同模板库,且不能删除历史版本”
客户需求:企业知识库,不同部门对同一内容库有不同操作权限,且需保留完整编辑历史。
Plone 方案:plone.app.versioningbehavior+plone.app.workflow+collective.workspace。plone.app.versioningbehavior为内容类型启用版本控制,每次编辑生成新版本,旧版本只读;plone.app.workflow提供simple_publication_workflow,可自定义状态(private,pending,published)及状态转换权限;collective.workspace则是关键——它创建“工作区”(Workspace)容器,为每个工作区分配独立的成员角色(WorkspaceMember,WorkspaceManager),并在工作区内覆盖全局权限。例如,为“合同模板库”创建 Workspace,将法务部成员加入,赋予WorkspaceMember角色,该角色在工作区内有Modify portal content权限(可编辑),但无Delete objects权限(不能删除),且plone.app.versioningbehavior保证所有编辑都生成新版本,旧版本自动归档到Versions文件夹,权限继承自父容器,无法被修改。
Drupal 方案:content_moderation(核心)+workspaces(核心)+group模块。content_moderation提供内容审核流(Draft, Archived, Published),但默认不控制删除权限;workspaces模块创建独立的工作区(Workspace),每个工作区有独立的数据库快照,内容在工作区间复制而非引用;group模块则创建“组”(Group),为组分配角色(group_admin,group_member),再为角色分配权限。要实现法务部只能编辑不能删除,需在group中创建 “Legal Team” 组,为其group_member角色勾选Edit any product content,但取消勾选Delete any product content。难点在于workspaces的快照机制:当法务部在工作区 A 编辑合同,该编辑只存在于 A 的快照中,要发布到生产环境,需手动执行workspace:publish命令,否则前台看不到。这增加了运维复杂度。
对比结论:Plone 的权限模型是“容器内覆盖”,天然适合部门隔离;Drupal 的权限是“角色全局分配”,需配合 workspaces 才能实现空间隔离。collective.workspace的权限是动态计算的(getSecurityManager().checkPermission('Modify portal content', obj)),group模块的权限是静态配置的,更易审计。
3.4 场景四:需高性能全文检索,且支持同义词、停用词、拼音搜索
客户需求:大型文档库(100 万+ PDF),用户搜索“云计算”应匹配“云服务”、“cloud computing”、“yun ji suan”。
Plone 方案:collective.solr是事实标准。它不是 Plone 自己实现搜索,而是作为 Solr 服务器的客户端,将 Plone 内容索引推送到 Solr,搜索请求全部转发给 Solr。collective.solr的核心价值在于其schema.xml适配:它预置了text_general字段类型,内置SynonymFilterFactory(同义词)、StopFilterFactory(停用词)、ICUFoldingFilterFactory(大小写折叠),并针对中文集成了SmartChineseAnalyzer。要支持拼音搜索,只需在schema.xml中为title字段添加PinyinTransformFilterFactory,然后重建索引(bin/instance run scripts/reindex_solr.py)。实测 100 万文档,Solr 查询响应 < 200ms,collective.solr的索引同步是异步队列(collective.solr.queue),不影响前台响应。
Drupal 方案:search_api+search_api_solr。search_api是抽象层,search_api_solr是 Solr 后端实现。它同样支持同义词、停用词,但配置分散:同义词在 Solr 的synonyms.txt文件中维护,停用词在stopwords.txt,拼音搜索需额外安装pinyin插件并修改managed-schema。最大痛点是索引同步时机不可控。search_api默认用SearchApiIndex::updateIndex()方法批量更新,但该方法在hook_cron中执行,受 Drupal cron 运行频率限制(默认每小时一次)。如果客户要求实时索引(PDF 上传后 5 秒内可搜到),必须用search_api的track_changes功能,监听hook_entity_insert等钩子,但这会显著增加 PHP 执行时间。我们曾因此导致上传大文件时超时。
对比结论:Plone 的collective.solr是“索引即服务”,配置集中;Drupal 的search_api_solr是“索引抽象层”,配置分散但可扩展。前者开箱即用,后者需深入 Solr 底层。
3.5 场景五:需与现有 .NET 企业服务总线(ESB)集成,调用 SOAP 接口获取主数据
客户需求:CMS 需从 ESB 获取客户主数据(Customer Master Data),在用户注册时实时校验。
Plone 方案:requests库 +plone.app.async。Plone 6 内置requests,可直接调用 SOAP 接口。但 SOAP 调用耗时(ESB 响应常 > 2s),不能阻塞前台请求。plone.app.async提供异步任务队列,用async.queueJob()提交任务,任务在后台线程执行,结果通过async.getJobResult()获取。关键技巧是任务函数必须是模块顶层函数(不能是类方法),且参数必须是 JSON 序列化类型。我们封装了esb_client.soap_call()函数,接收customer_id,调用 ESB,将结果存入 Plone 的portal_catalog作为缓存,有效期 1 小时。这样前台注册时,先查缓存,缓存失效再异步调用 ESB。
Drupal 方案:php-soap扩展 +queueAPI。Drupal 本身不带 SOAP 客户端,需确保服务器安装php-soap扩展,然后用原生SoapClient类。queueAPI 提供queue服务,用queue->createItem()添加任务。但queue的默认后端是数据库,高并发下易成瓶颈。我们改用redis作为队列后端(queue.redis模块),并用hook_user_insert触发任务。难点在于 SOAP WSDL 解析:ESB 的 WSDL 常含复杂嵌套类型,SoapClient自动生成的类可能与 Drupal 的实体结构冲突,需手动编写WSDL解析器,将 SOAP 响应 XML 映射为 Drupal 数组。
对比结论:Plone 的 Python 生态对 SOAP 更友好,requests+lxml处理 XML 更自然;Drupal 的 PHP SOAP 扩展更底层,但需更多手工适配。plone.app.async的任务模型更贴近现代微服务,queueAPI 则更传统。
4. 模块部署与运维实战:从开发环境到生产环境的 7 个关键步骤
4.1 步骤一:环境初始化——用约束文件锁定所有依赖
Plone 的buildout.cfg是灵魂。它不只是安装脚本,而是整个运行时环境的蓝图。一个健壮的buildout.cfg必须包含:
[buildout] extends = https://dist.plone.org/release/6.0.0b3/constraints.txt parts = instance find-links = https://dist.plone.org/release/6.0.0b3/ allow-picked-versions = false [instance] recipe = plone.recipe.zope2instance eggs = Plone Pillow collective.solr plone.app.contenttypes关键点:extends指向官方约束文件,它锁定了Plone==6.0.0b3、Zope2==4.8.3、Acquisition==4.7.1等所有包的精确版本。allow-picked-versions = false强制buildout严格遵守约束,拒绝任何版本漂移。我见过太多项目因pip install时setuptools升级到 60+,导致zcml解析失败,就是因为没用约束文件。
Drupal 的composer.json同样关键:
{ "require": { "drupal/core-composer-scaffold": "^9.5", "drupal/core-recommended": "^9.5" }, "config": { "sort-packages": true, "platform-check": false }, "extra": { "drupal-scaffold": { "locations": { "web-root": "web/" } } } }core-recommended是 Drupal 官方推荐的约束包,它声明了drupal/core及其所有依赖的兼容版本范围。platform-check: false是必须的——它禁用 Composer 对 PHP 版本的严格检查,因为 Drupal 9.5 要求 PHP 8.0+,但很多生产服务器仍是 PHP 7.4,此时需手动覆盖platform.php配置。drupal-scaffold确保web/目录结构正确,这是 Drupal 8+ 的标准布局。
实操心得:Plone 的
buildout每次运行都会重新下载所有 eggs,耗时很长。用buildout -n(no download)跳过下载,或在 CI 中缓存~/.buildout/eggs/目录。Drupal 的composer install也要缓存vendor/目录,否则每次 CI 构建都重装 200+ 包。
4.2 步骤二:模块安装——Plone 用 buildout,Drupal 用 composer,命令不能混
Plone 安装模块的唯一正统方式是修改buildout.cfg的eggs列表,然后运行./bin/buildout。例如安装collective.easyform:
[instance] eggs = Plone collective.easyform # ... 其他模块运行./bin/buildout后,buildout会:
- 从 PyPI 下载
collective.easyform及其依赖; - 将 egg 安装到
parts/instance/eggs/目录; - 解析
collective.easyform的configure.zcml,生成parts/instance/etc/site.zcml的 include 语句; - 重启 Zope 实例。
严禁用pip install collective.easyform!因为pip安装的 egg 不会被buildout管理,zcml不会自动加载,模块形同虚设。
Drupal 安装模块必须用composer require:
cd /path/to/drupal composer require drupal/webformcomposer会:
- 修改
composer.json的require字段; - 下载模块到
web/modules/contrib/webform/; - 更新
vendor/autoload.php的类映射; - 运行
drush en webform启用模块。
严禁直接下载 ZIP 包解压到modules/目录!因为composer不知道这个模块存在,drush pm:info webform会报错,且composer update时可能被覆盖。
注意:Plone 的
buildout输出日志极详细,Installing instance.后会列出所有加载的zcml文件路径,这是排查模块未生效的第一线索。Drupal 的composer require输出简略,要确认是否成功,必须运行drush pm:list | grep webform。
4.3 步骤三:配置导入——Plone 用 Generic Setup,Drupal 用 Configuration Management
Plone 的配置(内容类型、工作流、权限设置)全部通过Generic Setup导出为 XML 文件。在profiles/default/目录下,有types.xml(内容类型定义)、workflow.xml(工作流配置)、rolemap.xml(权限映射)。部署时,将整个profiles/目录打包为myaddon-1.0.tar.gz,用bin/instance run scripts/import_profile.py myaddon:default导入。关键技巧:import_profile.py的第二个参数是profile_id,格式为package_name:profile_name,package_name必须与setup.py中entry_points的z3c.autoinclude.plugin名称一致。
Drupal 的配置用Configuration Management(CM)。所有配置(内容类型、视图、区块放置)导出为 YAML 文件,存于config/sync/目录。部署命令是:
drush cim -y # 导入配置 drush cex -y # 导出配置cim会对比config/sync/与数据库中的配置,生成差异并应用。但 CM 有两大陷阱:第一,config/sync/目录必须是 Git 仓库的干净工作区,否则cim会拒绝执行;第二,某些配置(如system.site的 site name)是“敏感配置”,默认不导出,需在settings.php中设置$settings['config_exclude_modules'] = ['system.site'];才能导出。
实操心得:Plone 的 Generic Setup 导入是幂等的(多次运行无副作用),Drupal 的
cim则可能破坏数据。我们规定:所有cim操作前,必须drush sql-dump > backup.sql,且只在非生产环境运行cim,生产环境用drush php-eval手动执行配置变更。
4.4 步骤四:数据库迁移——Plone 用 upgradeStep,Drupal 用 hook_update_N
Plone 的模块升级必须提供upgradeStep。在upgrades/目录下,创建to2000.py(升级到 2.0.0 版本):
from plone import api def to2000(context): """Upgrade to 2000""" portal = api.portal.get() # 迁移逻辑:如为所有 Document 添加 description 字段 brains = api.content.find(portal_type='Document') for brain in brains: obj = brain.getObject() if not hasattr(obj, 'description'): obj.description = '' obj.reindexObject()然后在profiles/default/metadata.xml中声明:
<metadata> <version>2000</version> <dependencies> <dependency>profile-plone.app.contenttypes:default</dependency> </dependencies> </metadata>升级时,访问/@@plone-upgrade页面,选择目标版本,点击执行。upgradeStep在事务中运行,失败则回滚。
Drupal 的模块升级用hook_update_N。在mymodule.install文件中:
/** * Add description field to node. */ function mymodule_update_8101() { $database = \Drupal::database(); // SQL 迁移:为 node_field_data 表添加 description 字段 $database->schema()->addField('node_field_data', 'description', [ 'type' => 'text', 'size' => 'big', ]); }升级命令是drush updb -y。updb会查找mymodule_update_8101函数并执行。但 Drupal 的updb不是事务性的——如果 SQL 执行一半失败,数据库可能处于不一致状态。必须手动编写回滚脚本(hook_post_update_NAME)。
提示:Plone 的
upgradeStep可以调用api.content.find(),安全可靠;Drupal 的hook_update_N必须用原始 SQL 或DatabaseAPI,不能调用实体 API,因为实体定义可能还未加载。
4.5 步骤五:缓存清理——Plone 清 Zope 缓存,Drupal 清多重缓存层
Plone 的缓存分三层:Zope 的RAMCache(内存缓存)、plone.app.caching的代理缓存(Varnish)、浏览器缓存。生产环境必须清RAMCache:
bin/instance clearcache该命令清除 Zope 的RAMCache实例,但不影响 Varnish。Varnish 需单独curl -X PURGE。clearcache是安全的,它只清内存缓存,不重启进程。
Drupal 的缓存是七层地狱:PHP OPcache、Drupal 的cache.backend.database、cache.backend.apcu、page_cache、dynamic_page_cache、render、default。清缓存命令是:
drush cr # 清所有缓存 drush cr render # 只清渲染缓存但drush cr不清 OPcache,需手动opcache_reset()。更糟的是,drush cr在多服务器环境下只清当前服务器的缓存,其他服务器缓存未失效,导致用户看到过期内容。必须用redis作为统一缓存后端,并在 `
