博客

🍾 Gato GraphQL 现已完成命名空间隔离,感谢 PHP-Scoper!

Leonardo Losoviz
作者:Leonardo Losoviz ·

插件 Gato GraphQL 现已完成命名空间隔离(scoped)。这意味着该插件终于可以上传到 WordPress 插件目录了。

业务交流

为此,我使用了出色的 PHP-Scoper。在 WordPress 中使用这个库并非一帆风顺,因此我将在这篇博文中介绍我是如何克服重重困难的。

章节目录:

决定进行命名空间隔离

几周前,Matt Mullenweg 宣布他将密切关注「那个 GraphQL 插件」,显然是指 WPGraphQL。他的措辞表明他认为只有一个 GraphQL 插件,而实际上有两个(被忽略的那个,嗯,就是我的)。这让我意识到我的插件知名度有多低,心里很不是滋味。

Matt 不知道我的插件存在。WordPress 社区中的大多数人也不知道。显然我的宣传做得远远不够。我知道自己在营销和社交媒体方面很弱;技术上还算过得去(至少我自认为如此)。于是我决定在力所能及的范围内采取一些行动。

我正在做的事情如下:

  • 我刚完成了这个网站 gatographql.com 的开发,并在 2 周前正式上线(太棒了!🥳 顺便问一下,你觉得怎么样?欢迎通过 DM邮件 给我反馈)
  • 3 天前我终于开始对插件进行命名空间隔离,昨天完成了这项工作!(凌晨 3 点,但一切都值得 😅)
  • 最后,我已经在开发即将发布的 0.8 版本,这将是首个可在插件仓库中获取的版本

对插件进行命名空间隔离是上传到仓库的必要条件,因为否则它可能与另一个插件产生冲突——该插件依赖与我的插件相同的依赖库,但版本不同。完成这件事是一个真正重大的里程碑,没有任何其他开发工作比这更重要。例如,我仍然需要完善 GraphQL schema 以完全匹配 WordPress 数据模型,但这将在每个新版本中稳步推进。

几周后,当人们搜索「GraphQL」时,这个插件就会出现,真正需要实现 GraphQL API 的人将会了解到我的插件的存在。

确实,我希望我的插件在 WordPress 的未来中得到认真考量。我为此工作了好几年。代码仓库早在 2016 年 8 月就创建了,那甚至是在 WPGraphQL 诞生之前,也是 GraphQL 的起步阶段。但我当时并不知道这个项目会成为一个 GraphQL 服务器;大约 1.5 年前它才转向了这个方向。

(这个项目实际上是一个使用服务端组件构建应用程序的框架,用这种架构完全可以构建一个 GraphQL 服务器。所以我就这么做了。)

WPGraphQL 是一个成熟的插件,这是理所应当的:它创建于几年前,围绕它形成了一个社区。Jason Bahl(受雇于 Gatsby)以及项目贡献者们的工作十分出色:将 WordPress 集成到 Jamstack 从未如此便捷。

但 Gatsby 和 Jamstack 是一回事,WordPress 是另一回事。WordPress 占据了互联网的 40%,而不仅仅是静态网站生成器的输入来源。

因此,我们现在可以考量 WPGraphQL 是否是正确的选择,而不必因缺乏替代品而被迫接受这一决定。我们可以分析两款插件,看看哪一款的目标更符合 WordPress 的核心价值。

Gato GraphQL 也可以与 Jamstack 协同工作。但我认为它的主要目标更为宏大:「让数据发布民主化」,使编辑 API 变得像编辑一篇文章一样简单(人人皆可做到),并让 WordPress 成为互联网的操作系统。

一旦插件在仓库中可用,我希望更多人会试用它,并说「哇,这也太厉害了吧!我之前怎么会不知道这个东西?」。

到那时,「GraphQL 插件」的选择将不再是预先确定的,WordPress 社区可以根据各自的优缺点来权衡 WPGraphQL 和 Gato GraphQL。

好了,动机讲完了,让我们来聊聊技术层面的事 🤓。

检视可用方案

对插件进行命名空间隔离需要运行某些工具,这些工具以插件代码作为输入,输出隔离后的插件。没什么大不了的,对吧?能有多难?

技术讨论

实际上,根据代码库的不同,仅仅执行隔离命令往往是不够的。之后,我们需要检查控制台中的错误,修复它们,彻底测试应用程序,识别错误及其原因,修复,再循环。要做到完全正确,可能需要花费相当多的时间。

有两个用于命名空间隔离的库,它们的目标各不相同:

  • Mozart,专为 WordPress 代码设计
  • PHP-Scoper,适用于任何 PHP 代码,特别是生成 PHAR 的场景

因为我有一个 WordPress 插件,我首先尝试了 Mozart。让我们来看看结果如何。

尝试 Mozart,以失败告终

大约 1 年前我尝试了 Mozart。根据文档所说,「mozart compose 命令会完成所有魔法」。所以我期望一切会非常快速简单,然后就可以去享受一杯代基里酒(daiquiri)了。

然而,Mozart 在我的代码库上从未正常运行。它不断遇到问题,因此命名空间隔离始终无法完成。我也得不到所需的帮助:我提交了一个 PR,但它没有被考虑合并,而且我甚至没有收到任何通知,所以我一直等待,直到自然而然地对这个项目失去了兴趣。

我认为 Mozart 无法处理我插件中的某些依赖关系。我使用了 Symfony 的几个组件,包括 DependencyInjectionCacheDotenv,所有这些都通过 Composer 管理。

PHP 的命名空间隔离不仅仅是 PHP 的问题,因此隔离工具面临许多需要回避的障碍和需要解决的挑战。例如,Symfony DependencyInjection 使用 YAML 文件进行配置,这些文件也需要被隔离。而且 composer.json 文件包含 PSR-4 自动加载的配置,这也需要被隔离。我认为,Mozart 无法妥善处理这些复杂性。

但我相信我的经历不是唯一的,外面也有许多满意的用户。而且,我的失败尝试发生在 1 年前,也许这个工具此后已经有所改进。另外,别忘了这句话:「所有被隔离的插件都是相似的;每个未被隔离的插件都以自己独特的方式未被隔离」,所以也许这只是我的问题。

如果您的 WordPress 插件比较简单,逻辑自包含,并且隔离只需在 PHP 代码内部进行,那么 Mozart 很可能会有效。您只需亲自尝试一下。

研究 PHP-Scoper,随即陷入恐慌

于是我转向了 PHP-Scoper。然而,我甚至没有尝试去试用它,因为它立刻让我感到害怕。

首先,这个工具并不原生支持 WordPress。其次,他们建议查看他们自己的 Makefile,内容大致如下:

# See https://tech.davis-hansson.com/p/make/
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
 
.DEFAULT_GOAL := help
 
PHPBIN=php
PHPNOGC=php -d zend.enable_gc=0
IS_PHP8=$(shell php -r "echo version_compare(PHP_VERSION, '8.0.0', '>=') ? 'true' : 'false';")
 
SRC_FILES=$(shell find bin/ src/ -type f)
 
.PHONY: help
help:
	@echo "\033[33mUsage:\033[0m\n  make TARGET\n\n\033[32m#\n# Commands\n#---------------------------------------------------------------------------\033[0m\n"
	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | awk 'BEGIN {FS = ":"}; {printf "\033[33m%s:\033[0m%s\n", $$1, $$2}'
 
 
#
# Build
#---------------------------------------------------------------------------
 
.PHONY: clean
clean:	 ## Clean all created artifacts
clean:
	git clean --exclude=.idea/ -ffdx
 
update-root-version: ## Check the lastest GitHub release and update COMPOSER_ROOT_VERSION accordingly
update-root-version:
	rm .composer-root-version || true
	$(MAKE) .composer-root-version

还有 600 多行,全都是这种内容。看起来像是一道谜题。以为仅仅为了隔离我的插件就需要理解那些代码,我就落荒而逃了。

(好吧,理解那些代码是他们推荐的测试隔离后应用程序的方式,但并非必须。我们也可以直接运行 php-scoper add-prefix 命令,让它完成所有魔法,然后去喝我们的代基里酒。)

重返 PHP-Scoper,这次是认真的

于是,3 天前,我下定决心要以某种方式实现命名空间隔离。我必须让它成功。

我重新回到 PHP-Scoper,认真地去尝试它。通过阅读 PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies(来自 Delicious Brains 的才华横溢的团队),我知道可以用它来隔离 WordPress。这只是态度和坚持的问题。

我探索了一些现有的解决方案,包括:

但对我来说,这些方案都不够令人满意:要么代码看起来很hack,要么很脆弱,随时可能在某个时刻崩溃。

例如,Google Web Stories 插件对代码进行隔离,然后逐一撤销每一处冲突:

return [
  'patchers'                   => [
		function ( $file_path, $prefix, $contents ) {
			/*
			 * There is currently no easy way to simply whitelist all global WordPress functions.
			 *
			 * This list here is a manual attempt after scanning through the AMP plugin, which means
			 * it needs to be maintained and kept in sync with any changes to the dependency.
			 *
			 * As long as there's no built-in solution in PHP-Scoper for this, an alternative could be
			 * to generate a list based on php-stubs/wordpress-stubs. devowlio/wp-react-starter/ seems
			 * to be doing just this successfully.
			 *
			 * @see https://github.com/humbug/php-scoper/issues/303
			 * @see https://github.com/php-stubs/wordpress-stubs
			 * @see https://github.com/devowlio/wp-react-starter/
			 */
			$contents = str_replace( "\\$prefix\\_doing_it_wrong", '\\_doing_it_wrong', $contents );
			$contents = str_replace( "\\$prefix\\__", '\\__', $contents );
			$contents = str_replace( "\\$prefix\\esc_html_e", '\\esc_html_e', $contents );
			$contents = str_replace( "\\$prefix\\esc_html", '\\esc_html', $contents );
			$contents = str_replace( "\\$prefix\\esc_attr", '\\esc_attr', $contents );
			$contents = str_replace( "\\$prefix\\esc_url", '\\esc_url', $contents );
      $contents = str_replace( "\\$prefix\\do_action", '\\do_action', $contents );
      // ...
    }
  ]
]

我理解他们为什么这么做,但我并不喜欢。每当新的 WordPress 函数被引用时,他们都需要确保将其加入这个列表。太手动化了,太脆弱了。

这就是我面临的挑战:难道没有更简单的方式来隔离插件,并且依赖那种我们可以毫无愧色地展示给朋友和同事的代码吗?

PHP-Scoper 的简便用法 😎

事实证明,比我想象的要简单得多!仅仅几个小时,一切就都运行起来了。

几小时内完成隔离

当然,当我说「简单」和「几小时」时,我实际上的意思是:一切都立即运行起来了,但那是在花费了 2 个月时间为代码库创建适当结构之后(稍后我会详细解释)。

但重要的是:如果您为项目做好了正确的准备,命名空间隔离可以在很短的时间内完成。

隔离 WordPress 代码的问题,说白了就是 WordPress 代码本身。问题在这里有详细解释,但归根结底是所有 WordPress 函数和类也会被加上命名空间前缀。因此,如果我们在代码中引用 WP_Query 或调用 get_posts,它们将被转换为 MyPrefixedNamespace\WP_QueryMyPrefixedNamespace\get_posts,在运行时造成灾难性的失败。而这在 PHP-Scoper 中无法在不使用 hack 的情况下避免。

那么,解决方案是什么?很简单:在将要被隔离的代码库中,不要引用 WP_Query,不要调用 get_posts,不要使用任何 WordPress 代码。

我疯了?

不,我没疯,我相信您也没疯。是的,我知道我们正在构建一个 WordPress 插件……让我来解释。

我们如何做到不包含 WordPress 代码?通过将代码库拆分为两组包:

  • 包含 WordPress 代码的部分,不引用任何外部库的代码
  • 包含业务逻辑的部分,不包含任何 WordPress 代码,并且包含所有必要的依赖关系及对其代码的引用

这样,我们就不再拥有单一的代码库,而是拥有多个代码库(或包),其中一些会被隔离,一些不会,它们通过 Composer 绑定在一起形成插件。

然后,我们不对包含 WordPress 代码的包进行隔离,从而避免冲突。这样可行,因为它不引用任何属于外部依赖库的代码。所有引用都是内部的,例如 MyNamespace\MyPlugin\MyClass。但这些不需要隔离,因为我们可以安全地假设 WordPress 站点中只会安装一个版本的插件,并且我们可以将自己的命名空间 MyNamespace\* 加入白名单。

此外,如果我们的插件是可扩展的,那么将自己的命名空间加入白名单是必须的。例如,Gato GraphQL 的字段解析器(field resolver)通过继承类 PoP\ComponentModel\FieldResolvers\AbstractFieldResolver 来实现。如果我对它进行了隔离,开发者在开发时就必须引用 PoP\ComponentModel\FieldResolvers\AbstractFieldResolver,而在生产环境中则必须引用 PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver。这是不可接受的。

因此,我们只对包含所有外部库引用但不包含 WordPress 代码的业务逻辑包进行隔离。

总结一下,我们将这个策略:

「拥有单一代码库,对其进行隔离,然后痛苦地、耐心地撤销损坏,同时祈祷没有冲突被遗漏,在生产环境中 💣 爆炸」

切换为:

「将代码库拆分为两组,只对包含外部依赖引用且不包含 WordPress 代码的那组进行隔离,然后去喝您应得的代基里酒 🍹」

让我看看真实代码

是时候打开香肠,看看里面是否有真正的肉了 🌭。

4 天前,我的插件中有如下代码

namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
 
use Parsedown;
 
class MarkdownContentParser
{
  protected function getHTMLContent(string $fileContent): string
  {
    return (new Parsedown())->text($markdownContent);
  }
}

Parsedown 来自外部依赖库 erusev/parsedown,如插件的 composer.json 中所定义:

{
  "require": {
    "erusev/parsedown": "^1.7"
  }
}

因此,我的插件包含了对外部库的引用,所以我需要对其进行隔离,将 Parsedown 转换为 PrefixedByPoP\Parsedown。但这样做也会隔离插件中的所有 WordPress 代码,造成冲突。

于是我将代码提取到一个单独的包中,名为 graphql-api/markdown-convertor,并将 composer.json 中的第三方依赖替换为我自己的依赖

{
  "require": {
    "graphql-api/markdown-convertor": "^0.8"
  }
}

现在,插件不再直接引用外部库,而是引用新包中的服务 MarkdownConvertorInterface

namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
 
use GraphQLAPI\MarkdownConvertor\MarkdownConvertorInterface;
 
class MarkdownContentParser extends AbstractContentParser
{
    protected MarkdownConvertorInterface $markdownConvertorInterface;
 
    function __construct(MarkdownConvertorInterface $markdownConvertorInterface)
    {
        $this->markdownConvertorInterface = $markdownConvertorInterface;
    }
 
    protected function getHTMLContent(string $fileContent): string
    {
        return $this->markdownConvertorInterface->convertMarkdownToHTML($fileContent);
    }
}

对第三方依赖的引用在新包中完成

namespace GraphQLAPI\MarkdownConvertor;
 
use Parsedown;
 
class MarkdownConvertor implements MarkdownConvertorInterface
{
  public function convertMarkdownToHTML(string $markdownContent): string
  {
    return (new Parsedown())->text($markdownContent);
  }
}

最后,我们必须:

  • 对依赖 graphql-api/markdown-convertor 进行隔离
  • 跳过对插件代码的隔离
  • 将命名空间 GraphQLAPI\* 加入白名单,以避免我自己的类被隔离

这基本上就是整体策略。从现在起,将重复同样的思路,从代码中移除所有外部依赖,直到最终插件可以被隔离。

需要提取的依赖仅限于 composer.json 文件 require 部分中的那些;require-dev 中的依赖无论是否为外部依赖都可以保留,因为用于开发的依赖不需要隔离;只有用于创建和发布插件(即生产环境使用的)依赖才需要隔离。

最终,您插件的 composer.json 不应包含任何外部依赖。我的插件看起来是这样的

{
  "require": {
    "php": "^7.4|^8.0",
    "getpop/engine-wp": "^0.8",
    "graphql-api/markdown-convertor": "^0.8",
    "graphql-by-pop/graphql-clients-for-wp": "^0.8",
    "graphql-by-pop/graphql-endpoint-for-wp": "^0.8",
    "graphql-by-pop/graphql-server": "^0.8",
    "pop-schema/basic-directives": "^0.8",
    "pop-schema/comment-mutations-wp": "^0.8",
    "pop-schema/commentmeta-wp": "^0.8",
    "pop-schema/comments-wp": "^0.8",
    "pop-schema/custompost-mutations-wp": "^0.8",
    "pop-schema/custompostmedia-mutations-wp": "^0.8",
    "pop-schema/custompostmedia-wp": "^0.8",
    "pop-schema/custompostmeta-wp": "^0.8",
    "pop-schema/generic-customposts": "^0.8",
    "pop-schema/media-wp": "^0.8",
    "pop-schema/pages-wp": "^0.8",
    "pop-schema/post-mutations": "^0.8",
    "pop-schema/post-tags-wp": "^0.8",
    "pop-schema/posts-wp": "^0.8",
    "pop-schema/taxonomymeta-wp": "^0.8",
    "pop-schema/taxonomyquery-wp": "^0.8",
    "pop-schema/user-roles-access-control": "^0.8",
    "pop-schema/user-roles-wp": "^0.8",
    "pop-schema/user-state-mutations-wp": "^0.8",
    "pop-schema/user-state-wp": "^0.8",
    "pop-schema/usermeta-wp": "^0.8",
    "pop-schema/users-wp": "^0.8"
  }
}

所有这些包都是我自己的,命名空间分别为 getpopgraphql-apigraphql-by-poppop-schema:它们是包含插件全部代码的依赖。它们被分散到不同的命名空间以便更好地管理代码,但您不必这么做:使用单一命名空间同样可以正常工作。

随着应用程序中的包数量不断增加,您需要将所有包托管在一个 monorepo 中,否则在创建涉及多个包的 pull request 时会抓狂(相信我,我经历过)。就我而言,所有包都托管在 GatoGraphQL/GatoGraphQL monorepo 中,我通过出色的 Monorepo Builder 保持同步(我需要写一篇关于这个工具的文章,它真是救星!)。

这些包的命名空间是 PoPGraphQLAPIGraphQLByPoPPoPSchema。因为它们是我自己的,我知道它们在应用程序中只会出现一次,所以可以避免对它们进行隔离。

为此,我scoper.inc.php 中将它们加入白名单

return [
  'whitelist' => [
    // Own namespaces
    'PoPSchema\*',
    'PoP\*',
    'GraphQLByPoP\*',
    'GraphQLAPI\*',
    // Own container cache
    'PoPContainer\*',
  ],
];

最后一个条目对应依赖注入容器,它也需要被隔离。默认情况下,该容器被直接分配在全局命名空间下,名称为 ProjectServiceContainer。但 PHP-Scoper 不支持对全局命名空间中的特定类进行白名单处理。因此,我在白名单中添加了人工命名空间 PoPContainer,并在将容器转储到磁盘时分配了这个命名空间

$dumper = new PhpDumper($containerBuilder);
file_put_contents(
  self::$cacheFile,
  $dumper->dump(
    // Save under own namespace to avoid conflicts
    array('namespace' => 'PoPContainer')
  )
);

您可能会注意到,关于这些包,有些以 -wp 结尾(如 pop-schema/users-wp),有些则没有(如 graphql-by-pop/graphql-server)。没错,您猜对了:前者包含 WordPress 代码但不引用外部库,后者可能包含对外部库的引用,但绝对不包含任何 WordPress 代码。

然后,我跳过对 WordPress 包的隔离

return [
  'finders' => [
    // Scope packages under vendor/, excluding local WordPress packages
    Finder::create()
      ->files()
      ->notPath([
        // Exclude libraries ending in "-wp"
        '#getpop/[a-zA-Z0-9_-]*-wp/#',
        '#pop-schema/[a-zA-Z0-9_-]*-wp/#',
        '#graphql-by-pop/[a-zA-Z0-9_-]*-wp/#',
      ])
      ->in('vendor')
  ]
];

如果某个 WordPress 包需要引用外部库,而这个引用又无法提取到另一个包中,该怎么办?例如,我的包 getpop/routing-wp 依赖于 brain/cortex,而这是无法回避的

我无法隔离整个包,因为 getpop/routing-wp 包含 WordPress 代码。作为替代,我会找出这些引用所在的文件,并确保这些文件不包含任何 WordPress 代码。这样就可以只对这些文件进行隔离。

在这种情况下,对 Cortex/Brain 的引用在 2 个文件中进行,包括 layers/Engine/packages/routing-wp/src/Hooks/SetupCortexHookSet.php

namespace PoP\RoutingWP\Hooks;
 
use PoP\Hooks\AbstractHookSet;
use Brain\Cortex\Route\RouteCollectionInterface;
use Brain\Cortex\Route\RouteInterface;
use Brain\Cortex\Route\QueryRoute;
use PoP\RoutingWP\WPQueries;
use PoP\Routing\Facades\RoutingManagerFacade;
 
class SetupCortexHookSet extends AbstractHookSet
{
  protected function init()
  {
    $this->hooksAPI->addAction(
      'cortex.routes',
      [$this, 'setupCortex'],
      1
    );
  }
 
  /**
   * @param RouteCollectionInterface<RouteInterface> $routes
   */
  public function setupCortex(RouteCollectionInterface $routes): void
  {
    $routingManager = RoutingManagerFacade::getInstance();
    foreach ($routingManager->getRoutes() as $route) {
      $routes->addRoute(new QueryRoute(
        $route,
        function (array $matches) {
          return WPQueries::STANDARD_NATURE;
        }
      ));
    }
  }
}

您注意到这里的奇特之处了吗?这是一个 hook 的实现,但没有调用 add_action,因为我不能在这里使用任何 WordPress 代码。取而代之的是调用服务 HooksAPIInterface 的函数 addAction,而这个服务由包 getpop/hooks-wp 中的HooksAPI 实现,在那里我们可以使用 WordPress 代码:

namespace PoP\HooksWP;
 
use PoP\Hooks\HooksAPIInterface;
 
class HooksAPI implements HooksAPIInterface
{
  public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    add_action($tag, $function_to_add, $priority, $accepted_args);
  }
}

现在代码已经整洁地拆分,我们可以对引用外部依赖的这 2 个文件进行隔离

return [
  'finders' => [
    Finder::create()->append([
      'vendor/getpop/routing-wp/src/Component.php',
      'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
    ])
  ]
];

我之前提到,设置隔离只花了几个小时,但那是在 2 个月的工作之后。这个例子正好说明了我的意思:真正的工作在于将代码库整洁地拆分为这两组。

就我的情况而言,工作花了 2 个月,是因为细化程度极为严苛:插件最终由 125 个包组成!但这是一个特殊情况,目标是让插件的底层服务器与 CMS 无关,以便只需重新实现对应的 -wp 包就能支持其他 CMS/框架的实现。

(我在文章 Abstracting WordPress Code To Reuse With Other CMSs: ConceptsImplementation 中详细介绍了这一策略。)

这确实是相当大的工作量,但代码整洁度的提升让一切都值得。而且不仅仅是对插件进行命名空间隔离(这对我来说完全是一个惊喜,我对这份意外的喜悦至今仍心怀雀跃)。例如,我可以在 WordPress 和非 WordPress 代码上分别运行 PHPStan 和 PHPUnit,避免了许多令人头疼的问题。

一旦代码库被整理好,世界突然间就变成了一个更美好的地方。

测试

那么,我们如何测试这个庞然大物呢?

我想到的解决方案是依赖 Rector,这是我同样用于将 PHP 代码从开发用的 PHP 7.4 降级为生产用的 7.1 的工具。

思路如下:

  1. 对插件进行隔离
  2. 用 Rector 分析它,应用任意规则(用哪条规则无关紧要)

如果隔离过程中出了什么问题,Rector 将无法加载某些类,并会抛出错误。例如,如果类 Brain\Cortex 被隔离为 PrefixedByPoP\Brain\Cortex,但某些对它的引用仍然保留为 Brain\Cortex,那么自动加载这个类就会失败。

这是我用于测试的 GitHub Action(使用 working-directory 是因为我从 monorepo 的根目录进行操作,但隔离发生在插件文件夹中):

name: Scope Gato GraphQL tests
on:
  push:
    branches:
      - master
  pull_request: null
 
env:
  COMPOSER_ROOT_VERSION: "dev-master"
 
jobs:
  main:
    defaults:
      run:
        working-directory: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp
 
    name: Scope the plugin code via PHP-Scoper, and execute tests
 
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
 
      - name: Set-up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 7.4
          coverage: none
        env:
          COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Install root dependencies
        uses: "ramsey/composer-install@v1"
 
      - name: Install plugin dependencies for PROD
        run: composer install --no-dev --no-progress --no-interaction --ansi
 
      - name: Install PHP-Scoper
        run: |
          composer global config minimum-stability dev
          composer global config prefer-stable true
          composer global require humbug/php-scoper
 
      # The scoped results correspond to vendor/, so must generate them in such folder
      - name: Scope plugin into separate folder
        run: php-scoper add-prefix --output-dir ../../../../build-prefixed/vendor --ansi
 
      - name: Copy scoped code back into plugin
        run: rsync -av build-prefixed/ layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/ --quiet
        working-directory: .
 
      - name: Regenerate autoloader
        run: composer dumpautoload --optimize --classmap-authoritative --ansi
 
      - name: Run Rector on the scoped code
        run: vendor/bin/rector process --config=layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/rector-test-scoping.php --ansi
        working-directory: .
 

这是我的 Rector 配置

use Rector\CodeQuality\Rector\LogicalAnd\AndAssignsToSeparateLinesRector;
use Rector\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
 
return static function (ContainerConfigurator $containerConfigurator): void {
  $services = $containerConfigurator->services();
  $services->set(AndAssignsToSeparateLinesRector::class);
  $parameters->set(Option::AUTO_IMPORT_NAMES, true);
 
  $parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/vendor/scoper-autoload.php',
    __DIR__ . '/vendor/erusev/parsedown/Parsedown.php',
    __DIR__ . '/vendor/jrfnl/php-cast-to-type/cast-to-type.php',
    __DIR__ . '/vendor/jrfnl/php-cast-to-type/class.cast-to-type.php',
  ]);
 
  // files to rector
  $parameters->set(Option::PATHS, [
    __DIR__ . '/vendor',
  ]);
 
  // files to skip
  $parameters->set(Option::SKIP, [
    // Exclude tests
    '*/tests/*',
    __DIR__ . '/vendor/nikic/fast-route/test/*',
    __DIR__ . '/vendor/psr/log/Psr/Log/Test/*',
    __DIR__ . '/vendor/symfony/service-contracts/Test/*',
  ]);
};

您可能会注意到,某些依赖文件(如 erusev/parsedown/Parsedown.php')需要添加到 Option::AUTOLOAD_PATHS。这是因为对包的 composer.json 进行隔离并不是 100% 可靠的,其自动加载可能会失败。

每当发生这种情况时,Rector 会提示某个类的自动加载失败。由此我们找到对应的文件,并手动将其添加到自动加载路径中。

查看结果

这是插件的源代码这是其经过隔离(并降级到 PHP 7.1)的版本

找找 7 处不同吧 😁。(给您一个提示:搜索 PrefixedByPoP。)

这是最终的 graphql-api.zip 插件文件,可以直接安装到您的站点上。

就这些了。希望这对您有所帮助 😃💪🚀


订阅我们的新闻通讯

及时了解 Gato GraphQL 的所有更新。