跳到主要内容
smallPict
免费开始

Engineering8 min read

WordPress 插件 CI/CD:WPCS、PHPStan 与 PHP 7.4 至 8.5

在 GitHub Actions 中搭建 WordPress 插件 CI/CD:在 PHP 7.4 至 8.5 上运行 WPCS、PHPStan 和自动化测试,并在发布到 WordPress.org 之前设置部署前关卡。

smallPict Engineering

Engineering

smallPict 的 WordPress 插件会把上传的图片转换为 WebP 和 AVIF,并提供给访客。我们为某个版本打上 tag 后,GitHub Actions 会把它提交到 WordPress.org 的 SVN 仓库,再从那里分发到所有安装了该插件的网站。开启了自动更新的网站会直接安装新版本,没有任何人审查改动。插件声明了 Requires PHP: 7.4 和 Requires at least: 5.9,因此同一个构建既要能在仍停留在 PHP 7.4 的共享主机上运行,也要能在运行 PHP 8.5 的容器中运行。

插件中的一个 fatal error 就可能让 wp-admin 无法访问。设置页面上一个未转义的值,就是同时出现在所有网站上的 XSS 漏洞。缺少一次 nonce 检查,管理员操作就会成为 CSRF 攻击目标。这些问题都无法悄悄修补:修复需要再发一个版本,而在新版本到达之前,部分网站会一直运行有问题的版本。

本文介绍我们的 WordPress 插件 CI/CD 流水线:在七个 PHP 版本上运行的 WordPress Coding Standards(WPCS)、PHPStan 和自动化测试,以及位于 WordPress.org 之前的部署前关卡。以下内容全部来自 smallpict-wp 仓库的 1.2.9 版本。

要点

  • 每个合并到 master 的 pull request 都会在 GitHub Actions 中并行地在 PHP 7.4、8.0、8.1、8.2、8.3、8.4 和 8.5 上运行 WPCS、PHPStan 和 9 个自动化测试脚本。
  • WPCS 强制要求输入经过清理、输出经过转义、使用 nonce 以及预处理查询。PHPStan 在 level 5 下基于 WordPress stub 发现类型错误和永远不会执行的分支,无需运行 WordPress。
  • 发布 tag 会在全部七个 PHP 版本上再次运行 WPCS,只有七个版本全部通过后,提交到 WordPress.org SVN 的 job 才会开始。

WordPress 插件 CI/CD 流水线概览

插件由两个 GitHub Actions workflow 把关。

Workflow触发条件Job检查项
phpcs.yml(PHP Code Quality & Standards)push 到 master 或 develop,合并到 master 的 pull request每个 PHP 版本一个 job:7.4、8.0、8.1、8.2、8.3、8.4、8.5WPCS、PHPStan、自动化测试
deploy-all.yml(Deploy to WordPress.org)push v* tag在 7 个 PHP 版本上分别运行 WPCS,然后运行 deployWPCS,然后构建并提交到 SVN

关于 PHP 版本覆盖范围的说明: 本文中的截图展示的是我们早期覆盖 PHP 7.4 至 8.3 的 CI 运行。如今,我们的 GitHub Actions 测试 matrix 已扩展到覆盖 PHP 7.4 至 PHP 8.5,确保与现代 PHP 运行时持续向前兼容,同时在旧环境上保持零回归。

所有工具都是 Composer 的 dev 依赖,因此不会随插件分发给用户:

JSON
"require-dev": {    "wp-coding-standards/wpcs": "^3.4.1",    "phpcsstandards/php_codesniffer": "^3.0",    "phpstan/phpstan": "^2.1",    "szepeviktor/phpstan-wordpress": "^2.0 || ^1.3.5",    "phpstan/extension-installer": "^1.4"}

在 GitHub Actions 中进行多版本 PHP matrix 测试

PHP 7.4 与 PHP 8.5 之间有哪些变化

同一份插件源码,在 PHP 7.4 和 PHP 8.5 上的运行方式并不相同。以下是会影响 WordPress 插件代码的部分变化:

  • PHP 8.0 让许多内部函数在参数类型错误时抛出 TypeError,而 7.4 只会返回 null 并给出警告。向 strlen() 传入数组的代码在 7.4 上继续运行,在 8.0 上则会中断。
  • PHP 8.0 改变了数字与非数字字符串的比较方式。0 == "abc" 在 7.4 上为 true,在 8.0 上为 false,这会让一些针对选项值的松散比较在毫无提示的情况下结果反转。
  • PHP 8.0 新增了 str_starts_with() 及相关函数。我们的图片处理器调用了 str_starts_with()。它能在 PHP 7.4 上运行,只是因为 WordPress 5.9 及以上版本自带 polyfill,而我们的 Requires at least: 5.9 保证了这一点。
  • PHP 8.1 弃用了向内部函数的不可为 null 的参数传入 null。strtolower( wp_parse_url( $url, PHP_URL_HOST ) ) 在 7.4 上没有问题,但在 8.1 上,当 $url 是相对地址、host 为 null 时会记录一条弃用提示。有些主机会把弃用提示直接输出到页面上。
  • PHP 8.2 弃用了动态属性。在您自己的类上给未声明的属性赋值,每个请求都会记录一条弃用提示。

这些问题只有在行为不同的 PHP 版本上分析或执行代码时才会显现。正因如此,流水线会让每个合并到 master 的 pull request 同时在全部七个版本上运行。

GitHub Actions matrix

WordPress 插件在 GitHub Actions 中的 CI 运行:PHP matrix 测试在 PHP 7.4、8.0、8.1、8.2 和 8.3 上并行运行五个 WPCS 与 PHPStan job,全部通过

图 1:将 1.2.0 的工作合并到 master 时的 PHP Code Quality & Standards workflow。每个 PHP 版本都是一个独立的 job,PHP 8.3 的 job 展示了每个 job 都会执行的步骤:PHPCS、PHPStan,然后是自动化测试。

关于 PHP 版本覆盖范围的说明: 上方的截图展示的是我们早期覆盖 PHP 7.4 至 8.3 的 CI 运行。如今,我们的 GitHub Actions 测试 matrix 已扩展到覆盖 PHP 7.4 至 PHP 8.5,确保与现代 PHP 运行时持续向前兼容,同时在旧环境上保持零回归。

YAML
jobs:  code-quality:    name: WPCS & PHPStan (PHP ${{ matrix.php-version }})    runs-on: ubuntu-26.04    strategy:      matrix:        php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']    steps:      - uses: actions/checkout@v7      - uses: shivammathur/setup-php@v2        with:          php-version: ${{ matrix.php-version }}          tools: composer:v2, cs2pr          ini-values: memory_limit=2G          coverage: none      - run: composer install --prefer-dist --no-progress --no-interaction      - name: Run PHPCS (WordPress Coding Standards)        run: vendor/bin/phpcs -q --standard=phpcs.xml.dist --report=checkstyle | cs2pr      - name: Run PHPStan (WordPress Static Analysis)        run: vendor/bin/phpstan analyse --memory-limit=2G      - name: Run the standalone tests        run: |          for t in tests/test_*.php; do            echo "== $t"            php "$t" || exit 1          done

fail-fast 保持默认值 true,因此当一个版本失败时,GitHub 会取消仍在运行的 job。这样一个有问题的 pull request 花费的资源很少,代价是无法在同一次运行中看到每个版本的结果。

matrix 能证明什么,不能证明什么

三项检查从 matrix 中获得的价值并不相同。PHPStan 针对其运行所在的 PHP 版本分析代码,测试则在该版本上执行代码,所以两者在每个 job 中都可能给出不同的结果。PHPCS 基于 token 工作,实际上在七个版本上给出的结果相同;在 matrix 中,它主要证明工具链能在每个版本上安装并运行。一次性针对一系列 PHP 版本检查语法和函数,是 PHPCompatibility 的工作,而我们的规则集目前还没有包含它。

自动化 WordPress Coding Standards(WPCS)

对插件至关重要的安全 sniff

WPCS 是一组专为 WordPress 编写的 PHP_CodeSniffer sniff。对插件安全最重要的规则就在 WordPress 规则集本身中:

  • WordPress.Security.ValidatedSanitizedInput 会标记所有未经 unslash 和清理就读取 $_POST、$_GET、$_REQUEST 或 $_COOKIE 的地方。
  • WordPress.Security.EscapeOutput 会标记所有对未经 esc_html()、esc_attr() 或 esc_url() 等转义函数处理的值进行 echo 或 print 的地方。
  • WordPress.Security.NonceVerification 会标记未验证 nonce 就读取请求数据的处理函数。
  • WordPress.DB.PreparedSQL 和 PreparedSQLPlaceholders 会标记通过字符串拼接而非 $wpdb->prepare() 构建的查询,以及与参数不匹配的占位符。

我们的 WPCS 规则集

phpcs.xml.dist 添加了 WordPress-Extra 和 WordPress-Docs,为翻译相关的 sniff 固定了 text domain,并要求每个全局函数、类和常量都带上我们的前缀,这样插件就不会与同一网站上其他插件的名称冲突:

XML
<rule ref="WordPress">    <exclude name="Universal.Arrays.DisallowShortArraySyntax"/></rule><rule ref="WordPress-Extra"/><rule ref="WordPress-Docs"/>
<rule ref="WordPress.WP.I18n">    <properties>        <property name="text_domain" type="array">            <element value="smallpict"/>        </property>    </properties></rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">    <properties>        <property name="prefixes" type="array">            <element value="smallpict"/>            <element value="SmallPict"/>            <element value="SMALLPICT"/>        </property>    </properties></rule>

一个通过 WPCS 的 AJAX 处理函数

插件中的这个处理函数会分批恢复已迁移到云存储的媒体。所有输入边界都在开头几行处理:

PHP
public function ajax_restore_offloaded() {    if ( ! check_ajax_referer( 'smallpict_restore_offloaded', 'nonce', false ) || ! current_user_can( 'manage_options' ) ) {        wp_send_json_error( array( 'message' => 'forbidden' ), 403 );    }    global $wpdb;    $after = isset( $_POST['after'] ) ? absint( wp_unslash( $_POST['after'] ) ) : 0;    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin action, the rows change on every call.    $ids = $wpdb->get_col(        $wpdb->prepare(            "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s AND post_id > %d ORDER BY post_id ASC LIMIT %d",            '_smallpict_offloaded',            '1',            $after,            self::RESTORE_BATCH        )    );    // ...}

nonce 和 capability 会一起检查,因为有效的 nonce 只能证明请求来自我们的管理页面,并不能证明该用户有权执行这个操作。$_POST['after'] 在进入查询之前先经过 unslash,再用 absint() 转换;即使值已经是整数,查询仍然会经过 $wpdb->prepare()。

抑制规则必须写明 sniff 名称和原因

上面那行 phpcs:ignore 是有意为之。WPCS 会对未使用对象缓存的直接数据库查询发出警告,这对前端读取是好建议,但对每次调用时数据行都会变化的管理员操作并不适用。我们的规则是:每个 ignore 都必须写明确切的 sniff 名称,并在 -- 之后给出原因。不带说明的 // phpcs:ignore 无法通过代码审查。

在 CI 中,PHPCS 会输出 checkstyle 报告,cs2pr 再把它转换成 pull request diff 上的注释,这样审查者可以直接在出问题的那一行看到每条违规,而不必去翻日志。

用 PHPStan 静态分析发现逻辑缺陷

WPCS 逐行读取代码。它知道某个 echo 缺少转义调用,但不知道变量持有什么类型,也不知道某个分支是否有可能执行。PHPStan 会为流经每个函数的类型建立模型,并报告与模型矛盾的代码:调用不存在的方法、在需要 string 的地方传入 string|false、比较永远不可能相等的值、条件永远为 true 或永远为 false。

在不运行 WordPress 的情况下对 WordPress 使用 PHPStan

szepeviktor/phpstan-wordpress 通过 stub 文件向 PHPStan 提供 WordPress 核心函数和类的签名,并附带几个 WordPress 专用的类型扩展。phpstan/extension-installer 会自动注册它。PHPStan 只读取这些声明,从不加载 WordPress 或数据库。

我们的 PHPStan 配置

YAML
parameters:    level: 5    paths:        - smallpict.php        - includes/        - admin/    treatPhpDocTypesAsCertain: false    bootstrapFiles:        - phpstan-bootstrap.php

在较低 level 对未知类、方法和属性、返回类型以及死代码的检查之上,level 5 还会检查传给函数或方法的每个参数的类型。treatPhpDocTypesAsCertain: false 告诉 PHPStan 不要把我们自己写的 @param docblock 当作确凿依据。WordPress 会原样传递 filter 返回的任何值,所以一个注明为 int 的参数仍可能以字符串形式从其他插件传进来,我们希望 PHPStan 保留处理这种情况的防御性检查。bootstrap 文件定义了插件的常量(SMALLPICT_PATH、SMALLPICT_URL 等),这些常量通常在运行时才设置。

GitHub Actions 中 WordPress 的 PHPStan 静态分析日志:WPCS 步骤无问题,PHPStan 分析了 11 个插件文件中的 11 个,结果为“[OK] No errors”

图 2:PHP 8.3 job 中的 PHPCS 和 PHPStan 步骤。没有需要报告的内容时,PHPCS 不输出任何信息。PHPStan 分析了 11 个插件文件(插件主文件和 includes/ 中的 10 个类),没有报告错误。

一个真实的 PHPStan 发现:图片处理器第 626 行

2026 年 10 月 2 日,流水线因 PHPStan 报出一个错误而失败:

PHPStan 在 WordPress 插件中发现 bug:includes/class-smallpict-image-processor.php 第 626 行报出“Right side of && is always true”,规则为 booleanAnd.rightAlwaysTrue,job 在 PHP 8.2 上失败

图 3:PHP 8.2 job 在 PHPStan 步骤停止,报告 includes/class-smallpict-image-processor.php 第 626 行的 booleanAnd.rightAlwaysTrue。当时被分析的路径中共有 9 个插件文件。

这段代码是转换流程的最后一步。当插件的输出格式设置为 WebP 或 AVIF 时,转换后的文件会成为附件,原始上传文件会被删除以节省磁盘空间。其他选项“AVIF,以 WebP 作为后备”和“保留原始格式”会保留原始 JPEG 和 PNG 文件。删除分支原来是这样的:

PHP
// C. Pure 1-to-1 Mode: Delete raw uncompressed source file to free 100% hosting space.if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) {    wp_delete_file( $main_file_path );}

PHPStan 的判断是:在这个位置,file_exists( $main_file_path ) 永远为 true。原因在 150 行之前,同一个方法的开头:

PHP
$main_file_path = get_attached_file( $attachment_id );if ( ! $main_file_path || ! file_exists( $main_file_path ) ) {    return new WP_Error( 'file_missing', 'File not found on disk.' );}

只有文件存在时,这个方法才会继续执行,而 PHPStan 把这个事实一直带到了第 626 行。

我们没有直接照单全收。file_exists() 查询的是文件系统,两次调用之间答案可能会变。PHPStan 默认会记住这类调用的结果(rememberPossiblyImpureFunctionValues 选项),前提是两次调用之间没有任何操作改变磁盘。在删除这项检查之前,我们通过阅读代码确认了两点:

  1. 从 guard 到第 626 行之间,没有任何代码删除或移动源文件。转换流程会把每个输出写入临时文件,再移动到新路径;针对该路径的 wp_delete_file() 调用只用于删除临时文件。确实会删除本地文件的迁移到云存储步骤,在第 626 行之后才运行。
  2. 即使文件不存在,wp_delete_file() 也能正常处理。它会让路径经过 wp_delete_file filter,然后调用 @unlink(),文件已经不在时也不会造成任何损害。

两点都确认之后,这个条件就是死代码。而删除分支里的死代码会误导下一位读者,让人以为文件可能在方法执行到一半时消失。修复方式是删掉它:

DIFF
-if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) {+if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path ) {     wp_delete_file( $main_file_path ); }

这个发现并不是崩溃。它的价值在于揭示了这个方法的问题:在唯一会删除用户文件的分支里,代码的假设已经与其结构脱节。我们得到的教训比“信任分析器”更具体:当 PHPStan 对文件系统、网络或时间相关函数报告永远为 true 或永远为 false 时,在删除条件之前,应当手动检查两次调用之间会发生什么。

不依赖 PHPUnit 的 WordPress 插件自动化测试

每个 matrix job 的最后一步会运行 tests/ 中的文件。这些不是 PHPUnit 测试。每个文件都是一个普通的 PHP 脚本:为它需要的 WordPress 函数定义最小化的 stub(get_option()、update_option()、sanitize_key() 等),加载被测的插件类,执行测试,并在第一次失败时以非零状态码退出:

Shell
for t in tests/test_.php do  echo "== $t"  php "$t"  exit 1done

目前共有 9 个这样的脚本。它们覆盖的是一旦出错就会让客户损失数据或金钱的路径:WebP 和 AVIF 转换流水线及其回退、图片交付、将文件迁移到云存储及删除云端文件、分批恢复已迁移的文件、账户停止付费后的行为、存储配额,以及卸载。卸载测试会针对内存中的 SQLite 数据库运行真实的卸载查询,并检查剩下了什么:

PHP
check(    array( '_smallpict_cdn_base', '_smallpict_cdn_key', '_smallpict_offloaded', '_smallpictx', '_wp_attached_file' ) === $meta,    'offload markers and foreign meta remain (got ' . implode( ',', $meta ) . ')');

这个断言保护的是一个很容易被无意中推翻的决定。卸载插件会删除它的设置,但会保留记录哪些文件只存在于云存储中的 post meta,这样重新安装插件的网站仍然能找到自己的图片。一条草率的 DELETE ... LIKE '_smallpict%' 会删除这些标记,一旦发生,这个测试就会失败。

我们选择普通脚本而不是 WordPress 的 PHPUnit 测试套件,是出于速度和搭建成本的考虑:它们不需要数据库服务器,也不需要 WordPress 代码检出,9 个脚本全部运行完不到一秒,所以在七个 PHP 版本上运行几乎不会延长 pull request 的检查时间。代价是 stub 只能模拟我们写进去的行为。依赖真实 WordPress 核心的行为,例如媒体库自身的元数据处理,不在这些测试的覆盖范围内。

发布到 WordPress.org 之前的部署前关卡

版本通过一个独立的 workflow 发布。push 一个如 v1.2.9 这样的 tag 会触发它,而提交到 WordPress.org SVN 的 job 声明了对 lint matrix 的依赖:

YAML
jobs:  lint:    name: WPCS Check (PHP ${{ matrix.php-version }})    runs-on: ubuntu-26.04    strategy:      matrix:        php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']    steps:      - uses: actions/checkout@v7      - uses: shivammathur/setup-php@v2        with:          php-version: ${{ matrix.php-version }}      - run: composer install --prefer-dist --no-progress --no-interaction      - name: PHP CodeSniffer Check        run: vendor/bin/phpcs --standard=phpcs.xml.dist
  deploy:    name: Deploy    needs: lint    runs-on: ubuntu-26.04    permissions:      contents: read    steps:      - uses: actions/checkout@v7      - uses: shivammathur/setup-php@v2        with:          php-version: '7.4'      - name: Build WP.org Version        run: |          chmod +x build-wporg.sh          ./build-wporg.sh      - name: Deploy to WordPress.org        uses: 10up/action-wordpress-plugin-deploy@stable        with:          generate-zip: false        env:          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}          SLUG: smallpict          BUILD_DIR: build-wporg/smallpict          ASSETS_DIR: .wordpress-org

needs: lint 表示只有全部七个 lint job 都成功后,deploy job 才会开始。只要 WPCS 在任一版本上失败,GitHub 就会跳过 deploy,不会产生任何 SVN 提交。构建步骤在我们支持的最旧版本 PHP 7.4 上运行。该 job 的 token 对仓库只有只读权限;它唯一的写入操作是提交到 WordPress.org SVN,所用凭据保存在仓库 secret 中。

Deploy to WordPress.org GitHub Actions workflow 中的部署前关卡:在 Deploy job 运行之前,WPCS Check 已在 PHP 7.4、8.0、8.1、8.2 和 8.3 上通过

图 4:Deploy to WordPress.org workflow。五个 WPCS Check job 先运行;下方的 Deploy job 只有在五个全部通过后才会运行。

这个关卡会在打了 tag 的 commit 上重新运行 WPCS,而不是信任之前 pull request 的运行结果。tag 可以指向任意 commit,包括直接 push 的 commit 或检查从未完成的 commit,而发布 workflow 没有其他办法知道自己要发布的是什么。

在本地运行 CI 检查

Composer 脚本封装了与 CI 相同的命令,因此可以在 push 之前复现失败:

Shell
composer installcomposer lint        # vendor/bin/phpcs --standard=phpcs.xml.distcomposer lint-fix    # vendor/bin/phpcbf: fixes spacing and formatting sniffscomposer phpstan     # vendor/bin/phpstan analysefor t in tests/test_.php do php "$t"  break done

要复现某个 matrix job 的失败,请在对应的 PHP 版本下运行相同的命令,例如在 composer install 之后使用官方镜像:

Shell
docker run --rm -v "$PWD":/app -w /app php:7.4-cli \  sh -c 'vendor/bin/phpstan analyse --memory-limit=2G && for t in tests/test_*.php; do php "$t" || exit 1; done'

phpcbf 只修复 sniff 能够机械修复的问题。转义、清理和 nonce 相关的发现需要由人来判断哪个函数适合当前上下文,我们不会自动修复它们。

流水线尚未覆盖的部分

我们的待办清单上有三处缺口:

  1. 部署前关卡只运行 WPCS。PHPStan 和自动化测试在 push 到 master 和 develop 以及 pull request 时运行,但不会在发布 tag 上运行。把这两个步骤加入部署 workflow 的 lint job 即可补上这一缺口。
  2. 我们的规则集没有包含 PHPCompatibility。添加 phpcompatibility/phpcompatibility-wp 并将 testVersion 设为 7.4-,就能在一次运行中标记出 7.4 上不存在的语法和函数。
  3. 测试使用的是 stub,而不是真实的 WordPress 安装。针对 WordPress 5.9(我们支持的最低版本)和最新版本运行的集成测试 job,可以覆盖 stub 无法模拟的核心行为。

FAQ

为什么在 PHP 7.4 至 8.5 上进行 matrix 测试对 WordPress 插件至关重要?

WordPress.org 会把同一个版本分发给每个网站,而插件头(Requires PHP: 7.4)是决定哪些 PHP 版本可以安装它的唯一过滤条件。从 7.4 到 8.5,PHP 改变了内部函数处理错误参数类型的方式、数字与字符串的比较方式,以及哪些写法会触发弃用提示,因此在一个版本上正确的代码,在另一个版本上可能导致 fatal error 或记录警告。只在单个 PHP 版本上运行 CI,只能证明插件在那个版本上可用。在每个受支持的版本上运行 PHPStan 和测试,可以在版本到达用户之前发现这些差异。

WPCS 能取代传统的安全审计吗?

不能。WPCS 检查的是模式所在的那一行:未经清理就读取的请求变量、没有转义的 echo、没有 nonce 检查的处理函数、没有使用 $wpdb->prepare() 的查询。它无法判断处理函数检查的 capability 是否正确,转义函数是否适合输出上下文,服务器端请求是否会发往用户可控的 URL,或者一个用户能否访问另一个用户的数据。phpcs:ignore 注释也可以让某个 sniff 静默。我们把 WPCS 视为挡住已知错误的底线,涉及安全的改动仍由人工审查。

PHPStan 如何在不执行代码的情况下处理 WordPress 核心函数?

PHPStan 从不运行它分析的代码。它读取声明和 docblock 来建立类型模型。对于 WordPress,szepeviktor/phpstan-wordpress 提供 stub 文件:WordPress 核心函数和类的签名、参数类型和返回类型,但不含函数体。PHPStan 把这些 stub 当作 WordPress 本身来读取,因此能够检查对 wp_delete_file() 或 get_attached_file() 的调用是否传入并接收了正确的类型。WordPress 或插件在运行时定义的常量放在 bootstrap 文件中,PHPStan 会在分析前加载它。

该插件已在 WordPress.org 上架,名称为 smallPict。安装方法和输出格式设置请参阅安装指南和图片设置参考。

作者:smallPict Engineering

Engineering

我们为网站和应用构建图片转换与 CDN 分发。

用您自己的图片试试。

每月免费 20 MB。在 WordPress 上大约两分钟即可完成设置。

更多关于图片性能、Core Web Vitals 和云存储的内容。

全部文章