使用 Composer 安装代码质量工具

大多数 PHP 如今,超文本预处理器(PHP)项目使用 Composer 是有道理的。在 Composer 于 2012 年进入 PHP 世界之前,更新所有外部依赖项(即来自其它开发人员的代码)需要大量的手动工作。所需的文件必须从相应的网站下载,并添加到项目的正确文件夹中。自动加载(即根据类名自动解析文件路径)即使可用,也不是标准化的。因此,通常需要使用 require()require_once() 主动导入所需的类。如果软件包版本之间存在冲突,则必须自行解决。

Composer 通过解决这些问题大大简化了这些工作。它引入了一个名为 Packagist( https://packagist.org )的中央存储库,所有可用的软件包都存放在这里。此外,它还通过引入版本约束来解决版本问题。这些规则告诉 Composer 某个软件包支持其它软件包的哪些版本,从而使 Composer 能够经常自动解析正确的版本。另一项突破性功能是支持已安装软件包的自动加载。现在,我们通常只使用 require() 来导入 Composer 的自动加载器。

所有这些特性都帮助 PHP 与其它网络语言(如 Python 或 Ruby)竞争,如果没有这些特性,PHP 可能不会像今天这样成为 万维网(WWW) 上使用最广泛的语言。因此,我们希望在本书中给予 Composer 应有的篇幅。在本节中,我们将向你介绍最常用的安装方法。此外,我们还将介绍在项目中使用 Composer 的另一种鲜为人知的方法。

使用 require-dev 安装代码质量工具

在前几章中,我们已经使用 Composer 安装了很多工具,所以现在你应该已经熟悉了最常见的使用情况:为项目添加依赖项。依赖项是由其它开发人员编写的代码包,可以快速集成到你的项目中。

概括地说,这是使用 require 关键字和包名来完成的。例如,如果你想添加 PhpMetrics,可以运行以下命令:

$ composer require phpmetrics/phpmetrics --dev

通常情况下,软件包由开发者的名称(即所谓的 vendor)和用斜线分隔的软件包名称来标识。在上例中,供应商和软件包名称是一致的,但情况并非总是如此。

现在让我们详细了解一下 --dev 选项。当我们使用该选项运行 composer require 命令时,Composer 会将软件包添加到 composer. 在这里,你可以看到一个典型的 composer.json 文件节选:

{
    "name": "vendor/package",
    ...
    "require": {
        "doctrine/dbal": "^2.10",
        "monolog/monolog": "^2.2",
        ...
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5",
        "phpmetrics/phpmetrics": "^2.8",
        ...
    },
    ...
}

require-dev 部分背后的理念是,该部分中的所有软件包都不是在生产环境中运行应用程序所必需的。在本地环境中或在构建过程中,你肯定需要 PHPUnit 和我们所有的代码质量工具;但在生产环境中,就不再需要它们了。

事实上,你应该努力在生产中使用尽可能少的软件包。这主要有以下两个原因:

  1. 你添加的每个软件包都将包含在 Composer 的自动加载机制中,这将降低每次请求的性能。在内部,Composer 构建了一个所谓的 classmap,它是一个简单的数组,可将类名映射到相应的文件位置。如果你对此感到好奇,请查看 vendor/composer/autoload_classmap.php 文件。根据项目使用的软件包数量,该文件可能会变得非常庞大,从而拖慢应用程序的运行速度。

  2. 每一个额外的软件包都可能带来安全问题。代码越少,攻击向量就越少。

默认情况下,Composer 会安装所有依赖包。因此,在生产构建时,请确保使用 --no-dev 选项运行 Composer,以避免安装 require-dev 中的软件包。不过,在本地环境中,此时你无需担心其它任何问题。

前面介绍的安装方法是你会遇到最多的方法,原因很简单:它不需要任何额外的工具,而且在生产环境中安装时只需使用一个额外的选项。这使它成为一个完美的起点,通常完全可以满足小型项目的需要。另一种值得了解的方法是 Composer 的全局安装,我们将在下一节讨论。

全局安装

如果你在本地系统上同时处理多个项目,可以选择全局安装 Composer 和软件包,这意味着它们不会安装在任何项目根目录下,因此也不会添加到任何 composer.json 文件中。相反,Composer 和软件包都会安装在一个文件夹中,通常是 ~/.composer。在这个文件夹中,你可以找到另一个 composer.json 文件,它记录了全局安装的软件包,以及另一个安装了供应商代码的文件夹。

全局安装软件包只需添加全局修饰符,就像这样:

$ composer global require phpmetrics/phpmetrics

同样,更新所有全局软件包也毫不费力,如图所示:

$ composer global update

全局安装后,PHP Coding Standards Fixer(PHP-CS-Fixer)等工具无需指定路径即可直接执行,就像这样:

$ php-cs-fixer fix src

不过,要使这种方法奏效,你需要将此全局文件夹添加到执行路径中。请参阅 Composer 文档 ( https://getcomposer.org/ ),了解如何在你使用的操作系统中执行此操作的详细信息。

只有当你独自完成项目且不使用任何构建管道时,才可选择使用全局安装功能。如果你在团队中工作和/或使用持续集成 (CI) 管道,则应为每个项目单独安装。

根据常见的最佳实践,如 "十二要素应用程序原则"( https://12factor.net ),所有依赖关系都应明确声明,并且不应依赖全局依赖关系,因为你永远无法确定将安装哪个版本。虽然代码质量工具包不是实际程序代码的一部分,但它们仍然是构建过程的一部分。即使安装版本之间存在微小差异,也会导致不可预见的行为,并在本地无法重现错误时造成混乱。

此外,你还希望项目的初始安装尽可能简单。让队友手动安装所有必需的工具既浪费时间又容易出错,还会让人产生挫败感。

基于上述原因,我们不鼓励使用全局安装方法。

Composer 脚本

一旦确定了安装 Composer 的可能方式,并使用它下载了所需的工具,就可以开始以最直接的方式使用它们了。在第 11 章 "持续集成"(Continuous Integration)中,我们将介绍如何在构建过程中自动运行这些工具。但现在,我们想向你展示 Composer 如何在需要时帮助你手动运行这些工具。

请看下面的示例:第一步,我们要运行 PHP-CS-Fixer,自动修复 src 文件夹中的代码。然后,我们还想在代码上运行 1 级的 PHPStan。当然,你可以分别运行这两个步骤,但我们想更舒适一些,一次性执行这两个工具。

为此,我们可以利用项目根目录下 composer.json 文件的脚本部分。在这里,我们必须将想要执行的工具添加到一个简洁的命令名称下,例如 analyze。下面的示例展示了这样做的效果:

{
    ...
    "scripts": {
        "analyze": [
            "vendor/bin/php-cs-fixer fix src",
            "vendor/bin/phpstan analyse --level 1 src"
        ]
    }
}

我们在这里使用了 JavaScript Object Notation(JSON) 数组符号,将每条命令都单独添加到一行中,这样比将所有命令都写在一行中更易于阅读和维护。

如果你想共享这些 Composer 命令,你可能还想添加一段简短的说明文字,当你执行 composer list 查看可用命令列表时,就会显示这段文字。为此,你需要在 composer.json 文件中添加脚本描述(script-descriptions)部分。对于之前介绍的 analyze 命令,可以这样写:

{
    ...
    "scripts": {
      ...
    },
    "scripts-descriptions": {
      "analyze": "Perform code cleanup and analysis"
    }
}

通过将工具安装到子文件夹中,我们找到了一种合适的方式来组织代码质量工具,而不会让它们干扰我们的应用程序依赖关系。但是,如果出于某种原因,你没有在项目中使用 Composer,或者你不喜欢在版本库中有两个 composer.json 文件,那该怎么办呢?在下一节中,我们将介绍一种不使用 Composer 的替代方法。