Drupal 11:以编程方式更改布局段落的布局
布局段落(Layout Paragraphs)模块巧妙地将布局系统的灵活性与段落(Paragraphs)模块的内容组件系统相结合。借助这个模块,你能够设置一个能理解不同布局的段落,然后把段落注入该布局,而且所有操作都在一个字段范围内就能完成。
这就意味着,网站用户可以在 Drupal 网站的编辑页面内构建他们想要的布局,无需去猜测段落最终会在网站中的位置。这样一来,网站编辑变得更加容易,同时在发布页面之前需要预览的次数也会减少。
在最近的一个 Drupal 开发项目中,成都长风云 Drupal 开发团队发现使用了布局段落,这本身倒不是问题。问题在于,该网站相当简单,但却有 12 种不同的布局可供选择。结果,页面由各种不同的布局组成,这不仅让网站编辑变得困难,还使最终效果看起来有点杂乱。
解决方案是将一些现有的布局合并为一种类型,并从选择项中移除这些布局。这样做使得页面编辑更容易,也能让我们更容易预测在进行一些样式更改时网站的外观。
虽然手动完成这项工作当然是可行的,但要找出特定布局的每一个实例并将它们全部转换并不容易。我们希望采用一种更自动化的方法来解决这个问题,这样就可以运行一个 Drush 命令,将一种类型的布局段落全部转换为另一种类型。
在本文中,我们将了解布局段落模块的结构,以及如何使用 PHP 将布局段落从一种布局转换为另一种布局,这在 Drupal 11 的开发和升级中是非常实用的技巧,也与 Drupal 模块开发密切相关。
一、布局段落结构
在探讨如何移动布局段落之前,我们先快速了解一下布局段落的结构。布局段落系统的结构看似是嵌套的段落结构,但实际上非常简单。
假设我们有一个布局段落,其布局为 "layout_twocol",其中包含另外两个段落。这可以像使用 Drupal 管理页面创建任何其他段落一样创建。
网站中的每个段落都在 paragraphs_item_field_data 表中有一个名为 behavior_settings 的字段,可用于存储有关该段落的信息。这是一个 PHP 序列化的数组字段,不同的模块使用它为段落提供额外的设置,以影响其功能并创建自定义特性。布局段落模块使用此功能来存储与布局相关的数据。
就我们的布局而言,布局段落在 behavior_settings 字段中具有以下结构。
array (
'layout_paragraphs' => array (
'layout' => 'layout_twocol',
'config' => array (
'label' => '',
),
'parent_uuid' => NULL,
'region' => NULL
),
)
除了段落类型为 "layout" 之外,这还告诉我们正在使用哪种布局,在这种情况下是 "layout_twocol"。我们可以通过对段落实体本身使用几种方法来获取网站内的此信息。
behavior_settings 有时会有一个额外的设置 more_items,当手动将布局从一种类型移动到另一种类型时,布局段落模块会在内部使用此设置。在手动过程之外,此设置不是必需的。
getBehaviorSetting() 方法接受两个参数,第一个是要查找数据的插件 ID(为此目的是 layout_paragraphs),第二个是我们想要访问的数据。
$paragraphLayoutType = $paragraph->getBehaviorSetting('layout_paragraphs', 'layout');
由于布局是一个字符串,这里的返回值包含正在使用的布局的单个值。
或者,我们可以一次性获取所有行为设置,然后从数组的相关部分提取我们需要的信息。
$parentBehaviorSettings = $paragraph->getAllBehaviorSettings();
$paragraphLayoutType = $parentBehaviorSettings['layout_paragraphs']['layout'];
现在我们有了布局父段落,我们需要找到属于该父布局的子段落。
属于此布局的段落与数据库中的段落处于同一级别。虽然我称它们为子段落,但实际上这里没有嵌套或其他递归操作。将父/子关系更多地视为一种理解辅助。
属于此布局的每个子段落都会有一个 behavior_settings,它将指向父布局段落(通过 UUID)。此设置还将包含子段落所属的布局段落内的区域。
以下是布局段落的子段落的示例。
array (
'layout_paragraphs' => array (
'parent_uuid' => 'd635a666-f36f-4d76-a8a5-deee32736440',
'region' => 'first',
),
)
可以通过加载字段内的所有段落并遍历它们来构建布局列表,从而找到子段落。模块在加载和渲染段落时会在内部完成此操作。
如果你愿意,可以使用以下数据库查询查看此结构。
select nfp.bundle, nfp.entity_id, pi.uuid, pifd.`type`, pifd.behavior_settings
from node__field_paragraphs as nfp
inner join paragraphs_item pi on pi.id = nfp.field_paragraphs_target_id and pi.revision_id = nfp.field_paragraphs_target_revision_id
inner join paragraphs_item_field_data as pifd on pifd.id = nfp.field_paragraphs_target_id and pifd.revision_id = nfp.field_paragraphs_target_revision_id
where nfp.entity_id = 19
order by nfp.delta asc;
此查询查看节点 19 中 field_paragraphs 字段内的段落结构。你可能需要修改上述查询以使其适用于你的设置。
二、移动布局段落
利用上述关于布局段落结构的信息,我们现在可以移动它们。
首先,我们需要定义几个变量,用于将布局段落从一种布局迁移到另一种布局。
$matchLayoutParagraphType = 'layout_twocol_right';
$newLayout = 'layout_onecol';
$layoutMovementMap = [
'mappings' => [
'first' => 'content',
'second' => 'content',
],
];
我们在这里定义的变量如下:
- $matchLayoutParagraphType - 这是我们想要更改的布局。
- $newLayout - 这是我们想要将该布局更改为的布局。
- $layoutMovementMap - 这包含一个数组,用于将布局区域从一个区域映射到另一个区域。
使用这些变量,我们现在可以找到具有相关布局的布局段落。由于无法查询 PHP 序列化数组,我们需要加载系统中所有类型为 "layout" 的段落。
$paragraphStorage = $this->entityTypeManager->getStorage('paragraph');
// 加载所有布局段落。
$pids = $paragraphStorage->getQuery()
->condition('type', 'layout')
->accessCheck(FALSE)
->execute();
然后,我们可以遍历所有布局段落,通过从行为设置中提取信息来找到我们想要的特定布局的段落。
foreach ($pids as $pid) {
// 加载布局段落。
/** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
$paragraph = $paragraphStorage->load($pid);
// -- 在此处移动布局。
}
对于每个段落,我们需要使用 getBehaviorSetting() 方法来查找我们正在寻找的布局类型。
// 提取布局段落的布局行为设置并
// 匹配布局名称。
$paragraphLayoutType = $paragraph->getBehaviorSetting('layout_paragraphs', 'layout');
if ($paragraphLayoutType === $matchLayoutParagraphType) {
// 移动布局段落的代码。
}
一旦我们验证布局类型正确,我们就可以加载段落的整个行为设置数组,并在保存之前更新 layout_paragraphs 部分中的布局。
// 使用新设置保存当前段落。
$parentBehaviorSettings = $paragraph->getAllBehaviorSettings();
$parentBehaviorSettings['layout_paragraphs']['layout'] = $newLayout;
$paragraph->setBehaviorSettings('layout_paragraphs', $parentBehaviorSettings['layout_paragraphs']);
$paragraph->save();
这更改了根段落,但我们仍然需要将子段落重新指向布局内的正确区域。
这个过程稍微复杂一些,但需要加载字段内的所有段落并遍历它们,直到找到与父段落相连的段落(使用 UUID)。一旦我们找到了一个子段落,我们就需要将其区域重新映射到 $layoutMovementMap 变量中的新区域。
// 从同一字段中获取所有其他段落。
$field_name = $paragraph->get('parent_field_name')->value;
/** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
$contentParagraphs = $parentEntity->get($field_name)->referencedEntities();
foreach ($contentParagraphs as $contentParagraph) {
// 遍历段落以找到将此段落称为父段落的项目。
$behaviorSettings = $contentParagraph->getAllBehaviorSettings();
if (isset($behaviorSettings['layout_paragraphs']['parent_uuid']) && $behaviorSettings['layout_paragraphs']['parent_uuid'] === $paragraph->uuid()) {
// 如果匹配,则重新指向区域并保存段落。
$behaviorSettings['layout_paragraphs']['region'] = $layoutMovementMap[$behaviorSettings['layout_paragraphs']['region']];
$contentParagraph->setBehaviorSettings('layout_paragraphs', $behaviorSettings['layout_paragraphs']);
$contentParagraph->save();
}
}
运行此代码后,所有布局为 layout_twocol_right 的布局段落现在都转换为布局 layout_onecol,内容也被放置到了正确的位置。
当然,可以通过将此代码包装在批处理系统中来改进。当时我们处理的网站相当小,只需要更新大约 100 个段落,因此没有使用批处理系统。
三、结论
将布局段落从一种类型转换为另一种类型意味着要同时更改父段落和子段落。成都长风云 Drupal 开发团队能够创建一个简单的服务,在几秒钟内更改一个约有 200 页的网站上的所有布局段落。布局映射基本上是硬编码的,但非常适合我们的需求。
整个过程中最困难的部分是找出执行这些操作的机制。虽然布局段落模块在管理区域中有更改布局段落布局的机制,但它不会一次性完成此移动。在那种情况下,更改是在保存节点时使用行为设置系统的 move_items 部分进行的。
最后,可以卸载并移除网站上不需要的布局,这简化了后端编辑器的体验。网站从近 12 种不同的布局减少到仅 3 种,所有现有布局都被正确映射。
如果大家对此有足够的兴趣,我们会考虑将其制作成一个 Drupal 模块并进行贡献。甚至尝试将其回馈给布局段落模块。我们唯一担心的是如何创建用户界面,因为创建映射系统可能相当复杂。在系统中进行硬编码相当简单。


