脏简单/强制器

Wordpress 的模块化配置管理

dev-master 2020-07-26 17:22 UTC

This package is auto-updated.

Last update: 2024-09-27 02:31:37 UTC


README

将配置存储在数据库中 真的很糟糕。您无法轻松地进行文档编制、版本控制、复制或通过编程方式更改它。

不幸的是,Wordpress 并没有提供很多替代方案。尽管存在一些配置管理工具,但它们通常缺少以下一个或多个功能

  • 十二因素支持(例如,用于秘密、密钥、API URL 等)
  • 模块化分发(能够将状态作为插件、wp-cli 软件包或 composer 库的一部分包含在内)
  • 增量/可组合性(能够在多个文件中扩展配置,即使值是同一插件、页面、菜单等不同方面的设置)
  • 脚本性和元编程(能够使用 PHP 或其他脚本语言根据配置的其他方面或外部文件、环境变量等来决定如何生成数据)

虽然 wp-cli 在 对 Wordprss 数据库进行更改 方面非常出色,但这并不完全等同于在 Wordprss 数据库上 施加状态

这有什么区别?好吧,使用 wp-cli,您可以使用 wp menu list 来检查菜单是否存在,然后如果不存在,使用 wp menu create 来创建它。但是,没有方法可以给 wp-cli 一个 列表,您希望存在的菜单以及它们的项目,然后自动添加、删除或移动它们以使其匹配。没有方法可以给 wp-cli 一个您希望事物如何的 规范

这就是强制器的作用。

强制器是一个命令行工具(bash+PHP),它允许您创建和使用模块化的、可脚本化的 "状态模块":定义您希望 Wordprss 实例中某些子集如何的 "规范" 的文件。

状态模块类似于一个 Drupal "功能",因为它指定了 "使用案例所需的 '事物'" 作为可重用组件。例如,您可能有一个模块定义了一些电子商务产品和类别,以及一些要放置在特定侧边栏中的小部件以链接到这些类别。也许还有一个模块指定了您将使用的 SMTP 电子邮件插件以及要使用的凭据,这些凭据基于环境变量。

然后,您可以将这些状态模块单独或一起应用到您喜欢的任何 Wordprss 安装中。它们是可以放在版本控制中的文件,您可以对它们进行微调和调整,以针对您的开发站点进行测试,然后将它们应用到您的生产站点以立即部署您的站点规范的一些最新版本。

您可以拥有尽可能多的这些模块,并且它们可以相互依赖或覆盖前一个模块设置的某些内容,从而有效地从每个站点中 "子类化" 站点的某些方面。状态模块也可以作为 Wordprss 插件或主题、composer 或 wp-cli 软件包的一部分进行分发,或者简单地放在由 IMPOSER_PATH 环境变量列出的任何目录中。它们可以通过在命令行上列出它们来一次性应用,也可以作为您项目特定的、用户特定的和/或系统范围的配置的一部分,以便每次运行 imposer apply 时都应用。

每次运行 imposer apply 命令时,所选模块会共同创建一个名为 规范 的 JSON 数据结构。这是一个对象,其属性列出应激活或禁用的插件、应设置为何值的选项(或选项的部分)、应存在于何种顺序和主题位置的菜单和菜单项等。这个规范每次应用时都会动态构建,因此可以读取环境变量、自定义配置文件、连接到其他系统下载信息,或执行 任何其他您想要的 操作来创建最终的规范。

虽然规范不必定义整个数据库!即使您有多个需要通用菜单的站点,这也不意味着您必须通过 imposer 指定那些站点的所有菜单。在给定运行中不属于规范的部分,如选项、插件、菜单、帖子等,通常不会被 imposer 影响。

规范也不会定义其内容如何映射到 WordPress 数据库。这是通过 Imposer 的 任务 来完成的。Imposer 提供了用于 主题切换插件激活选项修补菜单/菜单项定义菜单位置分配 的内置任务,以及创建或更新 分类、标签和其他分类法术语小工具配置和侧边栏位置,但您的模块也可以包含 PHP 代码来定义其他类型的任务。

(任何 WordPress 插件或 wp-cli 软件包也可以这样做!例如,postmark wp-cli 软件包 提供了一个 状态模块,该模块注册了任务,用于从规范中列出的目录中的 Markdown 文件导入帖子、页面和自定义帖子类型。)

这种规范(内容)和任务(过程)的分离意味着您不必为每个站点编写不同的 wp-cli 脚本,而是可以编写一个通用的任务来配置,例如特定插件的商品或表单,然后重用该任务在不同站点上应用不同的规范...甚至可以将其作为 wp-cli 软件包、WordPress 插件或主题的一部分进行分发!

状态模块可以单独在命令行上应用,或在项目配置中定义,任何这些状态都可以 require 其他状态。YAML、JSON、shell 和 jq 块首先按源顺序执行,以创建一个 jq 程序,该程序构建一个表示 WordPress 中事物所需状态的 JSON 规范对象。

最后但同样重要的是,您的模块还可以包含“调整”:将作为模块化、可配置且主题无关的替代方案添加到动态生成的 WordPress 插件中的 PHP 代码。

目录

用户指南

状态模块如何工作

状态模块以Markdown文件的形式实现,文件名以.state.md结尾。Imposer使用mdsh(特别是mdsh的jqmd扩展)编译和解释这些文件,寻找以各种语言编写的三重反引号围栏代码块,例如YAML、JSON、shell脚本、PHP以及jq

jq、YAML和JSON中的块用于逐步构建规范对象,而shell脚本块允许您加载其他模块、定义和调用函数,以及添加条件逻辑或其他脚本任务来帮助计算规范。(例如,您可以包含一些使用curl下载一些JSON的脚本,并将其添加到规范中。)PHP块用于定义额外的imposer任务,或将运行时调整添加到Wordpress中(就像您通常通过编辑functions.php添加的代码片段一样)。

使用Markdown作为状态模块的文件格式除了包含多种语言在同一文件中的明显优势外,还提供了许多其他优势。Markdown文件可以包含文档以及代码,并且可以自动转换为带有语法高亮的漂亮网页(就像这个README的情况一样)。由于mdsh忽略非三重反引号代码块,您甚至可以使用缩进块、波浪号块或超过三个反引号来“注释”选定的代码块作为文档/使用示例。

选项、主题、插件和依赖关系

如果这份文档是一个状态模块,它可能包含一些像这样的YAML,用于设置wp-mail-smtp插件的选项

options:
  wp_mail_smtp:
    mail:
      from_email: \(env.WP_FROM_EMAIL // _)
      from_name:  \(env.WP_FROM_NAME  // _)
      mailer: mailgun
      return_path: true
    mailgun:
      api_key: \(env.MAILGUN_API_KEY)
      domain: \(env.MAILGUN_API_DOMAIN)

这已经足够成为一个有效且有用的状态模块。模块使用jqmd进行解析,因此YAML块中的字符串可以包含jq插值表达式,如\(env.MAILGUN_API_KEY)来获取环境变量的值。(JSON块也可以这样做,并使用纯jq表达式以及字符串插值。)

(注意:jq的env对于不存在的环境变量返回null,因此上面的代码使用jq默认操作符(//,类似于PHP的?:)将其替换为空字符串,这样缺失的变量就不会在结果中放置单词“null”。_是imposer提供的jq函数,返回"",便于在YAML块插值中使用,因为它们不能包含引号字符串。)

一个模块可以包含多个YAML或JSON块(未缩进,并用三重反引号围栏),它们的 内容会被递归合并,任何级别的相同键的较晚值会覆盖较早的值(或在列表的情况下添加到它们)。这种合并也发生在模块之间,这意味着您可以(例如)在一个模块中定义菜单,并在另一个模块中指定其位置,同时另一个模块向同一个菜单添加一些额外的条目。每个模块的YAML或JSON块只需要指定它们想强加的状态部分。

这不仅仅是选项。我们还可以选择主题,或指示特定的插件应该被激活或停用,或者执行定义了导入任务的任何其他操作

theme: twentyseventeen
plugins:
  wp-mail-smtp:      # if a value is omitted or true, the plugin is activated
  disable_me: false  # if the value is explicitly `false`, ensure the plugin is deactivated

当然,仅为了禁用而在规范中列出插件是没有太多意义的!但如果你使用了一个插件一段时间,然后切换到新的一个,你应该编辑它以添加false,直到所有使用该状态文件的网站都应用了它。此外,如果你有一个仅在开发中使用但应在生产中禁用的插件,你可能首先启用它,然后加载一个针对生产特定的模块来禁用它。(或者反之。)

说到加载其他状态模块,以下是使用shell块来完成此操作的方法

# Find and load a module, a bit like php's `require_once`
require "some/state"

# Use `have_module` to test for availability
if have_module "foo/other"; then
    require "foo/other" "this/that"
fi

现在我们已经做了这些,我们在模块中包含的任何YAML或JSON都会覆盖some/statefoo/otherthis/that在规范相同部分中设置的内容,并且我们包含的任何PHP代码都会在这些状态的PHP代码之后加载。(由于每个模块在imposer运行期间只加载一次,我们可以使用require来强制模块之间的优先级。)

顺便说一下,状态模块是使用模块名称而不是文件名来加载的!你不包括.state.md后缀,也不包括文件的完整路径。这是因为模块是在IMPOSER_PATH中搜索的,这样你就可以有全局安装的模块,并且可以用本地修补的版本覆盖其中的一些,也可以使分布式模块能够在不必须知道特定站点的本地目录布局的情况下引用其他模块。

默认情况下,IMPOSER_PATH包括你的项目的imposer目录、插件和主题目录、Composer的本地和全局vendor目录以及wp-cli包目录。如果你require状态模块foo/bar,这实际上可能指的是文件imposer/foo/bar.state.md或可能是~/.wp-cli/packages/vendor/foo/bar/default.state.md,具体取决于它首先在哪里找到。

脚本规范

除了shell、YAML和JSON之外,我们可以使用的第四种语言是用于操作JSON的函数式语言jq -- 对JSON进行操作的代码块。标记为jq的代码块是应用于顶层规范对象的过滤器表达式。例如,这个代码块修改了规范中插件部分以激活指定的插件

# activate `some-plugin`
.plugins["some-plugin"] = true

每个jq代码块都必须是有效的jq过滤器表达式。(如果你想在同一个代码块中做多项操作,请使用|分隔。)

尽管如此,对于大多数事情来说,使用YAML和JSON代码块既清晰又简单,将jq代码留到那些很少需要以YAML或JSON代码块不支持的方式操作配置的情况。但是,如果你需要以编程方式修改规范的部分,你也可以使用shell代码块。

# Shell function to activate a plugin:
activate-plugin() { FILTER ".plugins[%s] = true" "$1"; }

# Activate plugins 'xyz' 'abc' and 'def'
for plugin in "xyz" "abc" "def"; do
    activate-plugin "$plugin"
done

一旦由加载的模块定义,shell函数就可用在模块的所有其他shell代码块以及任何随后加载的模块中。这里使用的FILTER函数是jqmd API函数,它允许你以编程方式将jq表达式应用于正在构建的规范,其中每个%s被替换为对额外参数的引用,方式非常类似于$wpdb->prepare。(除了占位符总是%s并且总是传递字符串。)

如果您需要将除了字符串之外的数据类型传递到jq表达式中,可以直接在FILTER表达式中插入数字、常量或正确引用/转义的JSON值,或者使用jqmd的ARGJSON函数创建一个命名的JSON变量,该变量可以直接被jq或JSON块或FILTER表达式引用。有关这方面的更多信息以及您可以使用jq块和shell脚本来完成的其他事情,请参阅jqmd文档

处理其他 Markdown 块

尽管imposer默认只理解少数几种markdown块,但您可以使用jqmd和mdsh的扩展功能添加新的块类型,或者以特定方式解释单个块。例如,如果您想将状态模块中的CSS块组合起来配置Simple CSS插件,您可以编写如下Markdown:

```css +FILTER '.options.simple_css.css += %s'
/* Here's some CSS */
```

块中语言后面的+告诉jqmd,该行的其余部分是一个shell脚本片段,应该将块内容作为额外参数传递。因此,上面的Markdown相当于以下shell块:

FILTER '.options.simple_css.css += %s' $'/* Here\'s some CSS */\n'

这意味着您可以定义一个shell函数来简化这个过程并消除重复,例如:

```shell
simple-css() { FILTER '.options.simple_css.css += %s' "$1"; }
```

```css +simple-css
/* Here's some CSS */
```

```css +simple-css
/* Here's some more */
```

当然,这些块不必在同一个文件中。您可以在同时激活Simple CSS插件的模块中定义simple-css函数,然后从任何想要添加css块的模块中require该模块。

例如,您可以将以下内容放在simple-css.state.md中:

```shell
# Activate the plugin
FILTER '.plugins["simple-css"] = true'

# Define a function other modules can use to append CSS
simple-css() { FILTER '.options.simple_css.css += %s' "$1"; }
```

然后其他状态模块可以这样操作:

```shell
require simple-css
```

```css +simple-css
/* Here's some CSS */
```

请注意,这个simple-css函数也可以被程序化地使用,例如通过在shell块中放入类似simple-css "$SOME_VAR"的内容来从环境变量或类似的位置插入一些CSS。

块事件

我们之前的例子在小规模上运行得很好,但它让每个状态模块都依赖于simple-css模块来输出CSS。如果我们能这样设置,让任何状态模块中的任何css块自动发送到simple-css插件,或者可能是其他插件,会怎么样呢?

这就是imposer的块事件的作用。每当imposer编译一个markdown文件时,任何“杂项”代码块都会被转换为shell代码,并发出一个形式为block of Xshell事件,其中X是块的类型。因此,如果我们把我们的simple-css.state.md文件修改成这样:

```shell
# Activate the plugin
FILTER '.plugins["simple-css"] = true'

# Define a function other modules can use to append CSS
simple-css() { FILTER '.options.simple_css.css += %s' "$1"; }

# When a block of css is seen, pass it as an argument to simple-css
event off "block of css"                 # disable any previous handlers
event on  "block of css" @1 simple-css   # pass one arg from the event to `simple-css` function
```

...那么就不再需要在块的开头包含+simple-css。您可以简单地这样做:

```css
/* Here's some CSS */
```

这样,在项目级别,您可以在任何CSS块出现之前require simple-css,然后之后加载的每个状态模块中的每个CSS块都会发送到simple-css模块。

由于CSS块没有指定如何包含CSS,您可以将不同的CSS插件或使用主题设置或某些其他指定元素:只需在项目级别使用不同的事件处理器。

但是,如果您有一些特定于主题或插件的CSS呢?块事件包括markdown块的整个开头行,这意味着您可以这样做:

# in imposer-project.md:

```shell
require simple-css mytheme
```

```css for mytheme
/* Here's some CSS for mytheme */
```

# in mytheme.state.md:

```yaml
theme: mytheme
```

```shell
event on "block of css for mytheme" @_ event emit "block of css"
```

在这个例子中,mytheme状态模块为mytheme的css块注册了一个处理程序,该处理程序会以与原始事件相同的参数触发一个css块事件。这意味着任何mytheme的css块都将在mytheme模块被包含在项目中时添加CSS到项目(只有当mytheme模块被包含在项目中时才会添加CSS!)。(如果mytheme没有被包含,那么mytheme的css块事件将被忽略,因为它没有处理程序。)

通过这种方式,您实际上可以创建包含多个主题或插件的CSS自定义状态模块,使用适当的命名约定来命名这些块。

当然,您不仅限于CSS:您还可以使用JavaScript块,或者您能想到的任何其他内容。事件的使用允许您将状态模块松散耦合,通过让模块以不依赖于实现细节(例如,您使用的是哪个插件)的方式提供规范数据。

有关在imposer中如何使用这类shell事件等的更多信息,请参阅下面的事件钩子部分。

确定要设置哪些选项

在我们的上一个例子中,我们为Simple CSS插件设置了一个选项。为了做到这一点,我们必须知道插件在Wordpress数据库中使用的是哪些Wordpress选项键。但是,由于大多数用户仅通过Web UI配置Wordpress,插件开发者很少在数据库级别文档他们的插件选项。

当然,对于一个足够小的插件,您可以通过阅读源代码并查找get_option()调用来找出其选项。但如果是像电子商务商店或LMS这样的大型插件呢?

为了帮助您解析此类插件的配置格式,imposer提供了一些工具来检查和监控通过Wordpress UI进行的选项更改。这样,您就可以通过Wordpress UI配置插件,并观察它对数据库中选项所做的更改。

您将使用的主要工具是imposer options yaml(显示未施加的选项值)和imposer options review,后者允许您交互式地审查和批准最近的选项更改。

(注意:批准一个更改只是表示“我已经看到了这个更改,已经做了我需要做的事情,所以不要再让我看到了”。它不会将它们保存到状态模块中,尽管您可以在审查它们时肯定地将相关的YAML从更改中复制和粘贴到状态模块中。您还可以使用imposer options yaml以更方便的格式列出未批准的、未施加的更改,以便进行复制和粘贴。)

要开始审查,只需在相关开发网站上运行imposer options review。要么您会立即以YAML补丁块的形式呈现任何现有更改(通过git add --patch UI进行审查和批准),要么该命令将开始监控数据库的新更改,等待您通过Wordpress UI进行更改。(您还可以使用imposer options approve按名称批准单个选项更改。)

一旦您批准了更改(或将其添加到状态模块并应用),它就不会在未来的reviewdiffyamlwatch子命令运行中出现。这样,您可以过滤出已映射到状态文件的更改和无关的“噪音”更改,同时仍然观察您正在使用watchdiff处理的选项值。(您还可以让状态模块将像cron这样的不断变化的选项列入黑名单,这样它们就不会在您的差异中一直出现。)

有关imposer监控和检查Wordpress选项的更多详细信息,请参阅下面的imposer options命令参考。

PHP 块

除了shell、jq、YAML和JSON块之外,您还可以定义PHP块。与其他所有类型的块(它们在文件中出现的点执行)不同,PHP块会在以后执行时“保存”起来,无论是作为imposer apply运行的一部分,还是作为动态生成的插件(在“tweaks”的情况下)的一部分。

PHP块在编译状态文件时会单独进行语法检查,并且只要每个块本身以及与其他块的组合都是语法有效的,就可以包含命名空间代码。(换句话说,namespace ... { }包装不能跨越块边界,如果某个块使用这样的包装,则不能包含任何包装外的代码。)

PHP块主要有两种类型:扩展和tweaks。扩展被标记为php的块,其代码用于定义imposer将使用以将您的JSON规范对象转换为Wordpress数据库对象的任务、步骤和资源。另一方面,tweaks被标记为php tweak,并组合成一个动态生成的Wordpress插件。

使用use和命名空间

无论PHP块的类型如何,每个状态模块的PHP块与其他模块中相同类型的块在语法上是隔离的。也就是说,即使您在模块中任何地方都没有使用namespace {...},您的模块中给定类型的所有PHP块的内容仍然可以组合成一个或多个namespace {...}块,以确保其他模块的use语句不会渗透到您的模块中(反之亦然)。

但是请注意,这意味着如果您在一个PHP块中使用了某些内容,然后require其他具有相同类型PHP块的模块,并且您的模块中也有另一个相同类型的PHP块,那么您可能需要重新声明您的use语句,因为这两个块将位于不同的namespace {...}包装中。为了避免这种情况,您可以将您的require命令移动到模块的开始或结束处,合并您的PHP块,或显式使用namespace {...}并在需要它们的每个块中声明use语句。

添加代码tweaks

许多Wordpress插件要求您将代码添加到主题的functions.php中才能按您希望的方式运行。但是,管理这些代码片段可能很麻烦,尤其是在更换主题或需要将tweak应用到多个网站时。为了解决这个问题,状态文件也可以包含“tweaks”——标记为php tweak的三重反引号围起来的代码块,如下所示

add_filter('made_up_example', '__return_true');

当您运行imposer apply时,这些代码块将被连接在一起,并写入到$IMPOSER_PLUGINS目录中的虚拟插件imposer-tweaks(默认为wp插件路径)。此插件也会在处理第一个tweak时立即激活,除非您在到达第一个php tweak块之后某个时间点明确禁用该插件。

由于任何状态文件都可能包含tweaks,这是一个模块化和重用这些类型代码片段的有力工具。

但是请注意,仅包括在生成的插件中的PHP tweaks是那些直接或间接包含在(或由)您的imposer-project.md或全局imposer配置中,或者手动指定包含tweaks或加载其他包含tweaks的状态的模块。如果命令行中指定的状态模块包含tweaks或加载其他包含tweaks的状态,Imposer会警告您它不会包含来自这些模块的tweaks。(这是因为每次运行imposer apply时都会从头开始生成插件,所以这些tweaks会在下一次运行imposer时消失,除非您再次指定了相同的模块。)

使用PHP块扩展Imposer

带有 php 标签的三重反引号代码块不会被添加到插件中:它们作为 wp-cli 命令的一部分由 imposer apply 执行。这意味着您可以使用 WordPress 和 wp-cli API 同时将您的规范应用于数据库。

然而,与任何其他 WordPress 插件或 wp-cli 包一样,您的代码通常不应该 直接执行 任何操作,除了定义函数或类,以及注册钩子。在这种情况下,您可能会比动作或过滤器更频繁地注册 Imposer 的 "任务"、"资源" 和 "步骤",但基本原则仍然是相同的:设置代码以便在正确的时机(们)被调用,而不是立即采取直接行动。

扩展 WP-CLI 的 CLI 块

仅当 imposer apply 执行时,才会运行带有 php 标签的块,但有时您需要运行其他 WP-CLI 命令的钩子(例如 postmark)。您 可以 将它们作为 php tweak 块添加,但这样它们会在每次网站访问时运行。因此,imposer 提供了另一种块类型:php cli。这些块保存在一个单独的 imposer-tweaks.cli.php 文件中,如果并且仅当要运行 WP-CLI 命令时,imposer-tweaks 插件才会加载它。通过这种方式,您可以在不增加网站运行时开销的情况下,将 WP-CLI 特定的钩子添加到您的状态模块中。

扩展 Imposer

定义任务和步骤

imposer 的 任务 是一个可暂停步骤(回调)的命名集合,这些步骤使用由状态模块组装的完成规范对象中的数据调用。例如,如果我们想要创建一个处理类似于这样的顶级规范值的任务

hello: world

我们可以在状态文件中添加以下 PHP 代码块

Imposer::task("Hello World")
    -> reads('hello')
    -> steps(function ($msg) {
        WP_CLI::line("Hello $msg");
    });

任务只有在它们读取的值中至少有一个存在于规范中时才会运行。所以如果这个 PHP 块单独包含在一个状态模块中,它 不会 输出消息,除非另一个模块在规范的顶级定义了一个 hello 属性。此任务也只会运行一次,无论顶级 hello 属性指定或重新指定多少次(因为只有最后一次这样的指定会在最终的规范对象中)。

让我们尝试一个稍微复杂一些的版本,处理如下规范

hello-world:
  greeting: Hello
  recipient: World
Imposer::task("Parameterized Greetings")
    -> reads(['hello-world','greeting'], ['hello-world','recipient'])
    -> steps(function ($what, $who) { WP_CLI::line("$what, $who!"); });

如您所见,您可以将多个参数传递给 reads(),并且只要至少 一个 个键存在,它们都会被转发到您的步骤函数。如果参数名是一个数组,则它是一个从规范对象的根开始遍历的属性名路径。

因此,步骤函数接收 hello-worldgreetingrecipient 子键作为其 $what$who 参数。如果只有一个键存在,任务仍然会运行,但传递给每个步骤的其他参数将是 null

任务可以读取任何数量的规范属性。如果没有指定任何属性,步骤将不带参数调用,并且任务将始终运行。否则,任务只有在运行时规范中至少存在一个命名属性时才会运行。

可以多次调用 reads()steps() 方法,此时它们会添加更多参数或更多步骤。添加的参数传递给每个步骤,而添加的步骤在运行时接收所有定义的参数。步骤按添加的顺序启动(并且可以在任何时候添加,甚至由其他步骤添加),但步骤也可以 暂停,允许其他步骤和任务运行。

(这种暂停发生在其他任务或步骤负责导入暂停步骤需要引用的内容时,例如一个菜单项引用尚未导入的文章。)

任务依赖关系

任务通常按照它们定义的顺序运行,并且运行所有步骤直到没有剩余的任务。但是,Wordpress数据库有一个复杂的架构,其中有许多东西都指向其他地方,无论你以什么顺序运行任务,总有需要引用尚未创建的内容的可能性。

因此,imposer也有“资源”的概念:由任务创建或引用的内容。假设我们正在编写一个任务,在数据库中设置“限制小工具”插件作用的widget限制,并需要从规格说明中的信息查找页面ID。通常你可能会写一些像这样的东西

if ( ! $page_id = url_to_postid($url) ) {
    WP_CLI::error("No post/page found for path '$url'");
}

但是在Imposer任务中这样做,你怎么知道用户是否错误地指定了路径,或者页面是否尚未导入呢?

为此,Imposer提供了引用

$page_id = yield Imposer::ref('@wp-post', $url);

任务步骤函数可以使用yield来获取数据库ID。在上面的例子中,我们请求一个WordPress文章(@wp-post)资源。如果文章已经存在,则立即返回ID。但如果它不存在,函数的执行将被挂起,直到所需的文章被创建,或者直到所有其他非挂起的任务都完成...此时函数将使用无效URL的抛出异常来恢复。

(注意:当一个步骤被挂起时,同一任务中的下一个步骤开始,然后是任何其他还有未开始步骤的任务。该步骤将立即在Imposer可以确定所需资源可用时恢复。)

引用和查找

传递给Imposer::ref()的第一个参数是资源类型的名称。 (按照惯例,资源类型名称以@开头,这可能在将来被强制执行。)第二个参数是“键”,可选的第三个参数是“键类型”。

Imposer预先定义了一些资源类型:@wp-post@wp-user以及每个分类的@wp-*-term类型。 (例如,@wp-category-term用于分类,@wp-post_tag-term用于标签,等等。)

每个资源类型都可以定义查找:将特定类型的键转换为数据库ID的函数,或者如果项目当前不存在,则返回null。在@wp-post资源类型的例子中,定义了两种键类型,每种类型都有自己的注册查找:pathguidurl_to_postid()函数用于通过路径查找项目。

每个资源类型还有一个默认查找,这是当只给Imposer::ref()提供两个参数时使用的查找。通常,此查找会依次尝试其他查找。(例如,@wp-post默认查找首先尝试guid查找,然后是path查找,而@wp-user则尝试email查找,然后是login查找,@wp-*-term的默认查找则尝试slug然后是name。)

您可以使用Imposer::resource($typename)注册额外的查找函数,该方法用于获取(或创建)资源类型对象,然后调用其addLookup($handler, $keyType='')方法。例如,如果您想能够通过标题查找帖子,您可以这样做

Imposer::resource('@wp-post')->addLookup('some_function', 'title');

...只要some_function是一个接受标题并返回帖子ID的函数,或者如果找不到帖子,则返回null

查找函数还接收两个额外的参数:键类型(在这种情况下为"title")和资源对象(Imposer::resource('@wp-post'))。现在查找已经存在,我们可以在任务步骤中执行类似操作

$post_id = yield Imposer::ref('@wp-post', "some title", 'title');

您不仅限于内置的资源类型。例如,如果您有一个名为myforms的插件,您可能为@myforms-form资源类型定义查找,以便通过各种键在数据库中查找表单。基本思路是,您可以通过键查找任何可能的项,并在需要时自动暂停执行以允许其他步骤运行。

避免依赖循环和死锁

大多数任务之间的依赖关系都是不同类型的事物之间,它们不会相互引用。例如,菜单项可以引用分类术语或文章,但文章通常不会引用菜单项。然而,在某些情况下,例如课程引用其课程和反之亦然,您可能需要将任务分解成更小的部分,以避免死锁,其中课程导入任务需要加载课程,但课程导入任务正在等待课程。

在这种情况下,imposer将通过选择一个任意的查找失败来打破死锁。但鉴于这并不是很有帮助,通常更好的方法是为每个要创建或更新的项创建单独的步骤,并单独执行任何可能循环的链接(例如课程与其课程之间的双向链接,或文章之间的下一/上一链接)。这样,即使链接步骤因为等待创建项目而受阻,项目本身仍然可以创建。

尽管如此,您不必事先知道所有步骤,因为您可以在其他步骤中的任何一点调用Imposer::steps(function(){ ... });,以添加新的步骤函数到当前任务,该函数将在稍后运行。因此,您可以初始化一个任务,其中包含创建每个项的步骤,每个项步骤可以创建设置项间链接的步骤。这样,如果任何步骤需要暂停,其他步骤仍然可以继续进行。

分发模块和扩展

在定义和测试了您的任务定义之后,您有多种方法可以将它们分发到其他imposer用户。最简单的方法是将模块包含在您的插件、主题或wp-cli包中。如果将其命名为default.state.md(位于扩展的根目录或imposer-states/子目录中),那么安装了它的用户可以在shell块中简单地调用require "theme-or-plugin-name"require "yourorg/yourpackage",以在他们的项目中使用它。(或者他们可以运行imposer apply theme-or-pluginimposer apply yourorg/yourpackage)。

但是,如果您的任务很复杂,您可能希望将大部分代码放在PHP文件中而不是将其嵌入到.state.md文件中。您可以公开扩展中的API函数或类,并仅在状态文件中注册任务以传递所需规范值到API。(这种方法的一个好处是公开了一个适用于您扩展的JSON接受API。)

然而,您可能希望向用户提供一个选项,让他们能够在不要求或应用状态模块的情况下使用您的任务,除了他们自己的设置之外。如果是这样,您可以通过为imposer_tasks动作注册回调来实现这一点,并从该回调创建您的任务。

您当然也可以有除了default.state.md之外的状态模块:您可以使用它们来提供配置插件或主题的各种配置文件。例如,如果您的主题有各种示例或启动站点,您可以将其定义为状态文件,人们可以使用imposer apply my-theme/portfolio导入它们,以加载主题根目录或imposer-states/子目录中的portfolio.state.md。这样的模块可以依赖于您主题或其他项目中的其他模块,用户可以在这些模块之上构建自己的模块以扩展您的启动站点来创建他们自己的站点。

参考

安装、要求和用法

Imposer 与 composer 一同打包,旨在作为 wp-cli 或 composer 包安装,例如通过 wp package install dirtsimple/imposercomposer require dirtsimple/imposer:dev-mastercomposer global require dirtsimple/imposer:dev-master 安装。不过,无论您以何种方式安装,都要确保适当的 vendor/bin 目录已添加到您的 PATH 中,这样您就可以直接运行 imposer,无需每次都指定其确切位置。(例如,如果您使用 wp package install,您可能需要在 PATH 中添加 ~/.wp-cli/packages/vendor/bin。)

除了 PHP 5.6+、Composer 和 Wordpress 以外,imposer 还需要以下内容:

  • jq 1.5 或更高版本
  • bash shell,版本 3.2 或更高版本

Imposer 目前还没有在 Linux 以外进行常规测试,但它应该能在具有合适版本 bash 和 jq 的 OS/X 和其他类 Unix 操作系统上运行。可以在 Windows 上使用 Cygwin 版本的 PHP、jq、git 和 bash 来运行,但与 Windows 的 Linux 子系统或使用 Linux 虚拟机或 Docker 容器相比,运行速度会慢得多。

要使用 Imposer,您的项目根目录中至少需要有以下文件之一:

  • imposer-project.md,
  • composer.json,或
  • wp-cli.yml.

Imposer 将搜索当前目录及其父目录,直到找到这三个文件之一,并且所有相对路径(例如 IMPOSER_PATH 中的路径)都将相对于该目录进行解释。(并且状态模块文件中的所有代码都将在此目录下作为当前目录执行。)如果您有 imposer-project.md,它将被加载为名为 imposer:project 的模块的状态文件。

除了上述三个文件之一或多个之外,您还可以有一个包含环境变量的 .imposer-env 文件,格式为 docker-compose .env 文件(即不包含引号或转义序列的原始值)。这允许您将密钥、凭据、主机名和其他配置存储在单独的文件中,该文件可以排除在版本控制之外。在 imposer 运行时以及在 imposer 执行的任何命令中,都会将文件中找到的任何变量添加到操作系统环境。

基本用法是 imposer apply [modulename...],其中每个参数指定一个要加载的状态模块。模块将按指定顺序加载,除非早期模块 require 了较晚的模块,这会强制它在其列表位置之前加载。如果要将所有内容作为规范应用,并且这些内容已经在您的 imposer-project.md 中或由其 require 的模块中,则无需列出任何模块。

查找和处理顺序

状态模块到文件名的映射

为了方便起见,模块名称不包括 .state.md 后缀,也可以是 composer 包的名称(例如 foo/bar)或主题/插件目录的名称(例如 somethemesomeplugin)。给定一个如 foo/bar/baz 的字符串,imposer 将按以下顺序在每个 IMPOSER_PATH 目录中查找以下文件,使用找到的第一个文件

  • foo/bar/baz.state.md(精确名称,作为 .state.md 文件)
  • foo/bar/baz/default.state.md(精确名称,作为包含 default.state.md 文件的目录)
  • foo/bar/baz/imposer-states/default.state.md(精确名称,作为包含 imposer-states/default.state.md 文件的目录)
  • foo/bar/imposer-states/baz.state.md(名称空间作为目录,包含在 imposer-states 子目录中的最后一个名称部分)

(最后一条规则意味着您可以创建一个名为例如 mycompany/imposer-states 的 composer 包,其中包含一组状态模块库,然后需要例如 mycompany/foo 来从包的根目录(即 vendor/mycompany/imposer-states)加载 foo.state.md。或者,您可以创建一个名为 myplugin 的 WordPress 插件,然后需要 myplugin/bar 来从插件的 imposer-states/ 目录或其根目录加载 bar.state.md。)

IMPOSER_PATH

默认的 IMPOSER_PATH 由以下内容组成

  • ./imposer(即项目根目录下的 imposer 子目录)
  • $IMPOSER_THEMES,默认为 WordPress 主题目录,由 wp theme path 提供(例如 wp-content/themes/
  • $IMPOSER_PLUGINS,默认为 WordPress 插件目录,由 wp plugin path 提供(例如 wp-content/plugins/
  • $IMPOSER_VENDOR,默认为 COMPOSER_VENDOR_DIR(例如 vendor/),如果存在 composer.json 文件
  • $IMPOSER_PACKAGES,默认为 wp package pathvendor 子目录(通常为 ~/.wp-cli/packages/vendor
  • $IMPOSER_GLOBALS,默认为全局 composer 的 vendor 目录,例如 ${COMPOSER_HOME}/vendor

(您可以通过设置相应的变量为空字符串来从默认的 IMPOSER_PATH 中移除任何上述目录。)

这允许状态以多种方式分发和安装,同时仍然可以在项目级别(通过主 imposer/ 目录)进行覆盖。(例如,如果您向项目中添加 imposer/foo/bar.state.md 文件,它将替换任何名为 foo 的主题/插件的 bar 状态,或名为 foo/bar 的 composer 包的默认状态。)

imposer apply 的处理阶段

在讨论优先级顺序时,您可能会发现列出 imposer apply 执行的阶段很有用

  • 第一阶段:通过将它们转换为 imposer 通过 source 执行的(时间戳缓存的)shell 脚本来 加载和执行状态模块。(在此阶段结束时触发 all_modules_loaded 事件。)在此阶段中,执行 shell 块,并将大多数非 shell 代码块积累在内存中以供后续阶段使用。YAML 和 JSON 块转换为生成 jq 过滤器管道的 FILTER 命令。
  • 第二阶段:通过运行 jq 命令在上一阶段生成的 jq 过滤器管道代码上 生成 JSON 规范对象
  • 第三阶段:通过以下方式 将更改应用到 WordPress
    1. 触发 before_apply shell 事件(这将为任何积累的 代码调整生成 imposer-tweaks 插件文件)
    2. 通过 wp eval 运行 imposer 的 PHP 代码,然后运行 imposer 的 操作和过滤器以及任务来实际更新 WordPress 数据库。(如果调用 Imposer::request_restart(),则此步骤可能运行多次,例如更改主题或激活或停用插件。)
    3. 如果前一个步骤完成后没有出现致命错误,则运行 after_apply shell 事件。

因此,虽然看起来像 shell、PHP 和 YAML/JSON/jq 代码执行是交织的,但实际上首先执行的是 shell 代码:只是任何除了 shell 代码块之外的代码块都转换为 shell 代码,这些代码块累积 jq 或 PHP 代码以在第二阶段或第三阶段执行。这意味着您不能(例如)有两个 YAML 块引用相同的环境变量,并使用 shell 代码在它们之间更改其值,因为在第一阶段结束时环境变量的任何值都将用于在第二阶段执行时 两个 块。

状态和任务执行顺序

状态模块文件按照依赖顺序进行处理,这意味着任何模块都可以通过使用require来触发其他模块的处理。require如果请求的状态已经处理过,或者正在处理,则不会执行任何操作。(也就是说,在循环依赖的情况下,即使依赖尚未完成,完成循环的状态也会继续进行。imposer的未来版本可能会在这个情况下发出警告或终止。)

任务的工作方式略有不同。通常,任务按照定义顺序执行(这当然取决于提供定义的状态模块的执行顺序),步骤按照它们被添加到特定任务的顺序执行。(这意味着,如果你添加了十个没有步骤的任务,然后向第一个任务添加一个步骤,那么这个步骤将是第一个实际执行的。)

然而,步骤也可以暂停。这意味着,如果一个任务需要引用尚未存在的Wordpress对象,并且调用例如yield Imposer::ref('@wp-post', $xyz),而文章尚未存在,那么该步骤将被挂起,下一个步骤(或下一个任务的第一步)将运行,直到所有其他未暂停的步骤都有机会进行,或者直到目标文章创建。

请注意,这意味着如果你希望按特定顺序执行两个操作,它们必须是同一步骤的一部分,或者前面的步骤不能有yield。如果没有步骤暂停,那么所有步骤将按照原始定义顺序完全执行。但是,暂停的步骤可以在任何时候恢复,而其他步骤或任务在它们挂起期间可能已经运行。

这种方法的原理是,除非你在空数据库上施加状态,否则大多数状态需要引用的资源在大多数imposer运行中已经存在,所以请求宽恕(通过可能的yield暂停)比请求许可(即预先定义任务间的依赖关系)更容易。任务间的依赖关系可能会过度约束系统或导致意外的依赖循环,但动态暂停通常可以自行解决。

命令行界面

注意:imposer始终在包含imposer-project.mdwp-cli.yml和/或composer.json的当前目录或更高目录中操作。该目录被认为是你的项目根目录,所有相对路径都基于此。

(注意,imposer目前不支持在远程站点上操作:状态文件始终在本地机器上读取和执行,不能通过wp-cli远程执行。如果你需要远程运行命令,请使用类似ssh myserver bash -c 'cd /my/wp-instance; imposer apply'的方法。)

imposer apply [模块...] [wp-cli 选项...]

在将指定的状态模块加载并执行之前,构建JSON配置并累积PHP代码,然后将它们都交给wp eval来运行PHP代码,调用imposer的动作和过滤器,并执行定义的任务,在途中执行shell级别的事件钩子。输出是任务使用WP_CLI接口输出的内容。

--开头的第一个参数被认为是wp-cli全局选项,从该点开始的参数将直接传递给wp-cli,不再进一步解释。(可能有用的选项包括--[no-]color--quiet--debug[=<group>]--user=<id|login|email>--require=<path>。)

imposer options

imposer options子命令提供了各种子子命令,用于检查Wordpress选项表的内容,以及监视对它的更改。

大部分情况下,您只会在WordPress的开发实例上使用这些子命令,以发现和验证数据库中的哪些选项与插件配置UI的哪些部分相匹配。(这样您就可以确定在生产状态文件中应放置什么内容,或者验证您的状态文件是否正确设置了这些内容。)

默认情况下,选项更改是通过在$IMPOSER_CACHE/.options-snapshot中的git仓库进行监控的,但可以通过设置IMPOSER_OPTIONS_SNAPSHOT(例如,通过imposer env命令)或通过向imposer options传递--dir选项来覆盖此位置。(例如,imposer options --dir foo review将在当前项目根目录下的名为foo的git仓库中运行review命令。)

请注意,尽管快照目录是使用git管理的,但其内容不应被视为项目的一部分,也不应提交或推送到任何远程服务器,因为这些可能包含安全敏感数据。(此外,您可以在任何时候安全地删除(或imposer options reset)快照目录,因为除了从上次完全批准的review以来的选项更改知识之外,不会丢失任何内容。)

大多数imposer options子命令提供分页和彩色输出,除非它们的输出被管道或重定向到文件。JSON使用jq进行彩色显示,diff使用colordiff或(如果可用)pygments进行彩色显示,分页使用less -FRX进行。(您可以通过设置IMPOSER_COLORDIFF来覆盖diff彩色命令,通过设置IMPOSER_YAML_COLOR来覆盖yaml彩色命令,并通过设置IMPOSER_PAGER来覆盖分页命令。将它们设置为空将禁用相关的彩色和/或分页功能。)

由于许多插件和主题将频繁变化的状态信息存储在其选项中(如时间戳、计数器、撤销日志等),这可能会在您的选项列表、diff和审查中产生“噪音”。为了过滤掉这些选项,您可以在状态模块中添加exclude-optionsfilter-options调用,以排除相关插件的“噪音”选项,或者避免在快照目录中记录安全敏感数据。(有关更多信息,请参阅下面的选项过滤部分。)

imposer options review

交互式审查和批准自上次审查、上次擦除快照目录或强制将选项设置为当前值以来,所有非临时WordPress选项的YAML形式的更改。 (如果没有挂起的更改,该命令将等待,每10秒对数据库中的选项进行快照,直到检测到更改。)

审查过程具有与git add --patch相同的UI:您批准的任何更改都不会在未来的审查中显示,这样您可以形象地“签字”确认您理解或与您当前目标无关的更改。一旦您批准了更改,它将不会在未来的imposer options子命令(如reviewdiffwatch)运行期间显示。

(注意:在Alpine Linux上,默认的git包不包括git add --patch的UI。因此,如果您在Alpine环境中工作(例如,在一个docker容器中),您需要安装Alpine的git-perl包,以使options review正常工作。)

imposer options approve [name...]

批准指定选项的更改,而无需进行交互式审查。名称不得包含除字母、数字或_之外的任何字符;否则,它们需要在单引号内双引号中,例如'"some-option"',才能被正确处理。(这是因为名称实际上是在添加前缀.之后被解释为jq路径表达式。)

无论是添加、更新还是删除命名选项,更改都会立即得到批准,并且不会在 imposer optionsdiffreviewyaml 子命令中显示,除非这些选项再次被更改。

重置 imposer 选项

重置当前快照,隐式批准所有当前更改。此命令等同于运行审查并批准所有当前挂起的更改。

imposer options list [list-options...]

此命令输出所有非排除、非瞬时的 wordpress 选项的 JSON 映射,形式类似于 imposer 状态中 options 键下需要出现的格式。(您可以使用 wp option list 选项 --search=--exclude=--autoload= 限制输出到所需的选项子集。)

imposer options jq [jq-options...]

此命令将所有非排除、非瞬时的 Wordpress 选项的 JSON 映射传递给 jq,并将 jq-options 传递给 jq。如果发送到 TTY,则输出会着色和分页。

imposer options diff

imposer options list 的当前输出与最后一个批准的快照进行比较,以统一 diff 格式显示差异(可能着色和分页)。

imposer options yaml

imposer options list 的当前输出与最后一个批准的快照和当前项目规范进行比较,输出所需的最小 YAML,以更新或插入尚未批准且尚未在项目的当前状态中指定的任何新选项值,或需要更改规范中的值。如果发送到 TTY,则输出会分页和着色(如果已安装 pygments)。

注意:尽管此命令旨在使将选项更改“导出”到您的 imposer 状态模块更容易,但请注意,您不应盲目地将所有内容复制到状态模块:确保您 1)了解实际选项的功能,2)使用审查命令和筛选功能将 YAML 尽可能简化,3)在复制粘贴时进行选择。例如,强制使用看起来像 someplugin_versionsomeplugin_installed_date 的选项很少是好事,因为插件可能会使用这些值来驱动数据库更新或重新生成缓存文件。)

(imposer 价值的大部分来自将规范分组到逻辑功能中,而不仅仅是作为一个导入/导出功能。因此,如果您不加区分地复制选项,而不将选项分组到相关的功能中并记录为什么这些特定的值是可取的,那么您只是在移动维护问题,而不是真正解决它们。)

imposer options watch

每 10 秒清除屏幕并显示 imposer options diff 的第一屏输出。(使用 Control-C 退出。)

imposer env

imposer env 命令允许您读取和写入存储在项目目录中的 .imposer-env 文件中的环境变量。该文件采用 docker-compose 格式,这意味着忽略首尾空格,将引号视为字面字符,不识别转义序列。在文件中设置的变量在 imposer 运行时导出到操作系统环境,因此可以在 YAML 或 shell 块中进行插值,或由 PHP 代码读取以运行 imposer apply 的 PHP 部分。但是,它们不会影响 WordPress 运行时环境(除了 imposer 运行的 wp-cli 代码之外)。

imposer env set [+]VAR[=value]...

设置或取消一个或多个变量。如果变量名前面有一个 +,则只有在它尚未设置的情况下才会设置该变量。(换句话说,+ 表示“如果没有定义,则添加此值作为默认值”。)如果省略 = 和值,则取消设置并从文件中删除变量,允许操作系统环境的变量值(如果有的话)显示出来。

imposer env get VAR

如果文件中定义了指定的变量,则将其值写入标准输出。(否则,没有输出。)

imposer env parse [VAR...]

从文件中提取一个或多个变量定义,并以 VAR=value 格式输出,不进行任何转义。输出顺序基于文件中的顺序,而不是名称给出的顺序,跳过文件中未找到的变量。如果没有给出名称,则输出文件中的所有变量。

imposer env export [VAR...]

将指定的变量以 export VAR=value 格式输出(如果它们在文件中找到),在shell中安全执行时适合转义。输出顺序基于文件中的顺序,而不是名称给出的顺序,跳过文件中未找到的变量。如果没有给出名称,则输出文件中的所有变量。

诊断命令

注意:由于这些诊断命令实际上并未调用wp-cli,因此任何以 -- 开头的选项都将被忽略。然而,与 imposer apply 一样,第一个以 -- 开头的参数仍然终止模块列表。(这样可以轻松编辑复杂的命令行,以便将 apply 更改为 jsonphp 等,并使用相同的参数。)

imposer json [module...]

imposer apply 类似,但不是真正应用更改,而是将JSON规范写入标准输出以进行调试。将触发 all_modules_loaded shell事件,但不会触发 before_applyafter_apply 事件。(由于JSON最初就是这样创建的,所以状态中的任何jq和shell代码仍然会执行。)

如果输出是tty并且 less 可用,则输出将着色并分页。《code>IMPOSER_PAGER 可以设置为覆盖默认值 less -FRX;空字符串将禁用分页。

imposer php [module...]

imposer apply 类似,但不是真正应用更改,而是将累积的PHP代码输出到stdout。将触发 all_modules_loaded shell事件,但不会触发 before_applyafter_apply 事件。

如果输出是tty并且 pygmentizeless 可用,则输出将着色并分页。《code>IMPOSER_PAGER 可以设置为覆盖默认值 less -FRX,而 IMPOSER_PHP_COLOR 可以设置为覆盖默认值 pygmentize -f 256 -O style=igor -l php;将它们设置为空字符串将禁用它们。

imposer sources [module...]

jsonphp 命令类似,但输出所有源状态文件列表,每行一个文件,使用相对于项目根的路径。(这可以作为文件监视器(如 entr)的输入,监视文件更改并重新运行 imposer apply。)

如果输出是tty并且 $IMPOSER_PAGER 可用(默认为 less -FRX),则输出将分页。

输出包括读取(或缓存)的所有源文件,包括任何全局配置文件和 imposer-project.md(如果有的话)。如果状态文件在操作过程中读取非状态文件,则它应调用shell函数 mark-read 并传递一个或多个文件名。当运行此命令时,将输出这些命名的文件。

imposer tweaks

输出如果在运行 imposer apply 时写入 imposer-tweaks.php 的PHP。输出将着色并分页(如果可能)。

imposer tweaks cli

输出如果在运行 imposer apply 时写入 imposer-tweaks.cli.php 的PHP。输出将着色并分页(如果可能)。

imposer path

输出一个以:分隔的绝对路径列表,imposer将在当前查找状态。输出考虑了当前IMPOSER_PATH的值(如果有的话)。此值对于检查imposer是否正在搜索您认为的位置非常有用。

imposer默认路径

imposer path类似,但忽略当前IMPOSER_PATH的值。这有助于计算您可能想放入IMPOSER_PATH以加快启动时间的值。

规范模式

主题、插件和选项

主题切换

规范对象的theme属性指定了用于站点的主题的slug。如果它不同于数据库中的当前主题,imposer将切换主题,然后重新启动当前PHP进程和所有任务,以确保所有剩余任务都内存中有正确的函数、过滤器、缓存等。

theme: some-theme

插件激活

规范对象的plugins属性是一个将插件名称映射到其所需激活状态的对象。JSON truenull表示“激活”;false表示“停用”。如果任何插件的数据库激活状态与规范中的不同,imposer将激活或停用相关插件,然后重新启动当前PHP进程和所有任务,以确保所有剩余任务都内存中有正确的函数、过滤器、缓存等。

plugins:
   # Plugins with null or boolean `true` values will be activated
   this-plugin-will-be-activated: true
   so-will-this-one: yes
   me-too:

   # Plugins with boolean false value will be deactivated
   another-plugin: false

选项修补

规范的options属性是一个将WordPress选项名称映射到其所需内容的对象。如果选项在规范中是对象,而在数据库中是PHP数组或对象,则选项值在数据库中递归合并,因此规范不需要包含复杂选项值内容的所有内容。

options:
  blogname: My Blog
  blogdescription: Just another (Imposer-powered) WordPress site
  timezone_string: "\\(env.TZ)"
  gmt_offset: ""
  start_of_week: 0

使用的合并算法将JSON对象属性映射到现有选项中的PHP数组键或对象属性。但是,不合并不是JSON对象的选项值或子值:它们将被覆盖。这意味着如果选项值或子值在规范中是数组,则必须完全指定,以便在数据库中产生正确的选项值。

菜单和位置

菜单位于顶级规范属性menus下,作为菜单名称到菜单对象或菜单项数组的映射。如果菜单是对象,它可以有descriptionlocationitems属性。如果菜单是数组,则将其视为只有一个items属性的对象。

菜单作为整体对象进行同步:数据库中任何现有菜单项,列入规范中的菜单项将被删除。类似地,如果您为菜单指定了任何位置,则该菜单将从规范(对于相应主题)中未列出的任何菜单槽中删除。

menus:
  "Simple Menu, no Location":  # A menu w/five items: about, contact, posts, recipes, twitter
    - page: /about
    - page: /contact
    - archive: post
    - category: Recipes
    - url: https://twitter.com/pjeby  # custom link with title and _target
      title: Twitter
      target: _blank

  Menu with Properties:
    description: "Nothing really uses this, it's just an example"
    items:
      - page: /shop     # page with sub-items
        items:
          - page: /cart
          - page: /checkout
          - page: /my-account
      - page: /terms-and-conditions
        classes: popup

菜单项目的地

每个菜单项是一个对象,最多具有以下属性集之一,定义了项的类型和目的地

  • “自定义”菜单项具有url属性和title。结果菜单项将简单地是到给定URL的链接。
  • “页面/帖子”菜单项具有一个包含所需页面URL的page属性。结果菜单项将从命名页面或帖子获取默认标题。使用url_to_postid()查找页面或帖子,它必须存在。如果不存在,菜单导入将阻塞,直到所有产生@wp-posts资源的任务完成。
  • “存档”菜单项具有一个表示自定义帖子类型的字符串archive属性(使用其WordPress内部类型名称)。结果菜单项将是一个到该帖子类型存档的链接。
  • “术语”菜单项可以以三种方式之一创建
    • 具有term: {some_taxonomy: term_slug}属性的项将链接到some_taxonomy中的术语term_slug
    • 具有tag: tagname属性的项将链接到post_tag分类中的命名标签
    • 具有 category: categoryname 属性的项目将链接到该名称的分类。

菜单项数据

菜单项还可以包含以下任一或所有可选属性

  • items -- 一个菜单项对象的数组,将成为当前项的子项
  • title -- 显示在菜单项上的标题。对于“自定义”菜单项,此标题必须非空,否则菜单项将几乎不可见。(对于所有其他菜单项类型,Wordpress将根据目标生成默认标题。)
  • attr_title -- 菜单链接的 title 属性的值(浏览器通常会在悬停时显示为工具提示)
  • description -- 更详细的描述;某些主题可能在某些菜单位置显示此内容
  • classes -- 要添加到菜单项的 HTML 类的空格分隔列表。
  • xfn -- 链接目标的 XFN 链接关系
  • target -- 链接的 target HTML 属性,例如,使用 _blank 使菜单项在新的窗口中打开。
  • id -- 用于同步的标识符,应在特定菜单内是唯一的。如果没有给出,将根据菜单项的链接目标生成一个默认值。(id 仅用于在移动菜单项时最小化创建和删除菜单项的需要。)

菜单位置

如果存在菜单的 location 属性,它将被用来将菜单分配到您的网站上的一个或多个位置。如果位置是一个字符串,则菜单将被分配到当前主题中具有该名称的位置,并从所有其他位置中删除。如果位置是一个对象,其属性名被视为主题名,其值可以是字符串(命名特定主题中的特定位置)或数组(命名该主题中的多个位置)。如果位置是一个数组,则每个元素可以是一个字符串、对象或数组,方式相同。

menus:
  Simple Location, Current Theme:
    location: footer-menu   # Just one location, in whatever the current theme is

  Simple Location, Specific Theme:
    location: { someTheme: primary-menu }   # one location, specific theme

  Lots of Locations:
    location:
      - primary-menu              # location in the currently-active theme
      - aTheme: secondary-menu    # another location, in a specific theme
      - otherTheme:               # multiple locations in a third theme
        - slot-a
        - footer-right

当菜单指定了特定主题的位置(无论是否明确命名),它也将从该主题的所有其他位置中删除,以防止移动菜单到新位置时的重复。(记住:多个模块或 YAML/JSON 块可以为同一规范对象贡献属性,因此您始终可以在一个模块中定义菜单,并从另一个模块添加其位置。)

分类法术语

您可以使用 terms 最高级规范属性创建分类术语(例如分类、标签等),例如

terms:
  post_tag:
    - Some Tag
    - Another Tag
  category:
    Things:
      term_meta: { tax_position: 1 }  # sort-order used by simple-taxonomy-ordering plugin
      children:
        - Stuff
    Other:
      term_meta: { tax_position: 2 }

术语集、对象和父级

terms 属性是一个对象,将 Wordpress 内部分类法别名(例如,post_tag 用于标签,category 用于分类等)映射到 术语集。术语集是一个术语的数组或对象,术语本身可以是字符串或对象。一个术语的字符串相当于具有 name 属性的对象,所以这

terms:
  post_tag:
    - Some Tag
    - Another Tag

与这

terms:
  post_tag:
    - name: Some Tag
    - name: Another Tag

术语对象接受与 wp_insert_term$args 相同的属性,以及一个用于设置或删除术语元数据的 term_meta 属性,以及一个默认将包含父术语的 children 术语集。您可以显式地将 parent 设置为 0 以使术语成为根术语,并且可以使用术语别名而不是 ID 作为父级。并非所有分类法都允许父级,所以除非分类法是分层的,否则不要使用 parentchildren

术语对象的term_meta属性可用于设置或删除术语元数据:任何值为null的键将删除该元数据项,而其他键将设置为非空值。

术语名称和别名

当术语集是对象时(如前面的category示例),对象属性名称将作为别名或名称,具体取决于集合中术语的需求。例如,这

terms:
  category:
    foo: Foo
    Bar: { slug: bar }
    baz:
      name: This
      slug: that

相当于

terms:
  category:
    - name: Foo
      slug: foo
    - name: Bar
      slug: bar
    - name: This
      slug: that

换句话说,如果术语没有名称,则使用包围的属性名称作为名称,否则使用包围的属性作为别名。

所有这些考虑都适用于顶层术语集和指定为另一个术语的children:的术语。

注意:虽然别名是可选的,但在某些情况下它们是必需的,以避免不良影响。没有别名的术语将通过名称在Wordpress数据库中查找,所以如果你在指定中重命名术语而没有别名,将创建一个新的术语,而保留旧的术语。同样,无法创建或更新具有相同名称的多个术语(如Wordpress在层次分类法中允许的),除非每个术语都有一个不同的别名。如果你没有这些情况中的任何一种,则只需使用名称。

术语钩子

虽然imposer对每个术语支持的性质有限,但你可以使用imposer_termimposer_term_$taxonomy动作来扩展或修改它。为这些动作注册的钩子会收到一个具有额外属性和方法的对象数组,例如

  • set_meta($path, $value) -- 在$path处设置术语元数据为$value$path可以是元数据名称,也可以是数组,指定在设置值之前遍历(可能存在的)元数据的路径,类似于wp term meta patch insert命令。
  • delete_meta($path) -- 与set_meta()类似,除了执行等效的wp term meta patch delete

可以通过数组样式(例如$data['slug'])或对象样式(例如$data->slug)访问对象上的数据。(这不适用于对象的内容,它们是标量值或数组。

这些动作的钩子函数可以接收第二个参数,它是术语在分类法或其父项中的数组偏移量(如果术语是在对象中而不是数组中,则为null)。

例如,以下“PHP调整”与simple-taxonomy-ordering插件一起使用,强制将类别按在imposer指定中的顺序显示

add_action('imposer_term_category', function($term, $offset) {
    if ( isset($offset) ) $term->set_meta('tax_position', $offset);
    else $term->delete_meta('tax_position');
}, 10, 2);

上面的代码将在每个类别(如果它在数组中找到)中添加一个tax_position元字段,或者如果它在对象中找到,则删除该属性。

小部件和侧边栏

Wordpress通过由小部件类型和数字组成的ID来内部识别小部件。但这并不非常适合组合,因为不同的状态模块可能会无意中选择相同的数字。为了解决这个问题,imposer允许你为小部件提供符号名称,它可以是你想要的任何内容,并且与Wordpress的小部件ID没有固有的关系。例如,以下YAML块定义了两个命名的小部件并将它们放置在两个侧边栏中

widgets:
  llms_syllabus:
    title: "Contents"
    widget_type: course_syllabus

  llms_progress:
    title: "Your Progress"
    widget_type: course_progress

sidebars:
  llms_course_widgets_side:
    - llms_progress
    - llms_syllabus

  llms_lesson_widgets_side:
    - llms_progress
    - llms_syllabus

侧边栏在顶层指定属性sidebars下定义,它将侧边栏ID映射到小部件名称列表。

侧边栏中使用的任何小部件名称必须在顶级规范属性 widgets 下定义,作为一个从小部件名称(可以是任何字符串)到小部件应具有的属性的映射。widget_type 属性是必需的,它命名了相应小部件对象的内部 "id base"。例如,内置的日历小部件的 widget 类型为 calendar,存档小部件为 archives 等。

有时,小部件属性需要引用 WordPress 数据库对象,如帖子、菜单、术语等。但由于数据库 ID 不可移植,您可能需要在规范中使用其他标识符,然后使用这些其他标识符查找相关对象 ID(例如帖子 GUID、菜单名称等)

为了支持执行这些类型的转换,小部件属性在实际存储到数据库之前会通过两个过滤器

  • apply_filters("imposer_widget", $props, $name) -- 过滤任何小部件的属性,无论类型如何
  • apply_filters("imposer_widget_$type", $props, $name) -- 过滤类型为 $type 的小部件的属性。

这些过滤器可以执行任何必要的查找,并根据需要将备用标识符替换为数据库 ID。

(实现说明:如果您需要知道强加小部件的 WordPress ID,选项 imposer_widget_ids 是从强加小部件名称到 WordPress 小部件 ID 的映射,该映射在每次运行 . 时自动更新)

API

注意:imposer 任务读取的规范对象在每个强加运行时从零开始构建,并且仅包含在 该强加运行期间 加载的状态模块放入的值。它不包含任何现有的插件或选项设置等。如果您需要读取此类值的现有 WordPress 值,必须使用调用 WordPress API 的 PHP 代码。将规范对象视为待办事项列表或“我们希望在本运行中确保 WordPress 中是这样的事物列表”。

操作和过滤器

对于状态文件中的插件和 PHP 块,imposer 提供以下操作和过滤器(按执行顺序列出)

  • do_action("imposer_tasks") -- 插件或 WP-CLI 软件包直接向 imposer 注册任务、资源、模型、查找等的机会,而不是通过状态模块。
  • apply_filters("imposer_spec_$propName", $value, $spec) -- JSON 配置映射的每个顶级属性都会通过名为 imposer_spec_PROP 的过滤器传递,其中 PROP 是属性名称。例如,如果您想更改 imposer 将应用 optionsplugins,您可以向 imposer_spec_optionsimposer_spec_plugins 添加过滤器。第一个参数是键的值,第二个是当前的整体配置映射内容。
  • apply_filters("imposer_spec", $spec) -- 向此钩子添加一个过滤器以对跨越多个键的配置映射进行任何更改:前一个 imposer_spec_KEY 过滤器已经修改了各个键。

事件钩子

除了其 PHP 操作和过滤器外,Imposer 还为 shell 代码提供一套事件钩子。状态文件可以使用 bashup events API 注册 bash 函数,这些函数将在特定事件被触发时调用。例如

my_plugin.message() { echo "$@"; }
my_plugin.handle_json() { echo "The JSON configuration is:"; echo "$IMPOSER_JSON"; }

event on "after_module"                my_plugin.message "The current state module ($IMPOSER_MODULE) is finished loading."
event on "module loaded" @1            my_plugin.message "Just loaded a module called:"
event on "module loaded this/that"     my_plugin.message "Module 'this/that' has been loaded"
event on "persistent_modules_loaded"   my_plugin.message "The project configuration has been loaded."
event on "all_modules_loaded"          my_plugin.message "All modules have finished loading."
event on "before_apply"                my_plugin.handle_json
event on "after_apply"                 my_plugin.message "All PHP code has been run."
event on "block of css for mytheme" @4 my_plugin.message "Got CSS for mytheme:"

该系统与 WordPress 操作非常相似,但没有优先级系统,您通过在回调前添加一个 @ 和一个数字来指定函数需要多少 附加 参数。因此,上面的 module loaded 事件将向 my_plugin.message 传递最多一个参数,除了 "Just loaded a module called:" 之外(在这个例子中将是加载的状态模块的名称)。

此外,您可以在函数名之后放置参数,并且事件提供的任何参数都会添加到这些参数之后。重复注册没有效果,但如果函数具有不同的参数或不同的参数数量,您可以为同一事件注册相同的函数多次。

Imposer当前提供以下内置事件

  • block of language-tag -- 当状态模块执行一个标记为非imposer原生支持的标记语言(例如json,yaml等)的markdown块时发出。因此,一个css块将发出一个block of css事件,其他状态模块可以通过它来更新带有提供数据的规范。该事件传递四个参数:块内容、找到块的模块、模块的文件名(截至编译时间)以及该文件中块的行号。

  • after_module -- 在当前加载的状态模块(及其所有依赖项)加载完成后触发。(注意,“当前加载”的模块不一定与注册回调的模块相同,这意味着状态模块可以定义API,以在调用状态模块完成后运行回调。)

  • module loaded modulename sourcefile -- 当任何模块加载完成后发出。回调可以注册接收最多两个参数:模块的名称和从其加载的源文件路径。

  • module loaded modulename -- 一个类似Promise的事件,当指定的状态加载时解决。如果您在模块加载之前注册了一个回调,则如果以后加载了模块,它将被调用。但如果您在模块已经加载之后注册了一个回调,则回调将立即运行。这允许您包含“插件”代码或配置,这些代码或配置仅在加载了某些其他模块时才包含,例如。

    # If some other state loads "otherplugin/something", load our addon for it:
    event on "module loaded otherplugin/moduleX" require "my_plugin/addons/my-moduleX-addon"
  • persistent_modules_loaded -- 在全局和项目特定配置文件以及它们require的任何状态加载后触发。这是一个类似Promise的事件:即使它已经发生,您也可以注册它,并且您的回调将立即调用。

    此事件的目的让您能够禁用仅对持久(即项目定义的)状态可用的功能,而不从命令行添加的状态。

  • all_modules_loaded -- 在所有状态模块加载完成后触发,但在jq运行以生成配置JSON之前。您可以将其挂钩以添加其他数据或jq代码,以某种方式后处理您的配置。

  • before_apply -- jq运行后触发,具有只读变量$IMPOSER_JSON中的JSON配置。您可以将此事件挂钩以在运行任何PHP代码之前运行shell操作。

  • after_apply -- imposer的所有任务成功完成后触发,允许您在之后可选地运行额外的shell命令。

当然,就像Wordpress一样,您不受内置事件的限制!您可以创建自己的自定义事件,并通过event emitevent fire等触发它们。(有关更多信息,请参阅事件API文档。)

注意:如果您的状态文件需要运行将按某种方式更改系统状态的shell命令,您必须仅在这些命令在before_applyafter_apply事件期间运行,这样它们才只由imposer apply子命令运行,而不是由诊断命令(如imposer jsonimposer php)运行!

选项筛选

为了防止imposer options reviewimposer options diff包括您不想监控的选项(因为“噪音”或安全原因),或者将特定选项指定为JSON值,您可以使用以下API函数从项目或状态模块中的shell块中使用

  • exclude-options option-name... -- 从 imposer options 命令中排除一个或多个命名选项,例如:exclude-options _edd_table_check。您可以使用点路径排除选项的一部分,例如:使用 foo.timestamp 排除 foo 选项下的 timestamp 项。

    注意:如果选项名称(或其中一部分)包含除 A-Z、a-z、0-9 或 _ 之外的其他字符,您必须在名称中用双引号括起来,然后整个名称用单引号括起来,例如:

    exclude-options '"option-with-dashes"."key with spaces".ordinary_key'

    这将排除 option-with-dasheskey with spacesordinary_key 部分。

    exclude-options X Y Z 等同于 filter-options 'del(.X, .Y, .Z)'

  • filter-options jq-filter-expression -- 使用 jq-filter-expression 在显示或 imposer options 命令使用选项之前过滤选项。过滤表达式将接收一个 JSON 对象,其顶层键是选项,输出格式必须相同。

  • json-options option-name... -- 将指定的选项(或其中一部分)指定为数据库中的 JSON 字符串,但在规范中为嵌套对象。这使得您可以更轻松地处理像 wpassetcleanup_settings 这样的选项,它们存储为大型 JSON 字符串,作为单个大型字符串值来施加会很不方便(并且非模块化)。

    当此指令在状态模块中使用时,目标选项或子选项可以在状态模块中设置为任何与 JSON 兼容的值,并且将以相同的方式显示给所有 imposer options 子命令(例如:diffyamllistjqreview),但实际存储在数据库中的值将编码为 JSON 字符串。

    exclude-options 类似,如果选项名称的任何部分包含在 JQ 属性名称中无效的字符,则选项名称必须是单引号和双引号。

您可以排除或 JSON 化任意多个选项(或添加任意多个过滤器),来自任何状态模块。这使得您可以为特定主题或插件添加排除、过滤器或 JSON 化的选项或其中的一部分。

项目状态

目前,该项目仍在开发中,尚未达到 100% 的文档或测试覆盖率。有一个版本 1.0 的路线图跟踪这些和其他问题的状态。

性能注意事项

虽然 imposer 通常不是性能关键,但在开发过程中您可能会频繁运行它,因此在快速开发过程中运行时间的一秒或两秒会很快积累。

  • 过程中最慢的部分可能是输入 imposer apply!...所以尝试使用文件监视器,如 entrmodd,在文件更改时自动运行它。
  • 由于 Windows 平台的限制,在 Cygwin 下 bash 脚本如 imposer 运行得很慢。如果可能,请使用虚拟机、Docker 容器或 Windows 的 Linux 子系统以获得良好的性能。
  • 平均而言,Imposer 大部分执行时间用于在命令行中运行大型 php 程序(composerwp),因此启用 CLI opcache 会非常有帮助。
  • 目前,计算默认的 IMPOSER_PATH 很慢,因为它每次运行 wpcomposer 都多达三次。您可以通过提供显式的 IMPOSER_PATH,或至少单个目录(如 IMPOSER_PLUGINS)来大大加快此过程。 (您可以通过运行 imposer path 来查找 imposer 当前使用的目录,或通过运行 imposer default-path 来获取如果未设置 IMPOSER_PATH,imposer 会使用的目录。)
  • 默认情况下,状态文件的编译版本会缓存在项目根目录下的imposer/.cache。您可以通过设置IMPOSER_CACHE为期望的目录来更改此设置。(但是请注意,此目录必须对每个WordPress实例不同,否则可能会发生数据损坏。)
  • 在缓存禁用或缓存频繁清除的情况下,YAML块比JSON块编译得慢,因为每个未缓存的文件中的每个YAML块都会运行一个PHP脚本。如果您在一个文件中有许多YAML块,您可能希望将模块拆分成更小的部分(因为未更改的模块将被缓存),或者使用JSON块(因为没有转换开销)。
  • wp-cli命令通常启动较慢:如果您可以选择从shell块运行wp-cli,还是直接编写PHP代码,后者会快得多。