From 986f9c3f6af5af3becfcc2853690fbdfb522a284 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:10:24 +0100 Subject: [PATCH 01/39] Align the plugins' workflows with the template's Each plugin's mirror workflows predated the template's T-tooling changes: a separate changelog job where X270 runs changelog:validate inside "Lint PHP", the old "PHP Lint" and "Scripts and styles lint" names, no concurrency group cancelling superseded pull request runs, an unretried WordPress version listing, CodeQL without TypeScript, Workflow Checks only on workflow files, and narrating comments. Each plugin's seven workflows now read as the template's, renamed for the plugin. They keep what only a wordpress.org plugin needs: the readme.txt lint, which gains the template's five-minute timeout for inline jobs, the release's wp-org deploy and its tag-only publish. The template-only scaffold rehearsal stays out, and the multisite jobs arrive with the multisite lane. Assisted-by: Claude Code:claude-opus-5-5 --- .../.github/workflows/audit.yml | 11 +++---- .../.github/workflows/codeql.yml | 3 ++ .../.github/workflows/quality.yml | 30 +++++++------------ .../.github/workflows/release.yml | 4 +-- .../.github/workflows/tests.yml | 8 +++-- .../.github/workflows/workflow-checks.yml | 8 ++--- .../.github/workflows/audit.yml | 11 +++---- .../.github/workflows/codeql.yml | 3 ++ .../.github/workflows/quality.yml | 28 ++++++----------- .../.github/workflows/release.yml | 2 +- .../.github/workflows/tests.yml | 8 +++-- .../.github/workflows/workflow-checks.yml | 8 ++--- .../.github/workflows/audit.yml | 11 +++---- .../.github/workflows/codeql.yml | 3 ++ .../.github/workflows/quality.yml | 28 ++++++----------- .../.github/workflows/release.yml | 2 +- .../.github/workflows/tests.yml | 8 +++-- .../.github/workflows/workflow-checks.yml | 8 ++--- 18 files changed, 86 insertions(+), 98 deletions(-) diff --git a/plugins/internal-comments/.github/workflows/audit.yml b/plugins/internal-comments/.github/workflows/audit.yml index ff6c180..345f398 100644 --- a/plugins/internal-comments/.github/workflows/audit.yml +++ b/plugins/internal-comments/.github/workflows/audit.yml @@ -5,17 +5,18 @@ on: branches: [trunk] pull_request: schedule: - # Weekly Monday at 07:00 UTC — staggered after dependabot (06:00) so PRs from - # dependency bumps surface audit results in the same morning batch. + # Weekly, so advisories published against an unchanged lock surface without a push. - cron: '0 7 * * 1' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: audit: name: Audit - # npm gate audits production deps only — the dev-tooling graph (wp-scripts/playwright trees) is advisory via dependabot alerts. + # Only production npm packages gate the audit, since a release ships none of the development tooling. uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@b107001cce9f2960495d9d50481c8540d228f60c - with: - npm-audit-flags: '--omit=dev --audit-level=high' diff --git a/plugins/internal-comments/.github/workflows/codeql.yml b/plugins/internal-comments/.github/workflows/codeql.yml index 1e142c0..cd74e65 100644 --- a/plugins/internal-comments/.github/workflows/codeql.yml +++ b/plugins/internal-comments/.github/workflows/codeql.yml @@ -16,8 +16,11 @@ permissions: {} jobs: analyze: + name: CodeQL permissions: actions: read contents: read security-events: write uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + languages: '["actions", "javascript-typescript"]' diff --git a/plugins/internal-comments/.github/workflows/quality.yml b/plugins/internal-comments/.github/workflows/quality.yml index 0fe5aa1..6263e15 100644 --- a/plugins/internal-comments/.github/workflows/quality.yml +++ b/plugins/internal-comments/.github/workflows/quality.yml @@ -5,19 +5,23 @@ on: branches: [trunk] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: lint-php: - name: PHP Lint + name: Lint PHP uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@b107001cce9f2960495d9d50481c8540d228f60c with: php-version: '8.5' - scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]' + scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan", "changelog:validate"]' lint-scripts-styles: - name: Scripts and styles lint + name: Lint scripts and styles uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@b107001cce9f2960495d9d50481c8540d228f60c with: scripts: '["lint:scripts", "lint:styles", "lint:types"]' @@ -36,12 +40,12 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares. + # The workflows read the slug the plugin declares, as the template's do, so a change to the template's applies here unedited. - name: Read the slug from composer.json id: slug run: | slug=$(jq -r '.extra["text-domain"] // empty' composer.json) - if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if ! [[ "$slug" =~ ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ]]; then echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'." exit 1 fi @@ -58,6 +62,7 @@ jobs: readme-lint: name: readme.txt lint runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -66,18 +71,3 @@ jobs: with: path: readme.txt fail-on: error - - changelog: - name: Changelog fragments - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 - with: - php-version: '8.5' - coverage: none - tools: composer:v2 - - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - - run: composer changelog:validate diff --git a/plugins/internal-comments/.github/workflows/release.yml b/plugins/internal-comments/.github/workflows/release.yml index 5e2dc60..d6b730a 100644 --- a/plugins/internal-comments/.github/workflows/release.yml +++ b/plugins/internal-comments/.github/workflows/release.yml @@ -24,12 +24,12 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares. + # The workflows read the slug the plugin declares, as the template's do, so a change to the template's applies here unedited. - name: Read the slug from composer.json id: slug run: | slug=$(jq -r '.extra["text-domain"] // empty' composer.json) - if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if ! [[ "$slug" =~ ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ]]; then echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'." exit 1 fi diff --git a/plugins/internal-comments/.github/workflows/tests.yml b/plugins/internal-comments/.github/workflows/tests.yml index fb6ef15..50c4d96 100644 --- a/plugins/internal-comments/.github/workflows/tests.yml +++ b/plugins/internal-comments/.github/workflows/tests.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -22,8 +26,6 @@ jobs: name: WordPress versions runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - contents: read outputs: versions: ${{ steps.versions.outputs.versions }} steps: @@ -41,7 +43,7 @@ jobs: echo "::error::$entry declares no Requires at least header." exit 1 fi - offers=$(curl -fsS https://api.wordpress.org/core/version-check/1.7/) + offers=$(curl -fsS --retry 3 --retry-all-errors --max-time 20 https://api.wordpress.org/core/version-check/1.7/) versions=$(printf '%s' "$offers" | jq -c --arg floor "$floor" '[.offers[].version | select((split(".")[0:2] | map(tonumber)) >= ($floor | split(".")[0:2] | map(tonumber)))] | unique | if length == 0 then error("No WordPress release at or above \($floor).") else . end') echo "versions=$versions" >> "$GITHUB_OUTPUT" diff --git a/plugins/internal-comments/.github/workflows/workflow-checks.yml b/plugins/internal-comments/.github/workflows/workflow-checks.yml index bafdc75..e954a68 100644 --- a/plugins/internal-comments/.github/workflows/workflow-checks.yml +++ b/plugins/internal-comments/.github/workflows/workflow-checks.yml @@ -3,12 +3,11 @@ name: Workflow Checks on: push: branches: [trunk] - paths: ['.github/workflows/**'] + paths: ['.github/**'] pull_request: - paths: ['.github/workflows/**'] + paths: ['.github/**'] schedule: - # Weekly Monday 06:00 UTC. Keeps zizmor's online audits (e.g. - # known-vulnerable-actions) fresh between workflow changes. + # Re-runs zizmor's online audits between workflow changes. - cron: '0 6 * * 1' workflow_dispatch: @@ -20,6 +19,7 @@ permissions: {} jobs: checks: + name: Workflow Checks permissions: contents: read security-events: write diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/audit.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/audit.yml index ff6c180..345f398 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/audit.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/audit.yml @@ -5,17 +5,18 @@ on: branches: [trunk] pull_request: schedule: - # Weekly Monday at 07:00 UTC — staggered after dependabot (06:00) so PRs from - # dependency bumps surface audit results in the same morning batch. + # Weekly, so advisories published against an unchanged lock surface without a push. - cron: '0 7 * * 1' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: audit: name: Audit - # npm gate audits production deps only — the dev-tooling graph (wp-scripts/playwright trees) is advisory via dependabot alerts. + # Only production npm packages gate the audit, since a release ships none of the development tooling. uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@b107001cce9f2960495d9d50481c8540d228f60c - with: - npm-audit-flags: '--omit=dev --audit-level=high' diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/codeql.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/codeql.yml index 1e142c0..cd74e65 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/codeql.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/codeql.yml @@ -16,8 +16,11 @@ permissions: {} jobs: analyze: + name: CodeQL permissions: actions: read contents: read security-events: write uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + languages: '["actions", "javascript-typescript"]' diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/quality.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/quality.yml index d362366..6263e15 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/quality.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/quality.yml @@ -5,19 +5,23 @@ on: branches: [trunk] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: lint-php: - name: PHP Lint + name: Lint PHP uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@b107001cce9f2960495d9d50481c8540d228f60c with: php-version: '8.5' - scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]' + scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan", "changelog:validate"]' lint-scripts-styles: - name: Scripts and styles lint + name: Lint scripts and styles uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@b107001cce9f2960495d9d50481c8540d228f60c with: scripts: '["lint:scripts", "lint:styles", "lint:types"]' @@ -41,7 +45,7 @@ jobs: id: slug run: | slug=$(jq -r '.extra["text-domain"] // empty' composer.json) - if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if ! [[ "$slug" =~ ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ]]; then echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'." exit 1 fi @@ -58,6 +62,7 @@ jobs: readme-lint: name: readme.txt lint runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -66,18 +71,3 @@ jobs: with: path: readme.txt fail-on: error - - changelog: - name: Changelog fragments - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 - with: - php-version: '8.5' - coverage: none - tools: composer:v2 - - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - - run: composer changelog:validate diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/release.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/release.yml index eba32e9..d6b730a 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/release.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: id: slug run: | slug=$(jq -r '.extra["text-domain"] // empty' composer.json) - if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if ! [[ "$slug" =~ ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ]]; then echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'." exit 1 fi diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml index fb6ef15..50c4d96 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -22,8 +26,6 @@ jobs: name: WordPress versions runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - contents: read outputs: versions: ${{ steps.versions.outputs.versions }} steps: @@ -41,7 +43,7 @@ jobs: echo "::error::$entry declares no Requires at least header." exit 1 fi - offers=$(curl -fsS https://api.wordpress.org/core/version-check/1.7/) + offers=$(curl -fsS --retry 3 --retry-all-errors --max-time 20 https://api.wordpress.org/core/version-check/1.7/) versions=$(printf '%s' "$offers" | jq -c --arg floor "$floor" '[.offers[].version | select((split(".")[0:2] | map(tonumber)) >= ($floor | split(".")[0:2] | map(tonumber)))] | unique | if length == 0 then error("No WordPress release at or above \($floor).") else . end') echo "versions=$versions" >> "$GITHUB_OUTPUT" diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/workflow-checks.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/workflow-checks.yml index bafdc75..e954a68 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/workflow-checks.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/workflow-checks.yml @@ -3,12 +3,11 @@ name: Workflow Checks on: push: branches: [trunk] - paths: ['.github/workflows/**'] + paths: ['.github/**'] pull_request: - paths: ['.github/workflows/**'] + paths: ['.github/**'] schedule: - # Weekly Monday 06:00 UTC. Keeps zizmor's online audits (e.g. - # known-vulnerable-actions) fresh between workflow changes. + # Re-runs zizmor's online audits between workflow changes. - cron: '0 6 * * 1' workflow_dispatch: @@ -20,6 +19,7 @@ permissions: {} jobs: checks: + name: Workflow Checks permissions: contents: read security-events: write diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml index ff6c180..345f398 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml @@ -5,17 +5,18 @@ on: branches: [trunk] pull_request: schedule: - # Weekly Monday at 07:00 UTC — staggered after dependabot (06:00) so PRs from - # dependency bumps surface audit results in the same morning batch. + # Weekly, so advisories published against an unchanged lock surface without a push. - cron: '0 7 * * 1' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: audit: name: Audit - # npm gate audits production deps only — the dev-tooling graph (wp-scripts/playwright trees) is advisory via dependabot alerts. + # Only production npm packages gate the audit, since a release ships none of the development tooling. uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@b107001cce9f2960495d9d50481c8540d228f60c - with: - npm-audit-flags: '--omit=dev --audit-level=high' diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml index 1e142c0..cd74e65 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml @@ -16,8 +16,11 @@ permissions: {} jobs: analyze: + name: CodeQL permissions: actions: read contents: read security-events: write uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + languages: '["actions", "javascript-typescript"]' diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml index d362366..6263e15 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml @@ -5,19 +5,23 @@ on: branches: [trunk] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: lint-php: - name: PHP Lint + name: Lint PHP uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@b107001cce9f2960495d9d50481c8540d228f60c with: php-version: '8.5' - scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]' + scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan", "changelog:validate"]' lint-scripts-styles: - name: Scripts and styles lint + name: Lint scripts and styles uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@b107001cce9f2960495d9d50481c8540d228f60c with: scripts: '["lint:scripts", "lint:styles", "lint:types"]' @@ -41,7 +45,7 @@ jobs: id: slug run: | slug=$(jq -r '.extra["text-domain"] // empty' composer.json) - if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if ! [[ "$slug" =~ ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ]]; then echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'." exit 1 fi @@ -58,6 +62,7 @@ jobs: readme-lint: name: readme.txt lint runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -66,18 +71,3 @@ jobs: with: path: readme.txt fail-on: error - - changelog: - name: Changelog fragments - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 - with: - php-version: '8.5' - coverage: none - tools: composer:v2 - - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - - run: composer changelog:validate diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml index eba32e9..d6b730a 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: id: slug run: | slug=$(jq -r '.extra["text-domain"] // empty' composer.json) - if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if ! [[ "$slug" =~ ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ]]; then echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'." exit 1 fi diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml index fb6ef15..50c4d96 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -22,8 +26,6 @@ jobs: name: WordPress versions runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - contents: read outputs: versions: ${{ steps.versions.outputs.versions }} steps: @@ -41,7 +43,7 @@ jobs: echo "::error::$entry declares no Requires at least header." exit 1 fi - offers=$(curl -fsS https://api.wordpress.org/core/version-check/1.7/) + offers=$(curl -fsS --retry 3 --retry-all-errors --max-time 20 https://api.wordpress.org/core/version-check/1.7/) versions=$(printf '%s' "$offers" | jq -c --arg floor "$floor" '[.offers[].version | select((split(".")[0:2] | map(tonumber)) >= ($floor | split(".")[0:2] | map(tonumber)))] | unique | if length == 0 then error("No WordPress release at or above \($floor).") else . end') echo "versions=$versions" >> "$GITHUB_OUTPUT" diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml index bafdc75..e954a68 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml @@ -3,12 +3,11 @@ name: Workflow Checks on: push: branches: [trunk] - paths: ['.github/workflows/**'] + paths: ['.github/**'] pull_request: - paths: ['.github/workflows/**'] + paths: ['.github/**'] schedule: - # Weekly Monday 06:00 UTC. Keeps zizmor's online audits (e.g. - # known-vulnerable-actions) fresh between workflow changes. + # Re-runs zizmor's online audits between workflow changes. - cron: '0 6 * * 1' workflow_dispatch: @@ -20,6 +19,7 @@ permissions: {} jobs: checks: + name: Workflow Checks permissions: contents: read security-events: write From 23ae7281242b7e6a90971885a0096ba21a5bb5d0 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:11:01 +0100 Subject: [PATCH 02/39] Ignore secret files and every wp-env override in each plugin Each plugin ignored only .wp-env.override.json, so an override of its tests or multisite environment could be committed (A232, A724), no backstop kept secret material out of an accidental git add (A721), and its .distignore listed files by hand where the template starts from configs' baseline (X190's A179). Each plugin's .gitignore takes the template's block above the toptal block, without the build trees it does not have, and its .distignore is the template's, configs' baseline with the project block, less the scaffold README only the template carries. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/.distignore | 64 +++++++++++-------- plugins/internal-comments/.gitignore | 10 ++- .../linked-orders-for-woocommerce/.distignore | 64 +++++++++++-------- .../linked-orders-for-woocommerce/.gitignore | 10 ++- .../.distignore | 64 +++++++++++-------- .../.gitignore | 10 ++- 6 files changed, 141 insertions(+), 81 deletions(-) diff --git a/plugins/internal-comments/.distignore b/plugins/internal-comments/.distignore index 5faf091..8aa76ac 100644 --- a/plugins/internal-comments/.distignore +++ b/plugins/internal-comments/.distignore @@ -1,40 +1,52 @@ -.cache -.distignore -.editorconfig +# Baseline wp dist-archive exclusions for WordPress projects. + .git .github -.gitignore .idea .vscode -.wp-env.json -.wp-env.override.json -.wp-env.tests.json - +.editorconfig +.gitattributes +.gitignore +.distignore AGENTS.md -CHANGELOG.md CLAUDE.md CONTRIBUTING.md -README.md SECURITY.md - -changelog/ -docs/ - -node_modules/ -tests/ -vendor/bin/ - +/changelog +node_modules +tests +vendor/bin composer.json composer.lock package.json package-lock.json +.phpcs*.xml* +phpcs*.xml* +.phpstan*.neon* +phpstan*.neon* +phpunit*.xml* +.phpunit.result.cache +.phpunit.cache +infection.json* +eslint.config.* +stylelint.config.* tsconfig.json -eslint.config.mjs -stylelint.config.js -phpcs.dist.xml -phpcs.tests.dist.xml -phpstan.dist.neon -phpstan.safeguards.neon -phpunit.dist.xml -playwright.config.js +playwright.config.* +postcss.config.* scoper.inc.php +.wp-env* +.wordpress-org + +# Secret material, with .pem anchored to the root so certificates bundled under vendor/ still ship. +.env* +/*.pem +*.key +id_rsa* +.npmrc +auth.json + +# The plugin's own exclusions, beyond configs' baseline above. +.cache +CHANGELOG.md +README.md +docs/ diff --git a/plugins/internal-comments/.gitignore b/plugins/internal-comments/.gitignore index 216741b..df3cc85 100644 --- a/plugins/internal-comments/.gitignore +++ b/plugins/internal-comments/.gitignore @@ -1,7 +1,15 @@ vendor-prefixed assets/build tests/.cache -.wp-env.override.json +.wp-env*.override.json + +# Secret material is never tracked, so these match at any depth as a backstop against an accidental git add. +.env* +*.pem +*.key +id_rsa* +.npmrc +auth.json # Created by https://www.toptal.com/developers/gitignore/api/node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode diff --git a/plugins/linked-orders-for-woocommerce/.distignore b/plugins/linked-orders-for-woocommerce/.distignore index 5faf091..8aa76ac 100644 --- a/plugins/linked-orders-for-woocommerce/.distignore +++ b/plugins/linked-orders-for-woocommerce/.distignore @@ -1,40 +1,52 @@ -.cache -.distignore -.editorconfig +# Baseline wp dist-archive exclusions for WordPress projects. + .git .github -.gitignore .idea .vscode -.wp-env.json -.wp-env.override.json -.wp-env.tests.json - +.editorconfig +.gitattributes +.gitignore +.distignore AGENTS.md -CHANGELOG.md CLAUDE.md CONTRIBUTING.md -README.md SECURITY.md - -changelog/ -docs/ - -node_modules/ -tests/ -vendor/bin/ - +/changelog +node_modules +tests +vendor/bin composer.json composer.lock package.json package-lock.json +.phpcs*.xml* +phpcs*.xml* +.phpstan*.neon* +phpstan*.neon* +phpunit*.xml* +.phpunit.result.cache +.phpunit.cache +infection.json* +eslint.config.* +stylelint.config.* tsconfig.json -eslint.config.mjs -stylelint.config.js -phpcs.dist.xml -phpcs.tests.dist.xml -phpstan.dist.neon -phpstan.safeguards.neon -phpunit.dist.xml -playwright.config.js +playwright.config.* +postcss.config.* scoper.inc.php +.wp-env* +.wordpress-org + +# Secret material, with .pem anchored to the root so certificates bundled under vendor/ still ship. +.env* +/*.pem +*.key +id_rsa* +.npmrc +auth.json + +# The plugin's own exclusions, beyond configs' baseline above. +.cache +CHANGELOG.md +README.md +docs/ diff --git a/plugins/linked-orders-for-woocommerce/.gitignore b/plugins/linked-orders-for-woocommerce/.gitignore index 216741b..df3cc85 100644 --- a/plugins/linked-orders-for-woocommerce/.gitignore +++ b/plugins/linked-orders-for-woocommerce/.gitignore @@ -1,7 +1,15 @@ vendor-prefixed assets/build tests/.cache -.wp-env.override.json +.wp-env*.override.json + +# Secret material is never tracked, so these match at any depth as a backstop against an accidental git add. +.env* +*.pem +*.key +id_rsa* +.npmrc +auth.json # Created by https://www.toptal.com/developers/gitignore/api/node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode diff --git a/plugins/locked-payment-methods-for-woocommerce/.distignore b/plugins/locked-payment-methods-for-woocommerce/.distignore index 5faf091..8aa76ac 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.distignore +++ b/plugins/locked-payment-methods-for-woocommerce/.distignore @@ -1,40 +1,52 @@ -.cache -.distignore -.editorconfig +# Baseline wp dist-archive exclusions for WordPress projects. + .git .github -.gitignore .idea .vscode -.wp-env.json -.wp-env.override.json -.wp-env.tests.json - +.editorconfig +.gitattributes +.gitignore +.distignore AGENTS.md -CHANGELOG.md CLAUDE.md CONTRIBUTING.md -README.md SECURITY.md - -changelog/ -docs/ - -node_modules/ -tests/ -vendor/bin/ - +/changelog +node_modules +tests +vendor/bin composer.json composer.lock package.json package-lock.json +.phpcs*.xml* +phpcs*.xml* +.phpstan*.neon* +phpstan*.neon* +phpunit*.xml* +.phpunit.result.cache +.phpunit.cache +infection.json* +eslint.config.* +stylelint.config.* tsconfig.json -eslint.config.mjs -stylelint.config.js -phpcs.dist.xml -phpcs.tests.dist.xml -phpstan.dist.neon -phpstan.safeguards.neon -phpunit.dist.xml -playwright.config.js +playwright.config.* +postcss.config.* scoper.inc.php +.wp-env* +.wordpress-org + +# Secret material, with .pem anchored to the root so certificates bundled under vendor/ still ship. +.env* +/*.pem +*.key +id_rsa* +.npmrc +auth.json + +# The plugin's own exclusions, beyond configs' baseline above. +.cache +CHANGELOG.md +README.md +docs/ diff --git a/plugins/locked-payment-methods-for-woocommerce/.gitignore b/plugins/locked-payment-methods-for-woocommerce/.gitignore index 4f496cf..543ae90 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.gitignore +++ b/plugins/locked-payment-methods-for-woocommerce/.gitignore @@ -1,6 +1,14 @@ vendor-prefixed tests/.cache -.wp-env.override.json +.wp-env*.override.json + +# Secret material is never tracked, so these match at any depth as a backstop against an accidental git add. +.env* +*.pem +*.key +id_rsa* +.npmrc +auth.json # Created by https://www.toptal.com/developers/gitignore/api/node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode From a2569116d6a8fb22a3ccf2c5862ec26f687899b4 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:12:30 +0100 Subject: [PATCH 03/39] Lint the plugins' configs and enforce each plugin's prefix Each plugin's ESLint, Stylelint and Playwright configs went unlinted (A235), the lint wrappers spread configs' baseline where the template re-exports it (A236), the Playwright config repeated the tests environment's port by hand (A237), and PHPCS did not check that every global starts with the plugin's prefix or sits in its namespace (A239). Each plugin takes the template's three config files and lint:scripts paths, and its phpcs.dist.xml gains the template's PrefixAllGlobals rule beside its own. Linked Orders' DWS_Order_Node and Internal Comments' DWS_Internal_Comment keep v1's names as API (X49, X69), so the class-name sniff skips those two files, since a trailing ignore cannot follow a class's opening brace. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/eslint.config.mjs | 2 +- plugins/internal-comments/package.json | 2 +- plugins/internal-comments/phpcs.dist.xml | 15 +++++++++++++++ plugins/internal-comments/playwright.config.js | 6 ++++-- plugins/internal-comments/stylelint.config.js | 4 +--- .../eslint.config.mjs | 2 +- .../linked-orders-for-woocommerce/package.json | 2 +- .../linked-orders-for-woocommerce/phpcs.dist.xml | 15 +++++++++++++++ .../playwright.config.js | 6 ++++-- .../stylelint.config.js | 4 +--- .../eslint.config.mjs | 2 +- .../package.json | 2 +- .../phpcs.dist.xml | 10 ++++++++++ .../playwright.config.js | 6 ++++-- .../stylelint.config.js | 4 +--- 15 files changed, 61 insertions(+), 21 deletions(-) diff --git a/plugins/internal-comments/eslint.config.mjs b/plugins/internal-comments/eslint.config.mjs index edf6c09..2ef2859 100644 --- a/plugins/internal-comments/eslint.config.mjs +++ b/plugins/internal-comments/eslint.config.mjs @@ -1,3 +1,3 @@ import baseConfig from '@ahegyes/wordpress-configs/node/eslint.config.base.mjs'; -export default [ ...baseConfig ]; +export default baseConfig; diff --git a/plugins/internal-comments/package.json b/plugins/internal-comments/package.json index 59b1e7e..7f59fd6 100644 --- a/plugins/internal-comments/package.json +++ b/plugins/internal-comments/package.json @@ -45,7 +45,7 @@ "start": "npm run start:assets", "start:assets": "wp-scripts start meta-box=./assets/src/meta-box.ts quick-view=./assets/src/quick-view.ts --output-path=assets/build", "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", - "lint:scripts": "wp-scripts lint-js assets/src tests/EndToEnd", + "lint:scripts": "wp-scripts lint-js assets/src tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss'", "lint:types": "tsc", "audit": "composer audit && npm audit", diff --git a/plugins/internal-comments/phpcs.dist.xml b/plugins/internal-comments/phpcs.dist.xml index 9edca1b..9284451 100644 --- a/plugins/internal-comments/phpcs.dist.xml +++ b/plugins/internal-comments/phpcs.dist.xml @@ -25,4 +25,19 @@ + + + + + + + + + + + + + + */models/DWS_Internal_Comment\.php + diff --git a/plugins/internal-comments/playwright.config.js b/plugins/internal-comments/playwright.config.js index 4a295de..e1fdf65 100644 --- a/plugins/internal-comments/playwright.config.js +++ b/plugins/internal-comments/playwright.config.js @@ -1,6 +1,8 @@ const { defineConfig } = require( '@playwright/test' ); -// The port of .wp-env.tests.json, so the suite runs on the production tests environment and leaves the development one's data alone. -const baseConfig = require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { port: 8825 } ); +const baseConfig = + require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { + port: require( './.wp-env.tests.json' ).port, + } ); module.exports = defineConfig( { ...baseConfig, diff --git a/plugins/internal-comments/stylelint.config.js b/plugins/internal-comments/stylelint.config.js index 84c3761..23802a6 100644 --- a/plugins/internal-comments/stylelint.config.js +++ b/plugins/internal-comments/stylelint.config.js @@ -1,3 +1 @@ -const baseConfig = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); - -module.exports = { ...baseConfig }; +module.exports = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); diff --git a/plugins/linked-orders-for-woocommerce/eslint.config.mjs b/plugins/linked-orders-for-woocommerce/eslint.config.mjs index edf6c09..2ef2859 100644 --- a/plugins/linked-orders-for-woocommerce/eslint.config.mjs +++ b/plugins/linked-orders-for-woocommerce/eslint.config.mjs @@ -1,3 +1,3 @@ import baseConfig from '@ahegyes/wordpress-configs/node/eslint.config.base.mjs'; -export default [ ...baseConfig ]; +export default baseConfig; diff --git a/plugins/linked-orders-for-woocommerce/package.json b/plugins/linked-orders-for-woocommerce/package.json index fc72df9..2617ad6 100644 --- a/plugins/linked-orders-for-woocommerce/package.json +++ b/plugins/linked-orders-for-woocommerce/package.json @@ -43,7 +43,7 @@ "start": "npm run start:assets", "start:assets": "wp-scripts start orders-list-table=./assets/src/orders-list-table.ts orders-list-table-icons=./assets/src/orders-list-table-icons.scss --output-path=assets/build", "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", - "lint:scripts": "wp-scripts lint-js assets/src tests/EndToEnd", + "lint:scripts": "wp-scripts lint-js assets/src tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss'", "lint:types": "tsc", "audit": "composer audit && npm audit", diff --git a/plugins/linked-orders-for-woocommerce/phpcs.dist.xml b/plugins/linked-orders-for-woocommerce/phpcs.dist.xml index d541a27..2b54bad 100644 --- a/plugins/linked-orders-for-woocommerce/phpcs.dist.xml +++ b/plugins/linked-orders-for-woocommerce/phpcs.dist.xml @@ -23,4 +23,19 @@ + + + + + + + + + + + + + + */models/DWS_Order_Node\.php + diff --git a/plugins/linked-orders-for-woocommerce/playwright.config.js b/plugins/linked-orders-for-woocommerce/playwright.config.js index 966d4c2..e1fdf65 100644 --- a/plugins/linked-orders-for-woocommerce/playwright.config.js +++ b/plugins/linked-orders-for-woocommerce/playwright.config.js @@ -1,6 +1,8 @@ const { defineConfig } = require( '@playwright/test' ); -// The port of .wp-env.tests.json, so the suite runs on the production tests environment and leaves the development one's data alone. -const baseConfig = require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { port: 8823 } ); +const baseConfig = + require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { + port: require( './.wp-env.tests.json' ).port, + } ); module.exports = defineConfig( { ...baseConfig, diff --git a/plugins/linked-orders-for-woocommerce/stylelint.config.js b/plugins/linked-orders-for-woocommerce/stylelint.config.js index 84c3761..23802a6 100644 --- a/plugins/linked-orders-for-woocommerce/stylelint.config.js +++ b/plugins/linked-orders-for-woocommerce/stylelint.config.js @@ -1,3 +1 @@ -const baseConfig = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); - -module.exports = { ...baseConfig }; +module.exports = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); diff --git a/plugins/locked-payment-methods-for-woocommerce/eslint.config.mjs b/plugins/locked-payment-methods-for-woocommerce/eslint.config.mjs index edf6c09..2ef2859 100644 --- a/plugins/locked-payment-methods-for-woocommerce/eslint.config.mjs +++ b/plugins/locked-payment-methods-for-woocommerce/eslint.config.mjs @@ -1,3 +1,3 @@ import baseConfig from '@ahegyes/wordpress-configs/node/eslint.config.base.mjs'; -export default [ ...baseConfig ]; +export default baseConfig; diff --git a/plugins/locked-payment-methods-for-woocommerce/package.json b/plugins/locked-payment-methods-for-woocommerce/package.json index 6da8c24..027fa1b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/package.json +++ b/plugins/locked-payment-methods-for-woocommerce/package.json @@ -40,7 +40,7 @@ "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", "build": "exit 0", "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", - "lint:scripts": "wp-scripts lint-js tests/EndToEnd", + "lint:scripts": "wp-scripts lint-js tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss' --allow-empty-input", "lint:types": "tsc", "audit": "composer audit && npm audit", diff --git a/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml b/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml index cc94b8b..4a41cb2 100644 --- a/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml +++ b/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml @@ -11,4 +11,14 @@ + + + + + + + + + + diff --git a/plugins/locked-payment-methods-for-woocommerce/playwright.config.js b/plugins/locked-payment-methods-for-woocommerce/playwright.config.js index 59effb6..e1fdf65 100644 --- a/plugins/locked-payment-methods-for-woocommerce/playwright.config.js +++ b/plugins/locked-payment-methods-for-woocommerce/playwright.config.js @@ -1,6 +1,8 @@ const { defineConfig } = require( '@playwright/test' ); -// The port of .wp-env.tests.json, so the suite runs on the production tests environment and leaves the development one's data alone. -const baseConfig = require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { port: 8827 } ); +const baseConfig = + require( '@ahegyes/wordpress-configs/node/playwright.config.base.js' )( { + port: require( './.wp-env.tests.json' ).port, + } ); module.exports = defineConfig( { ...baseConfig, diff --git a/plugins/locked-payment-methods-for-woocommerce/stylelint.config.js b/plugins/locked-payment-methods-for-woocommerce/stylelint.config.js index 84c3761..23802a6 100644 --- a/plugins/locked-payment-methods-for-woocommerce/stylelint.config.js +++ b/plugins/locked-payment-methods-for-woocommerce/stylelint.config.js @@ -1,3 +1 @@ -const baseConfig = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); - -module.exports = { ...baseConfig }; +module.exports = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); From e4ccf786aea75eaf3425d19cd92807d3e0ccfeb1 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:14:16 +0100 Subject: [PATCH 04/39] Give each plugin the template's npm aggregators and audit flags Each plugin's npm scripts lacked the template's format, format:all, lint:all, test:all and quality-check:all aggregators (A687, X318), kept the raw "wp-env" passthrough the template dropped, ran the audit without the supply-chain gate's flags (X199, X319), and sat in one ungrouped list against feedback_script_ordering's groups; the Composer manifests also allowed laravel/serializable-closure 2.1 where the template and every lock require 2.2. Each plugin's scripts follow the template's, in its order and groups, with its own build trees and lint paths; format:all survives PHPCBF's fix status (X283). Each manifest requires serializable-closure ^2.2, and each lock takes the new content hash only. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/composer.json | 2 +- plugins/internal-comments/composer.lock | 2 +- plugins/internal-comments/package.json | 37 +++++++++++++------ .../composer.json | 2 +- .../composer.lock | 2 +- .../package.json | 37 +++++++++++++------ .../composer.json | 2 +- .../composer.lock | 2 +- .../package.json | 31 ++++++++++++---- 9 files changed, 81 insertions(+), 36 deletions(-) diff --git a/plugins/internal-comments/composer.json b/plugins/internal-comments/composer.json index 8b6ca10..e9f81e2 100644 --- a/plugins/internal-comments/composer.json +++ b/plugins/internal-comments/composer.json @@ -51,7 +51,7 @@ "automattic/jetpack-changelogger": "^7", "humbug/php-scoper": "^0.18", "johnpbloch/wordpress-core": "^7.1", - "laravel/serializable-closure": "^2.1", + "laravel/serializable-closure": "^2.2", "php-di/php-di": "^7.1.1", "php-stubs/woocommerce-stubs": "^11.2", "phpunit/phpunit": "^13", diff --git a/plugins/internal-comments/composer.lock b/plugins/internal-comments/composer.lock index bd1eb57..d1045e6 100644 --- a/plugins/internal-comments/composer.lock +++ b/plugins/internal-comments/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d512da19d18941f8475765f30a3ec29", + "content-hash": "f8e215ebdff088fe9cea3a80e85eff68", "packages": [], "packages-dev": [ { diff --git a/plugins/internal-comments/package.json b/plugins/internal-comments/package.json index 7f59fd6..061558f 100644 --- a/plugins/internal-comments/package.json +++ b/plugins/internal-comments/package.json @@ -28,30 +28,45 @@ "setup": "npm run packages-install:all && npm run build && npm run wp-env:start", "packages-install:all": "composer packages-install && npm run packages-install", "packages-update:all": "composer packages-update && npm run packages-update && npm run packages-update:wp", + "packages-install": "npm install", "packages-update": "npm update", "packages-update:wp": "wp-scripts packages-update", - "wp-env": "wp-env", - "wp-env:start": "wp-env start", - "wp-env:stop": "wp-env stop", - "wp-env:update": "wp-env start --update", - "wp-env:destroy": "wp-env destroy", - "wp-env:tests:start": "wp-env --config .wp-env.tests.json start", - "wp-env:tests:stop": "wp-env --config .wp-env.tests.json stop", - "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", - "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + "build": "npm run build:assets", "build:assets": "wp-scripts build meta-box=./assets/src/meta-box.ts quick-view=./assets/src/quick-view.ts --output-path=assets/build", "start": "npm run start:assets", "start:assets": "wp-scripts start meta-box=./assets/src/meta-box.ts quick-view=./assets/src/quick-view.ts --output-path=assets/build", + + "format": "npm run format:scripts && npm run format:styles", + "format:scripts": "wp-scripts format assets/src tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", + "format:styles": "npm run lint:styles -- --fix", + "format:all": "( composer format:php || [ $? -eq 1 ] ) && npm run format", + "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", "lint:scripts": "wp-scripts lint-js assets/src tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss'", "lint:types": "tsc", - "audit": "composer audit && npm audit", + "lint:all": "composer lint:php && npm run lint", + + "wp-env:start": "wp-env start", + "wp-env:stop": "wp-env stop", + "wp-env:update": "wp-env start --update", + "wp-env:destroy": "wp-env destroy", + + "wp-env:tests:start": "wp-env --config .wp-env.tests.json start", + "wp-env:tests:stop": "wp-env --config .wp-env.tests.json stop", + "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", + "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", "check:licenses": "wp-scripts check-licenses", - "test:e2e": "playwright test --config=playwright.config.js" + + "test:e2e": "playwright test --config=playwright.config.js", + "test:all": "composer test && npm run test:e2e", + + "quality-check:all": "composer quality-check && npm run lint && npm run test:e2e" } } diff --git a/plugins/linked-orders-for-woocommerce/composer.json b/plugins/linked-orders-for-woocommerce/composer.json index 3948817..d067767 100644 --- a/plugins/linked-orders-for-woocommerce/composer.json +++ b/plugins/linked-orders-for-woocommerce/composer.json @@ -56,7 +56,7 @@ "automattic/jetpack-changelogger": "^7", "humbug/php-scoper": "^0.18", "johnpbloch/wordpress-core": "^7.1", - "laravel/serializable-closure": "^2.1", + "laravel/serializable-closure": "^2.2", "php-di/php-di": "^7.1.1", "php-stubs/woocommerce-stubs": "^11.2", "phpunit/phpunit": "^13", diff --git a/plugins/linked-orders-for-woocommerce/composer.lock b/plugins/linked-orders-for-woocommerce/composer.lock index be2c224..746d5dc 100644 --- a/plugins/linked-orders-for-woocommerce/composer.lock +++ b/plugins/linked-orders-for-woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4ad0ce56c13b7e17e6c4a34989d5c4b0", + "content-hash": "e171d9ca6844f3f474ef4b6b71dc8c8c", "packages": [], "packages-dev": [ { diff --git a/plugins/linked-orders-for-woocommerce/package.json b/plugins/linked-orders-for-woocommerce/package.json index 2617ad6..00839a5 100644 --- a/plugins/linked-orders-for-woocommerce/package.json +++ b/plugins/linked-orders-for-woocommerce/package.json @@ -26,30 +26,45 @@ "setup": "npm run packages-install:all && npm run build && npm run wp-env:start", "packages-install:all": "composer packages-install && npm run packages-install", "packages-update:all": "composer packages-update && npm run packages-update && npm run packages-update:wp", + "packages-install": "npm install", "packages-update": "npm update", "packages-update:wp": "wp-scripts packages-update", - "wp-env": "wp-env", - "wp-env:start": "wp-env start", - "wp-env:stop": "wp-env stop", - "wp-env:update": "wp-env start --update", - "wp-env:destroy": "wp-env destroy", - "wp-env:tests:start": "wp-env --config .wp-env.tests.json start", - "wp-env:tests:stop": "wp-env --config .wp-env.tests.json stop", - "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", - "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + "build": "npm run build:assets", "build:assets": "wp-scripts build orders-list-table=./assets/src/orders-list-table.ts orders-list-table-icons=./assets/src/orders-list-table-icons.scss --output-path=assets/build", "start": "npm run start:assets", "start:assets": "wp-scripts start orders-list-table=./assets/src/orders-list-table.ts orders-list-table-icons=./assets/src/orders-list-table-icons.scss --output-path=assets/build", + + "format": "npm run format:scripts && npm run format:styles", + "format:scripts": "wp-scripts format assets/src tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", + "format:styles": "npm run lint:styles -- --fix", + "format:all": "( composer format:php || [ $? -eq 1 ] ) && npm run format", + "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", "lint:scripts": "wp-scripts lint-js assets/src tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss'", "lint:types": "tsc", - "audit": "composer audit && npm audit", + "lint:all": "composer lint:php && npm run lint", + + "wp-env:start": "wp-env start", + "wp-env:stop": "wp-env stop", + "wp-env:update": "wp-env start --update", + "wp-env:destroy": "wp-env destroy", + + "wp-env:tests:start": "wp-env --config .wp-env.tests.json start", + "wp-env:tests:stop": "wp-env --config .wp-env.tests.json stop", + "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", + "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", "check:licenses": "wp-scripts check-licenses", - "test:e2e": "playwright test --config=playwright.config.js" + + "test:e2e": "playwright test --config=playwright.config.js", + "test:all": "composer test && npm run test:e2e", + + "quality-check:all": "composer quality-check && npm run lint && npm run test:e2e" } } diff --git a/plugins/locked-payment-methods-for-woocommerce/composer.json b/plugins/locked-payment-methods-for-woocommerce/composer.json index 2e7c36b..abfe16c 100644 --- a/plugins/locked-payment-methods-for-woocommerce/composer.json +++ b/plugins/locked-payment-methods-for-woocommerce/composer.json @@ -56,7 +56,7 @@ "automattic/jetpack-changelogger": "^7", "humbug/php-scoper": "^0.18", "johnpbloch/wordpress-core": "^7.1", - "laravel/serializable-closure": "^2.1", + "laravel/serializable-closure": "^2.2", "php-di/php-di": "^7.1.1", "php-stubs/woocommerce-stubs": "^11.2", "phpunit/phpunit": "^13", diff --git a/plugins/locked-payment-methods-for-woocommerce/composer.lock b/plugins/locked-payment-methods-for-woocommerce/composer.lock index 212e533..41f47d3 100644 --- a/plugins/locked-payment-methods-for-woocommerce/composer.lock +++ b/plugins/locked-payment-methods-for-woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8096846a869e42b7074b39c03e99073f", + "content-hash": "749695d245d7c277078b9c46424489f2", "packages": [], "packages-dev": [ { diff --git a/plugins/locked-payment-methods-for-woocommerce/package.json b/plugins/locked-payment-methods-for-woocommerce/package.json index 027fa1b..0619112 100644 --- a/plugins/locked-payment-methods-for-woocommerce/package.json +++ b/plugins/locked-payment-methods-for-woocommerce/package.json @@ -26,27 +26,42 @@ "setup": "npm run packages-install:all && npm run build && npm run wp-env:start", "packages-install:all": "composer packages-install && npm run packages-install", "packages-update:all": "composer packages-update && npm run packages-update && npm run packages-update:wp", + "packages-install": "npm install", "packages-update": "npm update", "packages-update:wp": "wp-scripts packages-update", - "wp-env": "wp-env", + + "build": "exit 0", + + "format": "npm run format:scripts && npm run format:styles", + "format:scripts": "wp-scripts format tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", + "format:styles": "npm run lint:styles -- --fix", + "format:all": "( composer format:php || [ $? -eq 1 ] ) && npm run format", + + "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", + "lint:scripts": "wp-scripts lint-js tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", + "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss' --allow-empty-input", + "lint:types": "tsc", + "lint:all": "composer lint:php && npm run lint", + "wp-env:start": "wp-env start", "wp-env:stop": "wp-env stop", "wp-env:update": "wp-env start --update", "wp-env:destroy": "wp-env destroy", + "wp-env:tests:start": "wp-env --config .wp-env.tests.json start", "wp-env:tests:stop": "wp-env --config .wp-env.tests.json stop", "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", - "build": "exit 0", - "lint": "npm run lint:scripts && npm run lint:styles && npm run lint:types", - "lint:scripts": "wp-scripts lint-js tests/EndToEnd eslint.config.mjs stylelint.config.js playwright.config.js", - "lint:styles": "wp-scripts lint-style 'assets/src/**/*.scss' --allow-empty-input", - "lint:types": "tsc", - "audit": "composer audit && npm audit", + + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", "check:licenses": "wp-scripts check-licenses", - "test:e2e": "playwright test --config=playwright.config.js" + + "test:e2e": "playwright test --config=playwright.config.js", + "test:all": "composer test && npm run test:e2e", + + "quality-check:all": "composer quality-check && npm run lint && npm run test:e2e" } } From 9b3b5e11a811e483eb1cb41cb79d1f09ec57c11d Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:15:56 +0100 Subject: [PATCH 05/39] Analyse each entry and its safeguards only at their oldest runtime Each plugin analysed its entry with the rest of the plugin at the PHP and WordPress floors, though the entry runs before the floor gate, and its main profile checked WordPress calls against configs' fixed floor rather than the entry's Requires at least (X272's A690, X352, X355); Internal Comments also left its model out of the main profile. Each plugin takes the template's two profiles with its own scan files: phpstan.safeguards.neon analyses the entry and the safeguards at the bootstrap package's oldest runtime, the main profile only scans them, and wp-compat reads the floor from the entry. Internal Comments' model now analysed, its three forwards to WP_Comment by name, v1's API (X69), name the dynamic-access identifiers they need. Assisted-by: Claude Code:claude-opus-5-5 --- .../models/DWS_Internal_Comment.php | 6 +++--- plugins/internal-comments/phpstan.dist.neon | 13 +++++++++++-- plugins/internal-comments/phpstan.safeguards.neon | 15 ++++++++++++++- .../phpstan.dist.neon | 12 ++++++++++-- .../phpstan.safeguards.neon | 15 ++++++++++++++- .../phpstan.dist.neon | 12 ++++++++---- .../phpstan.safeguards.neon | 14 ++++++++++++-- 7 files changed, 72 insertions(+), 15 deletions(-) diff --git a/plugins/internal-comments/models/DWS_Internal_Comment.php b/plugins/internal-comments/models/DWS_Internal_Comment.php index 6c7d96b..09761f4 100644 --- a/plugins/internal-comments/models/DWS_Internal_Comment.php +++ b/plugins/internal-comments/models/DWS_Internal_Comment.php @@ -59,7 +59,7 @@ public function __construct( $comment ) { * @return mixed */ public function __get( string $name ) { - return property_exists( $this->comment, $name ) ? $this->comment->$name : null; + return property_exists( $this->comment, $name ) ? $this->comment->$name : null; // @phpstan-ignore property.dynamicName (v1 forwards every field of the comment by name.) } /** @@ -73,7 +73,7 @@ public function __get( string $name ) { * @return bool */ public function __isset( string $name ): bool { - return isset( $this->comment->$name ); + return isset( $this->comment->$name ); // @phpstan-ignore property.dynamicName (v1 forwards every field of the comment by name.) } /** @@ -88,7 +88,7 @@ public function __isset( string $name ): bool { * @return mixed */ public function __call( string $name, array $args ) { - return method_exists( $this->comment, $name ) ? $this->comment->$name( ...$args ) : null; + return method_exists( $this->comment, $name ) ? $this->comment->$name( ...$args ) : null; // @phpstan-ignore method.dynamicName (v1 forwards every method of the comment by name.) } // endregion diff --git a/plugins/internal-comments/phpstan.dist.neon b/plugins/internal-comments/phpstan.dist.neon index f5fa6c4..2e01d39 100644 --- a/plugins/internal-comments/phpstan.dist.neon +++ b/plugins/internal-comments/phpstan.dist.neon @@ -1,4 +1,4 @@ -# The WordPress extensions are listed here because composer.json keeps them from installing themselves, and before configs' profile because wp-compat's neon resets WPCompat.requiresAtLeast to null. +# The WordPress extensions are listed here because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -7,14 +7,23 @@ includes: parameters: paths: - - internal-comments.php - functions.php - uninstall.php - includes + - models - src + # phpstan.safeguards.neon analyzes the entry and the safeguards at the oldest runtime they parse on, so this profile only scans them. + excludePaths: + - includes/safeguards.php scanFiles: + - internal-comments.php + - includes/safeguards.php - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php - vendor/php-stubs/woocommerce-stubs/woocommerce-packages-stubs.php # The scoped packages load through vendor-prefixed/scoper-autoload.php, which PHPStan does not read. scanDirectories: - vendor-prefixed + # wp-compat reads the floor from the entry's Requires at least only while requiresAtLeast, which configs' profile sets, is null. + WPCompat: + pluginFile: internal-comments.php + requiresAtLeast: null diff --git a/plugins/internal-comments/phpstan.safeguards.neon b/plugins/internal-comments/phpstan.safeguards.neon index e2f6abc..99f3cb7 100644 --- a/plugins/internal-comments/phpstan.safeguards.neon +++ b/plugins/internal-comments/phpstan.safeguards.neon @@ -1,4 +1,4 @@ -# The safeguards load before the floor gate, so they are analyzed at the oldest PHP and WordPress the bootstrap package runs on, which the framework's phpstan.bootstrap.neon sets; the WordPress extensions are listed because composer.json keeps them from installing themselves. +# The entry and the safeguards run before the floor gate, so they are analyzed at the oldest PHP and WordPress the bootstrap package runs on, which the framework's phpstan.bootstrap.neon sets; the WordPress extensions are listed because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -7,7 +7,20 @@ includes: parameters: paths: + - internal-comments.php - includes/safeguards.php + # A safeguard calls WooCommerce where the plugin requires it. + scanFiles: + - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php phpVersion: 70400 WPCompat: requiresAtLeast: '5.2' + # The entry loads the scoped bootstrap package by path, before any autoloader. + scanDirectories: + - vendor-prefixed/ahegyes/wp-framework-bootstrap + ignoreErrors: + # The entry's build-error handler runs only once the floor gate has passed. + - + identifier: WPCompat.functionNotAvailable + message: '#^wp_get_environment_type\(\) #' + path: internal-comments.php diff --git a/plugins/linked-orders-for-woocommerce/phpstan.dist.neon b/plugins/linked-orders-for-woocommerce/phpstan.dist.neon index 4274969..59038ea 100644 --- a/plugins/linked-orders-for-woocommerce/phpstan.dist.neon +++ b/plugins/linked-orders-for-woocommerce/phpstan.dist.neon @@ -1,4 +1,4 @@ -# The WordPress extensions are listed here because composer.json keeps them from installing themselves, and before configs' profile because wp-compat's neon resets WPCompat.requiresAtLeast to null. +# The WordPress extensions are listed here because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -7,13 +7,17 @@ includes: parameters: paths: - - linked-orders-for-woocommerce.php - functions.php - uninstall.php - includes - models - src + # phpstan.safeguards.neon analyzes the entry and the safeguards at the oldest runtime they parse on, so this profile only scans them. + excludePaths: + - includes/safeguards.php scanFiles: + - linked-orders-for-woocommerce.php + - includes/safeguards.php - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php - vendor/php-stubs/woocommerce-stubs/woocommerce-packages-stubs.php - tests/Fixtures/class-wc-seq-order-number-pro.php @@ -21,3 +25,7 @@ parameters: # The scoped packages load through vendor-prefixed/scoper-autoload.php, which PHPStan does not read. scanDirectories: - vendor-prefixed + # wp-compat reads the floor from the entry's Requires at least only while requiresAtLeast, which configs' profile sets, is null. + WPCompat: + pluginFile: linked-orders-for-woocommerce.php + requiresAtLeast: null diff --git a/plugins/linked-orders-for-woocommerce/phpstan.safeguards.neon b/plugins/linked-orders-for-woocommerce/phpstan.safeguards.neon index e2f6abc..6e1f37a 100644 --- a/plugins/linked-orders-for-woocommerce/phpstan.safeguards.neon +++ b/plugins/linked-orders-for-woocommerce/phpstan.safeguards.neon @@ -1,4 +1,4 @@ -# The safeguards load before the floor gate, so they are analyzed at the oldest PHP and WordPress the bootstrap package runs on, which the framework's phpstan.bootstrap.neon sets; the WordPress extensions are listed because composer.json keeps them from installing themselves. +# The entry and the safeguards run before the floor gate, so they are analyzed at the oldest PHP and WordPress the bootstrap package runs on, which the framework's phpstan.bootstrap.neon sets; the WordPress extensions are listed because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -7,7 +7,20 @@ includes: parameters: paths: + - linked-orders-for-woocommerce.php - includes/safeguards.php + # A safeguard calls WooCommerce where the plugin requires it. + scanFiles: + - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php phpVersion: 70400 WPCompat: requiresAtLeast: '5.2' + # The entry loads the scoped bootstrap package by path, before any autoloader. + scanDirectories: + - vendor-prefixed/ahegyes/wp-framework-bootstrap + ignoreErrors: + # The entry's build-error handler runs only once the floor gate has passed. + - + identifier: WPCompat.functionNotAvailable + message: '#^wp_get_environment_type\(\) #' + path: linked-orders-for-woocommerce.php diff --git a/plugins/locked-payment-methods-for-woocommerce/phpstan.dist.neon b/plugins/locked-payment-methods-for-woocommerce/phpstan.dist.neon index 95fdd84..a6aa3dd 100644 --- a/plugins/locked-payment-methods-for-woocommerce/phpstan.dist.neon +++ b/plugins/locked-payment-methods-for-woocommerce/phpstan.dist.neon @@ -1,4 +1,4 @@ -# The WordPress extensions are listed here because composer.json keeps them from installing themselves, and before configs' profile because wp-compat's neon resets WPCompat.requiresAtLeast to null. +# The WordPress extensions are listed here because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -7,20 +7,24 @@ includes: parameters: paths: - - locked-payment-methods-for-woocommerce.php - functions.php - uninstall.php - includes - src - # phpstan.safeguards.neon analyzes the safeguards at the oldest runtime they parse on. + # phpstan.safeguards.neon analyzes the entry and the safeguards at the oldest runtime they parse on, so this profile only scans them. excludePaths: - includes/safeguards.php scanFiles: + - locked-payment-methods-for-woocommerce.php - includes/safeguards.php - - tests/PHPStan/woocommerce-memberships.php - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php - vendor/php-stubs/woocommerce-stubs/woocommerce-packages-stubs.php + - tests/PHPStan/woocommerce-memberships.php # The scoped packages load through vendor-prefixed/scoper-autoload.php, which PHPStan does not read. scanDirectories: - vendor-prefixed - vendor/wp-plugin/groups/lib + # wp-compat reads the floor from the entry's Requires at least only while requiresAtLeast, which configs' profile sets, is null. + WPCompat: + pluginFile: locked-payment-methods-for-woocommerce.php + requiresAtLeast: null diff --git a/plugins/locked-payment-methods-for-woocommerce/phpstan.safeguards.neon b/plugins/locked-payment-methods-for-woocommerce/phpstan.safeguards.neon index 5868d66..9cadf54 100644 --- a/plugins/locked-payment-methods-for-woocommerce/phpstan.safeguards.neon +++ b/plugins/locked-payment-methods-for-woocommerce/phpstan.safeguards.neon @@ -1,4 +1,4 @@ -# The safeguards load before the floor gate, so they are analyzed at the oldest PHP and WordPress the bootstrap package runs on, which the framework's phpstan.bootstrap.neon sets; the WordPress extensions are listed because composer.json keeps them from installing themselves. +# The entry and the safeguards run before the floor gate, so they are analyzed at the oldest PHP and WordPress the bootstrap package runs on, which the framework's phpstan.bootstrap.neon sets; the WordPress extensions are listed because composer.json keeps them from installing themselves. includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon - vendor/johnbillion/wp-compat/extension.neon @@ -7,10 +7,20 @@ includes: parameters: paths: + - locked-payment-methods-for-woocommerce.php - includes/safeguards.php - # The lock filters WooCommerce's gateways, so it calls WooCommerce too. + # A safeguard calls WooCommerce where the plugin requires it. scanFiles: - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php phpVersion: 70400 WPCompat: requiresAtLeast: '5.2' + # The entry loads the scoped bootstrap package by path, before any autoloader. + scanDirectories: + - vendor-prefixed/ahegyes/wp-framework-bootstrap + ignoreErrors: + # The entry's build-error handler runs only once the floor gate has passed. + - + identifier: WPCompat.functionNotAvailable + message: '#^wp_get_environment_type\(\) #' + path: locked-payment-methods-for-woocommerce.php From 5c026bb8b034fdc5cde5df87b63fa443c7973f29 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:25:34 +0100 Subject: [PATCH 06/39] Share the template's test harnesses in every plugin Each plugin's suites repeated their harness: the order-storage trait lived in two folders without the Trait suffix and with an unused caching parameter (A520, A522, A523), fifteen classes hooked and unhooked their own deprecation recorder (A514), fourteen snapshotted and restored option rows by hand, the uninstall and data-guard rows and Linked Orders' second linking request shelled out to WP-CLI beside RunsRequestsTrait (A515, A516), and each end-to-end spec carried its own copy of the WP-CLI helper (A521). Each plugin takes the template's RunsRequestsTrait, with its variadic flags, OrderStorageTrait and RestoresOptionsTrait in tests/Support/, and tests/EndToEnd/cli.ts, unchanged but for the prefix, plus a RecordsDeprecationsTrait that records deprecated functions and hooks and listens on the v1 hooks a class names. The classes use them, and each hook method a class keeps is named for what it does. Linked Orders' two rows that also need the order tables' data caching switch it on through the trait's own filters. LinkingTest's background request stays inline, since it must not wait. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/AGENTS.md | 2 +- .../internal-comments/tests/EndToEnd/cli.ts | 13 +++++ .../tests/EndToEnd/settings.ts | 24 +++------- .../EndToEnd/without-woocommerce.spec.ts | 26 ++-------- .../tests/Integration/AdminScreensTest.php | 13 ++--- .../tests/Integration/CommentsTest.php | 13 ++--- .../tests/Integration/DataGuardsTest.php | 10 ++-- .../tests/Integration/DeprecatedApiTest.php | 26 ++-------- .../tests/Integration/MetaBoxTest.php | 24 ++-------- .../tests/Integration/MigrationsTest.php | 22 +++------ .../tests/Integration/OrdersTest.php | 3 +- .../tests/Integration/RestRoutesTest.php | 22 +++------ .../tests/Integration/SettingsApiTest.php | 35 +++----------- .../tests/Integration/SettingsTest.php | 31 ++++-------- .../tests/Integration/UninstallTest.php | 13 +++-- plugins/internal-comments/tests/README.md | 2 +- .../tests/Support/OrderStorageTrait.php} | 13 ++--- .../Support/RecordsDeprecationsTrait.php | 48 +++++++++++++++++++ .../tests/Support/RestoresOptionsTrait.php | 24 ++++++++++ .../tests/Support/RunsRequestsTrait.php | 9 ++-- .../linked-orders-for-woocommerce/AGENTS.md | 2 +- .../tests/EndToEnd/cli.ts | 13 +++++ .../EndToEnd/without-woocommerce.spec.ts | 26 ++-------- .../tests/Integration/AutocompletionTest.php | 3 +- .../tests/Integration/ChildCreationTest.php | 3 +- .../tests/Integration/DeprecatedApiTest.php | 26 ++-------- .../tests/Integration/DeprecatedHooksTest.php | 19 +++----- .../SequentialOrderNumbersProTest.php | 4 +- .../tests/Integration/LinkingTest.php | 26 +++++++--- .../tests/Integration/ListTableTest.php | 3 +- .../tests/Integration/MetaBoxTest.php | 3 +- .../tests/Integration/MigrationsTest.php | 22 +++------ .../tests/Integration/OptionsTest.php | 23 ++------- .../tests/Integration/SettingsApiTest.php | 35 +++----------- .../tests/Integration/SettingsTest.php | 34 ++++--------- .../tests/Integration/UninstallTest.php | 13 +++-- .../tests/README.md | 2 +- .../tests/Support/OrderStorageTrait.php} | 13 ++--- .../Support/RecordsDeprecationsTrait.php | 48 +++++++++++++++++++ .../tests/Support/RestoresOptionsTrait.php | 24 ++++++++++ .../tests/Support/RunsRequestsTrait.php | 9 ++-- .../AGENTS.md | 2 +- .../tests/EndToEnd/checkout.spec.ts | 14 +----- .../tests/EndToEnd/cli.ts | 13 +++++ .../EndToEnd/without-woocommerce.spec.ts | 26 ++-------- .../tests/Integration/DeprecatedApiTest.php | 36 +++----------- .../tests/Integration/MigrationsTest.php | 19 +++----- .../tests/Integration/OptionsTest.php | 20 ++------ .../tests/Integration/SafeguardsTest.php | 28 ++++------- .../tests/Integration/SettingsTest.php | 34 ++++--------- .../UnlockStrategies/GroupsStrategyTest.php | 17 +++---- .../UnlockStrategies/OrderStrategyTest.php | 20 +++----- .../UserProfileStrategyTest.php | 17 +++---- .../UnlockStrategies/UserRoleStrategyTest.php | 29 ++++------- .../WooCommerceMembershipsStrategyTest.php | 17 +++---- .../tests/README.md | 2 +- ...OrderStorage.php => OrderStorageTrait.php} | 11 +++-- .../Support/RecordsDeprecationsTrait.php | 48 +++++++++++++++++++ .../tests/Support/RestoresOptionsTrait.php | 24 ++++++++++ .../tests/Support/RunsRequestsTrait.php | 9 ++-- 60 files changed, 504 insertions(+), 606 deletions(-) create mode 100644 plugins/internal-comments/tests/EndToEnd/cli.ts rename plugins/{linked-orders-for-woocommerce/tests/Integration/OrderStorage.php => internal-comments/tests/Support/OrderStorageTrait.php} (82%) create mode 100644 plugins/internal-comments/tests/Support/RecordsDeprecationsTrait.php create mode 100644 plugins/internal-comments/tests/Support/RestoresOptionsTrait.php create mode 100644 plugins/linked-orders-for-woocommerce/tests/EndToEnd/cli.ts rename plugins/{internal-comments/tests/Integration/OrderStorage.php => linked-orders-for-woocommerce/tests/Support/OrderStorageTrait.php} (82%) create mode 100644 plugins/linked-orders-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php create mode 100644 plugins/linked-orders-for-woocommerce/tests/Support/RestoresOptionsTrait.php create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/cli.ts rename plugins/locked-payment-methods-for-woocommerce/tests/Support/{OrderStorage.php => OrderStorageTrait.php} (85%) create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Support/RestoresOptionsTrait.php diff --git a/plugins/internal-comments/AGENTS.md b/plugins/internal-comments/AGENTS.md index 108db38..8459701 100644 --- a/plugins/internal-comments/AGENTS.md +++ b/plugins/internal-comments/AGENTS.md @@ -67,7 +67,7 @@ Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI run 5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input. A REST route declares each parameter's type and sanitizer in its `args`. 6. **Options and data.** A setting is read through v1's API in `includes/settings.php`, whose validation is each setting's one typed read and whose reads apply v1's filters, safeguards excepted. v1's settings keep their `dws-internal-comments_` rows and its comments their `dws_internal` type and `dws_ic_is_private` and `_dws_ic_` meta; anything new lives under `dws_ic_`, or `_dws_ic_` for hidden meta. The footprint covers those prefixes and v1's other rows by name. 7. **Internal status.** An internal comment is stored under the `dws_internal` status, which keeps it out of WordPress's queries, counts, feeds and REST API without the plugin's help, so no code stores one under another status, and a query that lists them names the status. `Comments` puts back any that WordPress or another plugin moves, announcing the move as a status change after WordPress announces its own, so caches that follow status changes, such as WooCommerce's comment counts, end where the comment is; the migration's bulk move resets WooCommerce's counts instead. Until the migration has moved the comments v1 stored approved, it leaves the type out of every comment query and feed that names neither it nor the status. Every check of what a user may do with one goes through the `see_`, `edit_` and `trash_dws_internal_comment` meta capabilities. -8. **Order storage.** Order code takes ids WooCommerce resolves in either storage, through `dws_ic_get_post_type()` and `edit_post`, which WooCommerce maps for orders, and WooCommerce's screen ids and list hooks, never the posts table; every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorage`. +8. **Order storage.** Order code takes ids WooCommerce resolves in either storage, through `dws_ic_get_post_type()` and `edit_post`, which WooCommerce maps for orders, and WooCommerce's screen ids and list hooks, never the posts table; every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`. 9. **v1 hooks.** A hook v1 fired keeps its v1 literal, fired first through `apply_filters_deprecated()` or `do_action_deprecated()` with `'2.0.0'` and the v2 name, then the v2 name. 10. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_ic_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks. 11. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails; bulk work is a batched step, which runs only from the runner's scheduled hook. diff --git a/plugins/internal-comments/tests/EndToEnd/cli.ts b/plugins/internal-comments/tests/EndToEnd/cli.ts new file mode 100644 index 0000000..14aabda --- /dev/null +++ b/plugins/internal-comments/tests/EndToEnd/cli.ts @@ -0,0 +1,13 @@ +/// +import { execFileSync } from 'node:child_process'; + +// No screen shows some of what the specs set up or check, so they run WP-CLI on the tests site and read its last line. +export const cli = ( ...args: string[] ): string => + execFileSync( + 'node_modules/.bin/wp-env', + [ '--config', '.wp-env.tests.json', 'run', 'cli', 'wp', ...args ], + { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } + ) + .trim() + .split( '\n' ) + .pop() ?? ''; diff --git a/plugins/internal-comments/tests/EndToEnd/settings.ts b/plugins/internal-comments/tests/EndToEnd/settings.ts index 0f843fe..3490495 100644 --- a/plugins/internal-comments/tests/EndToEnd/settings.ts +++ b/plugins/internal-comments/tests/EndToEnd/settings.ts @@ -1,22 +1,12 @@ -/// -import { execFileSync } from 'node:child_process'; +import { cli } from './cli'; // No REST route exposes the plugin's settings, so the site's WP-CLI stores them. export const storeSettings = ( value: Record< string, unknown > ): void => { - execFileSync( - 'node_modules/.bin/wp-env', - [ - '--config', - '.wp-env.tests.json', - 'run', - 'cli', - 'wp', - 'option', - 'update', - 'dws-internal-comments_general', - JSON.stringify( value ), - '--format=json', - ], - { stdio: 'ignore' } + cli( + 'option', + 'update', + 'dws-internal-comments_general', + JSON.stringify( value ), + '--format=json' ); }; diff --git a/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts b/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts index 35caba3..1abdcd0 100644 --- a/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts +++ b/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts @@ -1,25 +1,5 @@ -/// -import { execFileSync } from 'node:child_process'; import { expect, test } from '@wordpress/e2e-test-utils-playwright'; - -// No page shows the boot's outcome, so the site's WP-CLI reads it. -const bootStatus = (): string => - execFileSync( - 'node_modules/.bin/wp-env', - [ - '--config', - '.wp-env.tests.json', - 'run', - 'cli', - 'wp', - 'eval', - 'echo dws_ic_plugin()->boot->status;', - ], - { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } - ) - .trim() - .split( '\n' ) - .pop() ?? ''; +import { cli } from './cli'; test.describe( 'Without WooCommerce', () => { test.beforeAll( async ( { requestUtils } ) => { @@ -33,7 +13,9 @@ test.describe( 'Without WooCommerce', () => { test( 'the plugin boots and every screen still loads', async ( { page, } ) => { - expect( bootStatus() ).toBe( 'booted' ); + expect( cli( 'eval', 'echo dws_ic_plugin()->boot->status;' ) ).toBe( + 'booted' + ); const front = await page.goto( '/' ); expect( front?.status() ).toBe( 200 ); diff --git a/plugins/internal-comments/tests/Integration/AdminScreensTest.php b/plugins/internal-comments/tests/Integration/AdminScreensTest.php index b48e65a..399dae1 100644 --- a/plugins/internal-comments/tests/Integration/AdminScreensTest.php +++ b/plugins/internal-comments/tests/Integration/AdminScreensTest.php @@ -3,6 +3,7 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; use DeepWebSolutions\InternalComments\AdminScreens; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -10,6 +11,8 @@ #[CoversNothing] final class AdminScreensTest extends TestCase { + use RecordsDeprecationsTrait; + // WordPress reports a deprecated hook only when something listens on it. protected const string V1_PRECEDING_COLUMN = 'internal_comments/wp_table_output/insert_after'; @@ -19,8 +22,6 @@ final class AdminScreensTest extends TestCase { protected mixed $stored; - protected array $deprecated = array(); - #[\Override] public static function setUpBeforeClass(): void { require_once ABSPATH . 'wp-admin/includes/user.php'; @@ -57,8 +58,6 @@ protected function create_post(): void { ) ); wp_set_current_user( self::$users['administrator'] ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); } #[After] @@ -67,12 +66,6 @@ protected function delete_post(): void { update_option( DWS_IC_OPTION_GENERAL, $this->stored ); wp_set_current_user( 0 ); unset( $_GET['comment_type'] ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } public function test_the_posts_list_column_follows_the_comments_column_and_passes_v1s_filter_first(): void { diff --git a/plugins/internal-comments/tests/Integration/CommentsTest.php b/plugins/internal-comments/tests/Integration/CommentsTest.php index 8470099..eee0b43 100644 --- a/plugins/internal-comments/tests/Integration/CommentsTest.php +++ b/plugins/internal-comments/tests/Integration/CommentsTest.php @@ -2,6 +2,7 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -9,12 +10,12 @@ #[CoversNothing] final class CommentsTest extends TestCase { + use RecordsDeprecationsTrait; + protected int $post; protected int $admin; - protected array $deprecated = array(); - #[Before] protected function create_post(): void { require_once ABSPATH . 'wp-admin/includes/user.php'; @@ -34,8 +35,6 @@ protected function create_post(): void { 'post_status' => 'publish', ) ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); } #[After] @@ -43,12 +42,6 @@ protected function delete_post(): void { wp_delete_post( $this->post, true ); wp_set_current_user( 0 ); wp_delete_user( $this->admin ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } public function test_an_internal_comment_keeps_v1s_type_under_its_own_status_by_its_author(): void { diff --git a/plugins/internal-comments/tests/Integration/DataGuardsTest.php b/plugins/internal-comments/tests/Integration/DataGuardsTest.php index cd05298..57f14d5 100644 --- a/plugins/internal-comments/tests/Integration/DataGuardsTest.php +++ b/plugins/internal-comments/tests/Integration/DataGuardsTest.php @@ -2,6 +2,8 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; +use DeepWebSolutions\InternalComments\Plugin; +use DeepWebSolutions\InternalComments\Tests\Support\RunsRequestsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -11,6 +13,8 @@ // Internal comments stay hidden by their status, which WordPress's queries, counts, feeds and REST API leave out without the plugin's help. #[CoversNothing] final class DataGuardsTest extends TestCase { + use RunsRequestsTrait; + protected int $admin; protected int $other; @@ -245,10 +249,10 @@ public function test_wordpress_reads_leave_internal_comments_out_without_the_plu $comment ); - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' --skip-plugins=internal-comments eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The row needs a request without the plugin. + $request = self::request( $php, '--skip-plugins=' . Plugin::SLUG ); - self::assertSame( 0, $status, \implode( "\n", $output ) ); - self::assertSame( wp_json_encode( array( array( $public ), array( $public ), 1, 1, 401, array( $public ) ) ), (string) \end( $output ) ); + self::assertSame( 0, $request['status'], $request['output'] ); + self::assertSame( wp_json_encode( array( array( $public ), array( $public ), 1, 1, 401, array( $public ) ) ), $request['last'], $request['output'] ); } protected function comment( string $content = 'Internal', array $meta = array() ): int { diff --git a/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php b/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php index 6343fb3..366f251 100644 --- a/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php +++ b/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php @@ -2,15 +2,15 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; -use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class DeprecatedApiTest extends TestCase { - protected array $deprecated = array(); + use RecordsDeprecationsTrait; + public static function hook_tags(): array { return array( @@ -28,26 +28,6 @@ public static function hook_tags(): array { ); } - #[Before] - protected function record_deprecations(): void { - add_filter( 'deprecated_function_trigger_error', '__return_false' ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_function_run', array( $this, 'record' ), 10, 2 ); - add_action( 'deprecated_hook_run', array( $this, 'record' ), 10, 2 ); - } - - #[After] - protected function stop_recording(): void { - remove_filter( 'deprecated_function_trigger_error', '__return_false' ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_function_run', array( $this, 'record' ) ); - remove_action( 'deprecated_hook_run', array( $this, 'record' ) ); - } - - public function record( string $name, string $replacement ): void { - $this->deprecated[] = array( $name, $replacement ); - } - public function test_the_v1_getters_answer_from_the_plugin_header(): void { $header = get_file_data( DWS_IC_FILE, diff --git a/plugins/internal-comments/tests/Integration/MetaBoxTest.php b/plugins/internal-comments/tests/Integration/MetaBoxTest.php index de2991e..ef8aa4d 100644 --- a/plugins/internal-comments/tests/Integration/MetaBoxTest.php +++ b/plugins/internal-comments/tests/Integration/MetaBoxTest.php @@ -3,6 +3,7 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; use DeepWebSolutions\InternalComments\MetaBox; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -10,6 +11,8 @@ #[CoversNothing] final class MetaBoxTest extends TestCase { + use RecordsDeprecationsTrait; + protected static array $users = array(); protected int $post; @@ -18,8 +21,6 @@ final class MetaBoxTest extends TestCase { protected mixed $stored; - protected array $deprecated = array(); - // WordPress reports a deprecated hook only when something listens on it. protected const array V1_HOOKS = array( 'internal_comments/metabox_output/comment_classes', @@ -75,11 +76,7 @@ protected function create_posts(): void { ) ); } - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); - foreach ( self::V1_HOOKS as $hook ) { - add_filter( $hook, array( $this, 'listen' ) ); - } + $this->listen_on( ...self::V1_HOOKS ); } #[After] @@ -92,19 +89,6 @@ protected function delete_posts(): void { wp_set_current_user( 0 ); wp_dequeue_script( 'internal-comments-meta-box' ); wp_dequeue_style( 'internal-comments-meta-box' ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - foreach ( self::V1_HOOKS as $hook ) { - remove_filter( $hook, array( $this, 'listen' ) ); - } - } - - public function listen( mixed $value = null ): mixed { - return $value; - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } public function test_the_meta_box_shows_on_a_selected_post_type_to_users_with_the_capability(): void { diff --git a/plugins/internal-comments/tests/Integration/MigrationsTest.php b/plugins/internal-comments/tests/Integration/MigrationsTest.php index b36f56d..20097dc 100644 --- a/plugins/internal-comments/tests/Integration/MigrationsTest.php +++ b/plugins/internal-comments/tests/Integration/MigrationsTest.php @@ -5,6 +5,7 @@ use DeepWebSolutions\InternalComments\Plugin; use DeepWebSolutions\InternalComments\Scoped\DeepWebSolutions\Framework\Core\Migration\BatchedMigrationStepInterface; use DeepWebSolutions\InternalComments\Scoped\DeepWebSolutions\Framework\Core\Migration\MigrationRunner; +use DeepWebSolutions\InternalComments\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -12,23 +13,12 @@ #[CoversNothing] final class MigrationsTest extends TestCase { - protected const array ROWS = array( DWS_IC_OPTION_GENERAL, DWS_IC_OPTION_PLUGIN ); + use RestoresOptionsTrait; - protected array $stored = array(); - - #[\Override] - protected function setUp(): void { - foreach ( self::ROWS as $row ) { - $this->stored[ $row ] = get_option( $row ); - } - } + protected const array OPTIONS = array( DWS_IC_OPTION_GENERAL, DWS_IC_OPTION_PLUGIN ); #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $row => $value ) { - false === $value ? delete_option( $row ) : update_option( $row, $value ); - } - + protected function restore_the_revision(): void { update_option( Plugin::PREFIX . '_revision', \count( dws_ic_get_migration_steps() ) ); } @@ -36,7 +26,7 @@ protected function restore_rows(): void { #[RunInSeparateProcess] public function test_the_runner_brings_a_new_site_to_the_latest_revision_with_v1s_defaults_autoloaded(): void { global $wpdb; - foreach ( self::ROWS as $row ) { + foreach ( self::OPTIONS as $row ) { delete_option( $row ); } update_option( Plugin::PREFIX . '_revision', 0 ); @@ -44,7 +34,7 @@ public function test_the_runner_brings_a_new_site_to_the_latest_revision_with_v1 do_action( Plugin::PREFIX . '_revision/migration/run' ); // A batched step runs only from the runner's scheduled hook. self::assertSame( \count( dws_ic_get_migration_steps() ), MigrationRunner::get_revision( Plugin::PREFIX . '_revision' ) ); - foreach ( self::ROWS as $row ) { + foreach ( self::OPTIONS as $row ) { self::assertSame( DWS_IC_SETTING_DEFAULTS[ $row ], get_option( $row ) ); self::assertSame( 'on', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s", $row ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Reads the stored autoload flag. } diff --git a/plugins/internal-comments/tests/Integration/OrdersTest.php b/plugins/internal-comments/tests/Integration/OrdersTest.php index 8a110f1..c0e7bf8 100644 --- a/plugins/internal-comments/tests/Integration/OrdersTest.php +++ b/plugins/internal-comments/tests/Integration/OrdersTest.php @@ -5,6 +5,7 @@ use Automattic\WooCommerce\Utilities\FeaturesUtil; use DeepWebSolutions\InternalComments\AdminScreens; use DeepWebSolutions\InternalComments\MetaBox; +use DeepWebSolutions\InternalComments\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -14,7 +15,7 @@ // Orders live in WooCommerce's order tables, in the posts table, or in both, so each row runs on every storage. #[CoversNothing] final class OrdersTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; protected static array $users = array(); diff --git a/plugins/internal-comments/tests/Integration/RestRoutesTest.php b/plugins/internal-comments/tests/Integration/RestRoutesTest.php index a3629d9..2d4202c 100644 --- a/plugins/internal-comments/tests/Integration/RestRoutesTest.php +++ b/plugins/internal-comments/tests/Integration/RestRoutesTest.php @@ -2,6 +2,7 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -9,6 +10,8 @@ #[CoversNothing] final class RestRoutesTest extends TestCase { + use RecordsDeprecationsTrait; + protected static array $users = array(); protected int $post; @@ -69,11 +72,7 @@ protected function create_posts(): void { ) ); } - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); - foreach ( self::V1_HOOKS as $hook ) { - add_filter( $hook, array( $this, 'listen' ) ); - } + $this->listen_on( ...self::V1_HOOKS ); } #[After] @@ -82,19 +81,10 @@ protected function delete_posts(): void { wp_delete_post( $this->page, true ); update_option( DWS_IC_OPTION_GENERAL, $this->stored ); wp_set_current_user( 0 ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - foreach ( self::V1_HOOKS as $hook ) { - remove_filter( $hook, array( $this, 'listen' ) ); - } - } - - public function listen( mixed $value = null ): mixed { - return $value; } - public function record_deprecated_hook( string $hook ): void { - $this->seen[] = "deprecated:$hook"; + public function record_deprecation( string $name, string $replacement ): void { + $this->seen[] = "deprecated:$name"; } public function test_posting_a_comment_stores_its_text_and_answers_with_the_list(): void { diff --git a/plugins/internal-comments/tests/Integration/SettingsApiTest.php b/plugins/internal-comments/tests/Integration/SettingsApiTest.php index fdb3595..bcf0154 100644 --- a/plugins/internal-comments/tests/Integration/SettingsApiTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsApiTest.php @@ -2,19 +2,18 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; -use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\InternalComments\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class SettingsApiTest extends TestCase { - protected const array ROWS = array( DWS_IC_OPTION_GENERAL, DWS_IC_OPTION_PLUGIN ); + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; - protected array $stored = array(); - - protected array $deprecated = array(); + protected const array OPTIONS = array( DWS_IC_OPTION_GENERAL, DWS_IC_OPTION_PLUGIN ); public static function stored_settings(): array { return array( @@ -30,28 +29,6 @@ public static function stored_settings(): array { ); } - #[Before] - protected function store_rows(): void { - foreach ( self::ROWS as $row ) { - $this->stored[ $row ] = get_option( $row ); - } - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); - } - - #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $row => $value ) { - false === $value ? delete_option( $row ) : update_option( $row, $value ); - } - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); - } - #[DataProvider( 'stored_settings' )] public function test_a_setting_reads_from_its_v1_row_validated_by_its_field( string $row, string $field, mixed $stored, string $field_id, ?string $group, array|bool $expected ): void { update_option( $row, array( $field => $stored ) ); @@ -61,7 +38,7 @@ public function test_a_setting_reads_from_its_v1_row_validated_by_its_field( str } public function test_missing_settings_read_as_v1_defaults(): void { - foreach ( self::ROWS as $row ) { + foreach ( self::OPTIONS as $row ) { delete_option( $row ); } diff --git a/plugins/internal-comments/tests/Integration/SettingsTest.php b/plugins/internal-comments/tests/Integration/SettingsTest.php index 772d65a..fdff2c9 100644 --- a/plugins/internal-comments/tests/Integration/SettingsTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsTest.php @@ -4,6 +4,8 @@ use DeepWebSolutions\InternalComments\Scoped\DeepWebSolutions\Framework\Settings\AdminPage\OptionsPage; use DeepWebSolutions\InternalComments\Settings; +use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\InternalComments\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -12,13 +14,12 @@ #[CoversNothing] final class SettingsTest extends TestCase { - protected const array ROWS = array( DWS_IC_OPTION_GENERAL, DWS_IC_OPTION_PLUGIN ); + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; - protected static OptionsPage $page; - - protected array $stored = array(); + protected const array OPTIONS = array( DWS_IC_OPTION_GENERAL, DWS_IC_OPTION_PLUGIN ); - protected array $deprecated = array(); + protected static OptionsPage $page; #[\Override] public static function setUpBeforeClass(): void { @@ -29,7 +30,7 @@ public static function setUpBeforeClass(): void { #[\Override] public static function tearDownAfterClass(): void { - foreach ( self::ROWS as $row ) { + foreach ( self::OPTIONS as $row ) { remove_filter( "pre_update_option_$row", array( self::$page, 'sanitize_submission' ) ); remove_filter( "option_page_capability_$row", array( self::$page, 'get_capability' ) ); unregister_setting( $row, $row ); @@ -44,27 +45,13 @@ public static function setting_groups(): array { } #[Before] - protected function store_rows(): void { - foreach ( self::ROWS as $row ) { - $this->stored[ $row ] = get_option( $row ); - } + protected function forget_the_settings_errors(): void { $GLOBALS['wp_settings_errors'] = array(); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); } #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $row => $value ) { - false === $value ? delete_option( $row ) : update_option( $row, $value ); - } + protected function forget_the_tab(): void { unset( $_GET['tab'] ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } public function test_the_page_keeps_v1s_slug_parent_capability_and_tabs(): void { diff --git a/plugins/internal-comments/tests/Integration/UninstallTest.php b/plugins/internal-comments/tests/Integration/UninstallTest.php index b74ba57..f38e103 100644 --- a/plugins/internal-comments/tests/Integration/UninstallTest.php +++ b/plugins/internal-comments/tests/Integration/UninstallTest.php @@ -3,6 +3,7 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; use DeepWebSolutions\InternalComments\Plugin; +use DeepWebSolutions\InternalComments\Tests\Support\RunsRequestsTrait; use PHPUnit\Framework\Attributes\AfterClass; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -10,6 +11,8 @@ #[CoversNothing] final class UninstallTest extends TestCase { + use RunsRequestsTrait; + protected const int USER = 1; #[AfterClass] @@ -40,16 +43,16 @@ public function test_an_uninstall_in_a_request_that_skips_the_plugin_removes_its update_option( DWS_IC_OPTION_PLUGIN, array( DWS_IC_SETTING_DELETE_DATA => 'yes' ) ); $php = 'uninstall_plugin( "internal-comments/internal-comments.php" ); echo wp_json_encode( array( defined( "DWS_IC_FILE" ), get_option( "' . DWS_IC_OPTION_PLUGIN . '" ) ) );'; - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' --skip-plugins=internal-comments eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The uninstall needs a request without the plugin. + $request = self::request( $php, '--skip-plugins=' . Plugin::SLUG ); // The other tests share the database, so a request with the plugin stores again what a new site starts with, as the site's next request would. - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval ' . \escapeshellarg( 'do_action( "' . Plugin::PREFIX . '_revision/migration/run" );' ) . ' 2>&1', $restored, $restore_status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The restore needs a request with the plugin. + $restored = self::request( 'do_action( "' . Plugin::PREFIX . '_revision/migration/run" );' ); wp_cache_flush(); wp_roles()->for_site(); - self::assertSame( 0, $status, \implode( "\n", $output ) ); - self::assertSame( '[false,false]', (string) \end( $output ) ); - self::assertSame( 0, $restore_status, \implode( "\n", $restored ) ); + self::assertSame( 0, $request['status'], $request['output'] ); + self::assertSame( '[false,false]', $request['last'], $request['output'] ); + self::assertSame( 0, $restored['status'], $restored['output'] ); self::assertSame( DWS_IC_SETTING_DEFAULTS[ DWS_IC_OPTION_PLUGIN ], get_option( DWS_IC_OPTION_PLUGIN ) ); self::assertTrue( get_role( 'editor' )?->has_cap( DWS_IC_CAP_INSERT ) ); } diff --git a/plugins/internal-comments/tests/README.md b/plugins/internal-comments/tests/README.md index 76d87c0..11bded6 100644 --- a/plugins/internal-comments/tests/README.md +++ b/plugins/internal-comments/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data and capabilities, and the readme's version and floors | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, v1's keys, settings page and API, comment API, model class, capabilities and deprecated functions and hooks, the meta capabilities and private threads, the guards that keep internal comments under their status and the reads that leave them out without the plugin, the REST routes, the meta box, the list column, Comments screen and edit screen, orders, the migrations and the batched move of v1's comments, uninstall after v1's consent, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorage` | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, v1's keys, settings page and API, comment API, model class, capabilities and deprecated functions and hooks, the meta capabilities and private threads, the guards that keep internal comments under their status and the reads that leave them out without the plugin, the REST routes, the meta box, the list column, Comments screen and edit screen, orders, the migrations and the batched move of v1's comments, uninstall after v1's consent, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | posting, replying to, editing and trashing comments in the meta box, on a post and on an order, an empty comment refused, the quick view and the Comments screen's view, axe on the meta box and the quick view, and the plugin with WooCommerce deactivated | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/OrderStorage.php b/plugins/internal-comments/tests/Support/OrderStorageTrait.php similarity index 82% rename from plugins/linked-orders-for-woocommerce/tests/Integration/OrderStorage.php rename to plugins/internal-comments/tests/Support/OrderStorageTrait.php index b64ad7a..c48e732 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/OrderStorage.php +++ b/plugins/internal-comments/tests/Support/OrderStorageTrait.php @@ -1,11 +1,11 @@ storage_filters = array( 'pre_option_woocommerce_custom_orders_table_enabled' => static fn (): string => $hpos ? 'yes' : 'no', 'pre_option_woocommerce_custom_orders_table_data_sync_enabled' => static fn (): string => $sync ? 'yes' : 'no', - 'pre_option_woocommerce_hpos_datastore_caching_enabled' => static fn (): string => $caching ? 'yes' : 'no', + 'pre_option_woocommerce_hpos_datastore_caching_enabled' => static fn (): string => 'no', ); foreach ( $this->storage_filters as $hook => $filter ) { add_filter( $hook, $filter ); @@ -34,7 +34,6 @@ protected function create_order( array $args = array() ): \WC_Order { $order = wc_create_order( $args + array( 'status' => 'pending' ) ); self::assertInstanceOf( \WC_Order::class, $order ); $this->orders[] = $order->get_id(); - return $order; } @@ -47,11 +46,13 @@ protected function delete_orders_and_restore_storage(): void { $order->delete( true ); } } + $this->orders = array(); foreach ( $this->storage_filters as $hook => $filter ) { remove_filter( $hook, $filter ); } + $this->storage_filters = array(); } } diff --git a/plugins/internal-comments/tests/Support/RecordsDeprecationsTrait.php b/plugins/internal-comments/tests/Support/RecordsDeprecationsTrait.php new file mode 100644 index 0000000..3764ad8 --- /dev/null +++ b/plugins/internal-comments/tests/Support/RecordsDeprecationsTrait.php @@ -0,0 +1,48 @@ +listened as $hook ) { + remove_filter( $hook, array( $this, 'pass_through' ) ); + } + + remove_action( 'deprecated_hook_run', array( $this, 'record_deprecation' ) ); + remove_action( 'deprecated_function_run', array( $this, 'record_deprecation' ) ); + remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); + remove_filter( 'deprecated_function_trigger_error', '__return_false' ); + } + + public function record_deprecation( string $name, string $replacement ): void { + $this->deprecated[] = array( $name, $replacement ); + } + + // WordPress reports a deprecated hook only when something listens on it. + protected function listen_on( string ...$hooks ): void { + foreach ( $hooks as $hook ) { + add_filter( $hook, array( $this, 'pass_through' ) ); + $this->listened[] = $hook; + } + } + + public function pass_through( mixed $value = null ): mixed { + return $value; + } +} diff --git a/plugins/internal-comments/tests/Support/RestoresOptionsTrait.php b/plugins/internal-comments/tests/Support/RestoresOptionsTrait.php new file mode 100644 index 0000000..225eef4 --- /dev/null +++ b/plugins/internal-comments/tests/Support/RestoresOptionsTrait.php @@ -0,0 +1,24 @@ +stored_options[ $key ] = get_option( $key ); + } + } + + #[After] + protected function restore_options(): void { + foreach ( $this->stored_options as $key => $value ) { + false === $value ? delete_option( $key ) : update_option( $key, $value ); + } + } +} diff --git a/plugins/internal-comments/tests/Support/RunsRequestsTrait.php b/plugins/internal-comments/tests/Support/RunsRequestsTrait.php index db63826..d5a82a5 100644 --- a/plugins/internal-comments/tests/Support/RunsRequestsTrait.php +++ b/plugins/internal-comments/tests/Support/RunsRequestsTrait.php @@ -3,13 +3,10 @@ namespace DeepWebSolutions\InternalComments\Tests\Support; trait RunsRequestsTrait { - /** - * @return array{status: int, output: string, last: string} - */ - protected static function request( string $php ): array { - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. + protected static function request( string $php, string ...$flags ): array { + $arguments = \implode( '', \array_map( static fn ( string $flag ): string => ' ' . \escapeshellarg( $flag ), $flags ) ); + \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . "$arguments eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); - return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/linked-orders-for-woocommerce/AGENTS.md b/plugins/linked-orders-for-woocommerce/AGENTS.md index e16d4dc..b2b13ce 100644 --- a/plugins/linked-orders-for-woocommerce/AGENTS.md +++ b/plugins/linked-orders-for-woocommerce/AGENTS.md @@ -66,7 +66,7 @@ Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI run 4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. It reads a template-built base plugin's slug, name and version only through that plugin's `{prefix}_get_plugin_*()` getters, from `plugins_loaded` on, since the add-on may load first, and behind `function_exists()`, since a base plugin below its floors or with a partial build may stay active without defining them. 5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input. 6. **Options and data.** A setting is read through v1's API in `includes/settings.php`, whose validation is each setting's one typed read and whose reads apply v1's filters, safeguards excepted. v1's settings keep their `dws-lowc_` keys and its order links their `_dws_lo_` meta; anything new lives under `dws_lowc_`, or `_dws_lowc_` for hidden meta. The footprint covers those prefixes and v1's other rows by name. -7. **Order storage.** Order code goes through WC CRUD, `get_edit_order_url()` and WooCommerce's screen ids and list hooks, never the posts table, and every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorage`: posts and the order tables, each with the sync on and off. Rows that check only the order of hooks, or a stand-in plugin, use one. +7. **Order storage.** Order code goes through WC CRUD, `get_edit_order_url()` and WooCommerce's screen ids and list hooks, never the posts table, and every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`: posts and the order tables, each with the sync on and off. Rows that check only the order of hooks, or a stand-in plugin, use one. 8. **v1 hooks.** A hook v1 fired keeps its v1 literal, fired first through `apply_filters_deprecated()` or `do_action_deprecated()` with `'2.0.0'` and the v2 name, then the v2 name, and `DeprecatedHooksTest` names the pair. 9. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_lowc_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks. 10. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails. diff --git a/plugins/linked-orders-for-woocommerce/tests/EndToEnd/cli.ts b/plugins/linked-orders-for-woocommerce/tests/EndToEnd/cli.ts new file mode 100644 index 0000000..14aabda --- /dev/null +++ b/plugins/linked-orders-for-woocommerce/tests/EndToEnd/cli.ts @@ -0,0 +1,13 @@ +/// +import { execFileSync } from 'node:child_process'; + +// No screen shows some of what the specs set up or check, so they run WP-CLI on the tests site and read its last line. +export const cli = ( ...args: string[] ): string => + execFileSync( + 'node_modules/.bin/wp-env', + [ '--config', '.wp-env.tests.json', 'run', 'cli', 'wp', ...args ], + { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } + ) + .trim() + .split( '\n' ) + .pop() ?? ''; diff --git a/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts b/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts index 8d95c9b..848fa88 100644 --- a/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts +++ b/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts @@ -1,30 +1,10 @@ -/// -import { execFileSync } from 'node:child_process'; import { expect, test } from '@wordpress/e2e-test-utils-playwright'; +import { cli } from './cli'; // WordPress core's own message for a plugin whose Requires Plugins header lists an inactive plugin. const DEPENDENCY = 'Linked Orders for WooCommerce requires 1 plugin to be installed and activated: WooCommerce.'; -// No page shows the boot's outcome, so the site's WP-CLI reads it. -const bootStatus = (): string => - execFileSync( - 'node_modules/.bin/wp-env', - [ - '--config', - '.wp-env.tests.json', - 'run', - 'cli', - 'wp', - 'eval', - 'echo dws_lowc_plugin()->boot->status;', - ], - { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } - ) - .trim() - .split( '\n' ) - .pop() ?? ''; - test.describe( 'Without WooCommerce', () => { test.beforeAll( async ( { requestUtils } ) => { await requestUtils.deactivatePlugin( 'woocommerce' ); @@ -37,7 +17,9 @@ test.describe( 'Without WooCommerce', () => { test( 'the plugin stays blocked, WordPress names the missing plugin and every screen still loads', async ( { page, } ) => { - expect( bootStatus() ).toBe( 'blocked' ); + expect( cli( 'eval', 'echo dws_lowc_plugin()->boot->status;' ) ).toBe( + 'blocked' + ); const front = await page.goto( '/' ); expect( front?.status() ).toBe( 200 ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php index 54f83f5..33c4601 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php @@ -2,6 +2,7 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -10,7 +11,7 @@ #[CoversNothing] final class AutocompletionTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; protected mixed $stored = false; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php index f402037..e9e1dbe 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php @@ -7,6 +7,7 @@ use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\Notice; use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\NoticeScope; use DeepWebSolutions\LinkedOrders\Scoped\Psr\Log\NullLogger; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -15,7 +16,7 @@ #[CoversNothing] final class ChildCreationTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; protected const string ACTION = 'admin_post_dws_lowc_create_linked_child'; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php index ad5fa65..8a68e11 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php @@ -2,15 +2,15 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; -use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; +use DeepWebSolutions\LinkedOrders\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class DeprecatedApiTest extends TestCase { - protected array $deprecated = array(); + use RecordsDeprecationsTrait; + public static function hook_tags(): array { return array( @@ -25,26 +25,6 @@ public static function hook_tags(): array { ); } - #[Before] - protected function record_deprecations(): void { - add_filter( 'deprecated_function_trigger_error', '__return_false' ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_function_run', array( $this, 'record' ), 10, 2 ); - add_action( 'deprecated_hook_run', array( $this, 'record' ), 10, 2 ); - } - - #[After] - protected function stop_recording(): void { - remove_filter( 'deprecated_function_trigger_error', '__return_false' ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_function_run', array( $this, 'record' ) ); - remove_action( 'deprecated_hook_run', array( $this, 'record' ) ); - } - - public function record( string $name, string $replacement ): void { - $this->deprecated[] = array( $name, $replacement ); - } - public function test_the_v1_getters_answer_from_the_plugin_header(): void { $header = get_file_data( DWS_LOWC_FILE, diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php index e346bd0..dcc2496 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php @@ -2,6 +2,8 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; +use DeepWebSolutions\LinkedOrders\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -10,9 +12,8 @@ #[CoversNothing] final class DeprecatedHooksTest extends TestCase { - use OrderStorage; - - protected array $deprecated = array(); + use OrderStorageTrait; + use RecordsDeprecationsTrait; public static function renamed_hooks(): array { return array( @@ -32,24 +33,16 @@ public static function renamed_hooks(): array { } #[Before] - protected function record_deprecations(): void { + protected function log_in(): void { wp_set_current_user( 1 ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); } #[After] - protected function stop_recording(): void { + protected function reset_the_screen(): void { global $wp_meta_boxes; $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Resets what the rows registered. unset( $GLOBALS['current_screen'] ); wp_set_current_user( 0 ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } #[DataProvider( 'renamed_hooks' )] diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php index 6836cd1..02d5e85 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php @@ -3,14 +3,14 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration\Integration; use DeepWebSolutions\LinkedOrders\Integration\SequentialOrderNumbersPro; -use DeepWebSolutions\LinkedOrders\Tests\Integration\OrderStorage; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; #[CoversNothing] final class SequentialOrderNumbersProTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; public function test_the_integration_loads_only_beside_the_plugin(): void { self::assertFalse( SequentialOrderNumbersPro::should_load() ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php index 8b8aed7..f900996 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php @@ -4,6 +4,8 @@ use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\Storage\Lock; use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\Storage\LockClaim; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; +use DeepWebSolutions\LinkedOrders\Tests\Support\RunsRequestsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; @@ -12,7 +14,8 @@ #[CoversNothing] final class LinkingTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; + use RunsRequestsTrait; protected int $user = 0; @@ -55,7 +58,7 @@ public function test_a_link_stores_the_childs_parent_and_depth_and_the_parents_c // The parent's children are one meta value that every link reads and rewrites, so a link must not write back a list another request changed after this one loaded the parent. #[DataProvider( 'storages_and_data_caching' )] public function test_a_link_keeps_a_child_another_request_linked_after_the_parent_was_loaded( bool $hpos, bool $sync, bool $caching = false ): void { - $this->use_storage( $hpos, $sync, $caching ); + $this->use_storage_and_data_caching( $hpos, $sync, $caching ); $parent = $this->create_order(); $first = $this->create_order(); $second = $this->create_order(); @@ -74,7 +77,7 @@ public function test_a_link_keeps_a_child_another_request_linked_after_the_paren // A child another request linked after this one loaded it must be refused, so the child's meta is read past the caches too. #[DataProvider( 'storages_and_data_caching' )] public function test_a_link_is_refused_for_a_child_another_request_linked_after_the_child_was_loaded( bool $hpos, bool $sync, bool $caching = false ): void { - $this->use_storage( $hpos, $sync, $caching ); + $this->use_storage_and_data_caching( $hpos, $sync, $caching ); $parent = $this->create_order(); $other = $this->create_order(); $child = $this->create_order(); @@ -369,12 +372,21 @@ public static function storages_and_data_caching(): array { return self::storages() + array( 'hpos-with-data-caching' => array( true, false, true ) ); } + // The trait keeps the order tables' data caching off, which these rows also need on. + protected function use_storage_and_data_caching( bool $hpos, bool $sync, bool $caching ): void { + $this->use_storage( $hpos, $sync ); + $hook = 'pre_option_woocommerce_hpos_datastore_caching_enabled'; + remove_filter( $hook, $this->storage_filters[ $hook ] ); + $this->storage_filters[ $hook ] = static fn (): string => $caching ? 'yes' : 'no'; + add_filter( $hook, $this->storage_filters[ $hook ] ); + } + // The other request runs on the row's storage, as the shared tests site stores orders on the order tables. protected function link_in_another_request( bool $hpos, bool $sync, int $parent_id, int $child_id ): void { - $php = \sprintf( 'add_filter( "pre_option_woocommerce_custom_orders_table_enabled", fn () => "%s" ); add_filter( "pre_option_woocommerce_custom_orders_table_data_sync_enabled", fn () => "%s" ); echo var_export( dws_lowc_link_orders( %d, %d ), true );', $hpos ? 'yes' : 'no', $sync ? 'yes' : 'no', $parent_id, $child_id ); - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The race needs a second request. + $php = \sprintf( 'add_filter( "pre_option_woocommerce_custom_orders_table_enabled", fn () => "%s" ); add_filter( "pre_option_woocommerce_custom_orders_table_data_sync_enabled", fn () => "%s" ); echo var_export( dws_lowc_link_orders( %d, %d ), true );', $hpos ? 'yes' : 'no', $sync ? 'yes' : 'no', $parent_id, $child_id ); + $request = self::request( $php ); - self::assertSame( 0, $status, \implode( "\n", $output ) ); - self::assertSame( 'true', (string) \end( $output ) ); + self::assertSame( 0, $request['status'], $request['output'] ); + self::assertSame( 'true', $request['last'], $request['output'] ); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php index 10d8a6b..848aad9 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php @@ -4,6 +4,7 @@ use Automattic\WooCommerce\Utilities\OrderUtil; use DeepWebSolutions\LinkedOrders\Plugin; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; @@ -13,7 +14,7 @@ #[CoversNothing] final class ListTableTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; protected const array COLUMNS = array( 'cb' => '', diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php index b68da96..9ed8d14 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php @@ -3,6 +3,7 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; use Automattic\WooCommerce\Utilities\OrderUtil; +use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; @@ -12,7 +13,7 @@ #[CoversNothing] final class MetaBoxTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; protected int $user = 0; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MigrationsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MigrationsTest.php index 844a7e0..877dfcf 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MigrationsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MigrationsTest.php @@ -4,6 +4,7 @@ use DeepWebSolutions\LinkedOrders\Plugin; use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\Migration\MigrationRunner; +use DeepWebSolutions\LinkedOrders\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -11,23 +12,12 @@ #[CoversNothing] final class MigrationsTest extends TestCase { - protected const array KEYS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); + use RestoresOptionsTrait; - protected array $stored = array(); - - #[\Override] - protected function setUp(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - } + protected const array OPTIONS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } - + protected function restore_the_revision(): void { update_option( Plugin::PREFIX . '_revision', \count( dws_lowc_get_migration_steps() ) ); } @@ -35,7 +25,7 @@ protected function restore_rows(): void { #[RunInSeparateProcess] public function test_the_runner_brings_a_new_site_to_revision_one_with_the_untranslated_defaults_autoloaded(): void { global $wpdb; - foreach ( self::KEYS as $key ) { + foreach ( self::OPTIONS as $key ) { delete_option( $key ); } update_option( Plugin::PREFIX . '_revision', 0 ); @@ -46,7 +36,7 @@ public function test_the_runner_brings_a_new_site_to_revision_one_with_the_untra self::assertSame( '1', get_option( DWS_LOWC_OPTION_MAX_DEPTH ) ); self::assertSame( 'no', get_option( DWS_LOWC_OPTION_AUTOCOMPLETE ) ); self::assertSame( 'no', get_option( DWS_LOWC_OPTION_DELETE_DATA ) ); - foreach ( self::KEYS as $key ) { + foreach ( self::OPTIONS as $key ) { self::assertSame( 'on', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s", $key ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Reads the stored autoload flag. } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/OptionsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/OptionsTest.php index 1353ca7..4aae309 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/OptionsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/OptionsTest.php @@ -2,17 +2,16 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; -use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; +use DeepWebSolutions\LinkedOrders\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class OptionsTest extends TestCase { - protected const array RESTORED = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); + use RestoresOptionsTrait; - protected array $stored = array(); + protected const array OPTIONS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); public static function stored_depths(): array { return array( @@ -36,20 +35,6 @@ public static function submitted_depths(): array { ); } - #[Before] - protected function store_rows(): void { - foreach ( self::RESTORED as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - } - - #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } - } - public function test_the_settings_keep_the_keys_v1_stored_them_under(): void { self::assertSame( 'dws-lowc_general_max-depth', DWS_LOWC_OPTION_MAX_DEPTH ); self::assertSame( 'dws-lowc_general_autocomplete-descendants', DWS_LOWC_OPTION_AUTOCOMPLETE ); @@ -57,7 +42,7 @@ public function test_the_settings_keep_the_keys_v1_stored_them_under(): void { } public function test_a_missing_depth_reads_as_v1s_default(): void { - foreach ( self::RESTORED as $key ) { + foreach ( self::OPTIONS as $key ) { delete_option( $key ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php index 6d18481..4037dfe 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php @@ -2,19 +2,18 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; -use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; +use DeepWebSolutions\LinkedOrders\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\LinkedOrders\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class SettingsApiTest extends TestCase { - protected const array KEYS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; - protected array $stored = array(); - - protected array $deprecated = array(); + protected const array OPTIONS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); public static function stored_settings(): array { return array( @@ -28,28 +27,6 @@ public static function stored_settings(): array { ); } - #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); - } - - #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); - } - #[DataProvider( 'stored_settings' )] public function test_a_setting_reads_validated_by_its_v1_field_and_group( string $key, string $stored, string $field_id, ?string $group, int|bool $expected ): void { update_option( $key, $stored ); @@ -59,7 +36,7 @@ public function test_a_setting_reads_validated_by_its_v1_field_and_group( string } public function test_missing_settings_read_as_v1_defaults(): void { - foreach ( self::KEYS as $key ) { + foreach ( self::OPTIONS as $key ) { delete_option( $key ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php index 5b73310..df51043 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php @@ -3,24 +3,24 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; use DeepWebSolutions\LinkedOrders\Settings; +use DeepWebSolutions\LinkedOrders\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\LinkedOrders\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class SettingsTest extends TestCase { + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; + protected const string SECTION = 'dws-linked-orders'; - protected const array KEYS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); + protected const array OPTIONS = array( DWS_LOWC_OPTION_MAX_DEPTH, DWS_LOWC_OPTION_AUTOCOMPLETE, DWS_LOWC_OPTION_DELETE_DATA ); protected static \WC_Settings_Page $advanced; - protected array $stored = array(); - - protected array $deprecated = array(); - #[\Override] public static function setUpBeforeClass(): void { require_once WC_ABSPATH . 'includes/admin/class-wc-admin-settings.php'; @@ -49,27 +49,9 @@ public static function setting_groups(): array { ); } - #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); - } - #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } + protected function forget_the_rejections(): void { ( new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' ) )->setValue( null, array() ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } public function test_the_section_sits_on_the_advanced_tab_under_its_v1_id(): void { @@ -77,7 +59,7 @@ public function test_the_section_sits_on_the_advanced_tab_under_its_v1_id(): voi } public function test_the_section_shows_v1s_two_groups_with_their_defaults(): void { - foreach ( self::KEYS as $key ) { + foreach ( self::OPTIONS as $key ) { delete_option( $key ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php index fced113..2b08b5a 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php @@ -3,6 +3,7 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; use DeepWebSolutions\LinkedOrders\Plugin; +use DeepWebSolutions\LinkedOrders\Tests\Support\RunsRequestsTrait; use PHPUnit\Framework\Attributes\AfterClass; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -10,6 +11,8 @@ #[CoversNothing] final class UninstallTest extends TestCase { + use RunsRequestsTrait; + protected const int USER = 1; #[AfterClass] @@ -40,16 +43,16 @@ public function test_an_uninstall_in_a_request_that_skips_the_plugin_removes_its update_option( DWS_LOWC_OPTION_DELETE_DATA, 'yes' ); $php = 'uninstall_plugin( "linked-orders-for-woocommerce/linked-orders-for-woocommerce.php" ); echo wp_json_encode( array( defined( "DWS_LOWC_FILE" ), get_option( "' . DWS_LOWC_OPTION_DELETE_DATA . '" ) ) );'; - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' --skip-plugins=linked-orders-for-woocommerce eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The uninstall needs a request without the plugin. + $request = self::request( $php, '--skip-plugins=' . Plugin::SLUG ); // The other tests share the database, so a request with the plugin stores again what a new site starts with, as the site's next request would. - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval "" 2>&1', $restored, $restore_status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The restore needs a request with the plugin. + $restored = self::request( '' ); wp_cache_flush(); wp_roles()->for_site(); - self::assertSame( 0, $status, \implode( "\n", $output ) ); - self::assertSame( '[false,false]', (string) \end( $output ) ); - self::assertSame( 0, $restore_status, \implode( "\n", $restored ) ); + self::assertSame( 0, $request['status'], $request['output'] ); + self::assertSame( '[false,false]', $request['last'], $request['output'] ); + self::assertSame( 0, $restored['status'], $restored['output'] ); self::assertSame( 'no', get_option( DWS_LOWC_OPTION_DELETE_DATA ) ); self::assertTrue( get_role( 'shop_manager' )?->has_cap( DWS_LOWC_CAP_CREATE_LINKED_CHILDREN ) ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/README.md b/plugins/linked-orders-for-woocommerce/tests/README.md index 393dd25..c2f2817 100644 --- a/plugins/linked-orders-for-woocommerce/tests/README.md +++ b/plugins/linked-orders-for-woocommerce/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, v1's keys, settings API, linking API, node class, capabilities and deprecated functions and hooks, the options and their readers, the migrations, deactivation, uninstall after v1's consent, the assets, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorage` | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, v1's keys, settings API, linking API, node class, capabilities and deprecated functions and hooks, the options and their readers, the migrations, deactivation, uninstall after v1's consent, the assets, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | a child created from an order's meta box and both orders' depth in the orders list, the plugin row's link to its settings section, and the plugin with WooCommerce deactivated | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. diff --git a/plugins/internal-comments/tests/Integration/OrderStorage.php b/plugins/linked-orders-for-woocommerce/tests/Support/OrderStorageTrait.php similarity index 82% rename from plugins/internal-comments/tests/Integration/OrderStorage.php rename to plugins/linked-orders-for-woocommerce/tests/Support/OrderStorageTrait.php index 17b04ab..6e2dc53 100644 --- a/plugins/internal-comments/tests/Integration/OrderStorage.php +++ b/plugins/linked-orders-for-woocommerce/tests/Support/OrderStorageTrait.php @@ -1,11 +1,11 @@ storage_filters = array( 'pre_option_woocommerce_custom_orders_table_enabled' => static fn (): string => $hpos ? 'yes' : 'no', 'pre_option_woocommerce_custom_orders_table_data_sync_enabled' => static fn (): string => $sync ? 'yes' : 'no', - 'pre_option_woocommerce_hpos_datastore_caching_enabled' => static fn (): string => $caching ? 'yes' : 'no', + 'pre_option_woocommerce_hpos_datastore_caching_enabled' => static fn (): string => 'no', ); foreach ( $this->storage_filters as $hook => $filter ) { add_filter( $hook, $filter ); @@ -34,7 +34,6 @@ protected function create_order( array $args = array() ): \WC_Order { $order = wc_create_order( $args + array( 'status' => 'pending' ) ); self::assertInstanceOf( \WC_Order::class, $order ); $this->orders[] = $order->get_id(); - return $order; } @@ -47,11 +46,13 @@ protected function delete_orders_and_restore_storage(): void { $order->delete( true ); } } + $this->orders = array(); foreach ( $this->storage_filters as $hook => $filter ) { remove_filter( $hook, $filter ); } + $this->storage_filters = array(); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php b/plugins/linked-orders-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php new file mode 100644 index 0000000..439090f --- /dev/null +++ b/plugins/linked-orders-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php @@ -0,0 +1,48 @@ +listened as $hook ) { + remove_filter( $hook, array( $this, 'pass_through' ) ); + } + + remove_action( 'deprecated_hook_run', array( $this, 'record_deprecation' ) ); + remove_action( 'deprecated_function_run', array( $this, 'record_deprecation' ) ); + remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); + remove_filter( 'deprecated_function_trigger_error', '__return_false' ); + } + + public function record_deprecation( string $name, string $replacement ): void { + $this->deprecated[] = array( $name, $replacement ); + } + + // WordPress reports a deprecated hook only when something listens on it. + protected function listen_on( string ...$hooks ): void { + foreach ( $hooks as $hook ) { + add_filter( $hook, array( $this, 'pass_through' ) ); + $this->listened[] = $hook; + } + } + + public function pass_through( mixed $value = null ): mixed { + return $value; + } +} diff --git a/plugins/linked-orders-for-woocommerce/tests/Support/RestoresOptionsTrait.php b/plugins/linked-orders-for-woocommerce/tests/Support/RestoresOptionsTrait.php new file mode 100644 index 0000000..8d2d3a5 --- /dev/null +++ b/plugins/linked-orders-for-woocommerce/tests/Support/RestoresOptionsTrait.php @@ -0,0 +1,24 @@ +stored_options[ $key ] = get_option( $key ); + } + } + + #[After] + protected function restore_options(): void { + foreach ( $this->stored_options as $key => $value ) { + false === $value ? delete_option( $key ) : update_option( $key, $value ); + } + } +} diff --git a/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php b/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php index 25bbd72..30d630f 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php +++ b/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php @@ -3,13 +3,10 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Support; trait RunsRequestsTrait { - /** - * @return array{status: int, output: string, last: string} - */ - protected static function request( string $php ): array { - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. + protected static function request( string $php, string ...$flags ): array { + $arguments = \implode( '', \array_map( static fn ( string $flag ): string => ' ' . \escapeshellarg( $flag ), $flags ) ); + \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . "$arguments eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); - return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md index 69a186d..1ecc019 100644 --- a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md +++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md @@ -63,7 +63,7 @@ Both run PHP 8.5 with WooCommerce and Groups from `vendor/` and WooCommerce's or 5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input. 6. **Options and data.** The settings are read only through `includes/options.php`'s typed readers, safeguards excepted, and each subject's grants only through `includes/grants.php`'s readers. Everything stored lives under `dws_lpmwc_`, or `_dws_lpmwc_` for hidden meta; a user's grants are a user option under the site's table prefix, read from that key alone. The plugin's settings fire no settings hooks of their own, since WordPress's `option_{key}` reaches every read; v1's value filters fire where v1 read through them. 7. **Grants forms** offer the locked payment methods and the subject's other grants, through `dws_lpmwc_get_grant_choices()`, since a `checkboxes` save keeps only the choices it offers. The profile, order and plan forms are the framework's `ObjectForm`s; the Groups field rides Groups' own forms, whose nonce and capability Groups checks before its save actions, and checks the plugin's capability in each callback. -8. **Order storage.** Order code goes through WC CRUD and WooCommerce's screen ids, never the posts table, and every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorage`. +8. **Order storage.** Order code goes through WC CRUD and WooCommerce's screen ids, never the posts table, and every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`. 9. **v1 hooks.** A hook v1 fired keeps its v1 literal, fired through `apply_filters_deprecated()` or `do_action_deprecated()` with `'2.0.0'` and its replacement, before the v2 name; a filter on the locked list only adds ids. 10. **Safeguards** live in `includes/safeguards.php`, call only WordPress, WooCommerce and that file, and relax only through their own filter, through `dws_lpmwc_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon`, the file's only PHPStan profile, checks with WooCommerce's stubs. 11. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails. diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/checkout.spec.ts b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/checkout.spec.ts index 94a07ec..e882c01 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/checkout.spec.ts +++ b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/checkout.spec.ts @@ -1,17 +1,5 @@ -/// -import { execFileSync } from 'node:child_process'; import { expect, test } from '@wordpress/e2e-test-utils-playwright'; - -// No screen enables a payment method, locks it or grants it in one step, so the site's WP-CLI stores them. -const cli = ( ...args: string[] ): string => - execFileSync( - 'node_modules/.bin/wp-env', - [ '--config', '.wp-env.tests.json', 'run', 'cli', 'wp', ...args ], - { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } - ) - .trim() - .split( '\n' ) - .pop() ?? ''; +import { cli } from './cli'; // A context takes the config's storage state, the administrator's session, unless it names its own. const LOGGED_OUT = { cookies: [], origins: [] }; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/cli.ts b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/cli.ts new file mode 100644 index 0000000..14aabda --- /dev/null +++ b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/cli.ts @@ -0,0 +1,13 @@ +/// +import { execFileSync } from 'node:child_process'; + +// No screen shows some of what the specs set up or check, so they run WP-CLI on the tests site and read its last line. +export const cli = ( ...args: string[] ): string => + execFileSync( + 'node_modules/.bin/wp-env', + [ '--config', '.wp-env.tests.json', 'run', 'cli', 'wp', ...args ], + { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } + ) + .trim() + .split( '\n' ) + .pop() ?? ''; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts index cd51443..b178b94 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts +++ b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts @@ -1,30 +1,10 @@ -/// -import { execFileSync } from 'node:child_process'; import { expect, test } from '@wordpress/e2e-test-utils-playwright'; +import { cli } from './cli'; // WordPress core's own message for a plugin whose Requires Plugins header lists an inactive plugin. const DEPENDENCY = 'Locked Payment Methods for WooCommerce requires 1 plugin to be installed and activated: WooCommerce.'; -// No page shows the boot's outcome, so the site's WP-CLI reads it. -const bootStatus = (): string => - execFileSync( - 'node_modules/.bin/wp-env', - [ - '--config', - '.wp-env.tests.json', - 'run', - 'cli', - 'wp', - 'eval', - 'echo dws_lpmwc_plugin()->boot->status;', - ], - { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } - ) - .trim() - .split( '\n' ) - .pop() ?? ''; - test.describe( 'Without WooCommerce', () => { test.beforeAll( async ( { requestUtils } ) => { await requestUtils.deactivatePlugin( 'woocommerce' ); @@ -37,7 +17,9 @@ test.describe( 'Without WooCommerce', () => { test( 'the plugin stays blocked, WordPress names the missing plugin and every screen still loads', async ( { page, } ) => { - expect( bootStatus() ).toBe( 'blocked' ); + expect( cli( 'eval', 'echo dws_lpmwc_plugin()->boot->status;' ) ).toBe( + 'blocked' + ); const front = await page.goto( '/' ); expect( front?.status() ).toBe( 200 ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php index 7cddae8..47bf02a 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php @@ -2,7 +2,8 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration; -use PHPUnit\Framework\Attributes\After; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; @@ -10,13 +11,12 @@ #[CoversNothing] final class DeprecatedApiTest extends TestCase { - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS ); + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; - protected static array $users = array(); - - protected array $stored = array(); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS ); - protected array $deprecated = array(); + protected static array $users = array(); public static function hook_tags(): array { return array( @@ -54,31 +54,9 @@ public static function tearDownAfterClass(): void { } #[Before] - protected function record_deprecations(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } + protected function store_the_settings(): void { update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod' ) ); update_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, array( 'shop_manager' ) ); - add_filter( 'deprecated_function_trigger_error', '__return_false' ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_function_run', array( $this, 'record' ), 10, 2 ); - add_action( 'deprecated_hook_run', array( $this, 'record' ), 10, 2 ); - } - - #[After] - protected function stop_recording(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } - remove_filter( 'deprecated_function_trigger_error', '__return_false' ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_function_run', array( $this, 'record' ) ); - remove_action( 'deprecated_hook_run', array( $this, 'record' ) ); - } - - public function record( string $name, string $replacement ): void { - $this->deprecated[] = array( $name, $replacement ); } public function test_the_v1_getters_answer_from_the_plugin_header(): void { diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MigrationsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MigrationsTest.php index 45475dc..83a8a3f 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MigrationsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MigrationsTest.php @@ -4,6 +4,7 @@ use DeepWebSolutions\LockedPaymentMethods\Plugin; use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\Migration\MigrationRunner; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -11,6 +12,10 @@ #[CoversNothing] final class MigrationsTest extends TestCase { + use RestoresOptionsTrait; + + protected const array OPTIONS = array( ...self::KEYS, ...self::V1_KEYS ); + // v1.3.5's settings, as its free and premium builds stored them. protected const array V1_KEYS = array( 'dws-wc-lpm_general_locked-payment-methods', @@ -34,20 +39,8 @@ final class MigrationsTest extends TestCase { DWS_LPMWC_OPTION_DELETE_DATA, ); - protected array $stored = array(); - - #[\Override] - protected function setUp(): void { - foreach ( array( ...self::KEYS, ...self::V1_KEYS ) as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - } - #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } + protected function restore_the_revision(): void { update_option( Plugin::PREFIX . '_revision', \count( dws_lpmwc_get_migration_steps() ) ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/OptionsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/OptionsTest.php index 9155a90..f1c0ca9 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/OptionsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/OptionsTest.php @@ -3,19 +3,19 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration; use DeepWebSolutions\LockedPaymentMethods\Plugin; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class OptionsTest extends TestCase { - protected const string KEY = Plugin::PREFIX . '_options_test'; + use RestoresOptionsTrait; - protected const array RESTORED = array( DWS_LPMWC_OPTION_DELETE_DATA ); + protected const string KEY = Plugin::PREFIX . '_options_test'; - protected array $stored = array(); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_DELETE_DATA ); public static function stored_bools(): array { return array( @@ -59,19 +59,9 @@ public static function stored_lists(): array { ); } - #[Before] - protected function store_rows(): void { - foreach ( self::RESTORED as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - } - #[After] - protected function restore_rows(): void { + protected function delete_the_test_option(): void { delete_option( self::KEY ); - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } } #[DataProvider( 'stored_bools' )] diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php index ba418ad..274d817 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php @@ -2,6 +2,8 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RunsRequestsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; @@ -12,16 +14,16 @@ #[CoversNothing] final class SafeguardsTest extends TestCase { + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; use RunsRequestsTrait; + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, self::V1_KEY ); + protected const string FILTER = 'dws_lpmwc/payment_method/should_lock'; protected const string V1_KEY = 'dws-wc-lpm_general_locked-payment-methods'; - protected array $stored = array(); - - protected array $deprecated = array(); - public static function relaxations(): array { return array( 'none' => array( array(), false ), @@ -63,32 +65,18 @@ public static function floors(): array { } #[Before] - protected function store_rows(): void { - foreach ( array( DWS_LPMWC_OPTION_LOCKED_METHODS, self::V1_KEY ) as $key ) { - $this->stored[ $key ] = get_option( $key ); - } + protected function lock_cod(): void { update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod' ) ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); } #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); + protected function reset_the_request(): void { remove_filter( 'woocommerce_is_checkout', '__return_true' ); unset( $GLOBALS['wp']->query_vars['order-pay'], $_GET['key'] ); set_query_var( 'order-pay', '' ); wp_set_current_user( 0 ); } - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); - } - public function test_the_lock_removes_the_methods_the_plugins_option_lists(): void { $available = self::available(); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php index 4eefb9b..4d7c9b8 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php @@ -3,27 +3,27 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration; use DeepWebSolutions\LockedPaymentMethods\Settings; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\GroupsStrategy; use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversNothing] final class SettingsTest extends TestCase { + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; + protected const string SECTION = 'dws-locked-payment-methods'; protected const array CHECKBOXES = array( DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS, DWS_LPMWC_OPTION_DELETE_DATA ); - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, ...self::CHECKBOXES ); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, ...self::CHECKBOXES ); protected static \WC_Settings_Page $advanced; - protected array $stored = array(); - - protected array $deprecated = array(); - #[\Override] public static function setUpBeforeClass(): void { require_once WC_ABSPATH . 'includes/admin/class-wc-admin-settings.php'; @@ -43,27 +43,9 @@ public static function setting_groups(): array { ); } - #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); - } - #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } + protected function forget_the_rejections(): void { ( new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' ) )->setValue( null, array() ); - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); - } - - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); } public function test_the_section_sits_on_the_advanced_tab_under_its_v1_id(): void { @@ -71,7 +53,7 @@ public function test_the_section_sits_on_the_advanced_tab_under_its_v1_id(): voi } public function test_the_section_shows_v1s_groups_with_their_defaults(): void { - foreach ( self::KEYS as $key ) { + foreach ( self::OPTIONS as $key ) { delete_option( $key ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php index 025a00a..b7d6c2b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php @@ -4,6 +4,7 @@ use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\AdminNotices; use DeepWebSolutions\LockedPaymentMethods\Scoped\Psr\Log\NullLogger as PsrNullLogger; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\GroupsStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; @@ -14,7 +15,9 @@ #[CoversNothing] #[Group( 'groups' )] final class GroupsStrategyTest extends TestCase { - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_GROUPS, DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS ); + use RestoresOptionsTrait; + + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_GROUPS, DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS ); protected static GroupsStrategy $strategy; @@ -22,8 +25,6 @@ final class GroupsStrategyTest extends TestCase { protected static array $users = array(); - protected array $stored = array(); - protected array $groups = array(); // Groups is off by default, so the booted plugin runs no Groups strategy; the class hooks one of its own, as the plugin does with the switch on. @@ -54,22 +55,16 @@ public static function tearDownAfterClass(): void { } #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } + protected function lock_the_methods(): void { update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod', 'bacs' ) ); delete_option( DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS ); } #[After] - protected function restore_rows(): void { + protected function clean_up(): void { foreach ( $this->groups as $group ) { \Groups_Group::delete( $group ); } - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } unset( $_POST['dws_lpmwc_group'] ); wp_set_current_user( 0 ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php index 59b317f..5c03333 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php @@ -5,7 +5,8 @@ use DeepWebSolutions\LockedPaymentMethods\Plugin; use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\AdminNotices; use DeepWebSolutions\LockedPaymentMethods\Scoped\Psr\Log\NullLogger as PsrNullLogger; -use DeepWebSolutions\LockedPaymentMethods\Tests\Support\OrderStorage; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\OrderStorageTrait; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RunsRequestsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\OrderStrategy; use PHPUnit\Framework\Attributes\After; @@ -16,12 +17,13 @@ #[CoversNothing] final class OrderStrategyTest extends TestCase { - use OrderStorage; + use OrderStorageTrait; + use RestoresOptionsTrait; use RunsRequestsTrait; protected const string FORM = 'dws_lpmwc_order'; - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS ); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS ); protected static OrderStrategy $strategy; @@ -29,8 +31,6 @@ final class OrderStrategyTest extends TestCase { protected static int $admin; - protected array $stored = array(); - // Orders are off by default, so the booted plugin runs no order strategy; the class hooks one of its own, as the plugin does with the switch on. #[\Override] public static function setUpBeforeClass(): void { @@ -55,18 +55,12 @@ public static function tearDownAfterClass(): void { } #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } + protected function lock_the_methods(): void { update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod', 'bacs' ) ); } #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } + protected function clean_up(): void { unset( $_POST[ self::FORM ], $_POST[ self::FORM . '_nonce' ] ); wp_set_current_user( 0 ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php index df0ad8d..7a228f0 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php @@ -3,6 +3,7 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration\UnlockStrategies; use DeepWebSolutions\LockedPaymentMethods\Plugin; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserProfileStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; @@ -11,14 +12,14 @@ #[CoversNothing] final class UserProfileStrategyTest extends TestCase { + use RestoresOptionsTrait; + protected const string FORM = 'dws_lpmwc_user_profile'; - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ); protected static array $users = array(); - protected array $stored = array(); - #[\Override] public static function setUpBeforeClass(): void { require_once ABSPATH . 'wp-admin/includes/user.php'; @@ -40,20 +41,14 @@ public static function tearDownAfterClass(): void { // The form evaluates its definition and saves an object once per request, so every row edits a customer of its own. #[Before] - protected function store_rows(): void { + protected function create_the_customer(): void { self::$users['customer'] = self::create_user( 'customer' ); - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod', 'bacs' ) ); update_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, array( 'shop_manager' ) ); } #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } + protected function clean_up(): void { wp_delete_user( self::$users['customer'] ); foreach ( self::$users as $user ) { delete_user_meta( $user, self::key() ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php index f4c9d72..d48a264 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php @@ -3,6 +3,8 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration\UnlockStrategies; use DeepWebSolutions\LockedPaymentMethods\Plugin; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RecordsDeprecationsTrait; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserRoleStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; @@ -11,16 +13,15 @@ #[CoversNothing] final class UserRoleStrategyTest extends TestCase { + use RecordsDeprecationsTrait; + use RestoresOptionsTrait; + protected const string V1_HOOK = 'locked_payment_methods_for_woocommerce/general_settings/get_validated_option_value'; - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ); protected static array $users = array(); - protected array $stored = array(); - - protected array $deprecated = array(); - #[\Override] public static function setUpBeforeClass(): void { require_once ABSPATH . 'wp-admin/includes/user.php'; @@ -44,29 +45,15 @@ public static function tearDownAfterClass(): void { } #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } + protected function grant_shop_managers_every_method(): void { update_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, array( 'shop_manager' ) ); - add_filter( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ), 10, 2 ); } #[After] - protected function restore_rows(): void { - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } - remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); - remove_action( 'deprecated_hook_run', array( $this, 'record_deprecated_hook' ) ); + protected function log_out(): void { wp_set_current_user( 0 ); } - public function record_deprecated_hook( string $hook, string $replacement ): void { - $this->deprecated[] = array( $hook, $replacement ); - } - public function test_the_strategy_loads_while_its_switch_is_on_as_v1s_did_by_default(): void { delete_option( DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES ); self::assertTrue( UserRoleStrategy::should_load() ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php index a9c3ed1..695d7a0 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php @@ -5,6 +5,7 @@ use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\AdminNotices; use DeepWebSolutions\LockedPaymentMethods\Scoped\Psr\Log\NullLogger as PsrNullLogger; use DeepWebSolutions\LockedPaymentMethods\Tests\Support\MembershipsDouble; +use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\WooCommerceMembershipsStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; @@ -18,9 +19,11 @@ #[CoversNothing] #[Group( 'wc-memberships' )] final class WooCommerceMembershipsStrategyTest extends TestCase { + use RestoresOptionsTrait; + protected const string FORM = 'dws_lpmwc_membership_plan'; - protected const array KEYS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_WC_MEMBERSHIPS, 'wc_memberships_version' ); + protected const array OPTIONS = array( DWS_LPMWC_OPTION_LOCKED_METHODS, DWS_LPMWC_OPTION_UNLOCK_VIA_WC_MEMBERSHIPS, 'wc_memberships_version' ); protected static WooCommerceMembershipsStrategy $strategy; @@ -28,8 +31,6 @@ final class WooCommerceMembershipsStrategyTest extends TestCase { protected static array $users = array(); - protected array $stored = array(); - protected int $plan; // Memberships is off by default, so the booted plugin runs no Memberships strategy; the class hooks one of its own, as the plugin does with the switch on. @@ -63,10 +64,7 @@ public static function tearDownAfterClass(): void { // The form evaluates its definition and saves an object once per request, so every row edits a plan of its own. #[Before] - protected function store_rows(): void { - foreach ( self::KEYS as $key ) { - $this->stored[ $key ] = get_option( $key ); - } + protected function create_the_plan(): void { update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod', 'bacs' ) ); update_option( 'wc_memberships_version', '1.27.0' ); $this->plan = (int) wp_insert_post( @@ -79,11 +77,8 @@ protected function store_rows(): void { } #[After] - protected function restore_rows(): void { + protected function clean_up(): void { wp_delete_post( $this->plan, true ); - foreach ( $this->stored as $key => $value ) { - false === $value ? delete_option( $key ) : update_option( $key, $value ); - } MembershipsDouble::$memberships = array(); unset( $_POST[ self::FORM ], $_POST[ self::FORM . '_nonce' ], $GLOBALS['post'] ); wp_set_current_user( 0 ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/README.md b/plugins/locked-payment-methods-for-woocommerce/tests/README.md index ab7a784..587eacf 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/README.md +++ b/plugins/locked-payment-methods-for-woocommerce/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce, Groups and order tables loaded | the boot, the lock and its relaxation, fresh requests below the PHP and WooCommerce floors, every strategy and its form, the settings, the capabilities, v1's deprecated functions and hooks, the migrations from v1, deactivation, uninstall, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorage`, the Groups rows run in the `groups` group against Groups itself, and the Memberships rows in the `wc-memberships` group against a double of its one function | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce, Groups and order tables loaded | the boot, the lock and its relaxation, fresh requests below the PHP and WooCommerce floors, every strategy and its form, the settings, the capabilities, v1's deprecated functions and hooks, the migrations from v1, deactivation, uninstall, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`, the Groups rows run in the `groups` group against Groups itself, and the Memberships rows in the `wc-memberships` group against a double of its one function | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | a locked payment method a customer is granted on the block checkout, the Store API's refusal of a guest's checkout with a locked payment method, and the plugin with WooCommerce deactivated; the monorepo's shared environment checks a locked payment method's absence from a guest's classic and block checkout | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/OrderStorage.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/OrderStorageTrait.php similarity index 85% rename from plugins/locked-payment-methods-for-woocommerce/tests/Support/OrderStorage.php rename to plugins/locked-payment-methods-for-woocommerce/tests/Support/OrderStorageTrait.php index 13eea6b..18e4e0b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Support/OrderStorage.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/OrderStorageTrait.php @@ -4,8 +4,8 @@ use PHPUnit\Framework\Attributes\After; -// WooCommerce reads these options on every order load, so pre_option filters move this process to another storage, with or without the order tables' data caching, without changing the tests site. -trait OrderStorage { +// WooCommerce reads these options on every order load, so pre_option filters move this process to another storage, with the order tables' data cache off, without changing the tests site. +trait OrderStorageTrait { protected array $orders = array(); protected array $storage_filters = array(); @@ -19,11 +19,11 @@ public static function storages(): array { ); } - protected function use_storage( bool $hpos, bool $sync, bool $caching = false ): void { + protected function use_storage( bool $hpos, bool $sync ): void { $this->storage_filters = array( 'pre_option_woocommerce_custom_orders_table_enabled' => static fn (): string => $hpos ? 'yes' : 'no', 'pre_option_woocommerce_custom_orders_table_data_sync_enabled' => static fn (): string => $sync ? 'yes' : 'no', - 'pre_option_woocommerce_hpos_datastore_caching_enabled' => static fn (): string => $caching ? 'yes' : 'no', + 'pre_option_woocommerce_hpos_datastore_caching_enabled' => static fn (): string => 'no', ); foreach ( $this->storage_filters as $hook => $filter ) { add_filter( $hook, $filter ); @@ -34,7 +34,6 @@ protected function create_order( array $args = array() ): \WC_Order { $order = wc_create_order( $args + array( 'status' => 'pending' ) ); self::assertInstanceOf( \WC_Order::class, $order ); $this->orders[] = $order->get_id(); - return $order; } @@ -47,11 +46,13 @@ protected function delete_orders_and_restore_storage(): void { $order->delete( true ); } } + $this->orders = array(); foreach ( $this->storage_filters as $hook => $filter ) { remove_filter( $hook, $filter ); } + $this->storage_filters = array(); } } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php new file mode 100644 index 0000000..f85e9f5 --- /dev/null +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RecordsDeprecationsTrait.php @@ -0,0 +1,48 @@ +listened as $hook ) { + remove_filter( $hook, array( $this, 'pass_through' ) ); + } + + remove_action( 'deprecated_hook_run', array( $this, 'record_deprecation' ) ); + remove_action( 'deprecated_function_run', array( $this, 'record_deprecation' ) ); + remove_filter( 'deprecated_hook_trigger_error', '__return_false' ); + remove_filter( 'deprecated_function_trigger_error', '__return_false' ); + } + + public function record_deprecation( string $name, string $replacement ): void { + $this->deprecated[] = array( $name, $replacement ); + } + + // WordPress reports a deprecated hook only when something listens on it. + protected function listen_on( string ...$hooks ): void { + foreach ( $hooks as $hook ) { + add_filter( $hook, array( $this, 'pass_through' ) ); + $this->listened[] = $hook; + } + } + + public function pass_through( mixed $value = null ): mixed { + return $value; + } +} diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RestoresOptionsTrait.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RestoresOptionsTrait.php new file mode 100644 index 0000000..ee4b174 --- /dev/null +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RestoresOptionsTrait.php @@ -0,0 +1,24 @@ +stored_options[ $key ] = get_option( $key ); + } + } + + #[After] + protected function restore_options(): void { + foreach ( $this->stored_options as $key => $value ) { + false === $value ? delete_option( $key ) : update_option( $key, $value ); + } + } +} diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php index 2042376..fc96072 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php @@ -3,13 +3,10 @@ namespace DeepWebSolutions\LockedPaymentMethods\Tests\Support; trait RunsRequestsTrait { - /** - * @return array{status: int, output: string, last: string} - */ - protected static function request( string $php, string $flags = '' ): array { - \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . " $flags eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. + protected static function request( string $php, string ...$flags ): array { + $arguments = \implode( '', \array_map( static fn ( string $flag ): string => ' ' . \escapeshellarg( $flag ), $flags ) ); + \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . "$arguments eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); - return array( 'status' => $status, 'output' => \implode( "\n", $output ), From 96b15ae1776c470e665aeb0849a7d7f8c4e16a9b Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:25:54 +0100 Subject: [PATCH 07/39] Stop each Integration suite on what an aborted run left A row that breaks a plugin on disk, by hiding a build file or writing an include that throws, repairs it in a finally block that an aborted run never reaches, and UninstallTest's floor row raises the entry's Requires PHP until its finally block runs; the next run then tested the broken plugin and failed far from the cause. Each plugin's Integration bootstrap takes the template's two checks: it refuses to run while a hidden file or a row's include is left behind, naming each, and while the plugin's functions did not load. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/bootstrap.php | 22 +++++++++++++++++++ .../tests/Integration/bootstrap.php | 22 +++++++++++++++++++ .../tests/Integration/bootstrap.php | 22 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/plugins/internal-comments/tests/Integration/bootstrap.php b/plugins/internal-comments/tests/Integration/bootstrap.php index 287bb94..990f858 100644 --- a/plugins/internal-comments/tests/Integration/bootstrap.php +++ b/plugins/internal-comments/tests/Integration/bootstrap.php @@ -1,5 +1,21 @@ Date: Wed, 11 Nov 2026 16:29:19 +0100 Subject: [PATCH 08/39] Run every plugin's Integration suite on a network Nothing checked that a plugin boots and keeps its settings per site on a network: no plugin had a multisite group, environment or lane, and the root had no multisite lane (X132), which X366 moves here because PHPUnit 13 fails a run whose group matches no test. Each plugin takes the template's .wp-env.multisite.json, on 8828 for Linked Orders, 8829 for Internal Comments and 8830 for Locked Payment Methods, with WooCommerce and the plugin network-activated; a multisite group whose row boots the plugin on a second site and checks that the site's settings are its own; test:integration:multisite and its :run, which the default run's exclusion of the group mirrors; the wp-env:multisite:* scripts; and the template's Integration (multisite) jobs in Tests and the nightly. The root's .wp-env.multisite.json starts a network on 8821 with every plugin and WooCommerce network-activated, test:integration:multisite runs each plugin's group there, its :ci variant installs first, and Tests and the nightly run it as Integration (multisite); the shared single-site run skips the group. The docs describe the new sites and lanes, and Locked Payment Methods' no longer says the shared run skips its wc-memberships group, which A460 runs there. Assisted-by: Claude Code:claude-opus-5-5 --- .github/workflows/tests-nightly.yml | 10 ++++++ .github/workflows/tests.yml | 11 ++++++ .wp-env.multisite.json | 22 ++++++++++++ AGENTS.md | 8 +++-- README.md | 2 +- composer.json | 26 ++++++++++++-- package.json | 5 +++ .../.github/workflows/tests-nightly.yml | 9 +++++ .../.github/workflows/tests.yml | 14 ++++++++ .../internal-comments/.wp-env.multisite.json | 20 +++++++++++ plugins/internal-comments/AGENTS.md | 4 ++- plugins/internal-comments/composer.json | 14 +++++++- plugins/internal-comments/package.json | 5 +++ .../tests/Integration/MultisiteTest.php | 36 +++++++++++++++++++ plugins/internal-comments/tests/README.md | 11 ++++-- .../.github/workflows/tests-nightly.yml | 9 +++++ .../.github/workflows/tests.yml | 14 ++++++++ .../.wp-env.multisite.json | 20 +++++++++++ .../linked-orders-for-woocommerce/AGENTS.md | 4 ++- .../composer.json | 14 +++++++- .../package.json | 5 +++ .../tests/Integration/MultisiteTest.php | 36 +++++++++++++++++++ .../tests/README.md | 11 ++++-- .../.github/workflows/tests-nightly.yml | 9 +++++ .../.github/workflows/tests.yml | 14 ++++++++ .../.wp-env.multisite.json | 20 +++++++++++ .../AGENTS.md | 6 ++-- .../composer.json | 14 +++++++- .../package.json | 5 +++ .../tests/Integration/MultisiteTest.php | 36 +++++++++++++++++++ .../tests/README.md | 11 ++++-- 31 files changed, 405 insertions(+), 20 deletions(-) create mode 100644 .wp-env.multisite.json create mode 100644 plugins/internal-comments/.wp-env.multisite.json create mode 100644 plugins/internal-comments/tests/Integration/MultisiteTest.php create mode 100644 plugins/linked-orders-for-woocommerce/.wp-env.multisite.json create mode 100644 plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php create mode 100644 plugins/locked-payment-methods-for-woocommerce/.wp-env.multisite.json create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 15dce25..8f9b8d4 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -55,6 +55,16 @@ jobs: composer-script: 'test:integration:ci' wp-env-config-file: '.wp-env.tests.json' + multisite-integration: + name: Integration (multisite) + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + project-path: '.' + php-version: '8.5' + wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip' + composer-script: 'test:integration:multisite:ci' + wp-env-config-file: '.wp-env.multisite.json' + shared-e2e: name: E2E (shared environment) uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@b107001cce9f2960495d9d50481c8540d228f60c diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 927dcfa..61fd68a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,6 +76,17 @@ jobs: composer-script: 'test:integration:ci' wp-env-config-file: '.wp-env.tests.json' + multisite-integration: + name: Integration (multisite) + needs: plugins + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + project-path: '.' + php-version: '8.5' + wp-version: ${{ needs.plugins.outputs.newest-wordpress }} + composer-script: 'test:integration:multisite:ci' + wp-env-config-file: '.wp-env.multisite.json' + shared-e2e: name: E2E (shared environment) needs: plugins diff --git a/.wp-env.multisite.json b/.wp-env.multisite.json new file mode 100644 index 0000000..87c9d53 --- /dev/null +++ b/.wp-env.multisite.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "core": "WordPress/WordPress#tags/7.1.4", + "phpVersion": "8.5", + "port": 8821, + "testsEnvironment": false, + "multisite": true, + "mappings": { + "wp-content/plugins/linked-orders-for-woocommerce": "./plugins/linked-orders-for-woocommerce", + "wp-content/plugins/internal-comments": "./plugins/internal-comments", + "wp-content/plugins/locked-payment-methods-for-woocommerce": "./plugins/locked-payment-methods-for-woocommerce", + "wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce" + }, + "config": { + "WP_DEBUG": true, + "SCRIPT_DEBUG": true, + "WP_ENVIRONMENT_TYPE": false + }, + "lifecycleScripts": { + "afterStart": "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate woocommerce --network && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate linked-orders-for-woocommerce internal-comments locked-payment-methods-for-woocommerce --network" + } +} diff --git a/AGENTS.md b/AGENTS.md index 542a069..2433193 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,12 +14,13 @@ wordpress-plugins/ │ └── EndToEnd/ the shared environment's checks: every plugin is active and boots, and the plugins work side by side ├── .github/workflows/ │ ├── plugins.yml lists the plugins, their mirrors and readmes for the other workflows, checks a release tag names one, and lists each one's WordPress versions for Tests -│ ├── quality.yml, tests.yml, tests-nightly.yml, audit.yml the plugins' own lanes, once per plugin; Quality's script lint and the audit also over the root, and Tests and the nightly also over the shared environment +│ ├── quality.yml, tests.yml, tests-nightly.yml, audit.yml the plugins' own lanes, once per plugin; Quality's script lint and the audit also over the root, and Tests and the nightly also over the shared and multisite environments │ ├── codeql.yml, workflow-checks.yml the template's, workflow-checks.yml also linting each plugin's workflows │ ├── release-rehearsal.yml rehearses each plugin's release, from build to the built-artifact test, on pull requests and trunk, publishing nothing │ └── split-plugins.yml splits each plugin to the mirror its composer.json name gives, and forwards /vX.Y.Z as vX.Y.Z +├── .wp-env.multisite.json the multisite environment on port 8821: a network with every plugin and WooCommerce network-activated, as production ├── .wp-env.tests.json the shared environment on port 8820: every plugin and WooCommerce from vendor/, as production -├── composer.json WooCommerce for the shared environment, the shared Integration run and the plugins:* fan-outs +├── composer.json WooCommerce for the shared environments, their Integration runs and the plugins:* fan-outs └── package.json wp-env, Playwright and the shared spec's lint, and the plugins:* fan-outs ``` @@ -29,6 +30,7 @@ wordpress-plugins/ npm run setup # every install, then every plugin's build composer plugins:quality-check # each plugin's quality-check, in its own environments composer test:integration # starts the shared environment and runs each plugin's Integration suite in it +composer test:integration:multisite # starts the multisite environment and runs each plugin's multisite group in it npm run test:e2e # the shared environment's checks npm run plugins:test:e2e # each plugin's end-to-end suite, in its own environment npm run lint:all # the shared spec's checks, then every plugin's PHP, script, style and type checks @@ -39,7 +41,7 @@ actionlint .github/workflows/*.yml plugins/*/.github/workflows/*.yml && zizmor . ## Conventions 1. **Plugins.** A plugin starts from wordpress-plugin-template's trunk through the template's fill-in script, run in a clone of the template, and its directory keeps the template's tooling, docs and workflows but for `CONTRIBUTING.md`, the pull-request and issue templates, `.github/dependabot.yml` and the fill-in's workflow and script: its mirror is a public repository users land on, while contributions, issues and dependency updates belong here. Its `composer.json` name is its mirror, `ahegyes/wc-` for a WooCommerce plugin and `ahegyes/wp-` otherwise. -2. **Root lanes.** The root's workflows find the plugins through `plugins.yml`, so a plugin joins every lane without a workflow edit. The `plugins:*` fan-outs, the shared environment's mappings and activation, `test:integration:run`, the shared spec's boot row, the README table, the layout tree above and both issue forms name each plugin. +2. **Root lanes.** The root's workflows find the plugins through `plugins.yml`, so a plugin joins every lane without a workflow edit. The `plugins:*` fan-outs, the shared and multisite environments' mappings and activation, `test:integration:run`, `test:integration:multisite:run`, the shared spec's boot row, the README table, the layout tree above and both issue forms name each plugin. 3. **Ports.** wp-env ports are allocated in threes: the shared environment takes 8820 and the root's multisite environment 8821, the three plugins here take their development and tests pairs from 8822 to 8827 and their multisite ports from 8828 to 8830, and a new plugin takes the next free three, from 8831, for its development, tests and multisite sites. 4. **v1 successors.** A plugin released before v2 keeps its wordpress.org slug, text domain, main file name, capabilities, and every hook, function and constant another plugin can rely on, and starts at 2.0.0. It keeps its option and meta keys too, unless a major version migrates them in a way that preserves the data, with a changelog line and an Upgrade Notice line, as Locked Payment Methods' 2.0.0 does. Its v1 hook literals fire as deprecated beside their v2 names until the next major, and every break is listed in its changelog and its readme's Upgrade Notice. 5. **Releases.** A plugin releases from its mirror: the tag `/vX.Y.Z` here reaches the mirror as `vX.Y.Z`, never force-pushed, and the mirror holds the `wp-org-release` environment where the plugin is on wordpress.org. `CONTRIBUTING.md` holds the release step. diff --git a/README.md b/README.md index d270faf..aa86d3c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ composer test:integration npm run test:e2e ``` -`composer test:integration` runs each plugin's Integration suite in the shared environment, and `npm run test:e2e` its checks of the plugins side by side. +`composer test:integration` runs each plugin's Integration suite in the shared environment, `composer test:integration:multisite` each plugin's `multisite` group on a network on , and `npm run test:e2e` the shared environment's checks of the plugins side by side. ## Releases diff --git a/composer.json b/composer.json index e56d8e1..a178fd3 100644 --- a/composer.json +++ b/composer.json @@ -46,9 +46,9 @@ "Composer\\Config::disableProcessTimeout", "@putenv CI=1", "npm --prefix=. run plugins:build", - "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration", - "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration", - "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration --exclude-group groups" + "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration --exclude-group=multisite", + "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration --exclude-group=multisite", + "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration --exclude-group=groups --exclude-group=multisite" ], "test:integration:ci": [ "Composer\\Config::disableProcessTimeout", @@ -56,6 +56,26 @@ "npm --prefix=. run plugins:packages-install:ci", "@test:integration:run" ], + "test:integration:multisite": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run wp-env:multisite:start", + "@test:integration:multisite:run" + ], + "test:integration:multisite:run": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run plugins:build", + "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration --group=multisite", + "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration --group=multisite", + "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration --group=multisite" + ], + "test:integration:multisite:ci": [ + "Composer\\Config::disableProcessTimeout", + "@plugins:packages-install", + "npm --prefix=. run plugins:packages-install:ci", + "@test:integration:multisite:run" + ], "quality-check": [ "@composer validate --strict", diff --git a/package.json b/package.json index 1634993..ed4bb6c 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,11 @@ "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + "wp-env:multisite:start": "wp-env --config .wp-env.multisite.json start", + "wp-env:multisite:stop": "wp-env --config .wp-env.multisite.json stop", + "wp-env:multisite:update": "wp-env --config .wp-env.multisite.json start --update", + "wp-env:multisite:destroy": "wp-env --config .wp-env.multisite.json destroy", + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", diff --git a/plugins/internal-comments/.github/workflows/tests-nightly.yml b/plugins/internal-comments/.github/workflows/tests-nightly.yml index eec84c9..69824e8 100644 --- a/plugins/internal-comments/.github/workflows/tests-nightly.yml +++ b/plugins/internal-comments/.github/workflows/tests-nightly.yml @@ -19,6 +19,15 @@ jobs: composer-script: 'test:integration:run' wp-env-config-file: '.wp-env.tests.json' + integration-multisite: + name: Integration (multisite) + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + php-version: '8.5' + wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip' + composer-script: 'test:integration:multisite:run' + wp-env-config-file: '.wp-env.multisite.json' + e2e: name: E2E uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@b107001cce9f2960495d9d50481c8540d228f60c diff --git a/plugins/internal-comments/.github/workflows/tests.yml b/plugins/internal-comments/.github/workflows/tests.yml index 50c4d96..466d0d7 100644 --- a/plugins/internal-comments/.github/workflows/tests.yml +++ b/plugins/internal-comments/.github/workflows/tests.yml @@ -61,6 +61,20 @@ jobs: composer-script: 'test:integration:run' wp-env-config-file: '.wp-env.tests.json' + integration-multisite: + name: Integration (multisite) + needs: wordpress + strategy: + fail-fast: false + matrix: + wp: ${{ fromJSON(needs.wordpress.outputs.versions) }} + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + php-version: '8.5' + wp-version: ${{ matrix.wp }} + composer-script: 'test:integration:multisite:run' + wp-env-config-file: '.wp-env.multisite.json' + e2e: name: E2E needs: wordpress diff --git a/plugins/internal-comments/.wp-env.multisite.json b/plugins/internal-comments/.wp-env.multisite.json new file mode 100644 index 0000000..f188c57 --- /dev/null +++ b/plugins/internal-comments/.wp-env.multisite.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "core": "WordPress/WordPress#tags/7.1.4", + "phpVersion": "8.5", + "port": 8829, + "testsEnvironment": false, + "multisite": true, + "mappings": { + "wp-content/plugins/internal-comments": ".", + "wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce" + }, + "config": { + "WP_DEBUG": true, + "SCRIPT_DEBUG": true, + "WP_ENVIRONMENT_TYPE": false + }, + "lifecycleScripts": { + "afterStart": "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate woocommerce --network && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate internal-comments --network" + } +} diff --git a/plugins/internal-comments/AGENTS.md b/plugins/internal-comments/AGENTS.md index 8459701..c31a919 100644 --- a/plugins/internal-comments/AGENTS.md +++ b/plugins/internal-comments/AGENTS.md @@ -41,6 +41,7 @@ changelog/ the pending entries changelog:write turns into CHAN composer packages-install # dependencies, then php-scoper into vendor-prefixed/ composer test:unit composer test:integration # starts .wp-env.tests.json, then runs test:integration:run, which builds the scripts first +composer test:integration:multisite # starts .wp-env.multisite.json, then runs the Integration suite's multisite group, which the default run skips composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan (both profiles), Unit and Integration composer changelog:add # stages an entry in changelog/, which the release step in the monorepo's CONTRIBUTING.md writes npm run setup # both installs, the scripts' build, then the development site @@ -55,8 +56,9 @@ Run wp-scripts only through the npm scripts or `node_modules/.bin/wp-scripts`: ` - `.wp-env.json`: development, as `local`, on port 8824, behind `npm run setup` and `wp-env:*`. The framework rethrows boot and migration failures there. - `.wp-env.tests.json`: Integration and end to end, as `production`, on port 8825, behind `wp-env:tests:*`, `composer test:integration` and `npm run test:e2e`. The end-to-end suite deactivates and reactivates WooCommerce there. +- `.wp-env.multisite.json`: a network with WooCommerce and the plugin network-activated, as `production`, on port 8829, behind `wp-env:multisite:*` and `composer test:integration:multisite`, for the Integration suite's `multisite` group. -Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. +Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. ## Conventions diff --git a/plugins/internal-comments/composer.json b/plugins/internal-comments/composer.json index e9f81e2..c701b30 100644 --- a/plugins/internal-comments/composer.json +++ b/plugins/internal-comments/composer.json @@ -120,7 +120,19 @@ "Composer\\Config::disableProcessTimeout", "@putenv CI=1", "npm --prefix=. run build", - "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration" + "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration --exclude-group=multisite" + ], + "test:integration:multisite": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run wp-env:multisite:start", + "@test:integration:multisite:run" + ], + "test:integration:multisite:run": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run build", + "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration --group=multisite" ], "quality-check": [ diff --git a/plugins/internal-comments/package.json b/plugins/internal-comments/package.json index 061558f..9d53ece 100644 --- a/plugins/internal-comments/package.json +++ b/plugins/internal-comments/package.json @@ -59,6 +59,11 @@ "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + "wp-env:multisite:start": "wp-env --config .wp-env.multisite.json start", + "wp-env:multisite:stop": "wp-env --config .wp-env.multisite.json stop", + "wp-env:multisite:update": "wp-env --config .wp-env.multisite.json start --update", + "wp-env:multisite:destroy": "wp-env --config .wp-env.multisite.json destroy", + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", diff --git a/plugins/internal-comments/tests/Integration/MultisiteTest.php b/plugins/internal-comments/tests/Integration/MultisiteTest.php new file mode 100644 index 0000000..e2836b8 --- /dev/null +++ b/plugins/internal-comments/tests/Integration/MultisiteTest.php @@ -0,0 +1,36 @@ + get_network()?->domain, + 'path' => get_network()?->path . \str_replace( '_', '-', Plugin::PREFIX ) . '-second/', + ) + ); + self::assertIsInt( $site_id ); + + try { + $request = self::request( 'echo did_action( "dws_ic/boot/succeeded" );', '--url=' . get_home_url( $site_id, '/' ) ); + + self::assertSame( '1', $request['last'], $request['output'] ); + self::assertSame( 'no', get_blog_option( $site_id, DWS_IC_OPTION_PLUGIN )[ DWS_IC_SETTING_DELETE_DATA ] ?? null ); + self::assertTrue( update_blog_option( $site_id, DWS_IC_OPTION_PLUGIN, array( DWS_IC_SETTING_DELETE_DATA => 'yes' ) ) ); + self::assertSame( 'no', get_option( DWS_IC_OPTION_PLUGIN )[ DWS_IC_SETTING_DELETE_DATA ] ?? null ); + } finally { + wp_delete_site( $site_id ); + } + } +} diff --git a/plugins/internal-comments/tests/README.md b/plugins/internal-comments/tests/README.md index 11bded6..80f82ea 100644 --- a/plugins/internal-comments/tests/README.md +++ b/plugins/internal-comments/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data and capabilities, and the readme's version and floors | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, v1's keys, settings page and API, comment API, model class, capabilities and deprecated functions and hooks, the meta capabilities and private threads, the guards that keep internal comments under their status and the reads that leave them out without the plugin, the REST routes, the meta box, the list column, Comments screen and edit screen, orders, the migrations and the batched move of v1's comments, uninstall after v1's consent, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded, and the multisite site for its `multisite` group | the boot, every component, v1's keys, settings page and API, comment API, model class, capabilities and deprecated functions and hooks, the meta capabilities and private threads, the guards that keep internal comments under their status and the reads that leave them out without the plugin, the REST routes, the meta box, the list column, Comments screen and edit screen, orders, the migrations and the batched move of v1's comments, uninstall after v1's consent, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | posting, replying to, editing and trashing comments in the meta box, on a post and on an order, an empty comment refused, the quick view and the Comments screen's view, axe on the meta box and the quick view, and the plugin with WooCommerce deactivated | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. @@ -16,8 +16,9 @@ PHPUnit runs both PHP suites in random order, so each row sets up what it needs - `.wp-env.json` is the development site on , as `local`, where the framework rethrows boot and migration failures. - `.wp-env.tests.json` is the tests site on , as `production`, which the Integration and end-to-end suites share. A row whose separate process changes the database flushes the parent's object cache, and the end-to-end specs set their own preconditions, because either suite may run first. +- `.wp-env.multisite.json` is a network on , as `production`, with WooCommerce and the plugin network-activated, where the Integration suite's `multisite` group runs and the default run skips it. -Both start WordPress on PHP 8.5 with WooCommerce from `vendor/` and its order tables on. +Each starts WordPress on PHP 8.5 with WooCommerce from `vendor/` and its order tables on. ## Running @@ -33,6 +34,12 @@ Start the tests site, build the scripts and run the Integration suite: composer test:integration ``` +Start the multisite site, build the scripts and run the Integration suite's `multisite` group: + +```sh +composer test:integration:multisite +``` + Run the end-to-end suite, which starts the tests site when it is down and needs the built scripts and, once per machine, Playwright's Chromium: ```sh diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/tests-nightly.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/tests-nightly.yml index eec84c9..69824e8 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/tests-nightly.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/tests-nightly.yml @@ -19,6 +19,15 @@ jobs: composer-script: 'test:integration:run' wp-env-config-file: '.wp-env.tests.json' + integration-multisite: + name: Integration (multisite) + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + php-version: '8.5' + wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip' + composer-script: 'test:integration:multisite:run' + wp-env-config-file: '.wp-env.multisite.json' + e2e: name: E2E uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@b107001cce9f2960495d9d50481c8540d228f60c diff --git a/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml b/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml index 50c4d96..466d0d7 100644 --- a/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml +++ b/plugins/linked-orders-for-woocommerce/.github/workflows/tests.yml @@ -61,6 +61,20 @@ jobs: composer-script: 'test:integration:run' wp-env-config-file: '.wp-env.tests.json' + integration-multisite: + name: Integration (multisite) + needs: wordpress + strategy: + fail-fast: false + matrix: + wp: ${{ fromJSON(needs.wordpress.outputs.versions) }} + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + php-version: '8.5' + wp-version: ${{ matrix.wp }} + composer-script: 'test:integration:multisite:run' + wp-env-config-file: '.wp-env.multisite.json' + e2e: name: E2E needs: wordpress diff --git a/plugins/linked-orders-for-woocommerce/.wp-env.multisite.json b/plugins/linked-orders-for-woocommerce/.wp-env.multisite.json new file mode 100644 index 0000000..986fc8a --- /dev/null +++ b/plugins/linked-orders-for-woocommerce/.wp-env.multisite.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "core": "WordPress/WordPress#tags/7.1.4", + "phpVersion": "8.5", + "port": 8828, + "testsEnvironment": false, + "multisite": true, + "mappings": { + "wp-content/plugins/linked-orders-for-woocommerce": ".", + "wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce" + }, + "config": { + "WP_DEBUG": true, + "SCRIPT_DEBUG": true, + "WP_ENVIRONMENT_TYPE": false + }, + "lifecycleScripts": { + "afterStart": "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate woocommerce --network && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate linked-orders-for-woocommerce --network" + } +} diff --git a/plugins/linked-orders-for-woocommerce/AGENTS.md b/plugins/linked-orders-for-woocommerce/AGENTS.md index b2b13ce..c379eec 100644 --- a/plugins/linked-orders-for-woocommerce/AGENTS.md +++ b/plugins/linked-orders-for-woocommerce/AGENTS.md @@ -41,6 +41,7 @@ changelog/ the pending entries changelog:write turns into CHAN composer packages-install # dependencies, then php-scoper into vendor-prefixed/ composer test:unit composer test:integration # starts .wp-env.tests.json, then runs test:integration:run, which builds the scripts first +composer test:integration:multisite # starts .wp-env.multisite.json, then runs the Integration suite's multisite group, which the default run skips composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan (both profiles), Unit and Integration composer changelog:add # stages an entry in changelog/, which the release step in the monorepo's CONTRIBUTING.md writes npm run setup # both installs, the scripts' build, then the development site @@ -55,8 +56,9 @@ Run wp-scripts only through the npm scripts or `node_modules/.bin/wp-scripts`: ` - `.wp-env.json`: development, as `local`, on port 8822, behind `npm run setup` and `wp-env:*`. The framework rethrows boot and migration failures there. - `.wp-env.tests.json`: Integration and end to end, as `production`, on port 8823, behind `wp-env:tests:*`, `composer test:integration` and `npm run test:e2e`. The end-to-end suite deactivates and reactivates WooCommerce there. +- `.wp-env.multisite.json`: a network with WooCommerce and the plugin network-activated, as `production`, on port 8828, behind `wp-env:multisite:*` and `composer test:integration:multisite`, for the Integration suite's `multisite` group. -Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. +Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. ## Conventions diff --git a/plugins/linked-orders-for-woocommerce/composer.json b/plugins/linked-orders-for-woocommerce/composer.json index d067767..68ffba6 100644 --- a/plugins/linked-orders-for-woocommerce/composer.json +++ b/plugins/linked-orders-for-woocommerce/composer.json @@ -125,7 +125,19 @@ "Composer\\Config::disableProcessTimeout", "@putenv CI=1", "npm --prefix=. run build", - "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration" + "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration --exclude-group=multisite" + ], + "test:integration:multisite": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run wp-env:multisite:start", + "@test:integration:multisite:run" + ], + "test:integration:multisite:run": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run build", + "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration --group=multisite" ], "quality-check": [ diff --git a/plugins/linked-orders-for-woocommerce/package.json b/plugins/linked-orders-for-woocommerce/package.json index 00839a5..df4050a 100644 --- a/plugins/linked-orders-for-woocommerce/package.json +++ b/plugins/linked-orders-for-woocommerce/package.json @@ -57,6 +57,11 @@ "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + "wp-env:multisite:start": "wp-env --config .wp-env.multisite.json start", + "wp-env:multisite:stop": "wp-env --config .wp-env.multisite.json stop", + "wp-env:multisite:update": "wp-env --config .wp-env.multisite.json start --update", + "wp-env:multisite:destroy": "wp-env --config .wp-env.multisite.json destroy", + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php new file mode 100644 index 0000000..8f1caea --- /dev/null +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php @@ -0,0 +1,36 @@ + get_network()?->domain, + 'path' => get_network()?->path . \str_replace( '_', '-', Plugin::PREFIX ) . '-second/', + ) + ); + self::assertIsInt( $site_id ); + + try { + $request = self::request( 'echo did_action( "dws_lowc/boot/succeeded" );', '--url=' . get_home_url( $site_id, '/' ) ); + + self::assertSame( '1', $request['last'], $request['output'] ); + self::assertSame( 'no', get_blog_option( $site_id, DWS_LOWC_OPTION_DELETE_DATA ) ); + self::assertTrue( update_blog_option( $site_id, DWS_LOWC_OPTION_DELETE_DATA, 'yes' ) ); + self::assertSame( 'no', get_option( DWS_LOWC_OPTION_DELETE_DATA ) ); + } finally { + wp_delete_site( $site_id ); + } + } +} diff --git a/plugins/linked-orders-for-woocommerce/tests/README.md b/plugins/linked-orders-for-woocommerce/tests/README.md index c2f2817..1050a8b 100644 --- a/plugins/linked-orders-for-woocommerce/tests/README.md +++ b/plugins/linked-orders-for-woocommerce/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, v1's keys, settings API, linking API, node class, capabilities and deprecated functions and hooks, the options and their readers, the migrations, deactivation, uninstall after v1's consent, the assets, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded, and the multisite site for its `multisite` group | the boot, every component, v1's keys, settings API, linking API, node class, capabilities and deprecated functions and hooks, the options and their readers, the migrations, deactivation, uninstall after v1's consent, the assets, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | a child created from an order's meta box and both orders' depth in the orders list, the plugin row's link to its settings section, and the plugin with WooCommerce deactivated | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. @@ -16,8 +16,9 @@ PHPUnit runs both PHP suites in random order, so each row sets up what it needs - `.wp-env.json` is the development site on , as `local`, where the framework rethrows boot and migration failures. - `.wp-env.tests.json` is the tests site on , as `production`, which the Integration and end-to-end suites share. A row whose separate process changes the database flushes the parent's object cache, and the end-to-end specs set their own preconditions, because either suite may run first. +- `.wp-env.multisite.json` is a network on , as `production`, with WooCommerce and the plugin network-activated, where the Integration suite's `multisite` group runs and the default run skips it. -Both start WordPress on PHP 8.5 with WooCommerce from `vendor/` and its order tables on. +Each starts WordPress on PHP 8.5 with WooCommerce from `vendor/` and its order tables on. ## Running @@ -33,6 +34,12 @@ Start the tests site, build the scripts and run the Integration suite: composer test:integration ``` +Start the multisite site, build the scripts and run the Integration suite's `multisite` group: + +```sh +composer test:integration:multisite +``` + Run the end-to-end suite, which starts the tests site when it is down and needs the built scripts and, once per machine, Playwright's Chromium: ```sh diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml index eec84c9..69824e8 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml @@ -19,6 +19,15 @@ jobs: composer-script: 'test:integration:run' wp-env-config-file: '.wp-env.tests.json' + integration-multisite: + name: Integration (multisite) + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + php-version: '8.5' + wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip' + composer-script: 'test:integration:multisite:run' + wp-env-config-file: '.wp-env.multisite.json' + e2e: name: E2E uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@b107001cce9f2960495d9d50481c8540d228f60c diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml index 50c4d96..466d0d7 100644 --- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml +++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml @@ -61,6 +61,20 @@ jobs: composer-script: 'test:integration:run' wp-env-config-file: '.wp-env.tests.json' + integration-multisite: + name: Integration (multisite) + needs: wordpress + strategy: + fail-fast: false + matrix: + wp: ${{ fromJSON(needs.wordpress.outputs.versions) }} + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@b107001cce9f2960495d9d50481c8540d228f60c + with: + php-version: '8.5' + wp-version: ${{ matrix.wp }} + composer-script: 'test:integration:multisite:run' + wp-env-config-file: '.wp-env.multisite.json' + e2e: name: E2E needs: wordpress diff --git a/plugins/locked-payment-methods-for-woocommerce/.wp-env.multisite.json b/plugins/locked-payment-methods-for-woocommerce/.wp-env.multisite.json new file mode 100644 index 0000000..fa5d4a0 --- /dev/null +++ b/plugins/locked-payment-methods-for-woocommerce/.wp-env.multisite.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "core": "WordPress/WordPress#tags/7.1.4", + "phpVersion": "8.5", + "port": 8830, + "testsEnvironment": false, + "multisite": true, + "mappings": { + "wp-content/plugins/locked-payment-methods-for-woocommerce": ".", + "wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce" + }, + "config": { + "WP_DEBUG": true, + "SCRIPT_DEBUG": true, + "WP_ENVIRONMENT_TYPE": false + }, + "lifecycleScripts": { + "afterStart": "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate woocommerce --network && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.multisite.json run cli wp plugin activate locked-payment-methods-for-woocommerce --network" + } +} diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md index 1ecc019..140ab58 100644 --- a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md +++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md @@ -38,6 +38,7 @@ changelog/ the pending entries changelog:write turns into CHAN composer packages-install # dependencies, then php-scoper into vendor-prefixed/ composer test:unit composer test:integration # starts .wp-env.tests.json, then runs test:integration:run +composer test:integration:multisite # starts .wp-env.multisite.json, then runs the Integration suite's multisite group, which the default run skips composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan (both profiles), Unit and Integration composer changelog:add # stages an entry in changelog/, which the release step in the monorepo's CONTRIBUTING.md writes npm run setup # both installs, then the development site @@ -51,8 +52,9 @@ Run wp-scripts only through the npm scripts or `node_modules/.bin/wp-scripts`: ` - `.wp-env.json`: development, as `local`, on port 8826, behind `npm run setup` and `wp-env:*`. The framework rethrows boot and migration failures there. - `.wp-env.tests.json`: Integration and end to end, as `production`, on port 8827, behind `wp-env:tests:*`, `composer test:integration` and `npm run test:e2e`. The end-to-end suite deactivates and reactivates WooCommerce there. +- `.wp-env.multisite.json`: a network with WooCommerce and the plugin network-activated, as `production`, on port 8830, behind `wp-env:multisite:*` and `composer test:integration:multisite`, for the Integration suite's `multisite` group. -Both run PHP 8.5 with WooCommerce and Groups from `vendor/` and WooCommerce's order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. The monorepo's shared environment runs the Integration suite beside Linked Orders and Internal Comments, without the `groups` and `wc-memberships` groups. +Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on, and the development and tests sites with Groups too; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. The monorepo's shared environment runs the Integration suite beside Linked Orders and Internal Comments, without the `groups` and `multisite` groups, and its multisite environment runs the `multisite` group. ## Conventions @@ -82,5 +84,5 @@ Both run PHP 8.5 with WooCommerce and Groups from `vendor/` and WooCommerce's or - Integration and end to end share the tests site, so a row that changes shared data in a separate process flushes the parent's object cache. - The Memberships rows run against a double of its one function and its three hooks, since Memberships is a paid plugin absent from WP Packages; they are unverified against a licensed copy. -- No environment runs a multisite network, so the migration that copies v1's user grants into every site's list runs only on a single site in the suites. +- The `multisite` group seeds no v1 user grants, so their copy into every site's list is exercised only on a single site. - The bundled GitHub updater ships inert, since the entry file has no `Update URI`, and wordpress.org's reviewers may ask to remove it. diff --git a/plugins/locked-payment-methods-for-woocommerce/composer.json b/plugins/locked-payment-methods-for-woocommerce/composer.json index abfe16c..f43a836 100644 --- a/plugins/locked-payment-methods-for-woocommerce/composer.json +++ b/plugins/locked-payment-methods-for-woocommerce/composer.json @@ -123,7 +123,19 @@ "Composer\\Config::disableProcessTimeout", "@putenv CI=1", "npm --prefix=. run build", - "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration" + "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration --exclude-group=multisite" + ], + "test:integration:multisite": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run wp-env:multisite:start", + "@test:integration:multisite:run" + ], + "test:integration:multisite:run": [ + "Composer\\Config::disableProcessTimeout", + "@putenv CI=1", + "npm --prefix=. run build", + "node_modules/.bin/wp-env --config .wp-env.multisite.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration --group=multisite" ], "quality-check": [ diff --git a/plugins/locked-payment-methods-for-woocommerce/package.json b/plugins/locked-payment-methods-for-woocommerce/package.json index 0619112..aae4296 100644 --- a/plugins/locked-payment-methods-for-woocommerce/package.json +++ b/plugins/locked-payment-methods-for-woocommerce/package.json @@ -54,6 +54,11 @@ "wp-env:tests:update": "wp-env --config .wp-env.tests.json start --update", "wp-env:tests:destroy": "wp-env --config .wp-env.tests.json destroy", + "wp-env:multisite:start": "wp-env --config .wp-env.multisite.json start", + "wp-env:multisite:stop": "wp-env --config .wp-env.multisite.json stop", + "wp-env:multisite:update": "wp-env --config .wp-env.multisite.json start --update", + "wp-env:multisite:destroy": "wp-env --config .wp-env.multisite.json destroy", + "audit": "composer audit --locked --abandoned=report && npm audit --omit=dev --audit-level=high", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php new file mode 100644 index 0000000..946b32c --- /dev/null +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php @@ -0,0 +1,36 @@ + get_network()?->domain, + 'path' => get_network()?->path . \str_replace( '_', '-', Plugin::PREFIX ) . '-second/', + ) + ); + self::assertIsInt( $site_id ); + + try { + $request = self::request( 'echo did_action( "dws_lpmwc/boot/succeeded" );', '--url=' . get_home_url( $site_id, '/' ) ); + + self::assertSame( '1', $request['last'], $request['output'] ); + self::assertSame( 'no', get_blog_option( $site_id, DWS_LPMWC_OPTION_DELETE_DATA ) ); + self::assertTrue( update_blog_option( $site_id, DWS_LPMWC_OPTION_DELETE_DATA, 'yes' ) ); + self::assertSame( 'no', get_option( DWS_LPMWC_OPTION_DELETE_DATA ) ); + } finally { + wp_delete_site( $site_id ); + } + } +} diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/README.md b/plugins/locked-payment-methods-for-woocommerce/tests/README.md index 587eacf..cde4bcd 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/README.md +++ b/plugins/locked-payment-methods-for-woocommerce/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce, Groups and order tables loaded | the boot, the lock and its relaxation, fresh requests below the PHP and WooCommerce floors, every strategy and its form, the settings, the capabilities, v1's deprecated functions and hooks, the migrations from v1, deactivation, uninstall, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`, the Groups rows run in the `groups` group against Groups itself, and the Memberships rows in the `wc-memberships` group against a double of its one function | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce, Groups and order tables loaded, and the multisite site, without Groups, for its `multisite` group | the boot, the lock and its relaxation, fresh requests below the PHP and WooCommerce floors, every strategy and its form, the settings, the capabilities, v1's deprecated functions and hooks, the migrations from v1, deactivation, uninstall, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`, the Groups rows run in the `groups` group against Groups itself, and the Memberships rows in the `wc-memberships` group against a double of its one function | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | a locked payment method a customer is granted on the block checkout, the Store API's refusal of a guest's checkout with a locked payment method, and the plugin with WooCommerce deactivated; the monorepo's shared environment checks a locked payment method's absence from a guest's classic and block checkout | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. @@ -16,8 +16,9 @@ PHPUnit runs both PHP suites in random order, so each row sets up what it needs - `.wp-env.json` is the development site on , as `local`, where the framework rethrows boot and migration failures. - `.wp-env.tests.json` is the tests site on , as `production`, which the Integration and end-to-end suites share. A row whose separate process changes the database flushes the parent's object cache, and the end-to-end specs set their own preconditions, because either suite may run first. +- `.wp-env.multisite.json` is a network on , as `production`, with WooCommerce and the plugin network-activated, where the Integration suite's `multisite` group runs and the default run skips it. -Both start WordPress on PHP 8.5 with WooCommerce and Groups from `vendor/` and WooCommerce's order tables on. +Each starts WordPress on PHP 8.5 with WooCommerce from `vendor/` and WooCommerce's order tables on, and the development and tests sites load Groups from `vendor/` too. ## Running @@ -33,6 +34,12 @@ Start the tests site and run the Integration suite: composer test:integration ``` +Start the multisite site, build the scripts and run the Integration suite's `multisite` group: + +```sh +composer test:integration:multisite +``` + Run the end-to-end suite, which starts the tests site when it is down and needs, once per machine, Playwright's Chromium: ```sh From 7597cbe24705d6e6060edcbe0811c81abe476ed9 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:30:17 +0100 Subject: [PATCH 09/39] Test a web uninstall whose engine fails in each plugin When the uninstall engine cannot remove a row, it throws, and each plugin's uninstall.php lets the throw escape, so a web uninstall shows only the critical-error page and WordPress then deletes the plugin's files with its data half removed (A22). Each plugin gains the template's row: a web uninstall whose engine fails on the revision option must end with the engine's message as a 500 and keep the site's consent, so the next uninstall retries. The rows fail until uninstall.php catches the engine's failure. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/UninstallTest.php | 29 +++++++++++++++++++ .../tests/Integration/UninstallTest.php | 29 +++++++++++++++++++ .../tests/Integration/UninstallTest.php | 29 +++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/plugins/internal-comments/tests/Integration/UninstallTest.php b/plugins/internal-comments/tests/Integration/UninstallTest.php index f38e103..59f4b3f 100644 --- a/plugins/internal-comments/tests/Integration/UninstallTest.php +++ b/plugins/internal-comments/tests/Integration/UninstallTest.php @@ -57,6 +57,35 @@ public function test_an_uninstall_in_a_request_that_skips_the_plugin_removes_its self::assertTrue( get_role( 'editor' )?->has_cap( DWS_IC_CAP_INSERT ) ); } + #[RunInSeparateProcess] + public function test_a_web_uninstall_whose_engine_fails_ends_with_its_message_and_keeps_the_consent(): void { + global $wpdb; + + \define( 'WP_UNINSTALL_PLUGIN', plugin_basename( DWS_IC_FILE ) ); + update_option( DWS_IC_OPTION_PLUGIN, array( DWS_IC_SETTING_DELETE_DATA => 'yes' ) ); + $failing = "DELETE FROM `$wpdb->options` WHERE `option_name` = '" . Plugin::PREFIX . "_revision'"; + add_filter( 'query', static fn ( string $query ): string => \str_contains( $query, $failing ) ? 'SELECT * FROM dws_ic_missing_table' : $query ); + add_filter( 'wp_die_handler', static fn (): \Closure => static fn ( string $message, string $title, array $args ): never => throw new \LogicException( $args['response'] . ' ' . $message ) ); + $wpdb->suppress_errors(); + + try { + include \dirname( DWS_IC_FILE ) . '/uninstall.php'; + self::fail( 'The uninstall finished.' ); + } catch ( \LogicException $died ) { + self::assertStringStartsWith( '500 Uninstall could not remove everything: option ' . Plugin::PREFIX . '_revision', $died->getMessage() ); + self::assertSame( 'yes', get_option( DWS_IC_OPTION_PLUGIN )[ DWS_IC_SETTING_DELETE_DATA ] ?? null ); + } finally { + remove_all_filters( 'query' ); + delete_option( DWS_IC_OPTION_PLUGIN ); + delete_option( Plugin::PREFIX . '_revision' ); + wp_cache_flush(); + wp_roles()->for_site(); + // The other tests share the database, so the runner stores again what a new site starts with, and the grants act again. + do_action( Plugin::PREFIX . '_revision/migration/run' ); // A batched migration step runs only from the runner's scheduled hook. + do_action( 'wp_loaded' ); + } + } + #[RunInSeparateProcess] public function test_an_uninstall_after_v1_consent_removes_the_internal_comments_v1s_rows_and_the_capabilities(): void { update_option( DWS_IC_OPTION_GENERAL, array( DWS_IC_SETTING_POST_TYPES => array( 'post' ) ) ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php index 2b08b5a..3b8db06 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php @@ -57,6 +57,35 @@ public function test_an_uninstall_in_a_request_that_skips_the_plugin_removes_its self::assertTrue( get_role( 'shop_manager' )?->has_cap( DWS_LOWC_CAP_CREATE_LINKED_CHILDREN ) ); } + #[RunInSeparateProcess] + public function test_a_web_uninstall_whose_engine_fails_ends_with_its_message_and_keeps_the_consent(): void { + global $wpdb; + + \define( 'WP_UNINSTALL_PLUGIN', plugin_basename( DWS_LOWC_FILE ) ); + update_option( DWS_LOWC_OPTION_DELETE_DATA, 'yes' ); + $failing = "DELETE FROM `$wpdb->options` WHERE `option_name` = '" . Plugin::PREFIX . "_revision'"; + add_filter( 'query', static fn ( string $query ): string => \str_contains( $query, $failing ) ? 'SELECT * FROM dws_lowc_missing_table' : $query ); + add_filter( 'wp_die_handler', static fn (): \Closure => static fn ( string $message, string $title, array $args ): never => throw new \LogicException( $args['response'] . ' ' . $message ) ); + $wpdb->suppress_errors(); + + try { + include \dirname( DWS_LOWC_FILE ) . '/uninstall.php'; + self::fail( 'The uninstall finished.' ); + } catch ( \LogicException $died ) { + self::assertStringStartsWith( '500 Uninstall could not remove everything: option ' . Plugin::PREFIX . '_revision', $died->getMessage() ); + self::assertSame( 'yes', get_option( DWS_LOWC_OPTION_DELETE_DATA ) ); + } finally { + remove_all_filters( 'query' ); + delete_option( DWS_LOWC_OPTION_DELETE_DATA ); + delete_option( Plugin::PREFIX . '_revision' ); + wp_cache_flush(); + wp_roles()->for_site(); + // The other tests share the database, so the runner stores again what a new site starts with, and the grants act again. + dws_lowc_plugin()->migrations?->run(); + do_action( 'wp_loaded' ); + } + } + #[RunInSeparateProcess] public function test_an_uninstall_after_v1_consent_removes_the_links_on_both_storages_v1s_rows_and_the_capabilities(): void { global $wpdb; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php index 8815cf8..5f15e28 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php @@ -95,6 +95,35 @@ public function test_an_uninstall_in_a_request_that_never_loaded_the_plugin_remo } } + #[RunInSeparateProcess] + public function test_a_web_uninstall_whose_engine_fails_ends_with_its_message_and_keeps_the_consent(): void { + global $wpdb; + + \define( 'WP_UNINSTALL_PLUGIN', plugin_basename( DWS_LPMWC_FILE ) ); + update_option( DWS_LPMWC_OPTION_DELETE_DATA, 'yes' ); + $failing = "DELETE FROM `$wpdb->options` WHERE `option_name` = '" . Plugin::PREFIX . "_revision'"; + add_filter( 'query', static fn ( string $query ): string => \str_contains( $query, $failing ) ? 'SELECT * FROM dws_lpmwc_missing_table' : $query ); + add_filter( 'wp_die_handler', static fn (): \Closure => static fn ( string $message, string $title, array $args ): never => throw new \LogicException( $args['response'] . ' ' . $message ) ); + $wpdb->suppress_errors(); + + try { + include \dirname( DWS_LPMWC_FILE ) . '/uninstall.php'; + self::fail( 'The uninstall finished.' ); + } catch ( \LogicException $died ) { + self::assertStringStartsWith( '500 Uninstall could not remove everything: option ' . Plugin::PREFIX . '_revision', $died->getMessage() ); + self::assertSame( 'yes', get_option( DWS_LPMWC_OPTION_DELETE_DATA ) ); + } finally { + remove_all_filters( 'query' ); + delete_option( DWS_LPMWC_OPTION_DELETE_DATA ); + delete_option( Plugin::PREFIX . '_revision' ); + wp_cache_flush(); + wp_roles()->for_site(); + // The other tests share the database, so the runner stores again what a new site starts with, and the grants act again. + dws_lpmwc_plugin()->migrations?->run(); + do_action( 'wp_loaded' ); + } + } + #[RunInSeparateProcess] public function test_a_consenting_uninstall_removes_the_grants_v1s_rows_and_the_capabilities(): void { global $wpdb; From 1c7a278fc78a1bf6c279ff6fa7f9731921e6ed1b Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:30:54 +0100 Subject: [PATCH 10/39] End a failed web uninstall with the engine's reason Each plugin's uninstall.php let the engine's RuntimeException escape, so a web uninstall that could not remove every row showed the critical-error page and let WordPress delete the files anyway, leaving the remaining data with no plugin to retry it (A22). As in the template, uninstall.php catches the engine's failure: WP-CLI rethrows it, and a web request ends in wp_die() with the reason as a 500 before WordPress deletes the files, so the consent stays and the next uninstall retries. Each plugin passes its consent to the engine directly (A264), Internal Comments with the key inside its option. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/uninstall.php | 28 +++++++++++++------ .../uninstall.php | 26 +++++++++++------ .../uninstall.php | 26 +++++++++++------ 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/plugins/internal-comments/uninstall.php b/plugins/internal-comments/uninstall.php index 30f8e57..ddb0487 100644 --- a/plugins/internal-comments/uninstall.php +++ b/plugins/internal-comments/uninstall.php @@ -30,10 +30,6 @@ require_once __DIR__ . '/includes/footprint.php'; require_once __DIR__ . '/includes/options.php'; $dws_ic_footprint = dws_ic_get_footprint(); - $dws_ic_consent = array( - 'option' => DWS_IC_OPTION_PLUGIN, - 'key' => DWS_IC_SETTING_DELETE_DATA, - ); if ( ! class_exists( Uninstaller::class ) ) { throw new RuntimeException( 'its uninstall engine is missing.' ); } @@ -47,8 +43,22 @@ return; } -Uninstaller::run( - $dws_ic_file, - $dws_ic_footprint, - array( 'consent' => $dws_ic_consent ) -); +try { + Uninstaller::run( + $dws_ic_file, + $dws_ic_footprint, + array( + 'consent' => array( + 'option' => DWS_IC_OPTION_PLUGIN, + 'key' => DWS_IC_SETTING_DELETE_DATA, + ), + ) + ); +} catch ( RuntimeException $exception ) { + if ( defined( 'WP_CLI' ) && true === WP_CLI ) { + throw $exception; + } + + // wp_die() ends the request before WordPress deletes the files, so the next uninstall retries, and shows the reason where a throw shows only the critical-error page. + wp_die( esc_html( $exception->getMessage() ), '', array( 'response' => 500 ) ); +} diff --git a/plugins/linked-orders-for-woocommerce/uninstall.php b/plugins/linked-orders-for-woocommerce/uninstall.php index bc52a47..e789915 100644 --- a/plugins/linked-orders-for-woocommerce/uninstall.php +++ b/plugins/linked-orders-for-woocommerce/uninstall.php @@ -31,7 +31,6 @@ require_once __DIR__ . '/includes/footprint.php'; require_once __DIR__ . '/includes/options.php'; $dws_lowc_footprint = dws_lowc_get_footprint(); - $dws_lowc_consent = DWS_LOWC_OPTION_DELETE_DATA; $dws_lowc_meta_types = MetaTypes::DEFINITIONS; if ( ! class_exists( Uninstaller::class ) ) { throw new RuntimeException( 'its uninstall engine is missing.' ); @@ -46,11 +45,20 @@ return; } -Uninstaller::run( - $dws_lowc_file, - $dws_lowc_footprint, - array( - 'consent' => array( 'option' => $dws_lowc_consent ), - 'meta_types' => $dws_lowc_meta_types, - ) -); +try { + Uninstaller::run( + $dws_lowc_file, + $dws_lowc_footprint, + array( + 'consent' => array( 'option' => DWS_LOWC_OPTION_DELETE_DATA ), + 'meta_types' => $dws_lowc_meta_types, + ) + ); +} catch ( RuntimeException $exception ) { + if ( defined( 'WP_CLI' ) && true === WP_CLI ) { + throw $exception; + } + + // wp_die() ends the request before WordPress deletes the files, so the next uninstall retries, and shows the reason where a throw shows only the critical-error page. + wp_die( esc_html( $exception->getMessage() ), '', array( 'response' => 500 ) ); +} diff --git a/plugins/locked-payment-methods-for-woocommerce/uninstall.php b/plugins/locked-payment-methods-for-woocommerce/uninstall.php index 2f184b5..bd2d658 100644 --- a/plugins/locked-payment-methods-for-woocommerce/uninstall.php +++ b/plugins/locked-payment-methods-for-woocommerce/uninstall.php @@ -32,7 +32,6 @@ require_once __DIR__ . '/includes/grants.php'; require_once __DIR__ . '/includes/options.php'; $dws_lpmwc_footprint = dws_lpmwc_get_footprint(); - $dws_lpmwc_consent = DWS_LPMWC_OPTION_DELETE_DATA; $dws_lpmwc_meta_types = MetaTypes::DEFINITIONS; if ( ! class_exists( Uninstaller::class ) ) { throw new RuntimeException( 'its uninstall engine is missing.' ); @@ -47,11 +46,20 @@ return; } -Uninstaller::run( - $dws_lpmwc_file, - $dws_lpmwc_footprint, - array( - 'consent' => array( 'option' => $dws_lpmwc_consent ), - 'meta_types' => $dws_lpmwc_meta_types, - ) -); +try { + Uninstaller::run( + $dws_lpmwc_file, + $dws_lpmwc_footprint, + array( + 'consent' => array( 'option' => DWS_LPMWC_OPTION_DELETE_DATA ), + 'meta_types' => $dws_lpmwc_meta_types, + ) + ); +} catch ( RuntimeException $exception ) { + if ( defined( 'WP_CLI' ) && true === WP_CLI ) { + throw $exception; + } + + // wp_die() ends the request before WordPress deletes the files, so the next uninstall retries, and shows the reason where a throw shows only the critical-error page. + wp_die( esc_html( $exception->getMessage() ), '', array( 'response' => 500 ) ); +} From 1190e9850a429fd33428711539c99a2c31e6c77d Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:31:22 +0100 Subject: [PATCH 11/39] Test a boot listener that throws in each plugin Each plugin's boot function wraps both building the composition root and booting it in one catch, so a listener on {prefix}/boot/succeeded that throws is reported as an incomplete build, with the notice that tells the site owner to reinstall a plugin whose files are complete. Each plugin gains the template's row: a throwing boot listener shows its own error and no incomplete-build report. The rows fail until the boot function catches only the root's construction. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/PluginBootTest.php | 13 +++++++++++++ .../tests/Integration/PluginBootTest.php | 13 +++++++++++++ .../tests/Integration/PluginBootTest.php | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/plugins/internal-comments/tests/Integration/PluginBootTest.php b/plugins/internal-comments/tests/Integration/PluginBootTest.php index 44d8f0c..9a30c5a 100644 --- a/plugins/internal-comments/tests/Integration/PluginBootTest.php +++ b/plugins/internal-comments/tests/Integration/PluginBootTest.php @@ -34,6 +34,19 @@ public function test_the_entry_loads_every_include_but_those_whose_names_start_w self::assertSame( 'skipped', $request['last'], $request['output'] ); } + public function test_a_boot_listener_that_throws_is_not_reported_as_an_incomplete_build(): void { + $include = \dirname( DWS_IC_FILE ) . '/includes/zz-boot-listener.php'; + \file_put_contents( $include, " Date: Wed, 11 Nov 2026 16:31:58 +0100 Subject: [PATCH 12/39] Report only an unbuildable composition root as a build error Each plugin's boot and deactivation functions caught every throw from building the composition root and from booting or deactivating it, so a hook listener's failure read as a partial deploy, which PluginBoot never is, since it reports a failed composition itself. As in the template, each catches only the root's construction and then boots or deactivates outside the catch (A211). Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/functions.php | 10 ++++++++-- plugins/linked-orders-for-woocommerce/functions.php | 10 ++++++++-- .../functions.php | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/plugins/internal-comments/functions.php b/plugins/internal-comments/functions.php index 79bd53b..2fb2e8b 100644 --- a/plugins/internal-comments/functions.php +++ b/plugins/internal-comments/functions.php @@ -59,10 +59,13 @@ function dws_ic_plugin(): Plugin { */ function dws_ic_boot(): void { try { - dws_ic_plugin()->boot(); + $plugin = dws_ic_plugin(); } catch ( Throwable $error ) { dws_ic_handle_build_error( $error ); + return; } + + $plugin->boot(); // PluginBoot reports a failed composition itself, and a hook listener's throw is not a build error. } /** @@ -75,10 +78,13 @@ function dws_ic_boot(): void { */ function dws_ic_deactivate(): void { try { - dws_ic_plugin()->deactivate(); + $plugin = dws_ic_plugin(); } catch ( Throwable $error ) { dws_ic_handle_build_error( $error ); + return; } + + $plugin->deactivate(); } /** diff --git a/plugins/linked-orders-for-woocommerce/functions.php b/plugins/linked-orders-for-woocommerce/functions.php index 2d686e8..509e17e 100644 --- a/plugins/linked-orders-for-woocommerce/functions.php +++ b/plugins/linked-orders-for-woocommerce/functions.php @@ -59,10 +59,13 @@ function dws_lowc_plugin(): Plugin { */ function dws_lowc_boot(): void { try { - dws_lowc_plugin()->boot(); + $plugin = dws_lowc_plugin(); } catch ( Throwable $error ) { dws_lowc_handle_build_error( $error ); + return; } + + $plugin->boot(); // PluginBoot reports a failed composition itself, and a hook listener's throw is not a build error. } /** @@ -75,10 +78,13 @@ function dws_lowc_boot(): void { */ function dws_lowc_deactivate(): void { try { - dws_lowc_plugin()->deactivate(); + $plugin = dws_lowc_plugin(); } catch ( Throwable $error ) { dws_lowc_handle_build_error( $error ); + return; } + + $plugin->deactivate(); } /** diff --git a/plugins/locked-payment-methods-for-woocommerce/functions.php b/plugins/locked-payment-methods-for-woocommerce/functions.php index ddf3ffc..937ec31 100644 --- a/plugins/locked-payment-methods-for-woocommerce/functions.php +++ b/plugins/locked-payment-methods-for-woocommerce/functions.php @@ -59,10 +59,13 @@ function dws_lpmwc_plugin(): Plugin { */ function dws_lpmwc_boot(): void { try { - dws_lpmwc_plugin()->boot(); + $plugin = dws_lpmwc_plugin(); } catch ( Throwable $error ) { dws_lpmwc_handle_build_error( $error ); + return; } + + $plugin->boot(); // PluginBoot reports a failed composition itself, and a hook listener's throw is not a build error. } /** @@ -75,10 +78,13 @@ function dws_lpmwc_boot(): void { */ function dws_lpmwc_deactivate(): void { try { - dws_lpmwc_plugin()->deactivate(); + $plugin = dws_lpmwc_plugin(); } catch ( Throwable $error ) { dws_lpmwc_handle_build_error( $error ); + return; } + + $plugin->deactivate(); } /** From f52c5feb08428dec5abba2f9001845faf1d25ff4 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:33:21 +0100 Subject: [PATCH 13/39] Gate each entry's build in the template's form Each entry folded the bootstrap package's absence into its build check behind an if that skipped the floor gate, where the template, after consumer-a, returns at once without the bootstrap package and then checks the build tree the plugin reads (A212). Each entry now shows the build notice and returns when the bootstrap package is missing, runs the updater and the floor gate unguarded, and then refuses a build without its own tree: Linked Orders' and Internal Comments' assets, while Locked Payment Methods builds none. A plugin builds one tree in one command, so the existing partial-deploy rows cover each check and the behaviour is unchanged. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/internal-comments.php | 17 ++++++++++------- .../linked-orders-for-woocommerce.php | 17 ++++++++++------- .../locked-payment-methods-for-woocommerce.php | 14 ++++++-------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/plugins/internal-comments/internal-comments.php b/plugins/internal-comments/internal-comments.php index 4c02ae4..0a1136f 100644 --- a/plugins/internal-comments/internal-comments.php +++ b/plugins/internal-comments/internal-comments.php @@ -28,15 +28,18 @@ require_once __DIR__ . '/includes/safeguards.php'; // Before the floor gate, so the restrictions hold on any runtime that parses the plugin. $dws_ic_bootstrap = __DIR__ . '/vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php'; -if ( is_readable( $dws_ic_bootstrap ) ) { - require_once $dws_ic_bootstrap; - Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. - if ( ! Requirements::check_floors( __FILE__ ) ) { - return; - } +if ( ! is_readable( $dws_ic_bootstrap ) ) { // The floor gate lives in the bootstrap package, so nothing built may load without it. + add_action( 'all_admin_notices', 'dws_ic_render_build_notice' ); + return; +} + +require_once $dws_ic_bootstrap; +Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. +if ( ! Requirements::check_floors( __FILE__ ) ) { + return; } -if ( ! is_readable( $dws_ic_bootstrap ) || ! is_readable( __DIR__ . '/assets/build/meta-box.asset.php' ) ) { // The floor gate never ran, or the scripts were never built, so nothing built may load. +if ( ! is_readable( __DIR__ . '/assets/build/meta-box.asset.php' ) ) { // The plugin reads its build tree unchecked, so a build that did not write it must not load. add_action( 'all_admin_notices', 'dws_ic_render_build_notice' ); return; } diff --git a/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php b/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php index 5742bba..00953ae 100644 --- a/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php +++ b/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php @@ -29,15 +29,18 @@ require_once __DIR__ . '/includes/safeguards.php'; // Before the floor gate, so the restrictions hold on any runtime that parses the plugin. $dws_lowc_bootstrap = __DIR__ . '/vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php'; -if ( is_readable( $dws_lowc_bootstrap ) ) { - require_once $dws_lowc_bootstrap; - Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. - if ( ! Requirements::check_floors( __FILE__ ) ) { - return; - } +if ( ! is_readable( $dws_lowc_bootstrap ) ) { // The floor gate lives in the bootstrap package, so nothing built may load without it. + add_action( 'all_admin_notices', 'dws_lowc_render_build_notice' ); + return; +} + +require_once $dws_lowc_bootstrap; +Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. +if ( ! Requirements::check_floors( __FILE__ ) ) { + return; } -if ( ! is_readable( $dws_lowc_bootstrap ) || ! is_readable( __DIR__ . '/assets/build/orders-list-table.asset.php' ) ) { // The floor gate never ran, or the scripts were never built, so nothing built may load. +if ( ! is_readable( __DIR__ . '/assets/build/orders-list-table.asset.php' ) ) { // The plugin reads its build tree unchecked, so a build that did not write it must not load. add_action( 'all_admin_notices', 'dws_lowc_render_build_notice' ); return; } diff --git a/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php b/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php index ea15055..517250a 100644 --- a/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php +++ b/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php @@ -29,16 +29,14 @@ require_once __DIR__ . '/includes/safeguards.php'; // Before the floor gate, so the restrictions hold on any runtime that parses the plugin. $dws_lpmwc_bootstrap = __DIR__ . '/vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php'; -if ( is_readable( $dws_lpmwc_bootstrap ) ) { - require_once $dws_lpmwc_bootstrap; - Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. - if ( ! Requirements::check_floors( __FILE__ ) ) { - return; - } +if ( ! is_readable( $dws_lpmwc_bootstrap ) ) { // The floor gate lives in the bootstrap package, so nothing built may load without it. + add_action( 'all_admin_notices', 'dws_lpmwc_render_build_notice' ); + return; } -if ( ! is_readable( $dws_lpmwc_bootstrap ) ) { // The floor gate never ran, so nothing built may load. - add_action( 'all_admin_notices', 'dws_lpmwc_render_build_notice' ); +require_once $dws_lpmwc_bootstrap; +Updater::register( __FILE__ ); // Before the floor gate, so a release that lowers the floors still reaches a site below them. +if ( ! Requirements::check_floors( __FILE__ ) ) { return; } From bd291260f087dad40f477642681ac4ff33ec314d Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:34:06 +0100 Subject: [PATCH 14/39] Test the build-error log's location and frequency in each plugin A partial deploy fails the same way on every request, and each plugin logs every failure with its message alone, so the log floods while naming no file to look at (A213). Each plugin gains the template's row: a build error is logged with its file and line on the first request and not again within the hour. The rows fail until the entry throttles the log and adds the location. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/PartialDeployTest.php | 17 +++++++++++++++++ .../tests/Integration/PartialDeployTest.php | 17 +++++++++++++++++ .../tests/Integration/PartialDeployTest.php | 17 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/plugins/internal-comments/tests/Integration/PartialDeployTest.php b/plugins/internal-comments/tests/Integration/PartialDeployTest.php index ec28df5..4c0fbc4 100644 --- a/plugins/internal-comments/tests/Integration/PartialDeployTest.php +++ b/plugins/internal-comments/tests/Integration/PartialDeployTest.php @@ -2,6 +2,7 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; +use DeepWebSolutions\InternalComments\Plugin; use DeepWebSolutions\InternalComments\Scoped\DeepWebSolutions\Framework\Core\Uninstall\Uninstaller; use DeepWebSolutions\InternalComments\Tests\Support\RunsRequestsTrait; use PHPUnit\Framework\Attributes\CoversNothing; @@ -86,4 +87,20 @@ static function ( string $class_name ): void { \unlink( $log ); update_option( DWS_IC_OPTION_PLUGIN, DWS_IC_SETTING_DEFAULTS[ DWS_IC_OPTION_PLUGIN ] ); } + + public function test_a_build_error_is_logged_with_its_location_once_an_hour(): void { + $message = 'A partial deploy ' . \uniqid() . '.'; + $include = \dirname( DWS_IC_FILE ) . '/includes/zz-throws.php'; + \file_put_contents( $include, " Date: Wed, 11 Nov 2026 16:34:40 +0100 Subject: [PATCH 15/39] Log build errors with their location, about once an hour Each entry logged a build that could not load on every request and without its file and line, so a partial deploy flooded the log and left the reader to guess where it broke (A213). As in the template, each entry logs a distinct message with its file and line once, then not again for an hour, through a transient under the plugin's prefix, which its footprint already removes and which expires on its own; the log stays best effort. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/internal-comments.php | 10 ++++++++-- .../linked-orders-for-woocommerce.php | 10 ++++++++-- .../locked-payment-methods-for-woocommerce.php | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/plugins/internal-comments/internal-comments.php b/plugins/internal-comments/internal-comments.php index 0a1136f..9ac1664 100644 --- a/plugins/internal-comments/internal-comments.php +++ b/plugins/internal-comments/internal-comments.php @@ -53,7 +53,7 @@ } /** - * Reports a build that could not load, rethrowing in local and development environments and otherwise logging the error and showing the build notice. + * Reports a build that could not load, rethrowing in local and development environments and otherwise logging the error with its location and showing the build notice. * * @since 2.0.0 * @version 2.0.0 @@ -69,7 +69,13 @@ function dws_ic_handle_build_error( Throwable $error ): void { throw $error; } - error_log( 'Internal Comments did not finish loading: ' . $error->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- The plugin's logger is part of the build that failed. + // A partial deploy fails the same way on every request, so each distinct message is logged about once an hour, as best effort. + $key = 'dws_ic_build_error_' . md5( $error->getMessage() ); + if ( false === get_transient( $key ) ) { + set_transient( $key, 1, HOUR_IN_SECONDS ); + error_log( 'Internal Comments did not finish loading: ' . $error->getMessage() . ' in ' . $error->getFile() . ':' . $error->getLine() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- The plugin's logger is part of the build that failed. + } + add_action( 'all_admin_notices', 'dws_ic_render_build_notice' ); } diff --git a/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php b/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php index 00953ae..0682d09 100644 --- a/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php +++ b/plugins/linked-orders-for-woocommerce/linked-orders-for-woocommerce.php @@ -54,7 +54,7 @@ } /** - * Reports a build that could not load, rethrowing in local and development environments and otherwise logging the error and showing the build notice. + * Reports a build that could not load, rethrowing in local and development environments and otherwise logging the error with its location and showing the build notice. * * @since 2.0.0 * @version 2.0.0 @@ -70,7 +70,13 @@ function dws_lowc_handle_build_error( Throwable $error ): void { throw $error; } - error_log( 'Linked Orders for WooCommerce did not finish loading: ' . $error->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- The plugin's logger is part of the build that failed. + // A partial deploy fails the same way on every request, so each distinct message is logged about once an hour, as best effort. + $key = 'dws_lowc_build_error_' . md5( $error->getMessage() ); + if ( false === get_transient( $key ) ) { + set_transient( $key, 1, HOUR_IN_SECONDS ); + error_log( 'Linked Orders for WooCommerce did not finish loading: ' . $error->getMessage() . ' in ' . $error->getFile() . ':' . $error->getLine() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- The plugin's logger is part of the build that failed. + } + add_action( 'all_admin_notices', 'dws_lowc_render_build_notice' ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php b/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php index 517250a..bd3c4f5 100644 --- a/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php +++ b/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php @@ -49,7 +49,7 @@ } /** - * Reports a build that could not load, rethrowing in local and development environments and otherwise logging the error and showing the build notice. + * Reports a build that could not load, rethrowing in local and development environments and otherwise logging the error with its location and showing the build notice. * * @since 2.0.0 * @version 2.0.0 @@ -65,7 +65,13 @@ function dws_lpmwc_handle_build_error( Throwable $error ): void { throw $error; } - error_log( 'Locked Payment Methods for WooCommerce did not finish loading: ' . $error->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- The plugin's logger is part of the build that failed. + // A partial deploy fails the same way on every request, so each distinct message is logged about once an hour, as best effort. + $key = 'dws_lpmwc_build_error_' . md5( $error->getMessage() ); + if ( false === get_transient( $key ) ) { + set_transient( $key, 1, HOUR_IN_SECONDS ); + error_log( 'Locked Payment Methods for WooCommerce did not finish loading: ' . $error->getMessage() . ' in ' . $error->getFile() . ':' . $error->getLine() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- The plugin's logger is part of the build that failed. + } + add_action( 'all_admin_notices', 'dws_lpmwc_render_build_notice' ); } From 71f59091270781e1cb4376d416890477b8630763 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:35:22 +0100 Subject: [PATCH 16/39] Test the entry's tested-up-to headers in each plugin Each plugin's readme names the WordPress it is tested up to, which Plugin Check refuses in the entry (X373, amending A762), and nothing tied the entry's WC tested up to to the WooCommerce the suites run. Each plugin's ReadmeTest gains a row that the entry declares no Tested up to while the readme does, and a Unit EntryHeadersTest takes the template's row that WC tested up to names the locked WooCommerce's minor; the template's block-version row stays out, since no plugin builds blocks. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Unit/EntryHeadersTest.php | 20 +++++++++++++++++++ .../tests/Unit/ReadmeTest.php | 6 ++++++ .../tests/Unit/EntryHeadersTest.php | 20 +++++++++++++++++++ .../tests/Unit/ReadmeTest.php | 6 ++++++ .../tests/Unit/EntryHeadersTest.php | 20 +++++++++++++++++++ .../tests/Unit/ReadmeTest.php | 6 ++++++ 6 files changed, 78 insertions(+) create mode 100644 plugins/internal-comments/tests/Unit/EntryHeadersTest.php create mode 100644 plugins/linked-orders-for-woocommerce/tests/Unit/EntryHeadersTest.php create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Unit/EntryHeadersTest.php diff --git a/plugins/internal-comments/tests/Unit/EntryHeadersTest.php b/plugins/internal-comments/tests/Unit/EntryHeadersTest.php new file mode 100644 index 0000000..4953067 --- /dev/null +++ b/plugins/internal-comments/tests/Unit/EntryHeadersTest.php @@ -0,0 +1,20 @@ + 'wp-plugin/woocommerce' === $package['name'] ); + $entry = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/' . Plugin::SLUG . '.php' ); + + self::assertIsArray( $woocommerce ); + self::assertSame( 1, \preg_match( '/^\s*\*\s*WC tested up to:\s*(\S+)\s*$/m', $entry, $header ) ); + self::assertSame( \implode( '.', \array_slice( \explode( '.', $woocommerce['version'] ), 0, 2 ) ), $header[1] ); + } +} diff --git a/plugins/internal-comments/tests/Unit/ReadmeTest.php b/plugins/internal-comments/tests/Unit/ReadmeTest.php index ad3c00d..ceecefe 100644 --- a/plugins/internal-comments/tests/Unit/ReadmeTest.php +++ b/plugins/internal-comments/tests/Unit/ReadmeTest.php @@ -16,6 +16,12 @@ public function test_the_readme_repeats_the_entry_files_version_and_floors(): vo self::assertSame( $entry['Requires PHP'], $readme['Requires PHP'] ); } + // Plugin Check refuses a Tested up to in the entry, in any case, as WordPress reads headers, so the readme holds the only one. + public function test_the_entry_leaves_tested_up_to_to_the_readme(): void { + self::assertDoesNotMatchRegularExpression( '/^(?:[ \t]*<\?(?:php)?)?[ \t\/*#@]*Tested up to:/mi', (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/internal-comments.php' ) ); + self::assertArrayHasKey( 'Tested up to', self::read_fields( 'readme.txt' ) ); + } + public function test_the_readme_holds_the_changelog_markers_the_sync_fills(): void { $readme = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/readme.txt' ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Unit/EntryHeadersTest.php b/plugins/linked-orders-for-woocommerce/tests/Unit/EntryHeadersTest.php new file mode 100644 index 0000000..a2db4d5 --- /dev/null +++ b/plugins/linked-orders-for-woocommerce/tests/Unit/EntryHeadersTest.php @@ -0,0 +1,20 @@ + 'wp-plugin/woocommerce' === $package['name'] ); + $entry = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/' . Plugin::SLUG . '.php' ); + + self::assertIsArray( $woocommerce ); + self::assertSame( 1, \preg_match( '/^\s*\*\s*WC tested up to:\s*(\S+)\s*$/m', $entry, $header ) ); + self::assertSame( \implode( '.', \array_slice( \explode( '.', $woocommerce['version'] ), 0, 2 ) ), $header[1] ); + } +} diff --git a/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php b/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php index 499ad42..42f80d1 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php @@ -16,6 +16,12 @@ public function test_the_readme_repeats_the_entry_files_version_and_floors(): vo self::assertSame( $entry['Requires PHP'], $readme['Requires PHP'] ); } + // Plugin Check refuses a Tested up to in the entry, in any case, as WordPress reads headers, so the readme holds the only one. + public function test_the_entry_leaves_tested_up_to_to_the_readme(): void { + self::assertDoesNotMatchRegularExpression( '/^(?:[ \t]*<\?(?:php)?)?[ \t\/*#@]*Tested up to:/mi', (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/linked-orders-for-woocommerce.php' ) ); + self::assertArrayHasKey( 'Tested up to', self::read_fields( 'readme.txt' ) ); + } + public function test_the_readme_holds_the_changelog_markers_the_sync_fills(): void { $readme = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/readme.txt' ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/EntryHeadersTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/EntryHeadersTest.php new file mode 100644 index 0000000..d1cf813 --- /dev/null +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/EntryHeadersTest.php @@ -0,0 +1,20 @@ + 'wp-plugin/woocommerce' === $package['name'] ); + $entry = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/' . Plugin::SLUG . '.php' ); + + self::assertIsArray( $woocommerce ); + self::assertSame( 1, \preg_match( '/^\s*\*\s*WC tested up to:\s*(\S+)\s*$/m', $entry, $header ) ); + self::assertSame( \implode( '.', \array_slice( \explode( '.', $woocommerce['version'] ), 0, 2 ) ), $header[1] ); + } +} diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php index a17ccb3..a273f46 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php @@ -16,6 +16,12 @@ public function test_the_readme_repeats_the_entry_files_version_and_floors(): vo self::assertSame( $entry['Requires PHP'], $readme['Requires PHP'] ); } + // Plugin Check refuses a Tested up to in the entry, in any case, as WordPress reads headers, so the readme holds the only one. + public function test_the_entry_leaves_tested_up_to_to_the_readme(): void { + self::assertDoesNotMatchRegularExpression( '/^(?:[ \t]*<\?(?:php)?)?[ \t\/*#@]*Tested up to:/mi', (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/locked-payment-methods-for-woocommerce.php' ) ); + self::assertArrayHasKey( 'Tested up to', self::read_fields( 'readme.txt' ) ); + } + public function test_the_readme_holds_the_changelog_markers_the_sync_fills(): void { $readme = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/readme.txt' ); From f6063ccd1830ba24198154167576e552dbc8112a Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:36:13 +0100 Subject: [PATCH 17/39] Test that each v1 boot hook fires at the earliest priority Each plugin promises that a v1 hook literal fires before its v2 name, but relays {prefix}_initialized and {prefix}_initialization_failure from a default-priority callback on {prefix}/boot/succeeded and /failed, so a v2 listener below priority 10 runs first (X145). Each plugin's boot-hook row adds a listener at PHP_INT_MIN + 1 on both v2 hooks, expects every v1 hook first, and reads both relays' priority, which must be PHP_INT_MIN; the rows fail until the relays run there. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/DeprecatedApiTest.php | 11 ++++++++++- .../tests/Integration/DeprecatedApiTest.php | 11 ++++++++++- .../tests/Integration/DeprecatedApiTest.php | 11 ++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php b/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php index 366f251..1e7be77 100644 --- a/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php +++ b/plugins/internal-comments/tests/Integration/DeprecatedApiTest.php @@ -78,13 +78,16 @@ public function test_a_v1_hook_tag_builds_as_v1_built_it( string $shim, array $a self::assertSame( array( array( $shim, '' ) ), $this->deprecated ); } - public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure(): void { + // The v1 hooks fire at the earliest priority, so every v1 listener runs before a v2 listener of any later one. + public function test_the_v1_boot_hooks_fire_at_the_earliest_priority_with_the_failure(): void { $fired = array(); $spy = static function ( mixed $error = null ) use ( &$fired ): void { $fired[] = array( current_action(), $error ); }; add_action( 'dws_ic_initialized', $spy ); add_action( 'dws_ic_initialization_failure', $spy ); + add_action( 'dws_ic/boot/succeeded', $spy, PHP_INT_MIN + 1 ); + add_action( 'dws_ic/boot/failed', $spy, PHP_INT_MIN + 1 ); $error = new \RuntimeException( 'Failed.' ); try { @@ -93,12 +96,16 @@ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure() } finally { remove_action( 'dws_ic_initialized', $spy ); remove_action( 'dws_ic_initialization_failure', $spy ); + remove_action( 'dws_ic/boot/succeeded', $spy, PHP_INT_MIN + 1 ); + remove_action( 'dws_ic/boot/failed', $spy, PHP_INT_MIN + 1 ); } self::assertSame( array( array( 'dws_ic_initialized', null ), + array( 'dws_ic/boot/succeeded', '' ), array( 'dws_ic_initialization_failure', $error ), + array( 'dws_ic/boot/failed', $error ), ), $fired ); @@ -109,5 +116,7 @@ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure() ), $this->deprecated ); + self::assertSame( PHP_INT_MIN, has_action( 'dws_ic/boot/succeeded', 'dws_ic_fire_v1_initialized' ) ); + self::assertSame( PHP_INT_MIN, has_action( 'dws_ic/boot/failed', 'dws_ic_fire_v1_initialization_failure' ) ); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php index 8a68e11..2356ded 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedApiTest.php @@ -75,13 +75,16 @@ public function test_a_v1_hook_tag_builds_as_v1_built_it( string $shim, array $a self::assertSame( array( array( $shim, '' ) ), $this->deprecated ); } - public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure(): void { + // The v1 hooks fire at the earliest priority, so every v1 listener runs before a v2 listener of any later one. + public function test_the_v1_boot_hooks_fire_at_the_earliest_priority_with_the_failure(): void { $fired = array(); $spy = static function ( mixed $error = null ) use ( &$fired ): void { $fired[] = array( current_action(), $error ); }; add_action( 'dws_lowc_initialized', $spy ); add_action( 'dws_lowc_initialization_failure', $spy ); + add_action( 'dws_lowc/boot/succeeded', $spy, PHP_INT_MIN + 1 ); + add_action( 'dws_lowc/boot/failed', $spy, PHP_INT_MIN + 1 ); $error = new \RuntimeException( 'Failed.' ); try { @@ -90,12 +93,16 @@ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure() } finally { remove_action( 'dws_lowc_initialized', $spy ); remove_action( 'dws_lowc_initialization_failure', $spy ); + remove_action( 'dws_lowc/boot/succeeded', $spy, PHP_INT_MIN + 1 ); + remove_action( 'dws_lowc/boot/failed', $spy, PHP_INT_MIN + 1 ); } self::assertSame( array( array( 'dws_lowc_initialized', null ), + array( 'dws_lowc/boot/succeeded', '' ), array( 'dws_lowc_initialization_failure', $error ), + array( 'dws_lowc/boot/failed', $error ), ), $fired ); @@ -106,5 +113,7 @@ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure() ), $this->deprecated ); + self::assertSame( PHP_INT_MIN, has_action( 'dws_lowc/boot/succeeded', 'dws_lowc_fire_v1_initialized' ) ); + self::assertSame( PHP_INT_MIN, has_action( 'dws_lowc/boot/failed', 'dws_lowc_fire_v1_initialization_failure' ) ); } } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php index 47bf02a..0b41685 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php @@ -109,13 +109,16 @@ public function test_a_v1_hook_tag_builds_as_v1_built_it( string $shim, array $a self::assertSame( array( array( $shim, '' ) ), $this->deprecated ); } - public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure(): void { + // The v1 hooks fire at the earliest priority, so every v1 listener runs before a v2 listener of any later one. + public function test_the_v1_boot_hooks_fire_at_the_earliest_priority_with_the_failure(): void { $fired = array(); $spy = static function ( mixed $error = null ) use ( &$fired ): void { $fired[] = array( current_action(), $error ); }; add_action( 'dws_lpmwc_initialized', $spy ); add_action( 'dws_lpmwc_initialization_failure', $spy ); + add_action( 'dws_lpmwc/boot/succeeded', $spy, PHP_INT_MIN + 1 ); + add_action( 'dws_lpmwc/boot/failed', $spy, PHP_INT_MIN + 1 ); $error = new \RuntimeException( 'Failed.' ); try { @@ -124,12 +127,16 @@ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure() } finally { remove_action( 'dws_lpmwc_initialized', $spy ); remove_action( 'dws_lpmwc_initialization_failure', $spy ); + remove_action( 'dws_lpmwc/boot/succeeded', $spy, PHP_INT_MIN + 1 ); + remove_action( 'dws_lpmwc/boot/failed', $spy, PHP_INT_MIN + 1 ); } self::assertSame( array( array( 'dws_lpmwc_initialized', null ), + array( 'dws_lpmwc/boot/succeeded', '' ), array( 'dws_lpmwc_initialization_failure', $error ), + array( 'dws_lpmwc/boot/failed', $error ), ), $fired ); @@ -140,6 +147,8 @@ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure() ), $this->deprecated ); + self::assertSame( PHP_INT_MIN, has_action( 'dws_lpmwc/boot/succeeded', 'dws_lpmwc_fire_v1_initialized' ) ); + self::assertSame( PHP_INT_MIN, has_action( 'dws_lpmwc/boot/failed', 'dws_lpmwc_fire_v1_initialization_failure' ) ); } public function test_a_payment_method_is_locked_for_a_user_when_it_is_listed_and_no_strategy_unlocks_it(): void { From 7746d301d355de570e3fa51528649c27d9ea797c Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:37:08 +0100 Subject: [PATCH 18/39] Relay the v1 boot hooks at the earliest priority Each plugin relayed {prefix}_initialized and {prefix}_initialization_failure at the default priority, so a v2 listener below 10 ran before every v1 listener, against the plugins' conventions and their changelogs' promise of v1 first (X145). functions.php registers the six relays at PHP_INT_MIN at include time, before any listener a plugins_loaded callback adds, and Linked Orders' and Internal Comments' boot-hooks fragments say the v1 hooks fire before their replacements, as Locked Payment Methods' already does. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/changelog/boot-hooks | 2 +- plugins/internal-comments/functions.php | 4 ++-- plugins/linked-orders-for-woocommerce/changelog/boot-hooks | 2 +- plugins/linked-orders-for-woocommerce/functions.php | 4 ++-- plugins/locked-payment-methods-for-woocommerce/functions.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/internal-comments/changelog/boot-hooks b/plugins/internal-comments/changelog/boot-hooks index 2861230..e3ef183 100644 --- a/plugins/internal-comments/changelog/boot-hooks +++ b/plugins/internal-comments/changelog/boot-hooks @@ -1,4 +1,4 @@ Significance: minor Type: deprecated -dws_ic_initialized and dws_ic_initialization_failure fire deprecated, after dws_ic/boot/succeeded and dws_ic/boot/failed; the failure hook passes the Throwable that failed the boot. +dws_ic_initialized and dws_ic_initialization_failure fire deprecated, before dws_ic/boot/succeeded and dws_ic/boot/failed; the failure hook passes the Throwable that failed the boot. diff --git a/plugins/internal-comments/functions.php b/plugins/internal-comments/functions.php index 2fb2e8b..c5f0105 100644 --- a/plugins/internal-comments/functions.php +++ b/plugins/internal-comments/functions.php @@ -28,8 +28,8 @@ // The hooks come last, so an include that throws leaves nothing of the plugin hooked. add_action( 'plugins_loaded', 'dws_ic_boot', 15 ); add_action( 'before_woocommerce_init', 'dws_ic_declare_compatibility' ); -add_action( 'dws_ic/boot/succeeded', 'dws_ic_fire_v1_initialized' ); -add_action( 'dws_ic/boot/failed', 'dws_ic_fire_v1_initialization_failure' ); +add_action( 'dws_ic/boot/succeeded', 'dws_ic_fire_v1_initialized', PHP_INT_MIN ); +add_action( 'dws_ic/boot/failed', 'dws_ic_fire_v1_initialization_failure', PHP_INT_MIN ); register_deactivation_hook( DWS_IC_FILE, 'dws_ic_deactivate' ); /** diff --git a/plugins/linked-orders-for-woocommerce/changelog/boot-hooks b/plugins/linked-orders-for-woocommerce/changelog/boot-hooks index d4376ba..c740d0d 100644 --- a/plugins/linked-orders-for-woocommerce/changelog/boot-hooks +++ b/plugins/linked-orders-for-woocommerce/changelog/boot-hooks @@ -1,4 +1,4 @@ Significance: minor Type: deprecated -dws_lowc_initialized and dws_lowc_initialization_failure fire deprecated, after dws_lowc/boot/succeeded and dws_lowc/boot/failed; the failure hook passes the Throwable that failed the boot. +dws_lowc_initialized and dws_lowc_initialization_failure fire deprecated, before dws_lowc/boot/succeeded and dws_lowc/boot/failed; the failure hook passes the Throwable that failed the boot. diff --git a/plugins/linked-orders-for-woocommerce/functions.php b/plugins/linked-orders-for-woocommerce/functions.php index 509e17e..91dde82 100644 --- a/plugins/linked-orders-for-woocommerce/functions.php +++ b/plugins/linked-orders-for-woocommerce/functions.php @@ -28,8 +28,8 @@ // The hooks come last, so an include that throws leaves nothing of the plugin hooked. add_action( 'plugins_loaded', 'dws_lowc_boot', 15 ); add_action( 'before_woocommerce_init', 'dws_lowc_declare_compatibility' ); -add_action( 'dws_lowc/boot/succeeded', 'dws_lowc_fire_v1_initialized' ); -add_action( 'dws_lowc/boot/failed', 'dws_lowc_fire_v1_initialization_failure' ); +add_action( 'dws_lowc/boot/succeeded', 'dws_lowc_fire_v1_initialized', PHP_INT_MIN ); +add_action( 'dws_lowc/boot/failed', 'dws_lowc_fire_v1_initialization_failure', PHP_INT_MIN ); register_deactivation_hook( DWS_LOWC_FILE, 'dws_lowc_deactivate' ); /** diff --git a/plugins/locked-payment-methods-for-woocommerce/functions.php b/plugins/locked-payment-methods-for-woocommerce/functions.php index 937ec31..334c2c8 100644 --- a/plugins/locked-payment-methods-for-woocommerce/functions.php +++ b/plugins/locked-payment-methods-for-woocommerce/functions.php @@ -28,8 +28,8 @@ // The hooks come last, so an include that throws leaves nothing of the plugin hooked. add_action( 'plugins_loaded', 'dws_lpmwc_boot', 15 ); add_action( 'before_woocommerce_init', 'dws_lpmwc_declare_compatibility' ); -add_action( 'dws_lpmwc/boot/succeeded', 'dws_lpmwc_fire_v1_initialized' ); -add_action( 'dws_lpmwc/boot/failed', 'dws_lpmwc_fire_v1_initialization_failure' ); +add_action( 'dws_lpmwc/boot/succeeded', 'dws_lpmwc_fire_v1_initialized', PHP_INT_MIN ); +add_action( 'dws_lpmwc/boot/failed', 'dws_lpmwc_fire_v1_initialization_failure', PHP_INT_MIN ); register_deactivation_hook( DWS_LPMWC_FILE, 'dws_lpmwc_deactivate' ); /** From baafb7e93c80acf18e6473882685f9578a9697b7 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:37:47 +0100 Subject: [PATCH 19/39] Test that Locked Payment Methods hooks its lock once WooCommerce loads The gateway lock, a safeguard that calls WooCommerce, is hooked when the safeguards file loads, before the floor gate and while WooCommerce may be inactive, where X361 hooks every such safeguard from the file's one woocommerce_loaded callback. A row reads the lock's hook in a request with WooCommerce and in one that skips it: hooked with WooCommerce loaded, and absent without it. It fails until the lock waits for woocommerce_loaded. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/SafeguardsTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php index 274d817..1b155e6 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php @@ -383,4 +383,14 @@ protected static function visit_order_pay( int $order_id, ?string $key ): void { $_GET['key'] = $key; } } + + // WordPress loads the plugin while WooCommerce is inactive, so a safeguard that calls WooCommerce waits for it to load. + public function test_the_gateway_lock_is_hooked_only_once_woocommerce_has_loaded(): void { + $php = 'echo wp_json_encode( array( did_action( "woocommerce_loaded" ), false !== has_filter( "woocommerce_available_payment_gateways", "dws_lpmwc_remove_locked_payment_methods" ) ) );'; + $with = self::request( $php ); + $without = self::request( $php, '--skip-plugins=woocommerce' ); + + self::assertSame( '[1,true]', $with['last'], $with['output'] ); + self::assertSame( '[0,false]', $without['last'], $without['output'] ); + } } From 41eab195b74ef209d174540211185d5e32d21ad0 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:38:15 +0100 Subject: [PATCH 20/39] Hook Locked Payment Methods' lock once WooCommerce has loaded The safeguards file hooked the gateway lock as it loaded, before the floor gate, though WordPress loads the plugin while WooCommerce is inactive and the lock's callbacks call WooCommerce; X361 hooks every safeguard that calls WooCommerce from one function the file runs once WooCommerce has loaded. dws_lpmwc_add_woocommerce_safeguards() hooks the lock, and the file runs it on woocommerce_loaded, or at once where that has fired, as on the request that activates the plugin. Assisted-by: Claude Code:claude-opus-5-5 --- .../AGENTS.md | 2 +- .../includes/safeguards.php | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md index 140ab58..7cffe49 100644 --- a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md +++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md @@ -67,7 +67,7 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on, and t 7. **Grants forms** offer the locked payment methods and the subject's other grants, through `dws_lpmwc_get_grant_choices()`, since a `checkboxes` save keeps only the choices it offers. The profile, order and plan forms are the framework's `ObjectForm`s; the Groups field rides Groups' own forms, whose nonce and capability Groups checks before its save actions, and checks the plugin's capability in each callback. 8. **Order storage.** Order code goes through WC CRUD and WooCommerce's screen ids, never the posts table, and every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`. 9. **v1 hooks.** A hook v1 fired keeps its v1 literal, fired through `apply_filters_deprecated()` or `do_action_deprecated()` with `'2.0.0'` and its replacement, before the v2 name; a filter on the locked list only adds ids. -10. **Safeguards** live in `includes/safeguards.php`, call only WordPress, WooCommerce and that file, and relax only through their own filter, through `dws_lpmwc_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon`, the file's only PHPStan profile, checks with WooCommerce's stubs. +10. **Safeguards** live in `includes/safeguards.php`, call WordPress, that file and WooCommerce, and relax only through their own filter, through `dws_lpmwc_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only what their hook hands them, WordPress APIs present at the oldest WordPress the bootstrap package runs on and WooCommerce APIs present since 3.6, guarding anything newer with `function_exists()` or `method_exists()`. WordPress loads the plugin while WooCommerce is inactive, so a safeguard that calls WooCommerce is hooked from `dws_lpmwc_add_woocommerce_safeguards()`, which the file runs on `woocommerce_loaded`, or at once where that has fired already, as on the request that activates the plugin; WooCommerce fires it, since 3.6, once every plugin has loaded. `phpstan.safeguards.neon`, the file's only PHPStan profile, reads the whole file, with WooCommerce's stubs loaded, and checks the WordPress floor; review checks the WooCommerce floor, which no tool reads. 11. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails. ## Style diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php index fd0384b..ee8c61b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php @@ -10,7 +10,23 @@ defined( 'ABSPATH' ) || exit; -add_filter( 'woocommerce_available_payment_gateways', 'dws_lpmwc_remove_locked_payment_methods', 999 ); +if ( 0 < did_action( 'woocommerce_loaded' ) ) { + dws_lpmwc_add_woocommerce_safeguards(); +} else { + add_action( 'woocommerce_loaded', 'dws_lpmwc_add_woocommerce_safeguards' ); +} + +/** + * Hooks the safeguards that call WooCommerce. + * + * @since 2.0.0 + * @version 2.0.0 + * + * @return void + */ +function dws_lpmwc_add_woocommerce_safeguards(): void { + add_filter( 'woocommerce_available_payment_gateways', 'dws_lpmwc_remove_locked_payment_methods', 999 ); +} /** * Returns whether a safeguard's filtered value relaxes it, which takes an explicit false once the plugin has finished booting. From 111ba1811c5bb8ad903ae69d169b32ade1a3e3c0 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:39:10 +0100 Subject: [PATCH 21/39] Check that every shipped directory holds the stub in each plugin Locked Payment Methods ships three directories without the silence-is-golden index.php, and nothing in any suite checks for one (A780); X300 asked for the template's stub check, which the template never had, so X371 has the plugins write it first, in the form the template takes in its next substantial branch. Each plugin's Unit suite walks every directory a release ships, the build trees, development folders and hidden ones aside, and expects the stub in each, byte for byte. Linked Orders and Internal Comments pass; Locked Payment Methods fails until its three stubs exist. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Unit/DirectoryStubsTest.php | 33 +++++++++++++++++++ .../tests/Unit/DirectoryStubsTest.php | 33 +++++++++++++++++++ .../tests/Unit/DirectoryStubsTest.php | 33 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 plugins/internal-comments/tests/Unit/DirectoryStubsTest.php create mode 100644 plugins/linked-orders-for-woocommerce/tests/Unit/DirectoryStubsTest.php create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Unit/DirectoryStubsTest.php diff --git a/plugins/internal-comments/tests/Unit/DirectoryStubsTest.php b/plugins/internal-comments/tests/Unit/DirectoryStubsTest.php new file mode 100644 index 0000000..7ad8dc6 --- /dev/null +++ b/plugins/internal-comments/tests/Unit/DirectoryStubsTest.php @@ -0,0 +1,33 @@ + $file->isDir() && ! \str_starts_with( $file->getFilename(), '.' ) && ! \in_array( $file->getFilename(), self::SKIPPED, true ) + ), + \RecursiveIteratorIterator::SELF_FIRST + ); + $missing = array(); + foreach ( array( $root, ...\array_keys( \iterator_to_array( $directories ) ) ) as $directory ) { + $stub = "$directory/index.php"; + if ( ! \is_file( $stub ) || " $file->isDir() && ! \str_starts_with( $file->getFilename(), '.' ) && ! \in_array( $file->getFilename(), self::SKIPPED, true ) + ), + \RecursiveIteratorIterator::SELF_FIRST + ); + $missing = array(); + foreach ( array( $root, ...\array_keys( \iterator_to_array( $directories ) ) ) as $directory ) { + $stub = "$directory/index.php"; + if ( ! \is_file( $stub ) || " $file->isDir() && ! \str_starts_with( $file->getFilename(), '.' ) && ! \in_array( $file->getFilename(), self::SKIPPED, true ) + ), + \RecursiveIteratorIterator::SELF_FIRST + ); + $missing = array(); + foreach ( array( $root, ...\array_keys( \iterator_to_array( $directories ) ) ) as $directory ) { + $stub = "$directory/index.php"; + if ( ! \is_file( $stub ) || " Date: Wed, 11 Nov 2026 16:39:11 +0100 Subject: [PATCH 22/39] Add the stubs Locked Payment Methods' source folders lacked src/EditScreen/, src/EditScreen/Hosts/ and src/UnlockStrategies/ shipped without the silence-is-golden index.php every other directory of the plugins and the template carries (A780). Each gains Date: Wed, 11 Nov 2026 16:43:10 +0100 Subject: [PATCH 23/39] Reject diagnostics and stray output in fresh requests RunsRequestsTrait::request() handed back whatever a fresh request printed, so a row that read the last line passed over PHP warnings, notices or output printed before its answer (A557). request() now fails a row on any output before the request's answer. A row whose fixture provokes a diagnostic calls request_provoking() with a string from it, and only output before that diagnostic fails the row, since a fatal error's trace and a build's log follow it: the partial-deploy rows name their log line or the file they hide, and the throwing boot listener its message. WooCommerce queries two of its tables on a new site's first request before it installs them, so each multisite row first sends a request without the plugin, naming that database error, and then checks the plugin's own request in full. X371 writes the trait here first, in the form the template takes in its next substantial branch. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/MultisiteTest.php | 5 +++++ .../tests/Integration/PartialDeployTest.php | 10 +++++----- .../tests/Integration/PluginBootTest.php | 2 +- .../tests/Support/RunsRequestsTrait.php | 11 ++++++++++- .../tests/Integration/MultisiteTest.php | 5 +++++ .../tests/Integration/PartialDeployTest.php | 10 +++++----- .../tests/Integration/PluginBootTest.php | 2 +- .../tests/Support/RunsRequestsTrait.php | 11 ++++++++++- .../tests/Integration/MultisiteTest.php | 5 +++++ .../tests/Integration/PartialDeployTest.php | 10 +++++----- .../tests/Integration/PluginBootTest.php | 2 +- .../tests/Support/RunsRequestsTrait.php | 11 ++++++++++- 12 files changed, 63 insertions(+), 21 deletions(-) diff --git a/plugins/internal-comments/tests/Integration/MultisiteTest.php b/plugins/internal-comments/tests/Integration/MultisiteTest.php index e2836b8..c723344 100644 --- a/plugins/internal-comments/tests/Integration/MultisiteTest.php +++ b/plugins/internal-comments/tests/Integration/MultisiteTest.php @@ -23,6 +23,11 @@ public function test_the_plugin_boots_on_another_site_of_the_network_and_stores_ self::assertIsInt( $site_id ); try { + // WooCommerce queries two of its tables during a new site's first init, before its version check installs them, so a request without the plugin installs WooCommerce first. + $installed = self::request_provoking( 'WordPress database error', 'echo (int) function_exists( "dws_ic_boot" );', '--url=' . get_home_url( $site_id, '/' ), '--skip-plugins=' . Plugin::SLUG ); + self::assertSame( 0, $installed['status'], $installed['output'] ); + self::assertSame( '0', $installed['last'], $installed['output'] ); + $request = self::request( 'echo did_action( "dws_ic/boot/succeeded" );', '--url=' . get_home_url( $site_id, '/' ) ); self::assertSame( '1', $request['last'], $request['output'] ); diff --git a/plugins/internal-comments/tests/Integration/PartialDeployTest.php b/plugins/internal-comments/tests/Integration/PartialDeployTest.php index 4c0fbc4..2033ce2 100644 --- a/plugins/internal-comments/tests/Integration/PartialDeployTest.php +++ b/plugins/internal-comments/tests/Integration/PartialDeployTest.php @@ -30,7 +30,7 @@ public function test_an_include_that_throws_shows_the_build_notice_and_boots_not $include = \dirname( DWS_IC_FILE ) . '/includes/zz-throws.php'; \file_put_contents( $include, " ' ' . \escapeshellarg( $flag ), $flags ) ); \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . "$arguments eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. - $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + $provoked = '' === $diagnostic ? null : \array_find_key( $output, static fn ( string $line ): bool => \str_contains( $line, $diagnostic ) ); + + self::assertSame( array(), \array_slice( $output, 0, \is_int( $provoked ) ? $provoked : \max( 0, \count( $output ) - 1 ) ), \implode( "\n", $output ) ); + return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php index 8f1caea..223971c 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MultisiteTest.php @@ -23,6 +23,11 @@ public function test_the_plugin_boots_on_another_site_of_the_network_and_stores_ self::assertIsInt( $site_id ); try { + // WooCommerce queries two of its tables during a new site's first init, before its version check installs them, so a request without the plugin installs WooCommerce first. + $installed = self::request_provoking( 'WordPress database error', 'echo (int) function_exists( "dws_lowc_boot" );', '--url=' . get_home_url( $site_id, '/' ), '--skip-plugins=' . Plugin::SLUG ); + self::assertSame( 0, $installed['status'], $installed['output'] ); + self::assertSame( '0', $installed['last'], $installed['output'] ); + $request = self::request( 'echo did_action( "dws_lowc/boot/succeeded" );', '--url=' . get_home_url( $site_id, '/' ) ); self::assertSame( '1', $request['last'], $request['output'] ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php index eff1f2e..60763c0 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php @@ -30,7 +30,7 @@ public function test_an_include_that_throws_shows_the_build_notice_and_boots_not $include = \dirname( DWS_LOWC_FILE ) . '/includes/zz-throws.php'; \file_put_contents( $include, " ' ' . \escapeshellarg( $flag ), $flags ) ); \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . "$arguments eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. - $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + $provoked = '' === $diagnostic ? null : \array_find_key( $output, static fn ( string $line ): bool => \str_contains( $line, $diagnostic ) ); + + self::assertSame( array(), \array_slice( $output, 0, \is_int( $provoked ) ? $provoked : \max( 0, \count( $output ) - 1 ) ), \implode( "\n", $output ) ); + return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php index 946b32c..cbb087d 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MultisiteTest.php @@ -23,6 +23,11 @@ public function test_the_plugin_boots_on_another_site_of_the_network_and_stores_ self::assertIsInt( $site_id ); try { + // WooCommerce queries two of its tables during a new site's first init, before its version check installs them, so a request without the plugin installs WooCommerce first. + $installed = self::request_provoking( 'WordPress database error', 'echo (int) function_exists( "dws_lpmwc_boot" );', '--url=' . get_home_url( $site_id, '/' ), '--skip-plugins=' . Plugin::SLUG ); + self::assertSame( 0, $installed['status'], $installed['output'] ); + self::assertSame( '0', $installed['last'], $installed['output'] ); + $request = self::request( 'echo did_action( "dws_lpmwc/boot/succeeded" );', '--url=' . get_home_url( $site_id, '/' ) ); self::assertSame( '1', $request['last'], $request['output'] ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php index c82b89e..0334c53 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php @@ -30,7 +30,7 @@ public function test_an_include_that_throws_shows_the_build_notice_and_boots_not $include = \dirname( DWS_LPMWC_FILE ) . '/includes/zz-throws.php'; \file_put_contents( $include, " ' ' . \escapeshellarg( $flag ), $flags ) ); \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . "$arguments eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. - $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + $provoked = '' === $diagnostic ? null : \array_find_key( $output, static fn ( string $line ): bool => \str_contains( $line, $diagnostic ) ); + + self::assertSame( array(), \array_slice( $output, 0, \is_int( $provoked ) ? $provoked : \max( 0, \count( $output ) - 1 ) ), \implode( "\n", $output ) ); + return array( 'status' => $status, 'output' => \implode( "\n", $output ), From bcbeecbcf4080604311727e602c7f585f374d2ac Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:44:02 +0100 Subject: [PATCH 24/39] Drop test ignores of sniffs the tests profile excludes Twenty-six phpcs:ignore comments in the plugins' test trees named sniffs configs' tests profile already excludes, such as GlobalVariablesOverride, IniSet and UnusedFunctionParameter, so they silenced nothing and read as if the line needed an exemption (A572). Each such comment goes, and none named a sniff the profile keeps. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/AdminScreensTest.php | 10 +++++----- .../tests/Integration/MetaBoxTest.php | 6 +++--- .../internal-comments/tests/Integration/OrdersTest.php | 2 +- .../tests/Integration/PartialDeployTest.php | 2 +- .../tests/Integration/PermissionsTest.php | 4 ++-- .../tests/Integration/UninstallTest.php | 2 +- .../tests/Integration/DeprecatedHooksTest.php | 2 +- .../tests/Integration/ListTableTest.php | 4 ++-- .../tests/Integration/MetaBoxTest.php | 2 +- .../tests/Integration/PartialDeployTest.php | 2 +- .../tests/Integration/UninstallTest.php | 2 +- .../tests/Integration/PartialDeployTest.php | 2 +- .../tests/Integration/UninstallTest.php | 2 +- .../UnlockStrategies/GroupsStrategyTest.php | 2 +- .../Integration/UnlockStrategies/OrderStrategyTest.php | 2 +- .../WooCommerceMembershipsStrategyTest.php | 2 +- .../tests/PHPStan/woocommerce-memberships.php | 2 +- .../tests/Support/memberships-functions.php | 2 +- 18 files changed, 26 insertions(+), 26 deletions(-) diff --git a/plugins/internal-comments/tests/Integration/AdminScreensTest.php b/plugins/internal-comments/tests/Integration/AdminScreensTest.php index 399dae1..a8fa2f3 100644 --- a/plugins/internal-comments/tests/Integration/AdminScreensTest.php +++ b/plugins/internal-comments/tests/Integration/AdminScreensTest.php @@ -225,7 +225,7 @@ public function test_the_edit_screen_of_an_internal_comment_shows_its_revisions_ ); $screens = new AdminScreens(); - $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The row registers the meta boxes anew. + $wp_meta_boxes = array(); $screens->add_revisions_meta_box( get_comment( $comment ) ); $box = $wp_meta_boxes['comment']['normal']['default']['dws-internal-comments-revisions'] ?? null; self::assertIsArray( $box ); @@ -259,7 +259,7 @@ public function test_the_edit_screen_of_an_internal_comment_shows_its_revisions_ public function test_the_comment_edit_screen_hides_the_fields_an_internal_comment_keeps(): void { $screens = new AdminScreens(); $styles = static function ( int $comment ) use ( $screens ): string { - $GLOBALS['comment'] = get_comment( $comment ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Stands in for the edit screen's comment. + $GLOBALS['comment'] = get_comment( $comment ); try { $screens->enqueue_comment_screen_style( 'comment.php' ); @@ -284,7 +284,7 @@ public function test_the_comments_screen_counts_only_the_trashed_comments_the_us $public = (int) wp_insert_comment( array( 'comment_post_ID' => $this->post ) ); wp_trash_comment( $internal ); wp_trash_comment( $public ); - $GLOBALS['current_screen'] = \WP_Screen::get( 'edit-comments' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Sets the screen without firing current_screen, which other plugins hook with admin-only code. + $GLOBALS['current_screen'] = \WP_Screen::get( 'edit-comments' ); $counts = static function (): array { wp_set_current_user( self::$users['editor'] ); @@ -328,7 +328,7 @@ public function test_the_comments_screen_counts_trashed_comments_as_woocommerce_ ); wp_trash_comment( (int) dws_ic_create_internal_comment( array( 'comment_post_ID' => $product ), get_user_by( 'id', self::$users['administrator'] ) ?: null ) ); wp_trash_comment( (int) wp_insert_comment( array( 'comment_post_ID' => $this->post ) ) ); - $GLOBALS['current_screen'] = \WP_Screen::get( 'edit-comments' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Sets the screen without firing current_screen, which other plugins hook with admin-only code. + $GLOBALS['current_screen'] = \WP_Screen::get( 'edit-comments' ); \WC_Comments::delete_comments_count_cache(); wp_cache_delete( 'comments-0', 'counts' ); @@ -355,7 +355,7 @@ public function test_the_comments_screen_counts_trashed_comments_once_while_v1s_ wp_trash_comment( (int) wp_insert_comment( array( 'comment_post_ID' => $this->post ) ) ); $revision = get_option( 'dws_ic_revision' ); update_option( 'dws_ic_revision', 1 ); - $GLOBALS['current_screen'] = \WP_Screen::get( 'edit-comments' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Sets the screen without firing current_screen, which other plugins hook with admin-only code. + $GLOBALS['current_screen'] = \WP_Screen::get( 'edit-comments' ); \WC_Comments::delete_comments_count_cache(); wp_cache_delete( 'comments-0', 'counts' ); diff --git a/plugins/internal-comments/tests/Integration/MetaBoxTest.php b/plugins/internal-comments/tests/Integration/MetaBoxTest.php index ef8aa4d..6f97234 100644 --- a/plugins/internal-comments/tests/Integration/MetaBoxTest.php +++ b/plugins/internal-comments/tests/Integration/MetaBoxTest.php @@ -82,7 +82,7 @@ protected function create_posts(): void { #[After] protected function delete_posts(): void { global $wp_meta_boxes; - $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Each row registers the meta boxes anew. + $wp_meta_boxes = array(); wp_delete_post( $this->post, true ); wp_delete_post( $this->page, true ); update_option( DWS_IC_OPTION_GENERAL, $this->stored ); @@ -161,7 +161,7 @@ public function test_the_markup_hooks_fire_v1s_names_first_then_v2s(): void { } public function test_the_edit_screen_of_a_selected_post_loads_the_meta_boxs_assets(): void { - $GLOBALS['post'] = get_post( $this->post ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The edit screen sets its post. + $GLOBALS['post'] = get_post( $this->post ); wp_set_current_user( self::$users['administrator'] ); try { @@ -176,7 +176,7 @@ public function test_the_edit_screen_of_a_selected_post_loads_the_meta_boxs_asse protected function registers( string $role, int $post ): bool { global $wp_meta_boxes; - $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Each check registers the meta boxes anew. + $wp_meta_boxes = array(); wp_set_current_user( self::$users[ $role ] ); $type = (string) get_post_type( $post ); do_action( 'add_meta_boxes', $type, get_post( $post ) ); diff --git a/plugins/internal-comments/tests/Integration/OrdersTest.php b/plugins/internal-comments/tests/Integration/OrdersTest.php index c0e7bf8..f68f54f 100644 --- a/plugins/internal-comments/tests/Integration/OrdersTest.php +++ b/plugins/internal-comments/tests/Integration/OrdersTest.php @@ -55,7 +55,7 @@ protected function select_orders(): void { #[After] protected function restore_settings(): void { global $wp_meta_boxes; - $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Each row registers the meta boxes anew. + $wp_meta_boxes = array(); update_option( DWS_IC_OPTION_GENERAL, $this->stored ); wp_set_current_user( 0 ); } diff --git a/plugins/internal-comments/tests/Integration/PartialDeployTest.php b/plugins/internal-comments/tests/Integration/PartialDeployTest.php index 2033ce2..b749e75 100644 --- a/plugins/internal-comments/tests/Integration/PartialDeployTest.php +++ b/plugins/internal-comments/tests/Integration/PartialDeployTest.php @@ -69,7 +69,7 @@ public function test_a_deactivation_whose_composition_root_cannot_be_built_is_re public function test_an_uninstall_whose_engine_is_missing_keeps_the_data_and_lets_the_web_delete_the_files(): void { update_option( DWS_IC_OPTION_PLUGIN, array( DWS_IC_SETTING_DELETE_DATA => 'yes' ) ); $log = (string) \tempnam( \sys_get_temp_dir(), 'dws-tpl-uninstall-' ); - \ini_set( 'error_log', $log ); // phpcs:ignore WordPress.PHP.IniSet.Risky -- The child process's stderr would fail the test. + \ini_set( 'error_log', $log ); \spl_autoload_register( static function ( string $class_name ): void { if ( Uninstaller::class === $class_name ) { diff --git a/plugins/internal-comments/tests/Integration/PermissionsTest.php b/plugins/internal-comments/tests/Integration/PermissionsTest.php index eea0e85..5189f96 100644 --- a/plugins/internal-comments/tests/Integration/PermissionsTest.php +++ b/plugins/internal-comments/tests/Integration/PermissionsTest.php @@ -261,7 +261,7 @@ public function test_core_replies_post_an_internal_comment_only_for_users_who_ma $post = $this->post( 'administrator' ); $stop = static function (): callable { return static function ( mixed $message ): void { - throw new \RuntimeException( 'Refused: ' . \print_r( $message, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Names the refusal. + throw new \RuntimeException( 'Refused: ' . \print_r( $message, true ) ); }; }; $refuse = function ( string $type ) use ( $post ): bool { @@ -296,7 +296,7 @@ public function test_core_replies_post_an_internal_comment_only_for_users_who_ma $with = $refuse( DWS_IC_COMMENT_TYPE ); } finally { get_role( 'editor' )?->add_cap( DWS_IC_CAP_INSERT ); - $GLOBALS['wp_filter']['wp_ajax_replyto-comment'] = $hooked; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Puts WooCommerce's handler back. + $GLOBALS['wp_filter']['wp_ajax_replyto-comment'] = $hooked; remove_filter( 'wp_doing_ajax', '__return_true' ); remove_filter( 'wp_die_ajax_handler', $stop ); } diff --git a/plugins/internal-comments/tests/Integration/UninstallTest.php b/plugins/internal-comments/tests/Integration/UninstallTest.php index 59f4b3f..d067741 100644 --- a/plugins/internal-comments/tests/Integration/UninstallTest.php +++ b/plugins/internal-comments/tests/Integration/UninstallTest.php @@ -26,7 +26,7 @@ public function test_an_uninstall_checks_the_floors_of_its_own_main_file(): void $source = (string) \file_get_contents( DWS_IC_FILE ); \file_put_contents( DWS_IC_FILE, (string) \preg_replace( '/^(\s*\*\s*Requires PHP:\s*).*$/m', '${1}99.0', $source ) ); $log = (string) \tempnam( \sys_get_temp_dir(), 'dws-ic-uninstall-' ); - \ini_set( 'error_log', $log ); // phpcs:ignore WordPress.PHP.IniSet.Risky -- The child process's stderr would fail the test. + \ini_set( 'error_log', $log ); try { include \dirname( DWS_IC_FILE ) . '/uninstall.php'; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php index dcc2496..6605f90 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php @@ -40,7 +40,7 @@ protected function log_in(): void { #[After] protected function reset_the_screen(): void { global $wp_meta_boxes; - $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Resets what the rows registered. + $wp_meta_boxes = array(); unset( $GLOBALS['current_screen'] ); wp_set_current_user( 0 ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php index 848aad9..72b88a9 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php @@ -286,7 +286,7 @@ public function test_the_list_loads_the_script_and_the_icons_only_on_order_lists protected function open_list(): string { $screen = OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id( 'shop-order' ) : 'edit-shop_order'; set_current_screen( $screen ); - $GLOBALS['typenow'] = 'shop_order'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The posts list sets it from the request. + $GLOBALS['typenow'] = 'shop_order'; return $screen; } @@ -315,7 +315,7 @@ protected function render_filters(): string { // The found ids stand in for the orders a search narrowed the list to, under the key each storage's search sets. protected function query( ?array $found = null, array $meta_query = array() ): array { - $others = array() === $meta_query ? array() : array( 'meta_query' => $meta_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Another extension's filter. + $others = array() === $meta_query ? array() : array( 'meta_query' => $meta_query ); if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { $args = apply_filters( 'woocommerce_order_list_table_prepare_items_query_args', diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php index 9ed8d14..76e725e 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php @@ -34,7 +34,7 @@ protected function log_in(): void { #[After] protected function clear_meta_boxes(): void { global $wp_meta_boxes; - $wp_meta_boxes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Resets what the rows registered. + $wp_meta_boxes = array(); wp_set_current_user( 0 ); if ( 0 !== $this->user ) { require_once ABSPATH . 'wp-admin/includes/user.php'; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php index 60763c0..1682954 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/PartialDeployTest.php @@ -69,7 +69,7 @@ public function test_a_deactivation_whose_composition_root_cannot_be_built_is_re public function test_an_uninstall_whose_engine_is_missing_keeps_the_data_and_lets_the_web_delete_the_files(): void { update_option( DWS_LOWC_OPTION_DELETE_DATA, 'yes' ); $log = (string) \tempnam( \sys_get_temp_dir(), 'dws-tpl-uninstall-' ); - \ini_set( 'error_log', $log ); // phpcs:ignore WordPress.PHP.IniSet.Risky -- The child process's stderr would fail the test. + \ini_set( 'error_log', $log ); \spl_autoload_register( static function ( string $class_name ): void { if ( Uninstaller::class === $class_name ) { diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php index 3b8db06..7477760 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/UninstallTest.php @@ -26,7 +26,7 @@ public function test_an_uninstall_checks_the_floors_of_its_own_main_file(): void $source = (string) \file_get_contents( DWS_LOWC_FILE ); \file_put_contents( DWS_LOWC_FILE, (string) \preg_replace( '/^(\s*\*\s*Requires PHP:\s*).*$/m', '${1}99.0', $source ) ); $log = (string) \tempnam( \sys_get_temp_dir(), 'dws-tpl-uninstall-' ); - \ini_set( 'error_log', $log ); // phpcs:ignore WordPress.PHP.IniSet.Risky -- The child process's stderr would fail the test. + \ini_set( 'error_log', $log ); try { include \dirname( DWS_LOWC_FILE ) . '/uninstall.php'; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php index 0334c53..28a421e 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php @@ -69,7 +69,7 @@ public function test_a_deactivation_whose_composition_root_cannot_be_built_is_re public function test_an_uninstall_whose_engine_is_missing_keeps_the_data_and_lets_the_web_delete_the_files(): void { update_option( DWS_LPMWC_OPTION_DELETE_DATA, 'yes' ); $log = (string) \tempnam( \sys_get_temp_dir(), 'dws-tpl-uninstall-' ); - \ini_set( 'error_log', $log ); // phpcs:ignore WordPress.PHP.IniSet.Risky -- The child process's stderr would fail the test. + \ini_set( 'error_log', $log ); \spl_autoload_register( static function ( string $class_name ): void { if ( Uninstaller::class === $class_name ) { diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php index 5f15e28..c713ff4 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php @@ -26,7 +26,7 @@ public function test_an_uninstall_checks_the_floors_of_its_own_main_file(): void $source = (string) \file_get_contents( DWS_LPMWC_FILE ); \file_put_contents( DWS_LPMWC_FILE, (string) \preg_replace( '/^(\s*\*\s*Requires PHP:\s*).*$/m', '${1}99.0', $source ) ); $log = (string) \tempnam( \sys_get_temp_dir(), 'dws-tpl-uninstall-' ); - \ini_set( 'error_log', $log ); // phpcs:ignore WordPress.PHP.IniSet.Risky -- The child process's stderr would fail the test. + \ini_set( 'error_log', $log ); try { include \dirname( DWS_LPMWC_FILE ) . '/uninstall.php'; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php index b7d6c2b..3aed76e 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php @@ -51,7 +51,7 @@ public static function tearDownAfterClass(): void { foreach ( self::$users as $user ) { wp_delete_user( $user ); } - $GLOBALS['wp_filter'] = self::$hooks; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Unhooks the class's strategy and its form. + $GLOBALS['wp_filter'] = self::$hooks; } #[Before] diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php index 5c03333..69e4b2a 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php @@ -51,7 +51,7 @@ public static function setUpBeforeClass(): void { #[\Override] public static function tearDownAfterClass(): void { wp_delete_user( self::$admin ); - $GLOBALS['wp_filter'] = self::$hooks; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Unhooks the class's strategy and its form. + $GLOBALS['wp_filter'] = self::$hooks; } #[Before] diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php index 695d7a0..702a243 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php @@ -59,7 +59,7 @@ public static function tearDownAfterClass(): void { wp_delete_user( $user ); } unregister_post_type( 'wc_membership_plan' ); - $GLOBALS['wp_filter'] = self::$hooks; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Unhooks the class's strategy and its form. + $GLOBALS['wp_filter'] = self::$hooks; } // The form evaluates its definition and saves an object once per request, so every row edits a plan of its own. diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php b/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php index 5cd8268..fb41114 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php @@ -13,4 +13,4 @@ * * @return array */ -function wc_memberships_get_user_active_memberships( $user_id = null, $args = array() ) {} // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- A declaration only. +function wc_memberships_get_user_active_memberships( $user_id = null, $args = array() ) {} diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/memberships-functions.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/memberships-functions.php index 806a477..074d787 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Support/memberships-functions.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/memberships-functions.php @@ -8,7 +8,7 @@ use DeepWebSolutions\LockedPaymentMethods\Tests\Support\MembershipsDouble; if ( ! function_exists( 'wc_memberships_get_user_active_memberships' ) ) { - function wc_memberships_get_user_active_memberships( $user_id = null, $args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Memberships' signature. + function wc_memberships_get_user_active_memberships( $user_id = null, $args = array() ) { return MembershipsDouble::$memberships[ (int) $user_id ] ?? array(); } } From 7b6c3e2d07802bb79c2e7ab303303f2a3540cb7e Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:45:14 +0100 Subject: [PATCH 25/39] Name each plugin's boot boundary for its role Each composition root held its PluginBoot as $boot, beside the boot() method that runs it and the kernel's boot(), where the template names the property for what it is, the boundary the plugin boots inside (X203's A266). Plugin::$boot becomes $boundary in the three plugins, with the boot tests and the without-WooCommerce specs that read its status. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/src/Plugin.php | 6 +++--- .../tests/EndToEnd/without-woocommerce.spec.ts | 2 +- .../internal-comments/tests/Integration/PluginBootTest.php | 2 +- plugins/linked-orders-for-woocommerce/src/Plugin.php | 6 +++--- .../tests/EndToEnd/without-woocommerce.spec.ts | 6 +++--- .../tests/Integration/PluginBootTest.php | 2 +- .../locked-payment-methods-for-woocommerce/src/Plugin.php | 6 +++--- .../tests/EndToEnd/without-woocommerce.spec.ts | 6 +++--- .../tests/Integration/PluginBootTest.php | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/internal-comments/src/Plugin.php b/plugins/internal-comments/src/Plugin.php index 51e28f4..4988a2d 100644 --- a/plugins/internal-comments/src/Plugin.php +++ b/plugins/internal-comments/src/Plugin.php @@ -72,7 +72,7 @@ final class Plugin { * * @var PluginBoot */ - public readonly PluginBoot $boot; + public readonly PluginBoot $boundary; /** * The scheduler, which the components and the deactivation callback share. @@ -127,7 +127,7 @@ final class Plugin { public function __construct() { $this->logger = new ErrorLogLogger( self::SLUG ); $this->scheduler = new Scheduler( self::PREFIX ); - $this->boot = new PluginBoot( DWS_IC_FILE, self::PREFIX, $this->logger ); + $this->boundary = new PluginBoot( DWS_IC_FILE, self::PREFIX, $this->logger ); } // endregion @@ -143,7 +143,7 @@ public function __construct() { * @return void */ public function boot(): void { - $this->boot->run( $this->compose( ... ) ); + $this->boundary->run( $this->compose( ... ) ); } /** diff --git a/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts b/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts index 1abdcd0..10c9f5c 100644 --- a/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts +++ b/plugins/internal-comments/tests/EndToEnd/without-woocommerce.spec.ts @@ -13,7 +13,7 @@ test.describe( 'Without WooCommerce', () => { test( 'the plugin boots and every screen still loads', async ( { page, } ) => { - expect( cli( 'eval', 'echo dws_ic_plugin()->boot->status;' ) ).toBe( + expect( cli( 'eval', 'echo dws_ic_plugin()->boundary->status;' ) ).toBe( 'booted' ); diff --git a/plugins/internal-comments/tests/Integration/PluginBootTest.php b/plugins/internal-comments/tests/Integration/PluginBootTest.php index a9d279a..0774b96 100644 --- a/plugins/internal-comments/tests/Integration/PluginBootTest.php +++ b/plugins/internal-comments/tests/Integration/PluginBootTest.php @@ -15,7 +15,7 @@ final class PluginBootTest extends TestCase { public function test_the_plugin_boots_every_component(): void { $plugin = dws_ic_plugin(); - self::assertSame( 'booted', $plugin->boot->status ); + self::assertSame( 'booted', $plugin->boundary->status ); self::assertSame( array(), $plugin->kernel->skipped ); } diff --git a/plugins/linked-orders-for-woocommerce/src/Plugin.php b/plugins/linked-orders-for-woocommerce/src/Plugin.php index 8168c4e..15f7375 100644 --- a/plugins/linked-orders-for-woocommerce/src/Plugin.php +++ b/plugins/linked-orders-for-woocommerce/src/Plugin.php @@ -73,7 +73,7 @@ final class Plugin { * * @var PluginBoot */ - public readonly PluginBoot $boot; + public readonly PluginBoot $boundary; /** * The scheduler, which registers its WP-Cron schedules even while the plugin is blocked. @@ -128,7 +128,7 @@ final class Plugin { public function __construct() { $this->logger = new WooCommerceLogger( self::SLUG, \defined( 'DWS_LOWC_DEBUG' ) && true === \constant( 'DWS_LOWC_DEBUG' ) ? 'debug' : null ); $this->scheduler = new Scheduler( self::PREFIX ); - $this->boot = new PluginBoot( DWS_LOWC_FILE, self::PREFIX, $this->logger ); + $this->boundary = new PluginBoot( DWS_LOWC_FILE, self::PREFIX, $this->logger ); } // endregion @@ -144,7 +144,7 @@ public function __construct() { * @return void */ public function boot(): void { - $this->boot->run( $this->compose( ... ) ); + $this->boundary->run( $this->compose( ... ) ); } /** diff --git a/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts b/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts index 848fa88..cdce20f 100644 --- a/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts +++ b/plugins/linked-orders-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts @@ -17,9 +17,9 @@ test.describe( 'Without WooCommerce', () => { test( 'the plugin stays blocked, WordPress names the missing plugin and every screen still loads', async ( { page, } ) => { - expect( cli( 'eval', 'echo dws_lowc_plugin()->boot->status;' ) ).toBe( - 'blocked' - ); + expect( + cli( 'eval', 'echo dws_lowc_plugin()->boundary->status;' ) + ).toBe( 'blocked' ); const front = await page.goto( '/' ); expect( front?.status() ).toBe( 200 ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php index 95d14b5..8134734 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php @@ -16,7 +16,7 @@ final class PluginBootTest extends TestCase { public function test_the_plugin_boots_every_component_but_the_integration_with_an_absent_plugin(): void { $plugin = dws_lowc_plugin(); - self::assertSame( 'booted', $plugin->boot->status ); + self::assertSame( 'booted', $plugin->boundary->status ); self::assertSame( array( SequentialOrderNumbersPro::class ), $plugin->kernel->skipped ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php b/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php index 511d815..51cb1b0 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php @@ -78,7 +78,7 @@ final class Plugin { * * @var PluginBoot */ - public readonly PluginBoot $boot; + public readonly PluginBoot $boundary; /** * The scheduler, which registers its WP-Cron schedules even while the plugin is blocked. @@ -133,7 +133,7 @@ final class Plugin { public function __construct() { $this->logger = new WooCommerceLogger( self::SLUG, \defined( 'DWS_LPMWC_DEBUG' ) && true === \constant( 'DWS_LPMWC_DEBUG' ) ? 'debug' : null ); $this->scheduler = new Scheduler( self::PREFIX ); - $this->boot = new PluginBoot( DWS_LPMWC_FILE, self::PREFIX, $this->logger ); + $this->boundary = new PluginBoot( DWS_LPMWC_FILE, self::PREFIX, $this->logger ); } // endregion @@ -149,7 +149,7 @@ public function __construct() { * @return void */ public function boot(): void { - $this->boot->run( $this->compose( ... ) ); + $this->boundary->run( $this->compose( ... ) ); } /** diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts index b178b94..b566b62 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts +++ b/plugins/locked-payment-methods-for-woocommerce/tests/EndToEnd/without-woocommerce.spec.ts @@ -17,9 +17,9 @@ test.describe( 'Without WooCommerce', () => { test( 'the plugin stays blocked, WordPress names the missing plugin and every screen still loads', async ( { page, } ) => { - expect( cli( 'eval', 'echo dws_lpmwc_plugin()->boot->status;' ) ).toBe( - 'blocked' - ); + expect( + cli( 'eval', 'echo dws_lpmwc_plugin()->boundary->status;' ) + ).toBe( 'blocked' ); const front = await page.goto( '/' ); expect( front?.status() ).toBe( 200 ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php index d36e3cc..7e75864 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php @@ -18,7 +18,7 @@ final class PluginBootTest extends TestCase { public function test_the_plugin_boots_every_component_whose_switch_is_on(): void { $plugin = dws_lpmwc_plugin(); - self::assertSame( 'booted', $plugin->boot->status ); + self::assertSame( 'booted', $plugin->boundary->status ); self::assertSame( array( OrderStrategy::class, GroupsStrategy::class, WooCommerceMembershipsStrategy::class ), $plugin->kernel->skipped ); // Orders and the integrations are off by default, as v1's were. } From 0d65326b7ddec26fa3edfaf0fb9706fa70acea7c Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:45:53 +0100 Subject: [PATCH 26/39] Keep Linked Orders' and Internal Comments' assets.php whole Linked Orders' and Internal Comments' includes/assets.php had drifted from the template's, which X147 has a port keep whole: the stylesheet registrar still took a $media no caller passes, the payload path was built in two calls (A265), and the script's arguments shared the @param list instead of a @phpstan-param shape. Both files are the template's, renamed, and the AssetsTest rows that passed $media follow it. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/includes/assets.php | 18 +++++++++--------- .../tests/Integration/AssetsTest.php | 4 ++-- .../includes/assets.php | 18 +++++++++--------- .../tests/Integration/AssetsTest.php | 4 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/plugins/internal-comments/includes/assets.php b/plugins/internal-comments/includes/assets.php index f34bd0d..3ab48b6 100644 --- a/plugins/internal-comments/includes/assets.php +++ b/plugins/internal-comments/includes/assets.php @@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit; /** - * Returns the payload wp-scripts generates beside a built file, which lists its dependencies, script module imports included, and its version. + * Returns the payload wp-scripts generates beside a built file. * * @since 2.0.0 * @version 2.0.0 @@ -28,8 +28,7 @@ function dws_ic_get_asset( string $path ): array { * * @var array{dependencies: list, version: string, type?: string, handle?: string} $asset */ - $asset = require plugin_dir_path( DWS_IC_FILE ) . dirname( $path ) . '/' . basename( $path, '.' . pathinfo( $path, PATHINFO_EXTENSION ) ) . '.asset.php'; - + $asset = require plugin_dir_path( DWS_IC_FILE ) . dirname( $path ) . '/' . pathinfo( $path, PATHINFO_FILENAME ) . '.asset.php'; return $asset; } @@ -39,9 +38,11 @@ function dws_ic_get_asset( string $path ): array { * @since 2.0.0 * @version 2.0.0 * - * @param non-empty-string $handle The script's handle. - * @param non-empty-string $path The built script's path, relative to the plugin's directory. - * @param array{strategy?: 'async'|'defer', in_footer?: bool, fetchpriority?: 'auto'|'low'|'high'} $args Optional. How the script loads. Default none, which loads it in the head. + * @phpstan-param array{strategy?: 'async'|'defer', in_footer?: bool, fetchpriority?: 'auto'|'low'|'high'} $args + * + * @param non-empty-string $handle The script's handle. + * @param non-empty-string $path The built script's path, relative to the plugin's directory. + * @param array $args Optional. How the script loads. Default none, which loads it in the head. * * @return bool */ @@ -63,12 +64,11 @@ function dws_ic_register_script( string $handle, string $path, array $args = arr * * @param non-empty-string $handle The stylesheet's handle. * @param non-empty-string $path The built stylesheet's path, relative to the plugin's directory. - * @param string $media Optional. The media the stylesheet applies to. Default 'all'. * * @return bool */ -function dws_ic_register_style( string $handle, string $path, string $media = 'all' ): bool { - $registered = wp_register_style( $handle, plugin_dir_url( DWS_IC_FILE ) . $path, array(), dws_ic_get_asset( $path )['version'], $media ); +function dws_ic_register_style( string $handle, string $path ): bool { + $registered = wp_register_style( $handle, plugin_dir_url( DWS_IC_FILE ) . $path, array(), dws_ic_get_asset( $path )['version'] ); if ( $registered ) { wp_style_add_data( $handle, 'rtl', 'replace' ); } diff --git a/plugins/internal-comments/tests/Integration/AssetsTest.php b/plugins/internal-comments/tests/Integration/AssetsTest.php index cfb8feb..34a6b28 100644 --- a/plugins/internal-comments/tests/Integration/AssetsTest.php +++ b/plugins/internal-comments/tests/Integration/AssetsTest.php @@ -34,12 +34,12 @@ public function test_a_script_registers_with_its_generated_dependencies_and_vers } public function test_a_stylesheet_registers_with_its_generated_version_and_its_right_to_left_variant(): void { - self::assertTrue( dws_ic_register_style( self::HANDLE, 'tests/Fixtures/built.css', 'print' ) ); + self::assertTrue( dws_ic_register_style( self::HANDLE, 'tests/Fixtures/built.css' ) ); $style = wp_styles()->registered[ self::HANDLE ]; self::assertSame( plugins_url( 'tests/Fixtures/built.css', DWS_IC_FILE ), $style->src ); self::assertSame( 'fixture', $style->ver ); - self::assertSame( 'print', $style->args ); + self::assertSame( 'all', $style->args ); self::assertSame( 'replace', $style->extra['rtl'] ?? null ); } } diff --git a/plugins/linked-orders-for-woocommerce/includes/assets.php b/plugins/linked-orders-for-woocommerce/includes/assets.php index 9c55f48..9a9932c 100644 --- a/plugins/linked-orders-for-woocommerce/includes/assets.php +++ b/plugins/linked-orders-for-woocommerce/includes/assets.php @@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit; /** - * Returns the payload wp-scripts generates beside a built file, which lists its dependencies, script module imports included, and its version. + * Returns the payload wp-scripts generates beside a built file. * * @since 2.0.0 * @version 2.0.0 @@ -28,8 +28,7 @@ function dws_lowc_get_asset( string $path ): array { * * @var array{dependencies: list, version: string, type?: string, handle?: string} $asset */ - $asset = require plugin_dir_path( DWS_LOWC_FILE ) . dirname( $path ) . '/' . basename( $path, '.' . pathinfo( $path, PATHINFO_EXTENSION ) ) . '.asset.php'; - + $asset = require plugin_dir_path( DWS_LOWC_FILE ) . dirname( $path ) . '/' . pathinfo( $path, PATHINFO_FILENAME ) . '.asset.php'; return $asset; } @@ -39,9 +38,11 @@ function dws_lowc_get_asset( string $path ): array { * @since 2.0.0 * @version 2.0.0 * - * @param non-empty-string $handle The script's handle. - * @param non-empty-string $path The built script's path, relative to the plugin's directory. - * @param array{strategy?: 'async'|'defer', in_footer?: bool, fetchpriority?: 'auto'|'low'|'high'} $args Optional. How the script loads. Default none, which loads it in the head. + * @phpstan-param array{strategy?: 'async'|'defer', in_footer?: bool, fetchpriority?: 'auto'|'low'|'high'} $args + * + * @param non-empty-string $handle The script's handle. + * @param non-empty-string $path The built script's path, relative to the plugin's directory. + * @param array $args Optional. How the script loads. Default none, which loads it in the head. * * @return bool */ @@ -63,12 +64,11 @@ function dws_lowc_register_script( string $handle, string $path, array $args = a * * @param non-empty-string $handle The stylesheet's handle. * @param non-empty-string $path The built stylesheet's path, relative to the plugin's directory. - * @param string $media Optional. The media the stylesheet applies to. Default 'all'. * * @return bool */ -function dws_lowc_register_style( string $handle, string $path, string $media = 'all' ): bool { - $registered = wp_register_style( $handle, plugin_dir_url( DWS_LOWC_FILE ) . $path, array(), dws_lowc_get_asset( $path )['version'], $media ); +function dws_lowc_register_style( string $handle, string $path ): bool { + $registered = wp_register_style( $handle, plugin_dir_url( DWS_LOWC_FILE ) . $path, array(), dws_lowc_get_asset( $path )['version'] ); if ( $registered ) { wp_style_add_data( $handle, 'rtl', 'replace' ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/AssetsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/AssetsTest.php index 93d33fc..dc59eb4 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/AssetsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/AssetsTest.php @@ -34,12 +34,12 @@ public function test_a_script_registers_with_its_generated_dependencies_and_vers } public function test_a_stylesheet_registers_with_its_generated_version_and_its_right_to_left_variant(): void { - self::assertTrue( dws_lowc_register_style( self::HANDLE, 'tests/Fixtures/built.css', 'print' ) ); + self::assertTrue( dws_lowc_register_style( self::HANDLE, 'tests/Fixtures/built.css' ) ); $style = wp_styles()->registered[ self::HANDLE ]; self::assertSame( plugins_url( 'tests/Fixtures/built.css', DWS_LOWC_FILE ), $style->src ); self::assertSame( 'fixture', $style->ver ); - self::assertSame( 'print', $style->args ); + self::assertSame( 'all', $style->args ); self::assertSame( 'replace', $style->extra['rtl'] ?? null ); } } From 7c5b4346d56c932b6d764925f98bb03836b2437a Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:47:08 +0100 Subject: [PATCH 27/39] Write Locked Payment Methods' option keys as literals Eight of Locked Payment Methods' option keys were built from Plugin::PREFIX while includes/options.php loads, which forced a rule that an include reading Plugin sort after metadata.php, where the template and the other plugins write their keys as literals (X152). The eight keys are literals, options.php no longer imports Plugin, and convention 1 drops the sort-order sentence. The partial-deploy row for the peer getters, which hid src/Plugin.php to break options.php, takes the other plugins' include that throws after metadata.php instead. Assisted-by: Claude Code:claude-opus-5-5 --- .../AGENTS.md | 2 +- .../includes/options.php | 18 ++++++++---------- .../tests/Integration/PartialDeployTest.php | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md index 7cffe49..5ca1422 100644 --- a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md +++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md @@ -58,7 +58,7 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on, and t ## Conventions -1. **Placement.** `src/` holds the composition root, its components and the plan tab host. Code that registers no hooks, such as keys, readers, v1's helper API, the footprint and migration steps, is `dws_lpmwc_` functions and `DWS_LPMWC_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match`, nullsafe calls or first-class callables there. An include that reads `Plugin` while it loads must sort after `metadata.php`, so `capabilities.php` and `grants.php` name their keys as literals. Container definitions are composition and stay in `Plugin::get_definitions()`. +1. **Placement.** `src/` holds the composition root, its components and the plan tab host. Code that registers no hooks, such as keys, readers, v1's helper API, the footprint and migration steps, is `dws_lpmwc_` functions and `DWS_LPMWC_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match`, nullsafe calls or first-class callables there. Container definitions are composition and stay in `Plugin::get_definitions()`. 2. **Strategies.** Each way of unlocking is a component in `src/UnlockStrategies/`, gated on its switch, and an integration on its plugin too through a static `is_available()` that the settings read. It answers `dws_lpmwc/payment_method/should_lock` from the arguments alone, through an `@internal` static `unlocks()` that v1's per-strategy checks call, and returns `false` or the lock it was given. The safeguard passes the current user and, on an order's own payment page with its key, that order. 3. **Request context.** `is_admin()` is false on REST requests and true in admin-ajax ones, so an admin-only feature decides the request context in its callbacks, never in `should_load()`. 4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. It reads a template-built base plugin's slug, name and version only through that plugin's `{prefix}_get_plugin_*()` getters, from `plugins_loaded` on, since the add-on may load first, and behind `function_exists()`, since a base plugin below its floors or with a partial build may stay active without defining them. diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/options.php b/plugins/locked-payment-methods-for-woocommerce/includes/options.php index 1870b40..4e77e54 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/options.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/options.php @@ -8,8 +8,6 @@ * @package DeepWebSolutions\LockedPaymentMethods */ -use DeepWebSolutions\LockedPaymentMethods\Plugin; - defined( 'ABSPATH' ) || exit; /** @@ -20,7 +18,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_DELETE_DATA = Plugin::PREFIX . '_delete_data'; +const DWS_LPMWC_OPTION_DELETE_DATA = 'dws_lpmwc_delete_data'; /** * The option that holds the ids of the payment methods locked for every customer whom no unlock strategy unlocks. @@ -30,7 +28,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_LOCKED_METHODS = Plugin::PREFIX . '_locked_payment_methods'; +const DWS_LPMWC_OPTION_LOCKED_METHODS = 'dws_lpmwc_locked_payment_methods'; /** * The option that holds 'yes' where users with the full-access roles may use every locked payment method. @@ -40,7 +38,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES = Plugin::PREFIX . '_unlock_via_user_roles'; +const DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES = 'dws_lpmwc_unlock_via_user_roles'; /** * The option that holds the roles whose users may use every locked payment method while that strategy is on. @@ -50,7 +48,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_FULL_ACCESS_ROLES = Plugin::PREFIX . '_full_access_user_roles'; +const DWS_LPMWC_OPTION_FULL_ACCESS_ROLES = 'dws_lpmwc_full_access_user_roles'; /** * The option that holds 'yes' where a user's profile can unlock locked payment methods for that user. @@ -60,7 +58,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_UNLOCK_VIA_USER_PROFILES = Plugin::PREFIX . '_unlock_via_user_profiles'; +const DWS_LPMWC_OPTION_UNLOCK_VIA_USER_PROFILES = 'dws_lpmwc_unlock_via_user_profiles'; /** * The option that holds 'yes' where an order can unlock locked payment methods for paying that order. @@ -70,7 +68,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS = Plugin::PREFIX . '_unlock_via_orders'; +const DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS = 'dws_lpmwc_unlock_via_orders'; /** * The option that holds 'yes' where a Groups group can unlock locked payment methods for its members. @@ -80,7 +78,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_UNLOCK_VIA_GROUPS = Plugin::PREFIX . '_unlock_via_groups'; +const DWS_LPMWC_OPTION_UNLOCK_VIA_GROUPS = 'dws_lpmwc_unlock_via_groups'; /** * The option that holds 'yes' where a WooCommerce Memberships plan can unlock locked payment methods for its active members. @@ -90,7 +88,7 @@ * * @var string */ -const DWS_LPMWC_OPTION_UNLOCK_VIA_WC_MEMBERSHIPS = Plugin::PREFIX . '_unlock_via_wc_memberships'; +const DWS_LPMWC_OPTION_UNLOCK_VIA_WC_MEMBERSHIPS = 'dws_lpmwc_unlock_via_wc_memberships'; /** * The value each option reads as while nothing valid is stored. diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php index 28a421e..5e4c899 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php @@ -40,12 +40,12 @@ public function test_an_include_that_throws_shows_the_build_notice_and_boots_not } public function test_the_peer_getters_a_partial_deploy_defines_still_answer(): void { - $root = \dirname( DWS_LPMWC_FILE ) . '/src/Plugin.php'; // options.php reads it at include time, after metadata.php has loaded. - \rename( $root, "$root.hidden" ); + $include = \dirname( DWS_LPMWC_FILE ) . '/includes/zz-throws.php'; // It loads after metadata.php. + \file_put_contents( $include, " Date: Wed, 11 Nov 2026 16:49:06 +0100 Subject: [PATCH 28/39] Give each entry and scoper config their file docblock tags Each plugin's entry file docblock held only the header WordPress reads and @package, and each scoper.inc.php no @since or @version, where every other file, and the template's entry and scoper config, opens with a role sentence and both tags (X143). Each entry gains the template's role sentence and @since and @version 2.0.0, and each scoper config the two tags. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/internal-comments.php | 5 +++++ plugins/internal-comments/scoper.inc.php | 3 +++ .../linked-orders-for-woocommerce.php | 5 +++++ plugins/linked-orders-for-woocommerce/scoper.inc.php | 3 +++ .../locked-payment-methods-for-woocommerce.php | 5 +++++ .../locked-payment-methods-for-woocommerce/scoper.inc.php | 3 +++ 6 files changed, 24 insertions(+) diff --git a/plugins/internal-comments/internal-comments.php b/plugins/internal-comments/internal-comments.php index 9ac1664..dcb1f28 100644 --- a/plugins/internal-comments/internal-comments.php +++ b/plugins/internal-comments/internal-comments.php @@ -1,5 +1,7 @@ Date: Wed, 11 Nov 2026 16:49:51 +0100 Subject: [PATCH 29/39] Name each shared docblock's role and give its reason a paragraph The docblocks the plugins share with the template packed a role, its reasons and a listing into one summary, such as functions.php's list of everything it hooks and the footprint's list of what it covers, where the template names the role in one sentence and moves the one reason left to a second paragraph (X202's A257-A261, X290's A745). The entry's error reporter, functions.php, the order-tables declaration, the footprint, the metadata getters, the settings step, the option-text reader, the safeguards file and the composition root follow the template, each order-tables declaration naming its own evidence and each footprint and settings step its v1 rows, and the reporter's @throws reads "Thrown when the site runs in a local or development environment." Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/functions.php | 6 ++++-- plugins/internal-comments/includes/footprint.php | 4 +++- plugins/internal-comments/includes/metadata.php | 8 +++++--- plugins/internal-comments/includes/migrations.php | 4 +++- plugins/internal-comments/includes/safeguards.php | 4 +++- plugins/internal-comments/internal-comments.php | 4 ++-- plugins/internal-comments/src/Plugin.php | 2 +- plugins/linked-orders-for-woocommerce/functions.php | 6 ++++-- .../linked-orders-for-woocommerce/includes/footprint.php | 4 +++- .../linked-orders-for-woocommerce/includes/metadata.php | 8 +++++--- .../linked-orders-for-woocommerce/includes/migrations.php | 4 +++- .../linked-orders-for-woocommerce/includes/safeguards.php | 4 +++- .../linked-orders-for-woocommerce.php | 4 ++-- plugins/linked-orders-for-woocommerce/src/Plugin.php | 2 +- .../locked-payment-methods-for-woocommerce/functions.php | 6 ++++-- .../includes/footprint.php | 4 +++- .../includes/metadata.php | 8 +++++--- .../includes/migrations.php | 4 +++- .../includes/options.php | 6 ++++-- .../includes/safeguards.php | 4 +++- .../locked-payment-methods-for-woocommerce.php | 4 ++-- .../locked-payment-methods-for-woocommerce/src/Plugin.php | 2 +- 22 files changed, 67 insertions(+), 35 deletions(-) diff --git a/plugins/internal-comments/functions.php b/plugins/internal-comments/functions.php index c5f0105..76aa172 100644 --- a/plugins/internal-comments/functions.php +++ b/plugins/internal-comments/functions.php @@ -1,6 +1,6 @@ Date: Wed, 11 Nov 2026 16:50:54 +0100 Subject: [PATCH 30/39] Keep each return beside the statement before it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Style guide §5 puts no blank line between a statement and the return after it, yet 157 places in the three plugins' code and tests kept one, the option readers and v1's shims among them, as the template's copies did until its own sweep (X212's A306). The sweep closes each gap and changes no PHP token, which a comparison of every touched file's php -w output before and after shows. The asset fixtures keep the blank line after their opening tag, since they mirror the file wp-scripts generates. Assisted-by: Claude Code:claude-opus-5-5 --- .../internal-comments/includes/comments.php | 6 ------ .../internal-comments/includes/deprecated.php | 7 ------- plugins/internal-comments/includes/markup.php | 2 -- .../internal-comments/includes/metadata.php | 1 - .../internal-comments/includes/migrations.php | 1 - .../internal-comments/includes/settings.php | 2 -- .../models/DWS_Internal_Comment.php | 2 -- .../internal-comments/src/AdminScreens.php | 7 ------- plugins/internal-comments/src/Comments.php | 3 --- plugins/internal-comments/src/Permissions.php | 1 - plugins/internal-comments/src/Plugin.php | 1 - plugins/internal-comments/src/RestRoutes.php | 2 -- plugins/internal-comments/src/Settings.php | 1 - .../tests/Integration/AdminScreensTest.php | 5 ----- .../tests/Integration/CommentsTest.php | 2 -- .../tests/Integration/DataGuardsTest.php | 1 - .../tests/Integration/MetaBoxTest.php | 2 -- .../tests/Integration/MetadataTest.php | 1 - .../tests/Integration/MigrationsTest.php | 1 - .../tests/Integration/OrdersTest.php | 1 - .../tests/Integration/PermissionsTest.php | 1 - .../tests/Integration/SettingsApiTest.php | 3 --- .../tests/Integration/SettingsTest.php | 3 --- .../tests/Support/RunsRequestsTrait.php | 1 - .../tests/Unit/ReadmeTest.php | 1 - .../includes/deprecated.php | 7 ------- .../includes/linking.php | 1 - .../includes/metadata.php | 1 - .../includes/options.php | 1 - .../includes/post-type.php | 3 --- .../includes/settings.php | 1 - .../models/DWS_Order_Node.php | 4 ---- .../src/ListTable.php | 4 ---- .../src/Plugin.php | 1 - .../src/Settings.php | 1 - .../Fixtures/sequential-order-numbers-pro.php | 1 - .../tests/Integration/AutocompletionTest.php | 2 -- .../tests/Integration/DeprecatedHooksTest.php | 2 -- .../tests/Integration/ListTableTest.php | 3 --- .../tests/Integration/MetaBoxTest.php | 2 -- .../tests/Integration/MetadataTest.php | 1 - .../tests/Integration/SettingsApiTest.php | 3 --- .../tests/Integration/SettingsTest.php | 3 --- .../tests/Support/RunsRequestsTrait.php | 1 - .../tests/Unit/ReadmeTest.php | 1 - .../includes/deprecated.php | 19 ------------------- .../includes/grants.php | 5 ----- .../includes/metadata.php | 1 - .../includes/options.php | 3 --- .../includes/safeguards.php | 1 - .../src/Plugin.php | 1 - .../src/Settings.php | 1 - .../src/UnlockStrategies/GroupsStrategy.php | 2 -- .../UnlockStrategies/UserProfileStrategy.php | 3 --- .../src/UnlockStrategies/UserRoleStrategy.php | 1 - .../WooCommerceMembershipsStrategy.php | 2 -- .../tests/Integration/DeprecatedApiTest.php | 2 -- .../tests/Integration/MetadataTest.php | 1 - .../tests/Integration/SafeguardsTest.php | 3 --- .../tests/Integration/SettingsTest.php | 2 -- .../UnlockStrategies/GroupsStrategyTest.php | 1 - .../UserProfileStrategyTest.php | 3 --- .../UnlockStrategies/UserRoleStrategyTest.php | 1 - .../tests/Support/RunsRequestsTrait.php | 1 - .../tests/Unit/ReadmeTest.php | 1 - 65 files changed, 155 deletions(-) diff --git a/plugins/internal-comments/includes/comments.php b/plugins/internal-comments/includes/comments.php index fa4087a..d1ed667 100644 --- a/plugins/internal-comments/includes/comments.php +++ b/plugins/internal-comments/includes/comments.php @@ -59,7 +59,6 @@ function dws_ic_get_comment( $comment ): ?WP_Comment { // get_comment() reads an empty id as the comment of the current loop. $comment = $comment instanceof WP_Comment || ( is_numeric( $comment ) && (int) $comment > 0 ) ? get_comment( $comment instanceof WP_Comment ? $comment : (int) $comment ) : null; - return $comment instanceof WP_Comment ? $comment : null; } @@ -75,7 +74,6 @@ function dws_ic_get_comment( $comment ): ?WP_Comment { */ function dws_ic_is_internal_comment( $comment ): ?bool { $comment = dws_ic_get_comment( $comment ); - return null === $comment ? null : DWS_IC_COMMENT_TYPE === $comment->comment_type; } @@ -164,7 +162,6 @@ function dws_ic_create_internal_comment( array $args = array(), ?WP_User $author $data = apply_filters( 'dws_ic/internal_comment/creation_data', $data ); $comment_id = wp_insert_comment( wp_slash( $data ) ); // @phpstan-ignore argument.type (The fields pass through public filters, which wp_insert_comment() accepts in any shape.) - return false === $comment_id ? null : $comment_id; } @@ -245,7 +242,6 @@ function dws_ic_get_internal_comments_from_trash_meta( int $post_id, string $sta */ function dws_ic_is_private_comment( $comment ): ?bool { $owners = dws_ic_get_private_comment_owners( $comment ); - return null === $owners ? null : array() !== $owners; } @@ -294,7 +290,6 @@ function dws_ic_exclude_internal_type( $excluded ): array { } $types[] = DWS_IC_COMMENT_TYPE; - return $types; } @@ -331,6 +326,5 @@ function dws_ic_has_replies( int $comment_id ): bool { 'count' => true, ) ); - return is_int( $replies ) && 0 < $replies; } diff --git a/plugins/internal-comments/includes/deprecated.php b/plugins/internal-comments/includes/deprecated.php index 76c4c4f..790dedf 100644 --- a/plugins/internal-comments/includes/deprecated.php +++ b/plugins/internal-comments/includes/deprecated.php @@ -113,7 +113,6 @@ */ function dws_ic_name(): string { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_ic_get_plugin_name()' ); - return dws_ic_get_plugin_name(); } @@ -128,7 +127,6 @@ function dws_ic_name(): string { */ function dws_ic_version(): string { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_ic_get_plugin_version()' ); - return dws_ic_get_plugin_version(); } @@ -143,7 +141,6 @@ function dws_ic_version(): string { */ function dws_ic_path(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_IC_PATH; } @@ -158,7 +155,6 @@ function dws_ic_path(): string { */ function dws_ic_min_php(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_IC_MIN_PHP; } @@ -173,7 +169,6 @@ function dws_ic_min_php(): string { */ function dws_ic_min_wp(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_IC_MIN_WP; } @@ -191,7 +186,6 @@ function dws_ic_min_wp(): string { */ function dws_ic_get_hook_tag( string $name, $extra = array() ): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return dws_ic_join_v1_hook_tag( 'internal_comments', $name, $extra ); } @@ -210,7 +204,6 @@ function dws_ic_get_hook_tag( string $name, $extra = array() ): string { */ function dws_ic_get_component_hook_tag( string $component_id, string $name, $extra = array() ): ?string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return isset( DWS_IC_V1_COMPONENTS[ $component_id ] ) ? dws_ic_join_v1_hook_tag( 'internal_comments/' . DWS_IC_V1_COMPONENTS[ $component_id ], $name, $extra ) : null; } diff --git a/plugins/internal-comments/includes/markup.php b/plugins/internal-comments/includes/markup.php index a975f29..35733f7 100644 --- a/plugins/internal-comments/includes/markup.php +++ b/plugins/internal-comments/includes/markup.php @@ -23,7 +23,6 @@ function dws_ic_get_comments_list_html( int $post_id ): string { ob_start(); dws_ic_print_comments_list( $post_id ); - return (string) ob_get_clean(); } @@ -43,7 +42,6 @@ function dws_ic_print_comments_list( int $post_id ): void { echo '
    '; dws_ic_print_comment_thread( $comments, 0 ); echo '
'; - return; } diff --git a/plugins/internal-comments/includes/metadata.php b/plugins/internal-comments/includes/metadata.php index dcbb930..0ff03dd 100644 --- a/plugins/internal-comments/includes/metadata.php +++ b/plugins/internal-comments/includes/metadata.php @@ -36,7 +36,6 @@ function dws_ic_get_plugin_slug(): string { */ function dws_ic_get_plugin_name(): string { $name = get_file_data( DWS_IC_FILE, array( 'Name' => 'Plugin Name' ) )['Name']; - return 0 < did_action( 'init' ) ? __( $name, 'internal-comments' ) : $name; // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- The header's name reaches the POT through make-pot's header extraction. } diff --git a/plugins/internal-comments/includes/migrations.php b/plugins/internal-comments/includes/migrations.php index c7b19db..5c4eb3d 100644 --- a/plugins/internal-comments/includes/migrations.php +++ b/plugins/internal-comments/includes/migrations.php @@ -133,7 +133,6 @@ function dws_ic_convert_v1_comments(): bool { if ( $end >= $last ) { delete_option( 'dws_ic_v1_comments_cursor' ); - return false; } diff --git a/plugins/internal-comments/includes/settings.php b/plugins/internal-comments/includes/settings.php index 70cc0fd..1df3457 100644 --- a/plugins/internal-comments/includes/settings.php +++ b/plugins/internal-comments/includes/settings.php @@ -41,7 +41,6 @@ function dws_ic_get_raw_setting( string $field_id, ?string $group = null ) { } $row = get_option( DWS_IC_SETTINGS_GROUPS[ $field[0] ] ); - return is_array( $row ) && array_key_exists( $field[1], $row ) ? $row[ $field[1] ] : null; } @@ -122,7 +121,6 @@ function dws_ic_get_validated_setting( string $field_id, ?string $group = null ) */ function dws_ic_get_selected_post_types(): array { $selected = dws_ic_get_raw_setting( DWS_IC_SETTING_POST_TYPES, 'general' ); - return is_array( $selected ) ? array_values( array_filter( $selected, 'is_string' ) ) : array(); } diff --git a/plugins/internal-comments/models/DWS_Internal_Comment.php b/plugins/internal-comments/models/DWS_Internal_Comment.php index 09761f4..b955972 100644 --- a/plugins/internal-comments/models/DWS_Internal_Comment.php +++ b/plugins/internal-comments/models/DWS_Internal_Comment.php @@ -107,7 +107,6 @@ public function __call( string $name, array $args ) { */ public function is_author( ?int $user_id = null ): bool { $user_id = $user_id ?? get_current_user_id(); - return 0 !== $user_id && (int) $this->comment->user_id === $user_id; } @@ -177,7 +176,6 @@ public function can_trash( ?int $user_id = null ): bool { */ public function get_edit_comment_link(): ?string { $link = get_edit_comment_link( (int) $this->comment->comment_ID ); - return is_string( $link ) ? $link : null; } diff --git a/plugins/internal-comments/src/AdminScreens.php b/plugins/internal-comments/src/AdminScreens.php index 53c47c7..2d84914 100644 --- a/plugins/internal-comments/src/AdminScreens.php +++ b/plugins/internal-comments/src/AdminScreens.php @@ -108,7 +108,6 @@ public function add_column( array $columns, string $post_type ): array { $position = \array_search( $preceding, \array_keys( $columns ), true ); $position = false === $position ? \count( $columns ) : $position + 1; - return \array_slice( $columns, 0, $position, true ) + array( self::COLUMN => __( 'Internal Comments', 'internal-comments' ) ) + \array_slice( $columns, $position, null, true ); } @@ -202,7 +201,6 @@ public function render_cell( string $column, int $post_id ): void { } if ( 0 === $count ) { echo '' . esc_html__( 'No internal comments', 'internal-comments' ) . ''; - return; } @@ -314,7 +312,6 @@ public function filter_list_table_args( array $args ): array { $args['dws_ic_list_table'] = true; if ( ! current_user_can( DWS_IC_CAP_SEE_TABLE ) ) { $args['type__not_in'] = dws_ic_exclude_internal_type( $args['type__not_in'] ?? array() ); - return $args; } @@ -377,7 +374,6 @@ public function filter_row_actions( array $actions, \WP_Comment $comment ): arra */ public function filter_bulk_actions( array $actions ): array { $type = isset( $_GET['comment_type'] ) && \is_string( $_GET['comment_type'] ) ? sanitize_key( wp_unslash( $_GET['comment_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reads the screen's view. - return DWS_IC_COMMENT_TYPE === $type ? \array_intersect_key( $actions, \array_flip( array( 'trash', 'untrash', 'delete' ) ) ) : $actions; } @@ -426,7 +422,6 @@ public function count_listed_comments( mixed $count, int $post_id ): mixed { $counts = \get_object_vars( $count ); $counts['trash'] = \is_int( $listed ) ? $listed : 0; - return (object) $counts; } @@ -446,7 +441,6 @@ public function leave_out_internal_type( array $clauses ): array { global $wpdb; $clauses['where'] = ( $clauses['where'] ?? '' ) . ( $wpdb->prepare( ' AND %i.comment_type <> %s', $wpdb->comments, DWS_IC_COMMENT_TYPE ) ?? '' ); - return $clauses; } @@ -503,7 +497,6 @@ public function render_revisions( \WP_Comment $comment ): void { $changes = \array_reverse( \array_filter( (array) get_comment_meta( $comment_id, '_dws_ic_change_id', false ), 'is_string' ) ); if ( array() === $changes ) { echo '

' . esc_html__( 'No earlier versions of this comment are recorded.', 'internal-comments' ) . '

'; - return; } diff --git a/plugins/internal-comments/src/Comments.php b/plugins/internal-comments/src/Comments.php index ce862f1..ed42ebe 100644 --- a/plugins/internal-comments/src/Comments.php +++ b/plugins/internal-comments/src/Comments.php @@ -243,7 +243,6 @@ public function hide_unmoved_comments( \WP_Comment_Query $query ): void { */ public function hide_unmoved_feed_comments( string $where ): string { global $wpdb; - return dws_ic_is_v1_move_pending() ? $where . ( $wpdb->prepare( ' AND %i.comment_type <> %s', $wpdb->comments, DWS_IC_COMMENT_TYPE ) ?? '' ) : $where; } @@ -276,7 +275,6 @@ public function guard_privacy( mixed $check, int $comment_id, string $meta_key, } $comment = dws_ic_get_comment( $comment_id ); - return null !== $comment && get_current_user_id() !== (int) $comment->user_id ? false : $check; } @@ -307,7 +305,6 @@ protected function put_back( int|string $comment_id ): ?string { $wpdb->update( $wpdb->comments, array( 'comment_approved' => DWS_IC_COMMENT_STATUS ), array( 'comment_ID' => (int) $comment->comment_ID ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- wp_set_comment_status() refuses a status of its own, and the cache is cleaned below. clean_comment_cache( (int) $comment->comment_ID ); wp_update_comment_count( (int) $comment->comment_post_ID ); - return $comment->comment_approved; } diff --git a/plugins/internal-comments/src/Permissions.php b/plugins/internal-comments/src/Permissions.php index e816d90..dfbf092 100644 --- a/plugins/internal-comments/src/Permissions.php +++ b/plugins/internal-comments/src/Permissions.php @@ -121,7 +121,6 @@ public function guard_core_rest_comment( mixed $response, mixed $handler, \WP_RE */ public function exclude_from_core_rest_collections( array $args ): array { $args['type__not_in'] = dws_ic_exclude_internal_type( $args['type__not_in'] ?? array() ); - return $args; } diff --git a/plugins/internal-comments/src/Plugin.php b/plugins/internal-comments/src/Plugin.php index d556fa3..9327fb2 100644 --- a/plugins/internal-comments/src/Plugin.php +++ b/plugins/internal-comments/src/Plugin.php @@ -191,7 +191,6 @@ protected function compose(): bool { $this->kernel = new PluginKernel( new ContainerBuilder()->addDefinitions( $this->get_definitions( $this->logger, $this->scheduler ) )->build() ); $this->kernel->boot( self::COMPONENTS ); - return true; } diff --git a/plugins/internal-comments/src/RestRoutes.php b/plugins/internal-comments/src/RestRoutes.php index 854146c..e386292 100644 --- a/plugins/internal-comments/src/RestRoutes.php +++ b/plugins/internal-comments/src/RestRoutes.php @@ -135,7 +135,6 @@ public function register_routes(): void { */ public function can_read_post_comments( \WP_REST_Request $request ): bool { $post_id = absint( $request['post'] ); - return $this->can_use_post( $post_id ) && ( current_user_can( DWS_IC_CAP_SEE_METABOX ) || current_user_can( DWS_IC_CAP_SEE_COLUMN ) ); } @@ -272,7 +271,6 @@ public function create_comment( \WP_REST_Request $request ): \WP_REST_Response|\ */ public function get_comment( \WP_REST_Request $request ): \WP_REST_Response { $comment = dws_ic_get_comment( absint( $request['id'] ) ); - return new \WP_REST_Response( array( 'id' => absint( $request['id'] ), diff --git a/plugins/internal-comments/src/Settings.php b/plugins/internal-comments/src/Settings.php index 8a48e05..6b4b352 100644 --- a/plugins/internal-comments/src/Settings.php +++ b/plugins/internal-comments/src/Settings.php @@ -116,7 +116,6 @@ public function get_options_page( string $page_id ): array { */ public function add_settings_link( array $links ): array { \array_unshift( $links, \sprintf( '%s', esc_url( admin_url( 'options-general.php?page=' . self::PAGE ) ), esc_html__( 'Settings', 'internal-comments' ) ) ); - return $links; } diff --git a/plugins/internal-comments/tests/Integration/AdminScreensTest.php b/plugins/internal-comments/tests/Integration/AdminScreensTest.php index a8fa2f3..7d89a43 100644 --- a/plugins/internal-comments/tests/Integration/AdminScreensTest.php +++ b/plugins/internal-comments/tests/Integration/AdminScreensTest.php @@ -72,12 +72,10 @@ public function test_the_posts_list_column_follows_the_comments_column_and_passe $seen = array(); $v1 = static function ( string $column, ?string $post_type ) use ( &$seen ): string { $seen[] = array( 'v1', $column, $post_type ); - return $column; }; $v2 = static function ( string $column, ?string $post_type ) use ( &$seen ): string { $seen[] = array( 'v2', $column, $post_type ); - return 'title'; }; add_filter( self::V1_PRECEDING_COLUMN, $v1, 10, 2 ); @@ -262,7 +260,6 @@ public function test_the_comment_edit_screen_hides_the_fields_an_internal_commen $GLOBALS['comment'] = get_comment( $comment ); try { $screens->enqueue_comment_screen_style( 'comment.php' ); - return \implode( '', (array) wp_styles()->get_data( 'internal-comments-comment-screen', 'after' ) ); } finally { unset( $GLOBALS['comment'] ); @@ -291,7 +288,6 @@ public function test_the_comments_screen_counts_only_the_trashed_comments_the_us wp_get_current_user()->get_role_caps(); // An earlier row can leave the editor current with capabilities its role has since lost. $editor = (int) wp_count_comments()->trash; wp_set_current_user( self::$users['administrator'] ); - return array( $editor, (int) wp_count_comments()->trash ); }; $woocommerce = has_filter( 'wp_count_comments', array( 'WC_Comments', 'wp_count_comments' ) ); @@ -388,7 +384,6 @@ public function test_the_posts_list_loads_the_quick_views_assets(): void { protected function cell(): \Dom\HTMLDocument { \ob_start(); ( new AdminScreens() )->render_cell( 'dws-internal-comments', $this->post ); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/internal-comments/tests/Integration/CommentsTest.php b/plugins/internal-comments/tests/Integration/CommentsTest.php index eee0b43..e95dc20 100644 --- a/plugins/internal-comments/tests/Integration/CommentsTest.php +++ b/plugins/internal-comments/tests/Integration/CommentsTest.php @@ -68,13 +68,11 @@ public function test_the_creation_data_passes_v1s_filter_then_the_v2_filter(): v $seen = array(); $v1 = static function ( array $data ) use ( &$seen ): array { $seen[] = 'v1'; - return array( 'comment_content' => 'v1' ) + $data; }; $v2 = static function ( array $data ) use ( &$seen ): array { $seen[] = 'v2:' . $data['comment_content']; $data['comment_content'] = 'v2'; - return $data; }; add_filter( 'internal_comments/create_comment_data', $v1 ); diff --git a/plugins/internal-comments/tests/Integration/DataGuardsTest.php b/plugins/internal-comments/tests/Integration/DataGuardsTest.php index 57f14d5..35e861a 100644 --- a/plugins/internal-comments/tests/Integration/DataGuardsTest.php +++ b/plugins/internal-comments/tests/Integration/DataGuardsTest.php @@ -65,7 +65,6 @@ public function test_a_status_change_leaves_an_internal_comment_internal_and_not $mailed = array(); $record = static function ( mixed $result, array $mail ) use ( &$mailed ): bool { $mailed[] = $mail['subject']; - return true; }; $notify = get_option( 'comments_notify' ); diff --git a/plugins/internal-comments/tests/Integration/MetaBoxTest.php b/plugins/internal-comments/tests/Integration/MetaBoxTest.php index 6f97234..ca9ea97 100644 --- a/plugins/internal-comments/tests/Integration/MetaBoxTest.php +++ b/plugins/internal-comments/tests/Integration/MetaBoxTest.php @@ -180,7 +180,6 @@ protected function registers( string $role, int $post ): bool { wp_set_current_user( self::$users[ $role ] ); $type = (string) get_post_type( $post ); do_action( 'add_meta_boxes', $type, get_post( $post ) ); - return isset( $wp_meta_boxes[ $type ]['normal']['core']['dws-internal-comments'] ); } @@ -191,7 +190,6 @@ protected function render( string $role ): \Dom\HTMLDocument { $box = $wp_meta_boxes['post']['normal']['core']['dws-internal-comments']; \ob_start(); \call_user_func( $box['callback'], get_post( $this->post ), $box ); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/internal-comments/tests/Integration/MetadataTest.php b/plugins/internal-comments/tests/Integration/MetadataTest.php index 657eb10..e97bba9 100644 --- a/plugins/internal-comments/tests/Integration/MetadataTest.php +++ b/plugins/internal-comments/tests/Integration/MetadataTest.php @@ -51,7 +51,6 @@ protected static function get_listed_header( string $header ): string { protected static function translate_name(): \Closure { $name = self::get_listed_header( 'Name' ); - return static fn ( string $translation, string $text ): string => $name === $text ? self::TRANSLATION : $translation; } } diff --git a/plugins/internal-comments/tests/Integration/MigrationsTest.php b/plugins/internal-comments/tests/Integration/MigrationsTest.php index 20097dc..2f2a6af 100644 --- a/plugins/internal-comments/tests/Integration/MigrationsTest.php +++ b/plugins/internal-comments/tests/Integration/MigrationsTest.php @@ -120,7 +120,6 @@ public function test_the_second_step_converts_v1s_internal_comments_in_batches_a 'comment_date_gmt' => \current_time( 'mysql', true ), ) ); - return (int) $wpdb->insert_id; }; diff --git a/plugins/internal-comments/tests/Integration/OrdersTest.php b/plugins/internal-comments/tests/Integration/OrdersTest.php index f68f54f..19f05eb 100644 --- a/plugins/internal-comments/tests/Integration/OrdersTest.php +++ b/plugins/internal-comments/tests/Integration/OrdersTest.php @@ -120,7 +120,6 @@ protected function post_comment( string $role, int $order_id, string $content ): wp_set_current_user( self::$users[ $role ] ); $request = new \WP_REST_Request( 'POST', "/dws_ic/v1/posts/$order_id/comments" ); $request->set_param( 'content', $content ); - return rest_do_request( $request )->get_status(); } } diff --git a/plugins/internal-comments/tests/Integration/PermissionsTest.php b/plugins/internal-comments/tests/Integration/PermissionsTest.php index 5189f96..d9de004 100644 --- a/plugins/internal-comments/tests/Integration/PermissionsTest.php +++ b/plugins/internal-comments/tests/Integration/PermissionsTest.php @@ -168,7 +168,6 @@ protected function post( string $role ): int { ) ); $this->posts[] = $post; - return $post; } diff --git a/plugins/internal-comments/tests/Integration/SettingsApiTest.php b/plugins/internal-comments/tests/Integration/SettingsApiTest.php index bcf0154..4f63a73 100644 --- a/plugins/internal-comments/tests/Integration/SettingsApiTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsApiTest.php @@ -111,18 +111,15 @@ public function test_a_validated_value_passes_the_v1_filters_of_its_path_then_th $filters = array( 'internal_comments/general_settings/get_validated_option_value' => static function ( mixed $value, string $field_id ) use ( &$seen ): mixed { $seen[] = array( 'group', $value, $field_id ); - return 'group'; }, 'internal_comments/settings/get_validated_option_value' => static function ( mixed $value, string $field_id ) use ( &$seen ): mixed { $seen[] = array( 'section', $value, $field_id ); - return 'section'; }, ); $v2 = static function ( mixed $value, string $field_id, string $group ) use ( &$seen ): mixed { $seen[] = array( 'v2', $value, $field_id, $group ); - return 'v2'; }; foreach ( $filters as $hook => $filter ) { diff --git a/plugins/internal-comments/tests/Integration/SettingsTest.php b/plugins/internal-comments/tests/Integration/SettingsTest.php index fdff2c9..bf30e74 100644 --- a/plugins/internal-comments/tests/Integration/SettingsTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsTest.php @@ -138,7 +138,6 @@ public function test_a_groups_fields_pass_its_v1_filter_first_then_its_v2_filter $v2_hook = "dws_ic/settings/{$group}_fields"; $v1_extra = static function ( array $fields ) use ( &$seen ): array { $seen[] = array( 'v1', \array_keys( $fields ) ); - return $fields + array( 'extra' => array( 'type' => 'text', @@ -148,7 +147,6 @@ public function test_a_groups_fields_pass_its_v1_filter_first_then_its_v2_filter }; $v2_spy = static function ( array $fields ) use ( &$seen ): array { $seen[] = array( 'v2', \array_column( $fields, 'id' ) ); - return $fields; }; add_filter( $v1_hook, $v1_extra ); @@ -184,7 +182,6 @@ protected function render( string $tab ): \Dom\HTMLDocument { $_GET['tab'] = $tab; \ob_start(); self::$page->render_page(); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/internal-comments/tests/Support/RunsRequestsTrait.php b/plugins/internal-comments/tests/Support/RunsRequestsTrait.php index d776cf0..17a68af 100644 --- a/plugins/internal-comments/tests/Support/RunsRequestsTrait.php +++ b/plugins/internal-comments/tests/Support/RunsRequestsTrait.php @@ -15,7 +15,6 @@ protected static function request_provoking( string $diagnostic, string $php, st $provoked = '' === $diagnostic ? null : \array_find_key( $output, static fn ( string $line ): bool => \str_contains( $line, $diagnostic ) ); self::assertSame( array(), \array_slice( $output, 0, \is_int( $provoked ) ? $provoked : \max( 0, \count( $output ) - 1 ) ), \implode( "\n", $output ) ); - return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/internal-comments/tests/Unit/ReadmeTest.php b/plugins/internal-comments/tests/Unit/ReadmeTest.php index ceecefe..682ef08 100644 --- a/plugins/internal-comments/tests/Unit/ReadmeTest.php +++ b/plugins/internal-comments/tests/Unit/ReadmeTest.php @@ -30,7 +30,6 @@ public function test_the_readme_holds_the_changelog_markers_the_sync_fills(): vo protected static function read_fields( string $file ): array { \preg_match_all( '/^[\s*]*([A-Za-z ]+):\s*(\S.*?)\s*$/m', (string) \file_get_contents( \dirname( __DIR__, 2 ) . "/$file" ), $matches ); - return \array_combine( $matches[1], $matches[2] ); } } diff --git a/plugins/linked-orders-for-woocommerce/includes/deprecated.php b/plugins/linked-orders-for-woocommerce/includes/deprecated.php index 3a6d144..9675397 100644 --- a/plugins/linked-orders-for-woocommerce/includes/deprecated.php +++ b/plugins/linked-orders-for-woocommerce/includes/deprecated.php @@ -96,7 +96,6 @@ */ function dws_lowc_name(): string { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lowc_get_plugin_name()' ); - return dws_lowc_get_plugin_name(); } @@ -111,7 +110,6 @@ function dws_lowc_name(): string { */ function dws_lowc_version(): string { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lowc_get_plugin_version()' ); - return dws_lowc_get_plugin_version(); } @@ -126,7 +124,6 @@ function dws_lowc_version(): string { */ function dws_lowc_path(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_LOWC_PATH; } @@ -141,7 +138,6 @@ function dws_lowc_path(): string { */ function dws_lowc_min_php(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_LOWC_MIN_PHP; } @@ -156,7 +152,6 @@ function dws_lowc_min_php(): string { */ function dws_lowc_min_wp(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_LOWC_MIN_WP; } @@ -174,7 +169,6 @@ function dws_lowc_min_wp(): string { */ function dws_lowc_get_hook_tag( string $name, $extra = array() ): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return dws_lowc_join_v1_hook_tag( 'linked_orders_for_woocommerce', $name, $extra ); } @@ -193,7 +187,6 @@ function dws_lowc_get_hook_tag( string $name, $extra = array() ): string { */ function dws_lowc_get_component_hook_tag( string $component_id, string $name, $extra = array() ): ?string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return in_array( $component_id, DWS_LOWC_V1_COMPONENTS, true ) ? dws_lowc_join_v1_hook_tag( 'linked_orders_for_woocommerce/' . str_replace( '-', '_', $component_id ), $name, $extra ) : null; } diff --git a/plugins/linked-orders-for-woocommerce/includes/linking.php b/plugins/linked-orders-for-woocommerce/includes/linking.php index 2c84860..c8376a1 100644 --- a/plugins/linked-orders-for-woocommerce/includes/linking.php +++ b/plugins/linked-orders-for-woocommerce/includes/linking.php @@ -44,7 +44,6 @@ function dws_lowc_get_order_node( $order ): ?DWS_Order_Node { */ function dws_lowc_is_root_order( $order ): ?bool { $node = dws_lowc_get_order_node( $order ); - return null === $node ? null : 0 === $node->get_depth(); } diff --git a/plugins/linked-orders-for-woocommerce/includes/metadata.php b/plugins/linked-orders-for-woocommerce/includes/metadata.php index 690485e..ea31903 100644 --- a/plugins/linked-orders-for-woocommerce/includes/metadata.php +++ b/plugins/linked-orders-for-woocommerce/includes/metadata.php @@ -36,7 +36,6 @@ function dws_lowc_get_plugin_slug(): string { */ function dws_lowc_get_plugin_name(): string { $name = get_file_data( DWS_LOWC_FILE, array( 'Name' => 'Plugin Name' ) )['Name']; - return 0 < did_action( 'init' ) ? __( $name, 'linked-orders-for-woocommerce' ) : $name; // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- The header's name reaches the POT through make-pot's header extraction. } diff --git a/plugins/linked-orders-for-woocommerce/includes/options.php b/plugins/linked-orders-for-woocommerce/includes/options.php index 9b5737e..c57abf7 100644 --- a/plugins/linked-orders-for-woocommerce/includes/options.php +++ b/plugins/linked-orders-for-woocommerce/includes/options.php @@ -66,6 +66,5 @@ */ function dws_lowc_validate_max_depth( $submitted ) { $depth = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false; - return false === $depth ? new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 2.', 'linked-orders-for-woocommerce' ) ) : (string) $depth; } diff --git a/plugins/linked-orders-for-woocommerce/includes/post-type.php b/plugins/linked-orders-for-woocommerce/includes/post-type.php index 1ce9d6c..0222c16 100644 --- a/plugins/linked-orders-for-woocommerce/includes/post-type.php +++ b/plugins/linked-orders-for-woocommerce/includes/post-type.php @@ -55,7 +55,6 @@ function dws_lowc_get_supported_order_types(): array { */ function dws_lowc_get_singular_name( WP_Post_Type $post_type ): string { $name = $post_type->labels->singular_name ?? null; - return is_string( $name ) ? $name : $post_type->label; } @@ -75,7 +74,6 @@ function dws_lowc_is_supported_order( $order ): ?bool { } $order = wc_get_order( $order ); - return $order instanceof WC_Order ? in_array( $order->get_type(), dws_lowc_get_supported_order_types(), true ) : null; } @@ -230,7 +228,6 @@ function dws_lowc_create_linked_child( int $parent_id, array $args = array() ) { $child->save(); if ( true !== dws_lowc_link_orders( $parent_id, $child->get_id() ) ) { $child->delete( true ); - return new WP_Error( 'dws_lowc_link_refused', __( 'another order was being linked at the same time. Please try again.', 'linked-orders-for-woocommerce' ) ); } diff --git a/plugins/linked-orders-for-woocommerce/includes/settings.php b/plugins/linked-orders-for-woocommerce/includes/settings.php index 149293b..a3dc9e4 100644 --- a/plugins/linked-orders-for-woocommerce/includes/settings.php +++ b/plugins/linked-orders-for-woocommerce/includes/settings.php @@ -33,7 +33,6 @@ */ function dws_lowc_get_raw_setting( string $field_id, ?string $group = null ) { $field = dws_lowc_find_setting( $field_id, $group ); - return null === $field ? null : get_option( $field[2], null ); } diff --git a/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php b/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php index 46d7a0d..a4ef5b9 100644 --- a/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php +++ b/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php @@ -147,7 +147,6 @@ public function __get( string $name ): mixed { */ public function __call( string $name, array $arguments ): mixed { $method = array( $this->order, $name ); - return is_callable( $method ) ? $method( ...$arguments ) : null; } @@ -189,7 +188,6 @@ public function get_post_type(): WP_Post_Type { */ public function get_depth(): int { $this->maybe_read(); - return $this->depth; } @@ -233,7 +231,6 @@ public function has_parent(): bool { public function get_children(): array { $this->maybe_read(); $this->children ??= array_values( array_filter( array_map( static fn ( ?int $id ): ?DWS_Order_Node => dws_lowc_get_order_node( $id ), $this->child_ids ) ) ); - return $this->children; } @@ -323,7 +320,6 @@ public function add_child( DWS_Order_Node $child ): bool { $child->set_parent( $this ); $this->children = array( ...$this->get_children(), $child ); - return true; } diff --git a/plugins/linked-orders-for-woocommerce/src/ListTable.php b/plugins/linked-orders-for-woocommerce/src/ListTable.php index 17d21e3..1c48077 100644 --- a/plugins/linked-orders-for-woocommerce/src/ListTable.php +++ b/plugins/linked-orders-for-woocommerce/src/ListTable.php @@ -96,7 +96,6 @@ public function insert_depth_column( array $columns ): array { $preceding = apply_filters( 'dws_lowc/list_table/preceding_column', $preceding, $type, $columns ); $position = \array_search( $preceding, \array_keys( $columns ), true ); $position = false === $position ? \count( $columns ) : $position + 1; - return \array_slice( $columns, 0, $position, true ) + array( 'dws_lo_depth' => _x( 'Depth', 'list table heading', 'linked-orders-for-woocommerce' ) ) + \array_slice( $columns, $position, null, true ); } @@ -382,7 +381,6 @@ protected static function get_deepest_level( string $type ): int { ) ); $stored = \is_array( $deepest ) && array() !== $deepest ? dws_lowc_get_order_node( \reset( $deepest ) )?->get_depth() : null; - return \max( dws_lowc_get_max_depth(), $stored ?? 0 ); } @@ -396,7 +394,6 @@ protected static function get_deepest_level( string $type ): int { */ protected static function get_depth_filter(): ?int { $depth = \filter_var( self::get_query_arg( '_dws_lo_depth' ), \FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 0 ) ) ); - return false === $depth ? null : $depth; } @@ -418,7 +415,6 @@ protected static function get_depth_clauses(): array { 'key' => '_dws_lo_depth', 'value' => (string) $depth, ); - return array( 0 === $depth ? array( 'relation' => 'OR', diff --git a/plugins/linked-orders-for-woocommerce/src/Plugin.php b/plugins/linked-orders-for-woocommerce/src/Plugin.php index 72bb34b..a3e8941 100644 --- a/plugins/linked-orders-for-woocommerce/src/Plugin.php +++ b/plugins/linked-orders-for-woocommerce/src/Plugin.php @@ -196,7 +196,6 @@ protected function compose(): bool { $this->kernel = new PluginKernel( new ContainerBuilder()->addDefinitions( $this->get_definitions( $this->logger, $this->scheduler ) )->build() ); $this->kernel->boot( self::COMPONENTS ); - return true; } diff --git a/plugins/linked-orders-for-woocommerce/src/Settings.php b/plugins/linked-orders-for-woocommerce/src/Settings.php index f14647d..cdb8258 100644 --- a/plugins/linked-orders-for-woocommerce/src/Settings.php +++ b/plugins/linked-orders-for-woocommerce/src/Settings.php @@ -145,7 +145,6 @@ public function register_section( SettingsSectionRegistry $registry ): void { */ public function add_settings_link( array $links ): array { \array_unshift( $links, \sprintf( '%s', esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=' . self::SECTION ) ), esc_html__( 'Settings', 'linked-orders-for-woocommerce' ) ) ); - return $links; } diff --git a/plugins/linked-orders-for-woocommerce/tests/Fixtures/sequential-order-numbers-pro.php b/plugins/linked-orders-for-woocommerce/tests/Fixtures/sequential-order-numbers-pro.php index 96af337..a4e3f4b 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Fixtures/sequential-order-numbers-pro.php +++ b/plugins/linked-orders-for-woocommerce/tests/Fixtures/sequential-order-numbers-pro.php @@ -15,6 +15,5 @@ function wc_seq_order_number_pro(): WC_Seq_Order_Number_Pro { static $plugin = null; $plugin ??= new WC_Seq_Order_Number_Pro(); - return $plugin; } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php index 33c4601..876a848 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php @@ -49,7 +49,6 @@ public function test_the_completed_order_passes_the_filters_before_its_descendan $seen = array(); $record = static function ( bool $should, int $order_id ) use ( &$seen ): bool { $seen[] = $order_id; - return $should; }; add_filter( 'dws_lowc/autocompletion/should_autocomplete', $record, 10, 2 ); @@ -112,7 +111,6 @@ protected function create_tree(): array { $grandchild = $this->create_order(); self::assertTrue( dws_lowc_link_orders( $root->get_id(), $child->get_id() ) ); self::assertTrue( dws_lowc_link_orders( $child->get_id(), $grandchild->get_id() ) ); - return array( wc_get_order( $root->get_id() ), wc_get_order( $child->get_id() ), wc_get_order( $grandchild->get_id() ) ); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php index 6605f90..575d335 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/DeprecatedHooksTest.php @@ -51,7 +51,6 @@ public function test_a_renamed_hook_fires_its_v1_literal_then_its_v2_name( strin $fired = array(); $spy = static function ( mixed $value = null ) use ( &$fired ): mixed { $fired[] = current_filter(); - return $value; }; add_filter( $v1, $spy ); @@ -137,7 +136,6 @@ protected function open_list(): string { require_once ABSPATH . 'wp-admin/includes/screen.php'; $screen = wc_get_page_screen_id( 'shop-order' ); set_current_screen( $screen ); - return $screen; } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php index 72b88a9..09720b0 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php @@ -287,7 +287,6 @@ protected function open_list(): string { $screen = OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id( 'shop-order' ) : 'edit-shop_order'; set_current_screen( $screen ); $GLOBALS['typenow'] = 'shop_order'; - return $screen; } @@ -328,7 +327,6 @@ protected function query( ?array $found = null, array $meta_query = array() ): a 'order' => 'ASC', ) ); - return \array_map( 'intval', wc_get_orders( $args ) ); } @@ -343,7 +341,6 @@ protected function query( ?array $found = null, array $meta_query = array() ): a 'order' => 'ASC', ) ); - return \array_map( 'intval', ( new \WP_Query( $vars ) )->posts ); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php index 76e725e..84ae866 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php @@ -130,7 +130,6 @@ protected function register( \WC_Order|\WP_Post $post_or_order, ?string $screen self::assertInstanceOf( \WP_Post::class, $post_or_order ); } do_action( 'add_meta_boxes', $screen, $post_or_order ); - return $wp_meta_boxes[ $screen ]['side']['default']['dws-linked-orders'] ?? null; } @@ -142,7 +141,6 @@ protected function render( int $order_id ): \Dom\HTMLDocument { \ob_start(); \call_user_func( $box['callback'], $order, $box ); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MetadataTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MetadataTest.php index 5580493..0f66a61 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MetadataTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MetadataTest.php @@ -51,7 +51,6 @@ protected static function get_listed_header( string $header ): string { protected static function translate_name(): \Closure { $name = self::get_listed_header( 'Name' ); - return static fn ( string $translation, string $text ): string => $name === $text ? self::TRANSLATION : $translation; } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php index 4037dfe..3663bf2 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsApiTest.php @@ -105,18 +105,15 @@ public function test_a_validated_value_passes_the_v1_filters_of_its_path_then_th $filters = array( 'linked_orders_for_woocommerce/general_settings/get_validated_option_value' => static function ( mixed $value, string $field_id ) use ( &$seen ): mixed { $seen[] = array( 'group', $value, $field_id ); - return 4; }, 'linked_orders_for_woocommerce/settings/get_validated_option_value' => static function ( mixed $value, string $field_id ) use ( &$seen ): mixed { $seen[] = array( 'section', $value, $field_id ); - return 5; }, ); $v2 = static function ( mixed $value, string $field_id, string $group ) use ( &$seen ): mixed { $seen[] = array( 'v2', $value, $field_id, $group ); - return 6; }; foreach ( $filters as $hook => $filter ) { diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php index df51043..062bba6 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php @@ -117,7 +117,6 @@ public function test_a_groups_fields_pass_its_v1_filter_first_then_its_v2_filter $v2_hook = "dws_lowc/settings/{$group}_fields"; $v1_extra = static function ( array $fields ) use ( &$seen ): array { $seen[] = array( 'v1', \array_keys( $fields ) ); - return $fields + array( 'extra' => array( 'type' => 'text', @@ -127,7 +126,6 @@ public function test_a_groups_fields_pass_its_v1_filter_first_then_its_v2_filter }; $v2_spy = static function ( array $fields ) use ( &$seen ): array { $seen[] = array( 'v2', \array_column( $fields, 'id' ) ); - return $fields; }; add_filter( $v1_hook, $v1_extra ); @@ -161,7 +159,6 @@ public function test_the_plugin_row_links_to_the_section_and_to_the_support_foru protected function render(): \Dom\HTMLDocument { \ob_start(); \WC_Admin_Settings::output_fields( self::$advanced->get_settings_for_section( self::SECTION ) ); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php b/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php index f1a9d68..09d3ec9 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php +++ b/plugins/linked-orders-for-woocommerce/tests/Support/RunsRequestsTrait.php @@ -15,7 +15,6 @@ protected static function request_provoking( string $diagnostic, string $php, st $provoked = '' === $diagnostic ? null : \array_find_key( $output, static fn ( string $line ): bool => \str_contains( $line, $diagnostic ) ); self::assertSame( array(), \array_slice( $output, 0, \is_int( $provoked ) ? $provoked : \max( 0, \count( $output ) - 1 ) ), \implode( "\n", $output ) ); - return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php b/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php index 42f80d1..f014d5c 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Unit/ReadmeTest.php @@ -30,7 +30,6 @@ public function test_the_readme_holds_the_changelog_markers_the_sync_fills(): vo protected static function read_fields( string $file ): array { \preg_match_all( '/^[\s*]*([A-Za-z ]+):\s*(\S.*?)\s*$/m', (string) \file_get_contents( \dirname( __DIR__, 2 ) . "/$file" ), $matches ); - return \array_combine( $matches[1], $matches[2] ); } } diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php index e0604d2..0d653bb 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php @@ -115,7 +115,6 @@ */ function dws_lpmwc_name(): string { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_get_plugin_name()' ); - return dws_lpmwc_get_plugin_name(); } @@ -130,7 +129,6 @@ function dws_lpmwc_name(): string { */ function dws_lpmwc_version(): string { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_get_plugin_version()' ); - return dws_lpmwc_get_plugin_version(); } @@ -145,7 +143,6 @@ function dws_lpmwc_version(): string { */ function dws_lpmwc_path(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_LPMWC_PATH; } @@ -160,7 +157,6 @@ function dws_lpmwc_path(): string { */ function dws_lpmwc_min_php(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_LPMWC_MIN_PHP; } @@ -175,7 +171,6 @@ function dws_lpmwc_min_php(): string { */ function dws_lpmwc_min_wp(): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return DWS_LPMWC_MIN_WP; } @@ -193,7 +188,6 @@ function dws_lpmwc_min_wp(): string { */ function dws_lpmwc_get_hook_tag( string $name, $extra = array() ): string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return dws_lpmwc_join_v1_hook_tag( 'locked_payment_methods_for_woocommerce', $name, $extra ); } @@ -212,7 +206,6 @@ function dws_lpmwc_get_hook_tag( string $name, $extra = array() ): string { */ function dws_lpmwc_get_component_hook_tag( string $component_id, string $name, $extra = array() ): ?string { _deprecated_function( __FUNCTION__, '2.0.0' ); - return isset( DWS_LPMWC_V1_COMPONENTS[ $component_id ] ) ? dws_lpmwc_join_v1_hook_tag( 'locked_payment_methods_for_woocommerce/' . DWS_LPMWC_V1_COMPONENTS[ $component_id ], $name, $extra ) : null; } @@ -301,7 +294,6 @@ function dws_lpmwc_check_payment_method_access_for_user( string $locked_method_i * @param int $user_id The user's id. */ $lock = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_user', array( true, $locked_method_id, $user_id ), '2.0.0', 'dws_lpmwc/payment_method/should_lock' ); - return dws_lpmwc_should_lock( false !== $lock, $locked_method_id, $user_id, 0 ); } @@ -333,7 +325,6 @@ function dws_lpmwc_check_payment_method_access_for_wc_order( string $locked_meth * @param int $order_id The order's id. */ $lock = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_wc_order', array( true, $locked_method_id, $order_id ), '2.0.0', 'dws_lpmwc/payment_method/should_lock' ); - return dws_lpmwc_should_lock( false !== $lock, $locked_method_id, $order instanceof WC_Order ? $order->get_customer_id() : 0, $order_id ); } @@ -351,7 +342,6 @@ function dws_lpmwc_check_payment_method_access_for_wc_order( string $locked_meth */ function dws_lpmwc_check_payment_method_access_via_user_meta( string $locked_method_id, int $user_id ): bool { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' ); - return ! ( UserProfileStrategy::should_load() && UserProfileStrategy::unlocks( $locked_method_id, $user_id ) ); } @@ -369,7 +359,6 @@ function dws_lpmwc_check_payment_method_access_via_user_meta( string $locked_met */ function dws_lpmwc_check_payment_method_access_via_user_roles( string $locked_method_id, int $user_id ): bool { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' ); - return ! ( UserRoleStrategy::should_load() && UserRoleStrategy::unlocks( $locked_method_id, $user_id ) ); } @@ -387,7 +376,6 @@ function dws_lpmwc_check_payment_method_access_via_user_roles( string $locked_me */ function dws_lpmwc_check_payment_method_access_via_groups( string $locked_method_id, int $user_id ): bool { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' ); - return ! ( GroupsStrategy::should_load() && GroupsStrategy::unlocks( $locked_method_id, $user_id ) ); } @@ -405,7 +393,6 @@ function dws_lpmwc_check_payment_method_access_via_groups( string $locked_method */ function dws_lpmwc_check_payment_method_access_via_wc_memberships( string $locked_method_id, int $user_id ): bool { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' ); - return ! ( WooCommerceMembershipsStrategy::should_load() && WooCommerceMembershipsStrategy::unlocks( $locked_method_id, $user_id ) ); } @@ -423,7 +410,6 @@ function dws_lpmwc_check_payment_method_access_via_wc_memberships( string $locke */ function dws_lpmwc_check_payment_method_access_via_wc_order_meta( string $locked_method_id, int $order_id ): bool { _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' ); - return ! ( OrderStrategy::should_load() && OrderStrategy::unlocks( $locked_method_id, $order_id ) ); } @@ -442,7 +428,6 @@ function dws_lpmwc_check_payment_method_access_via_wc_order_meta( string $locked function dws_lpmwc_get_raw_setting( string $field_id, ?string $group = null ) { _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' ); $setting = dws_lpmwc_find_v1_setting( $field_id, $group ); - return null === $setting ? null : get_option( $setting['key'], null ); } @@ -460,7 +445,6 @@ function dws_lpmwc_get_raw_setting( string $field_id, ?string $group = null ) { */ function dws_lpmwc_get_validated_setting( string $field_id, ?string $group = null ) { _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' ); - return dws_lpmwc_read_v1_setting( $field_id, $group ); } @@ -480,7 +464,6 @@ function dws_lpmwc_get_validated_setting( string $field_id, ?string $group = nul function dws_lpmwc_validate_setting( $value, string $field_id, ?string $group = null ) { _deprecated_function( __FUNCTION__, '2.0.0' ); $setting = dws_lpmwc_find_v1_setting( $field_id, $group ); - return null === $setting ? null : dws_lpmwc_validate_v1_setting( $value, $setting['key'] ); } @@ -497,7 +480,6 @@ function dws_lpmwc_validate_setting( $value, string $field_id, ?string $group = */ function dws_lpmwc_get_validated_general_setting( string $field_id ) { _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' ); - return dws_lpmwc_read_v1_setting( $field_id, 'general' ); } @@ -522,7 +504,6 @@ function dws_lpmwc_find_v1_setting( string $field_id, ?string $group ): ?array { } $key = DWS_LPMWC_V1_SETTINGS[ $group ][ $field_id ] ?? null; - return null === $key ? null : array( 'group' => $group, 'field' => $field_id, diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php index 93eebb0..8f6b31c 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php @@ -32,7 +32,6 @@ */ function dws_lpmwc_get_user_grants_key(): string { global $wpdb; - return $wpdb->get_blog_prefix() . DWS_LPMWC_USER_OPTION_UNLOCKED_METHODS; } @@ -48,7 +47,6 @@ function dws_lpmwc_get_user_grants_key(): string { */ function dws_lpmwc_get_user_unlocked_payment_method_ids( int $user_id ): array { $ids = get_user_meta( $user_id, dws_lpmwc_get_user_grants_key(), true ); - return is_array( $ids ) ? array_values( array_filter( $ids, 'is_string' ) ) : array(); } @@ -75,7 +73,6 @@ function dws_lpmwc_get_user_unlocked_payment_method_ids( int $user_id ): array { function dws_lpmwc_get_order_unlocked_payment_method_ids( int $order_id ): array { $order = 0 === $order_id ? false : wc_get_order( $order_id ); $ids = $order instanceof WC_Order ? $order->get_meta( DWS_LPMWC_META_UNLOCKED_METHODS ) : array(); - return is_array( $ids ) ? array_values( array_filter( $ids, 'is_string' ) ) : array(); } @@ -91,7 +88,6 @@ function dws_lpmwc_get_order_unlocked_payment_method_ids( int $order_id ): array */ function dws_lpmwc_get_payment_method_title( string $gateway_id ): string { $gateway = WC()->payment_gateways()->payment_gateways()[ $gateway_id ] ?? null; - return $gateway instanceof WC_Payment_Gateway ? $gateway->get_title() : $gateway_id; } @@ -166,7 +162,6 @@ function dws_lpmwc_get_groups_unlocked_payment_method_ids(): array { */ function dws_lpmwc_get_plan_unlocked_payment_method_ids( int $plan_id ): array { $ids = get_post_meta( $plan_id, DWS_LPMWC_META_UNLOCKED_METHODS, true ); - return is_array( $ids ) ? array_values( array_filter( $ids, 'is_string' ) ) : array(); } diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php b/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php index 421f217..36fff2b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php @@ -36,7 +36,6 @@ function dws_lpmwc_get_plugin_slug(): string { */ function dws_lpmwc_get_plugin_name(): string { $name = get_file_data( DWS_LPMWC_FILE, array( 'Name' => 'Plugin Name' ) )['Name']; - return 0 < did_action( 'init' ) ? __( $name, 'locked-payment-methods-for-woocommerce' ) : $name; // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- The header's name reaches the POT through make-pot's header extraction. } diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/options.php b/plugins/locked-payment-methods-for-woocommerce/includes/options.php index aa269f8..2b4ade1 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/options.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/options.php @@ -146,7 +146,6 @@ */ function dws_lpmwc_get_bool_option( string $key ): bool { $default = dws_lpmwc_get_option_default( $key ); - return filter_var( get_option( $key, $default ), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ?? ( true === $default ); } @@ -162,7 +161,6 @@ function dws_lpmwc_get_bool_option( string $key ): bool { */ function dws_lpmwc_get_int_option( string $key ): int { $default = dws_lpmwc_get_option_default( $key ); - return filter_var( get_option( $key, $default ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? ( is_int( $default ) ? $default : 0 ); } @@ -201,7 +199,6 @@ function dws_lpmwc_get_list_option( string $key ): array { $default = dws_lpmwc_get_option_default( $key ); $stored = get_option( $key, $default ); $stored = is_array( $stored ) ? $stored : $default; - return is_array( $stored ) ? array_values( array_filter( $stored, 'is_string' ) ) : array(); } diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php index 90ec834..da23dc0 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php @@ -159,7 +159,6 @@ function dws_lpmwc_get_paid_order_id(): int { $order = wc_get_order( $wp->query_vars['order-pay'] ?? 0 ); $key = isset( $_GET['key'] ) && is_string( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The order key authorizes paying the order, as on WooCommerce's order-pay page. - return $order instanceof WC_Order && '' !== $key && hash_equals( $order->get_order_key(), $key ) ? $order->get_id() : 0; } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php b/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php index dd05dc3..94fb533 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php @@ -201,7 +201,6 @@ protected function compose(): bool { $this->kernel = new PluginKernel( new ContainerBuilder()->addDefinitions( $this->get_definitions( $this->logger, $this->scheduler ) )->build() ); $this->kernel->boot( self::COMPONENTS ); - return true; } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/Settings.php b/plugins/locked-payment-methods-for-woocommerce/src/Settings.php index 4d2833b..7dec0ef 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/Settings.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/Settings.php @@ -136,7 +136,6 @@ public function register_section( SettingsSectionRegistry $registry ): void { */ public function add_settings_link( array $links ): array { \array_unshift( $links, \sprintf( '%s', esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=' . self::SECTION ) ), esc_html__( 'Settings', 'locked-payment-methods-for-woocommerce' ) ) ); - return $links; } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php index 69b5ee5..c70a61b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php @@ -105,7 +105,6 @@ public function register_hooks(): void { */ public static function is_available(): bool { $version = \defined( 'GROUPS_CORE_VERSION' ) ? \constant( 'GROUPS_CORE_VERSION' ) : null; - return \is_string( $version ) && \class_exists( \Groups_User::class ) && \version_compare( $version, '2.11.0', '>=' ); } @@ -175,7 +174,6 @@ public function render_group_field( string $output, int $group_id = 0 ): string } $value = dws_lpmwc_get_groups_unlocked_payment_method_ids()[ $group_id ] ?? array(); - return $output . '

' . esc_html( $field['title'] ) . '

' . Forms::render_control( $field, $value, self::ENTRY . '[' . self::FIELD . ']', self::FIELD ) . '
'; } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php index e258854..aea2a23 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php @@ -97,7 +97,6 @@ public function register_hooks(): void { #[\Override] public function get_object_form( string $form_id, int $object_id ): array { $options = dws_lpmwc_get_grant_choices( dws_lpmwc_get_user_unlocked_payment_method_ids( $object_id ), $object_id ); - return array( 'title' => __( 'Locked Payment Methods', 'locked-payment-methods-for-woocommerce' ), 'capability' => DWS_LPMWC_CAP_UNLOCK_VIA_USER_PROFILES, @@ -167,7 +166,6 @@ public function unlock_granted_methods( mixed $lock, string $gateway_id, int $us public function add_users_column( array $columns ): array { $position = \array_search( 'role', \array_keys( $columns ), true ); $position = false === $position ? \count( $columns ) : $position + 1; - return \array_slice( $columns, 0, $position, true ) + array( self::COLUMN => __( 'Unlocked for', 'locked-payment-methods-for-woocommerce' ) ) + \array_slice( $columns, $position, null, true ); } @@ -253,7 +251,6 @@ public function filter_users( array $args ): array { 'compare' => 'LIKE', ); $args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- The filter matches a user meta value, which only a meta query reaches. - return $args; } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php index ae71926..b10e11c 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php @@ -62,7 +62,6 @@ public static function unlocks( string $gateway_id, int $user_id ): bool { } $roles = dws_lpmwc_apply_v1_setting_filter( dws_lpmwc_get_list_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), 'general', 'full-access-user-roles', 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ); - return \is_array( $roles ) && array() !== \array_intersect( $user->roles, \array_filter( $roles, 'is_string' ) ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php index 72cdf5d..3d8607f 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php @@ -83,7 +83,6 @@ public function register_hooks(): void { #[\Override] public function get_object_form( string $form_id, int $object_id ): array { $options = dws_lpmwc_get_grant_choices( dws_lpmwc_get_plan_unlocked_payment_method_ids( $object_id ), 0 ); - return array( 'title' => __( 'Unlocked Payment Methods', 'locked-payment-methods-for-woocommerce' ), 'capability' => DWS_LPMWC_CAP_UNLOCK_VIA_WC_MEMBERSHIPS, @@ -113,7 +112,6 @@ public function get_object_form( string $form_id, int $object_id ): array { */ public static function is_available(): bool { $version = get_option( 'wc_memberships_version', '0.0.0' ); - return \function_exists( 'wc_memberships_get_user_active_memberships' ) && \is_string( $version ) && \version_compare( $version, '1.18.0', '>=' ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php index 0b41685..2f38617 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php @@ -163,7 +163,6 @@ public function test_v1s_access_checks_answer_true_for_locked_from_the_strategie $seen = array(); $spy = static function ( mixed ...$args ) use ( &$seen ): mixed { $seen[] = $args; - return $args[0]; }; add_filter( $v1, $spy, 10, 3 ); @@ -187,7 +186,6 @@ public function test_v1s_order_access_check_judges_the_order_and_its_customer_af $seen = array(); $spy = static function ( mixed ...$args ) use ( &$seen ): mixed { $seen[] = $args; - return $args[0]; }; add_filter( $v1, $spy, 10, 3 ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MetadataTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MetadataTest.php index d542c6e..6d2953b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MetadataTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/MetadataTest.php @@ -51,7 +51,6 @@ protected static function get_listed_header( string $header ): string { protected static function translate_name(): \Closure { $name = self::get_listed_header( 'Name' ); - return static fn ( string $translation, string $text ): string => $name === $text ? self::TRANSLATION : $translation; } } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php index 1b155e6..5fd2e26 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php @@ -210,7 +210,6 @@ public function test_the_locked_ids_filter_follows_v1s_value_filter_and_v1s_list foreach ( $hooks as $hook => $id ) { $adders[ $hook ] = static function ( array $ids, mixed $context = null ) use ( &$seen, $hook, $id ): array { $seen[] = array( $hook, $ids, \is_array( $context ) ? \array_keys( $context ) : $context ); - return array( ...$ids, $id ); }; add_filter( $hook, $adders[ $hook ], 10, 2 ); @@ -243,12 +242,10 @@ public function test_v1s_lock_filters_fire_as_deprecated_before_should_lock_unde $v1_safe = "$v1/woocommerce_payments"; $record = static function ( mixed ...$args ) use ( &$seen ): mixed { $seen[] = $args; - return $args[0]; }; $returns_null = static function ( mixed $lock ) use ( &$seen ): mixed { $seen[] = array( $lock ); - return null; }; add_filter( $v1, $record, 10, 2 ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php index 4d7c9b8..ee2aa7f 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php @@ -130,7 +130,6 @@ public function test_a_groups_fields_pass_its_v1_filter_under_v1s_ids_and_keep_v $v1_hook = "locked_payment_methods_for_woocommerce/{$group}_settings/get_group_fields"; $v1_extra = static function ( array $fields ) use ( &$seen ): array { $seen[] = \array_keys( $fields ); - return $fields + array( 'extra' => array( 'type' => 'text', @@ -177,7 +176,6 @@ protected static function selected( \Dom\HTMLDocument $form, string $key ): arra protected function render(): \Dom\HTMLDocument { \ob_start(); \WC_Admin_Settings::output_fields( self::$advanced->get_settings_for_section( self::SECTION ) ); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php index 3aed76e..e8a727e 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php @@ -207,7 +207,6 @@ protected function create_group( string $name, int $parent_id = 0 ): int { ) ); $this->groups[] = $group; - return $group; } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php index 7a228f0..70599e0 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php @@ -248,7 +248,6 @@ public function test_the_users_screen_filter_leaves_other_user_queries_alone(): protected static function create_user( string $role ): int { $login = \uniqid( "lpm-profiles-$role-" ); - return (int) wp_insert_user( array( 'user_login' => $login, @@ -261,7 +260,6 @@ protected static function create_user( string $role ): int { protected static function key(): string { global $wpdb; - return $wpdb->get_blog_prefix() . DWS_LPMWC_USER_OPTION_UNLOCKED_METHODS; } @@ -272,7 +270,6 @@ protected static function should_lock( string $gateway_id, int $user_id ): mixed protected static function render( int $user_id ): \Dom\HTMLDocument { \ob_start(); do_action( 'edit_user_profile', get_userdata( $user_id ) ); - return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php index d48a264..417cef9 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php @@ -99,7 +99,6 @@ public function test_v1s_value_filter_on_the_roles_fires_as_deprecated_and_count $seen = array(); $replace = static function ( mixed $value, string $field_id ) use ( &$seen ): array { $seen[] = array( $value, $field_id ); - return array( 'customer' ); }; add_filter( self::V1_HOOK, $replace, 10, 2 ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php index 19bbf1e..86cd4a5 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php @@ -15,7 +15,6 @@ protected static function request_provoking( string $diagnostic, string $php, st $provoked = '' === $diagnostic ? null : \array_find_key( $output, static fn ( string $line ): bool => \str_contains( $line, $diagnostic ) ); self::assertSame( array(), \array_slice( $output, 0, \is_int( $provoked ) ? $provoked : \max( 0, \count( $output ) - 1 ) ), \implode( "\n", $output ) ); - return array( 'status' => $status, 'output' => \implode( "\n", $output ), diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php index a273f46..1ac0674 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php @@ -30,7 +30,6 @@ public function test_the_readme_holds_the_changelog_markers_the_sync_fills(): vo protected static function read_fields( string $file ): array { \preg_match_all( '/^[\s*]*([A-Za-z ]+):\s*(\S.*?)\s*$/m', (string) \file_get_contents( \dirname( __DIR__, 2 ) . "/$file" ), $matches ); - return \array_combine( $matches[1], $matches[2] ); } } From 0e090ce7d8f0db6f979fac327780bcad02b550ce Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:52:13 +0100 Subject: [PATCH 31/39] Call methods on new objects without parentheses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Style guide §5 calls a method on a new object in PHP 8.4's bare form outside the PHP 7.4 files, and 28 lines in the plugins, one in Locked Payment Methods' Groups strategy and the rest in tests, still wrapped the new expression in parentheses (X255). Each drops the parentheses; the entries, functions.php, uninstall.php and includes/, which parse on PHP 7.4, keep none to drop. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/Integration/AdminScreensTest.php | 16 ++++++++-------- .../tests/Integration/MetaBoxTest.php | 2 +- .../tests/Integration/OrdersTest.php | 2 +- .../tests/Integration/SettingsTest.php | 4 ++-- .../tests/Integration/XmlRpcTest.php | 2 +- .../tests/Unit/FootprintTest.php | 2 +- .../SequentialOrderNumbersProTest.php | 2 +- .../tests/Integration/ListTableTest.php | 2 +- .../tests/Integration/SettingsTest.php | 6 +++--- .../tests/Unit/FootprintTest.php | 2 +- .../src/UnlockStrategies/GroupsStrategy.php | 2 +- .../tests/Integration/SettingsTest.php | 4 ++-- .../UnlockStrategies/GroupsStrategyTest.php | 2 +- .../UnlockStrategies/UserProfileStrategyTest.php | 2 +- .../UnlockStrategies/UserRoleStrategyTest.php | 4 ++-- .../tests/Unit/FootprintTest.php | 2 +- 16 files changed, 28 insertions(+), 28 deletions(-) diff --git a/plugins/internal-comments/tests/Integration/AdminScreensTest.php b/plugins/internal-comments/tests/Integration/AdminScreensTest.php index 7d89a43..f91b348 100644 --- a/plugins/internal-comments/tests/Integration/AdminScreensTest.php +++ b/plugins/internal-comments/tests/Integration/AdminScreensTest.php @@ -82,7 +82,7 @@ public function test_the_posts_list_column_follows_the_comments_column_and_passe add_filter( 'dws_ic/list_table/preceding_column', $v2, 10, 2 ); try { - $columns = ( new AdminScreens() )->add_column( + $columns = new AdminScreens()->add_column( array( 'cb' => '', 'title' => 'Title', @@ -100,8 +100,8 @@ public function test_the_posts_list_column_follows_the_comments_column_and_passe self::assertSame( array( array( 'v1', 'comments', 'post' ), array( 'v2', 'comments', 'post' ) ), $seen ); self::assertSame( array( array( self::V1_PRECEDING_COLUMN, 'dws_ic/list_table/preceding_column' ) ), $this->deprecated ); wp_set_current_user( self::$users['editor'] ); - self::assertArrayHasKey( 'dws-internal-comments', ( new AdminScreens() )->add_column( array( 'title' => 'Title' ), 'post' ) ); - self::assertSame( array( 'title' => 'Title' ), ( new AdminScreens() )->add_column( array( 'title' => 'Title' ), 'page' ) ); + self::assertArrayHasKey( 'dws-internal-comments', new AdminScreens()->add_column( array( 'title' => 'Title' ), 'post' ) ); + self::assertSame( array( 'title' => 'Title' ), new AdminScreens()->add_column( array( 'title' => 'Title' ), 'page' ) ); } // WooCommerce rebuilds the columns of its orders and coupons lists on that hook at the default priority. @@ -180,14 +180,14 @@ public function test_the_comments_screen_leaves_out_the_rows_its_reader_cannot_s wp_set_current_user( self::$users['editor'] ); try { - $args = ( new AdminScreens() )->filter_list_table_args( + $args = new AdminScreens()->filter_list_table_args( array( 'post_id' => $this->post, 'type' => DWS_IC_COMMENT_TYPE, 'status' => 'all', ) ); - $rows = ( new \WP_Comment_Query( $args ) )->comments; + $rows = new \WP_Comment_Query( $args )->comments; $all = get_comments( array( 'post_id' => $this->post, 'type' => DWS_IC_COMMENT_TYPE, 'status' => DWS_IC_COMMENT_STATUS ) ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound -- A query's three arguments. } finally { get_role( 'editor' )?->remove_cap( DWS_IC_CAP_SEE_TABLE ); @@ -202,7 +202,7 @@ public function test_the_comments_screen_offers_only_the_actions_an_internal_com $actions = array_fill_keys( array( 'approve', 'unapprove', 'reply', 'quickedit', 'edit', 'spam', 'trash', 'untrash', 'delete' ), '' ); self::assertInstanceOf( \WP_Comment::class, $comment ); - self::assertSame( array( 'edit', 'trash', 'untrash', 'delete' ), \array_keys( ( new AdminScreens() )->filter_row_actions( $actions, $comment ) ) ); + self::assertSame( array( 'edit', 'trash', 'untrash', 'delete' ), \array_keys( new AdminScreens()->filter_row_actions( $actions, $comment ) ) ); } public function test_the_edit_screen_of_an_internal_comment_shows_its_revisions_and_its_author_its_privacy(): void { @@ -373,7 +373,7 @@ public function test_the_comments_screen_counts_trashed_comments_once_while_v1s_ } public function test_the_posts_list_loads_the_quick_views_assets(): void { - ( new AdminScreens() )->enqueue_list_assets( 'edit.php' ); + new AdminScreens()->enqueue_list_assets( 'edit.php' ); self::assertTrue( wp_script_is( 'internal-comments-quick-view' ) ); self::assertTrue( wp_style_is( 'internal-comments-quick-view' ) ); @@ -383,7 +383,7 @@ public function test_the_posts_list_loads_the_quick_views_assets(): void { protected function cell(): \Dom\HTMLDocument { \ob_start(); - ( new AdminScreens() )->render_cell( 'dws-internal-comments', $this->post ); + new AdminScreens()->render_cell( 'dws-internal-comments', $this->post ); return \Dom\HTMLDocument::createFromString( '' . \ob_get_clean() . '', \LIBXML_NOERROR ); } diff --git a/plugins/internal-comments/tests/Integration/MetaBoxTest.php b/plugins/internal-comments/tests/Integration/MetaBoxTest.php index ca9ea97..5d24678 100644 --- a/plugins/internal-comments/tests/Integration/MetaBoxTest.php +++ b/plugins/internal-comments/tests/Integration/MetaBoxTest.php @@ -165,7 +165,7 @@ public function test_the_edit_screen_of_a_selected_post_loads_the_meta_boxs_asse wp_set_current_user( self::$users['administrator'] ); try { - ( new MetaBox() )->enqueue_assets( 'post.php' ); + new MetaBox()->enqueue_assets( 'post.php' ); } finally { unset( $GLOBALS['post'] ); } diff --git a/plugins/internal-comments/tests/Integration/OrdersTest.php b/plugins/internal-comments/tests/Integration/OrdersTest.php index 19f05eb..b496ff6 100644 --- a/plugins/internal-comments/tests/Integration/OrdersTest.php +++ b/plugins/internal-comments/tests/Integration/OrdersTest.php @@ -87,7 +87,7 @@ public function test_the_order_screen_shows_the_meta_box( bool $hpos, bool $sync dws_ic_create_internal_comment( array( 'comment_post_ID' => $order->get_id(), 'comment_content' => 'On the order' ), get_user_by( 'id', self::$users['administrator'] ) ?: null ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound -- A comment's two fields. wp_set_current_user( self::$users['shop_manager'] ); - ( new MetaBox() )->add_meta_box( $screen, $object ); + new MetaBox()->add_meta_box( $screen, $object ); $box = $wp_meta_boxes[ $screen ]['normal']['core'][ MetaBox::ID ] ?? null; self::assertIsArray( $box ); \ob_start(); diff --git a/plugins/internal-comments/tests/Integration/SettingsTest.php b/plugins/internal-comments/tests/Integration/SettingsTest.php index bf30e74..2d7050b 100644 --- a/plugins/internal-comments/tests/Integration/SettingsTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsTest.php @@ -55,7 +55,7 @@ protected function forget_the_tab(): void { } public function test_the_page_keeps_v1s_slug_parent_capability_and_tabs(): void { - $page = ( new Settings() )->get_options_page( Settings::PAGE ); + $page = new Settings()->get_options_page( Settings::PAGE ); self::assertSame( 'dws-internal-comments', Settings::PAGE ); self::assertSame( 'options-general.php', $page['parent'] ); @@ -153,7 +153,7 @@ public function test_a_groups_fields_pass_its_v1_filter_first_then_its_v2_filter add_filter( $v2_hook, $v2_spy ); try { - $tabs = ( new Settings() )->get_options_page( Settings::PAGE )['tabs']; + $tabs = new Settings()->get_options_page( Settings::PAGE )['tabs']; } finally { remove_filter( $v1_hook, $v1_extra ); remove_filter( $v2_hook, $v2_spy ); diff --git a/plugins/internal-comments/tests/Integration/XmlRpcTest.php b/plugins/internal-comments/tests/Integration/XmlRpcTest.php index 805b965..0310296 100644 --- a/plugins/internal-comments/tests/Integration/XmlRpcTest.php +++ b/plugins/internal-comments/tests/Integration/XmlRpcTest.php @@ -48,7 +48,7 @@ public function test_xml_rpc_lists_no_internal_comments(): void { add_filter( 'xmlrpc_enabled', '__return_true' ); try { - $comments = ( new \wp_xmlrpc_server() )->wp_getComments( + $comments = new \wp_xmlrpc_server()->wp_getComments( array( 1, 'ic-xmlrpc-editor', diff --git a/plugins/internal-comments/tests/Unit/FootprintTest.php b/plugins/internal-comments/tests/Unit/FootprintTest.php index ea5af91..b1fd601 100644 --- a/plugins/internal-comments/tests/Unit/FootprintTest.php +++ b/plugins/internal-comments/tests/Unit/FootprintTest.php @@ -17,7 +17,7 @@ public function test_the_prefix_has_one_segment_after_dws_so_no_prefix_in_its_fa } public function test_every_kind_is_one_the_uninstall_engine_removes(): void { - $doc = (string) ( new \ReflectionClass( Uninstaller::class ) )->getDocComment(); + $doc = (string) new \ReflectionClass( Uninstaller::class )->getDocComment(); self::assertSame( 1, \preg_match( '/@phpstan-type Footprint (.+)$/m', $doc, $type ) ); \preg_match_all( '/(\w+)\?:/', $type[1], $kinds ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php index 02d5e85..f4aaa57 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php @@ -21,7 +21,7 @@ public function test_the_integration_loads_only_beside_the_plugin(): void { public function test_a_linked_child_is_numbered_before_it_is_saved(): void { require_once \dirname( __DIR__, 2 ) . '/Fixtures/sequential-order-numbers-pro.php'; self::assertTrue( SequentialOrderNumbersPro::should_load() ); - ( new SequentialOrderNumbersPro() )->register_hooks(); + new SequentialOrderNumbersPro()->register_hooks(); wp_set_current_user( 1 ); $id = dws_lowc_create_linked_child( $this->create_order()->get_id() ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php index 09720b0..e131b4f 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php @@ -341,6 +341,6 @@ protected function query( ?array $found = null, array $meta_query = array() ): a 'order' => 'ASC', ) ); - return \array_map( 'intval', ( new \WP_Query( $vars ) )->posts ); + return \array_map( 'intval', new \WP_Query( $vars )->posts ); } } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php index 062bba6..5ba3af5 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php @@ -51,7 +51,7 @@ public static function setting_groups(): array { #[After] protected function forget_the_rejections(): void { - ( new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' ) )->setValue( null, array() ); + new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' )->setValue( null, array() ); } public function test_the_section_sits_on_the_advanced_tab_under_its_v1_id(): void { @@ -100,7 +100,7 @@ public function test_a_depth_that_is_not_a_whole_number_of_at_least_one_keeps_th $this->save( array( DWS_LOWC_OPTION_MAX_DEPTH => $posted ) ); self::assertSame( '4', get_option( DWS_LOWC_OPTION_MAX_DEPTH ) ); - self::assertSame( array( 'Maximum linked orders depth was not saved and keeps its previous value. The other changes were saved. Enter a whole number of 1 or more, such as 2.' ), ( new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' ) )->getValue() ); + self::assertSame( array( 'Maximum linked orders depth was not saved and keeps its previous value. The other changes were saved. Enter a whole number of 1 or more, such as 2.' ), new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' )->getValue() ); } public function test_a_whole_depth_of_at_least_one_is_stored(): void { @@ -132,7 +132,7 @@ public function test_a_groups_fields_pass_its_v1_filter_first_then_its_v2_filter add_filter( $v2_hook, $v2_spy ); try { - $fields = ( new Settings() )->get_section_fields( 'advanced', self::SECTION ); + $fields = new Settings()->get_section_fields( 'advanced', self::SECTION ); } finally { remove_filter( $v1_hook, $v1_extra ); remove_filter( $v2_hook, $v2_spy ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Unit/FootprintTest.php b/plugins/linked-orders-for-woocommerce/tests/Unit/FootprintTest.php index 79a7bc7..8971684 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Unit/FootprintTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Unit/FootprintTest.php @@ -17,7 +17,7 @@ public function test_the_prefix_has_one_segment_after_dws_so_no_prefix_in_its_fa } public function test_every_kind_is_one_the_uninstall_engine_removes(): void { - $doc = (string) ( new \ReflectionClass( Uninstaller::class ) )->getDocComment(); + $doc = (string) new \ReflectionClass( Uninstaller::class )->getDocComment(); self::assertSame( 1, \preg_match( '/@phpstan-type Footprint (.+)$/m', $doc, $type ) ); \preg_match_all( '/(\w+)\?:/', $type[1], $kinds ); diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php index c70a61b..20ec9f4 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php @@ -127,7 +127,7 @@ public static function unlocks( string $gateway_id, int $user_id ): bool { } $groups = dws_lpmwc_get_groups_unlocked_payment_method_ids(); - foreach ( (array) ( new \Groups_User( $user_id ) )->group_ids_deep as $group_id ) { // @phpstan-ignore property.notFound (Groups_User resolves its group ids through __get, as at v1's oldest Groups.) + foreach ( (array) new \Groups_User( $user_id )->group_ids_deep as $group_id ) { // @phpstan-ignore property.notFound (Groups_User resolves its group ids through __get, as at v1's oldest Groups.) if ( \is_numeric( $group_id ) && \in_array( $gateway_id, $groups[ (int) $group_id ] ?? array(), true ) ) { return true; } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php index ee2aa7f..869ff0b 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php @@ -45,7 +45,7 @@ public static function setting_groups(): array { #[After] protected function forget_the_rejections(): void { - ( new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' ) )->setValue( null, array() ); + new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' )->setValue( null, array() ); } public function test_the_section_sits_on_the_advanced_tab_under_its_v1_id(): void { @@ -140,7 +140,7 @@ public function test_a_groups_fields_pass_its_v1_filter_under_v1s_ids_and_keep_v add_filter( $v1_hook, $v1_extra ); try { - $ids = \array_column( ( new Settings() )->get_section_fields( 'advanced', self::SECTION ), 'id' ); + $ids = \array_column( new Settings()->get_section_fields( 'advanced', self::SECTION ), 'id' ); } finally { remove_filter( $v1_hook, $v1_extra ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php index e8a727e..756f1dc 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php @@ -80,7 +80,7 @@ public function test_the_strategy_loads_only_while_groups_is_active_and_its_swit } public function test_the_settings_show_the_groups_switch_while_groups_is_active(): void { - $fields = ( new \DeepWebSolutions\LockedPaymentMethods\Settings() )->get_section_fields( 'advanced', 'dws-locked-payment-methods' ); + $fields = new \DeepWebSolutions\LockedPaymentMethods\Settings()->get_section_fields( 'advanced', 'dws-locked-payment-methods' ); $switch = \array_values( \array_filter( $fields, static fn ( array $field ): bool => DWS_LPMWC_OPTION_UNLOCK_VIA_GROUPS === ( $field['id'] ?? null ) ) ); self::assertSame( 'checkbox', $switch[0]['type'] ?? null ); diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php index 70599e0..2a3d864 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php @@ -294,7 +294,7 @@ protected static function listed_users(): array { } protected static function queried_users( ?array $args = null ): array { - return \array_map( 'intval', ( new \WP_User_Query( $args ?? self::query_args() ) )->get_results() ); + return \array_map( 'intval', new \WP_User_Query( $args ?? self::query_args() )->get_results() ); } protected static function query_args(): array { diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php index 417cef9..13fc6d9 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php @@ -82,7 +82,7 @@ public function test_only_a_user_with_a_full_access_role_is_unlocked(): void { public function test_an_empty_roles_list_unlocks_no_one(): void { update_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, array() ); - self::assertTrue( ( new UserRoleStrategy() )->unlock_for_full_access_roles( true, 'cod', self::$users['shop_manager'] ) ); + self::assertTrue( new UserRoleStrategy()->unlock_for_full_access_roles( true, 'cod', self::$users['shop_manager'] ) ); } public function test_at_checkout_the_lock_opens_for_a_full_access_user_only(): void { @@ -104,7 +104,7 @@ public function test_v1s_value_filter_on_the_roles_fires_as_deprecated_and_count add_filter( self::V1_HOOK, $replace, 10, 2 ); try { - $lock = ( new UserRoleStrategy() )->unlock_for_full_access_roles( true, 'cod', self::$users['customer'] ); + $lock = new UserRoleStrategy()->unlock_for_full_access_roles( true, 'cod', self::$users['customer'] ); } finally { remove_filter( self::V1_HOOK, $replace ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php index e9e4d0a..fdb780c 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php @@ -17,7 +17,7 @@ public function test_the_prefix_has_one_segment_after_dws_so_no_prefix_in_its_fa } public function test_every_kind_is_one_the_uninstall_engine_removes(): void { - $doc = (string) ( new \ReflectionClass( Uninstaller::class ) )->getDocComment(); + $doc = (string) new \ReflectionClass( Uninstaller::class )->getDocComment(); self::assertSame( 1, \preg_match( '/@phpstan-type Footprint (.+)$/m', $doc, $type ) ); \preg_match_all( '/(\w+)\?:/', $type[1], $kinds ); From cda17339bc5200e3502f16a5d2d45eaf4de2886e Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:54:23 +0100 Subject: [PATCH 32/39] Compare null through is_null() in each plugin Seventy comparisons in the plugins' code and tests tested null with === or !==, where the framework's sweep and the template's tests compare it through is_null() (X146's A270 and A332). Each becomes is_null(), qualified in a namespaced file and bare in a global one, as the style guide qualifies PHP's own functions. Internal Comments walks a comment's ancestors in a while loop, since PHPCS flags a function call in a for loop's test. Assisted-by: Claude Code:claude-opus-5-5 --- .../internal-comments/includes/comments.php | 11 ++++++---- plugins/internal-comments/includes/markup.php | 2 +- .../internal-comments/includes/settings.php | 8 ++++---- .../models/DWS_Internal_Comment.php | 2 +- .../internal-comments/src/AdminScreens.php | 4 ++-- plugins/internal-comments/src/Comments.php | 10 +++++----- plugins/internal-comments/src/Permissions.php | 4 ++-- plugins/internal-comments/src/RestRoutes.php | 14 ++++++------- .../includes/linking.php | 8 ++++---- .../includes/post-type.php | 2 +- .../includes/settings.php | 10 +++++----- .../models/DWS_Order_Node.php | 4 ++-- .../src/Autocompletion.php | 2 +- .../src/ChildCreation.php | 2 +- .../src/ListTable.php | 20 +++++++++---------- .../src/MetaBox.php | 6 +++--- .../tests/Integration/LinkingTest.php | 2 +- .../tests/Integration/ListTableTest.php | 4 ++-- .../includes/deprecated.php | 16 +++++++-------- .../includes/migrations.php | 4 ++-- .../tests/Integration/SafeguardsTest.php | 4 ++-- 21 files changed, 71 insertions(+), 68 deletions(-) diff --git a/plugins/internal-comments/includes/comments.php b/plugins/internal-comments/includes/comments.php index d1ed667..77b7ba9 100644 --- a/plugins/internal-comments/includes/comments.php +++ b/plugins/internal-comments/includes/comments.php @@ -74,7 +74,7 @@ function dws_ic_get_comment( $comment ): ?WP_Comment { */ function dws_ic_is_internal_comment( $comment ): ?bool { $comment = dws_ic_get_comment( $comment ); - return null === $comment ? null : DWS_IC_COMMENT_TYPE === $comment->comment_type; + return is_null( $comment ) ? null : DWS_IC_COMMENT_TYPE === $comment->comment_type; } /** @@ -242,7 +242,7 @@ function dws_ic_get_internal_comments_from_trash_meta( int $post_id, string $sta */ function dws_ic_is_private_comment( $comment ): ?bool { $owners = dws_ic_get_private_comment_owners( $comment ); - return null === $owners ? null : array() !== $owners; + return is_null( $owners ) ? null : array() !== $owners; } /** @@ -257,15 +257,18 @@ function dws_ic_is_private_comment( $comment ): ?bool { */ function dws_ic_get_private_comment_owners( $comment ): ?array { $comment = dws_ic_get_comment( $comment ); - if ( null === $comment || DWS_IC_COMMENT_TYPE !== $comment->comment_type ) { + if ( is_null( $comment ) || DWS_IC_COMMENT_TYPE !== $comment->comment_type ) { return null; } $owners = array(); - for ( $node = $comment; null !== $node; $node = dws_ic_get_comment( $node->comment_parent ) ) { + $node = $comment; + while ( ! is_null( $node ) ) { if ( true === filter_var( get_comment_meta( (int) $node->comment_ID, DWS_IC_META_PRIVATE, true ), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ) { $owners[] = (int) $node->user_id; } + + $node = dws_ic_get_comment( $node->comment_parent ); } return $owners; diff --git a/plugins/internal-comments/includes/markup.php b/plugins/internal-comments/includes/markup.php index 35733f7..e512902 100644 --- a/plugins/internal-comments/includes/markup.php +++ b/plugins/internal-comments/includes/markup.php @@ -46,7 +46,7 @@ function dws_ic_print_comments_list( int $post_id ): void { } $post_type = get_post_type_object( (string) dws_ic_get_post_type( $post_id ) ); - $singular = null === $post_type ? null : $post_type->labels->singular_name; + $singular = is_null( $post_type ) ? null : $post_type->labels->singular_name; ?>

comment_type ) { + if ( is_null( $comment ) || DWS_IC_COMMENT_TYPE !== $comment->comment_type ) { throw new InvalidArgumentException( 'The comment is not an internal comment.' ); } diff --git a/plugins/internal-comments/src/AdminScreens.php b/plugins/internal-comments/src/AdminScreens.php index 2d84914..8d2f573 100644 --- a/plugins/internal-comments/src/AdminScreens.php +++ b/plugins/internal-comments/src/AdminScreens.php @@ -247,7 +247,7 @@ public function render_cell( string $column, int $post_id ): void { */ public function enqueue_list_assets( string $hook_suffix ): void { $screen = \function_exists( 'get_current_screen' ) ? get_current_screen() : null; - $post_type = null === $screen || '' === $screen->post_type ? 'post' : $screen->post_type; + $post_type = \is_null( $screen ) || '' === $screen->post_type ? 'post' : $screen->post_type; if ( 1 === \preg_match( '/^woocommerce_page_wc-orders(?:--(.+))?$/', $hook_suffix, $matches ) && ! isset( $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tells the list from the edit screen. $post_type = $matches[1] ?? 'shop_order'; } elseif ( 'edit.php' !== $hook_suffix ) { @@ -543,7 +543,7 @@ public function render_revisions( \WP_Comment $comment ): void { */ public function add_privacy_field( string $html, \WP_Comment $comment ): string { $is_private = dws_ic_is_private_comment( $comment ); - if ( null === $is_private || ( ! $is_private && ! dws_ic_are_enabled_private_comments() ) ) { + if ( \is_null( $is_private ) || ( ! $is_private && ! dws_ic_are_enabled_private_comments() ) ) { return $html; } diff --git a/plugins/internal-comments/src/Comments.php b/plugins/internal-comments/src/Comments.php index ed42ebe..ccab769 100644 --- a/plugins/internal-comments/src/Comments.php +++ b/plugins/internal-comments/src/Comments.php @@ -70,12 +70,12 @@ public function keep_internal( int|string $comment_id ): void { */ public function keep_inserted_internal( int|string $comment_id ): void { $replaced = $this->put_back( $comment_id ); - if ( null === $replaced ) { + if ( \is_null( $replaced ) ) { return; } $comment = dws_ic_get_comment( $comment_id ); - if ( null !== $comment ) { + if ( ! \is_null( $comment ) ) { wp_transition_comment_status( DWS_IC_COMMENT_STATUS, $replaced, $comment ); } } @@ -98,7 +98,7 @@ public function announce_kept_internal( int|string $new_status, int|string $old_ } $stored = dws_ic_get_comment( (int) $comment->comment_ID ); - if ( null !== $stored && DWS_IC_COMMENT_STATUS === $stored->comment_approved ) { + if ( ! \is_null( $stored ) && DWS_IC_COMMENT_STATUS === $stored->comment_approved ) { wp_transition_comment_status( DWS_IC_COMMENT_STATUS, (string) $new_status, $stored ); } } @@ -275,7 +275,7 @@ public function guard_privacy( mixed $check, int $comment_id, string $meta_key, } $comment = dws_ic_get_comment( $comment_id ); - return null !== $comment && get_current_user_id() !== (int) $comment->user_id ? false : $check; + return ! \is_null( $comment ) && get_current_user_id() !== (int) $comment->user_id ? false : $check; } // endregion @@ -298,7 +298,7 @@ protected function put_back( int|string $comment_id ): ?string { global $wpdb; $comment = dws_ic_get_comment( $comment_id ); - if ( null === $comment || DWS_IC_COMMENT_TYPE !== $comment->comment_type || \in_array( $comment->comment_approved, array( DWS_IC_COMMENT_STATUS, 'trash', 'post-trashed' ), true ) ) { + if ( \is_null( $comment ) || DWS_IC_COMMENT_TYPE !== $comment->comment_type || \in_array( $comment->comment_approved, array( DWS_IC_COMMENT_STATUS, 'trash', 'post-trashed' ), true ) ) { return null; } diff --git a/plugins/internal-comments/src/Permissions.php b/plugins/internal-comments/src/Permissions.php index dfbf092..fcc2904 100644 --- a/plugins/internal-comments/src/Permissions.php +++ b/plugins/internal-comments/src/Permissions.php @@ -62,7 +62,7 @@ public function map_meta_cap( array $caps, string $cap, int $user_id, array $arg $comment = dws_ic_get_comment( $args[0] ?? null ); $owners = dws_ic_get_private_comment_owners( $comment ); - if ( null === $comment || null === $owners ) { + if ( \is_null( $comment ) || \is_null( $owners ) ) { return array( 'do_not_allow' ); } @@ -102,7 +102,7 @@ public function guard_core_rest_comment( mixed $response, mixed $handler, \WP_RE default => null, } : null; $comment_id = \is_numeric( $request['id'] ) ? (int) $request['id'] : 0; - if ( \is_wp_error( $response ) || null === $capability || true !== dws_ic_is_internal_comment( $comment_id ) || current_user_can( $capability, $comment_id ) ) { + if ( \is_wp_error( $response ) || \is_null( $capability ) || true !== dws_ic_is_internal_comment( $comment_id ) || current_user_can( $capability, $comment_id ) ) { return $response; } diff --git a/plugins/internal-comments/src/RestRoutes.php b/plugins/internal-comments/src/RestRoutes.php index e386292..a18180a 100644 --- a/plugins/internal-comments/src/RestRoutes.php +++ b/plugins/internal-comments/src/RestRoutes.php @@ -213,7 +213,7 @@ public function create_comment( \WP_REST_Request $request ): \WP_REST_Response|\ } $parent = 0 === $parent_id ? null : dws_ic_get_comment( $parent_id ); - if ( 0 !== $parent_id && ( null === $parent || DWS_IC_COMMENT_TYPE !== $parent->comment_type || (int) $parent->comment_post_ID !== $post_id || ! current_user_can( 'see_dws_internal_comment', $parent_id ) ) ) { + if ( 0 !== $parent_id && ( \is_null( $parent ) || DWS_IC_COMMENT_TYPE !== $parent->comment_type || (int) $parent->comment_post_ID !== $post_id || ! current_user_can( 'see_dws_internal_comment', $parent_id ) ) ) { return new \WP_Error( 'dws_ic_invalid_parent', __( 'The comment you are replying to is not one of this item\'s internal comments.', 'internal-comments' ), array( 'status' => 400 ) ); } @@ -252,7 +252,7 @@ public function create_comment( \WP_REST_Request $request ): \WP_REST_Response|\ 'comment_meta' => array( DWS_IC_META_PRIVATE => $private ? 'yes' : 'no' ), ) ); - if ( null === $comment_id ) { + if ( \is_null( $comment_id ) ) { return new \WP_Error( 'dws_ic_not_stored', __( 'The internal comment could not be stored. Please try again.', 'internal-comments' ), array( 'status' => 500 ) ); } @@ -274,7 +274,7 @@ public function get_comment( \WP_REST_Request $request ): \WP_REST_Response { return new \WP_REST_Response( array( 'id' => absint( $request['id'] ), - 'content' => null === $comment ? '' : $comment->comment_content, + 'content' => \is_null( $comment ) ? '' : $comment->comment_content, ) ); } @@ -297,7 +297,7 @@ public function update_comment( \WP_REST_Request $request ): \WP_REST_Response|\ } $stored = dws_ic_get_comment( $comment_id ); - $comment = null === $stored ? array() : $stored->to_array(); + $comment = \is_null( $stored ) ? array() : $stored->to_array(); $data = array( 'comment_ID' => $comment_id, 'comment_content' => $content, @@ -355,7 +355,7 @@ public function update_comment( \WP_REST_Request $request ): \WP_REST_Response|\ return new \WP_Error( 'dws_ic_not_updated', __( 'The internal comment could not be updated. Please try again.', 'internal-comments' ), array( 'status' => 500 ) ); } - return $this->respond( null === $stored ? 0 : (int) $stored->comment_post_ID ); + return $this->respond( \is_null( $stored ) ? 0 : (int) $stored->comment_post_ID ); } /** @@ -371,7 +371,7 @@ public function update_comment( \WP_REST_Request $request ): \WP_REST_Response|\ public function trash_comment( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error { $comment_id = absint( $request['id'] ); $stored = dws_ic_get_comment( $comment_id ); - $comment = null === $stored ? array() : $stored->to_array(); + $comment = \is_null( $stored ) ? array() : $stored->to_array(); /** * Fires before the meta box trashes an internal comment. @@ -398,7 +398,7 @@ public function trash_comment( \WP_REST_Request $request ): \WP_REST_Response|\W return new \WP_Error( 'dws_ic_not_trashed', __( 'The internal comment could not be moved to the trash. Please try again.', 'internal-comments' ), array( 'status' => 500 ) ); } - return $this->respond( null === $stored ? 0 : (int) $stored->comment_post_ID ); + return $this->respond( \is_null( $stored ) ? 0 : (int) $stored->comment_post_ID ); } // endregion diff --git a/plugins/linked-orders-for-woocommerce/includes/linking.php b/plugins/linked-orders-for-woocommerce/includes/linking.php index c8376a1..f589993 100644 --- a/plugins/linked-orders-for-woocommerce/includes/linking.php +++ b/plugins/linked-orders-for-woocommerce/includes/linking.php @@ -44,7 +44,7 @@ function dws_lowc_get_order_node( $order ): ?DWS_Order_Node { */ function dws_lowc_is_root_order( $order ): ?bool { $node = dws_lowc_get_order_node( $order ); - return null === $node ? null : 0 === $node->get_depth(); + return is_null( $node ) ? null : 0 === $node->get_depth(); } /** @@ -63,7 +63,7 @@ function dws_lowc_is_root_order( $order ): ?bool { function dws_lowc_link_orders( int $parent_id, int $child_id ): ?bool { $parent = dws_lowc_get_order_node( $parent_id ); $child = dws_lowc_get_order_node( $child_id ); - if ( null === $parent || null === $child ) { + if ( is_null( $parent ) || is_null( $child ) ) { return null; } @@ -114,7 +114,7 @@ function dws_lowc_link_orders( int $parent_id, int $child_id ): ?bool { */ function dws_lowc_get_root_order( $order ): ?DWS_Order_Node { $node = dws_lowc_get_order_node( $order ); - while ( null !== $node && $node->has_parent() ) { + while ( ! is_null( $node ) && $node->has_parent() ) { $node = $node->get_parent(); } @@ -133,7 +133,7 @@ function dws_lowc_get_root_order( $order ): ?DWS_Order_Node { */ function dws_lowc_get_orders_tree( $order ): ?array { $node = dws_lowc_get_order_node( $order ); - if ( null === $node ) { + if ( is_null( $node ) ) { return null; } diff --git a/plugins/linked-orders-for-woocommerce/includes/post-type.php b/plugins/linked-orders-for-woocommerce/includes/post-type.php index 0222c16..4f17b87 100644 --- a/plugins/linked-orders-for-woocommerce/includes/post-type.php +++ b/plugins/linked-orders-for-woocommerce/includes/post-type.php @@ -90,7 +90,7 @@ function dws_lowc_is_supported_order( $order ): ?bool { */ function dws_lowc_get_valid_statuses_for_new_child( string $order_type = 'shop_order', ?WC_Order $order = null ): array { $statuses = array_map( array( OrderUtil::class, 'remove_status_prefix' ), array_keys( wc_get_order_statuses() ) ); - $order_type = null === $order ? $order_type : $order->get_type(); + $order_type = is_null( $order ) ? $order_type : $order->get_type(); /** * Filters the statuses an order must have for a child to be created from it. diff --git a/plugins/linked-orders-for-woocommerce/includes/settings.php b/plugins/linked-orders-for-woocommerce/includes/settings.php index a3dc9e4..faf538f 100644 --- a/plugins/linked-orders-for-woocommerce/includes/settings.php +++ b/plugins/linked-orders-for-woocommerce/includes/settings.php @@ -33,7 +33,7 @@ */ function dws_lowc_get_raw_setting( string $field_id, ?string $group = null ) { $field = dws_lowc_find_setting( $field_id, $group ); - return null === $field ? null : get_option( $field[2], null ); + return is_null( $field ) ? null : get_option( $field[2], null ); } /** @@ -49,7 +49,7 @@ function dws_lowc_get_raw_setting( string $field_id, ?string $group = null ) { */ function dws_lowc_get_validated_setting( string $field_id, ?string $group = null ) { $field = dws_lowc_find_setting( $field_id, $group ); - if ( null === $field ) { + if ( is_null( $field ) ) { return null; } @@ -67,7 +67,7 @@ function dws_lowc_get_validated_setting( string $field_id, ?string $group = null * @param string $field_id The field's v1 id. */ $value = apply_filters_deprecated( "linked_orders_for_woocommerce/{$group_id}_settings/get_validated_option_value", array( $value, $id ), '2.0.0', 'dws_lowc/settings/validated_value' ); - if ( null === $group ) { + if ( is_null( $group ) ) { /** * Filters the validated value of a setting read through the section, after its group's filter. * @@ -108,7 +108,7 @@ function dws_lowc_get_validated_setting( string $field_id, ?string $group = null */ function dws_lowc_validate_setting( $value, string $field_id, ?string $group = null ) { $field = dws_lowc_find_setting( $field_id, $group ); - switch ( null === $field ? null : $field[2] ) { + switch ( is_null( $field ) ? null : $field[2] ) { case DWS_LOWC_OPTION_MAX_DEPTH: return max( filter_var( $value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? 1, 1 ); case DWS_LOWC_OPTION_AUTOCOMPLETE: @@ -145,7 +145,7 @@ function dws_lowc_get_max_depth(): int { * @return array{0: string, 1: string, 2: string}|null */ function dws_lowc_find_setting( string $field_id, ?string $group ): ?array { - if ( null === $group ) { + if ( is_null( $group ) ) { if ( false === strpos( $field_id, '/' ) ) { return null; } diff --git a/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php b/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php index a4ef5b9..3c44745 100644 --- a/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php +++ b/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php @@ -112,7 +112,7 @@ final class DWS_Order_Node { public function __construct( mixed $order ) { $order = wc_get_order( $order ); $post_type = $order instanceof WC_Order && true === dws_lowc_is_supported_order( $order ) ? get_post_type_object( $order->get_type() ) : null; - if ( ! $order instanceof WC_Order || null === $post_type ) { + if ( ! $order instanceof WC_Order || is_null( $post_type ) ) { throw new InvalidArgumentException( 'The order is missing or of an unsupported type.' ); } @@ -217,7 +217,7 @@ public function get_parent(): ?DWS_Order_Node { * @return bool */ public function has_parent(): bool { - return null !== $this->get_parent(); + return ! is_null( $this->get_parent() ); } /** diff --git a/plugins/linked-orders-for-woocommerce/src/Autocompletion.php b/plugins/linked-orders-for-woocommerce/src/Autocompletion.php index 3451ba1..961a2fa 100644 --- a/plugins/linked-orders-for-woocommerce/src/Autocompletion.php +++ b/plugins/linked-orders-for-woocommerce/src/Autocompletion.php @@ -68,7 +68,7 @@ public function complete_descendants( int $order_id ): void { * @param int $descendant_id The descendant's id. */ $node = true === apply_filters( 'dws_lowc/autocompletion/should_autocomplete', $should_autocomplete, $descendant_id ) ? dws_lowc_get_order_node( $descendant_id ) : null; - if ( null === $node ) { + if ( \is_null( $node ) ) { continue; } diff --git a/plugins/linked-orders-for-woocommerce/src/ChildCreation.php b/plugins/linked-orders-for-woocommerce/src/ChildCreation.php index 9f30fca..7544f4e 100644 --- a/plugins/linked-orders-for-woocommerce/src/ChildCreation.php +++ b/plugins/linked-orders-for-woocommerce/src/ChildCreation.php @@ -66,7 +66,7 @@ public function create_linked_child(): never { ) ) ?? 0; $node = dws_lowc_get_order_node( $parent_id ); - if ( null === $node ) { + if ( \is_null( $node ) ) { /* translators: %d: The parent order's id. */ $this->notices->add( new Notice( 'linked-child-missing-parent', esc_html( \sprintf( __( 'There is no order %d to create a linked child for.', 'linked-orders-for-woocommerce' ), $parent_id ) ) ) ); wp_safe_redirect( wp_get_referer() ?: admin_url( 'index.php' ) ); diff --git a/plugins/linked-orders-for-woocommerce/src/ListTable.php b/plugins/linked-orders-for-woocommerce/src/ListTable.php index 1c48077..2fc7890 100644 --- a/plugins/linked-orders-for-woocommerce/src/ListTable.php +++ b/plugins/linked-orders-for-woocommerce/src/ListTable.php @@ -49,7 +49,7 @@ public function register_hooks(): void { */ public function add_depth_column( \WP_Screen $screen ): void { $type = $this->can_see() ? self::get_list_type( $screen ) : null; - if ( null === $type ) { + if ( \is_null( $type ) ) { return; } @@ -112,7 +112,7 @@ public function insert_depth_column( array $columns ): array { */ public function render_depth_cell( string $column, mixed $post_or_order ): void { $node = 'dws_lo_depth' === $column ? dws_lowc_get_order_node( $post_or_order ) : null; - if ( null === $node ) { + if ( \is_null( $node ) ) { return; } @@ -157,7 +157,7 @@ public function render_depth_cell( string $column, mixed $post_or_order ): void */ public function render_filters( string $type ): void { $post_type = $this->can_see() && \in_array( $type, dws_lowc_get_supported_order_types(), true ) ? get_post_type_object( $type ) : null; - if ( null === $post_type ) { + if ( \is_null( $post_type ) ) { return; } @@ -208,7 +208,7 @@ public function filter_posts_query( array $query_vars ): array { $query_vars['meta_query'] = self::add_meta_clauses( $query_vars['meta_query'] ?? null, $clauses ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- The filters are meta values. } $tree = self::get_tree_filter( $query_vars['post__in'] ?? null ); - if ( null !== $tree ) { + if ( ! \is_null( $tree ) ) { $query_vars['post__in'] = $tree; } @@ -241,7 +241,7 @@ public function filter_orders_query( array $args ): array { $args['meta_query'] = self::add_meta_clauses( $args['meta_query'] ?? null, $clauses ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- The filters are meta values. } $tree = self::get_tree_filter( $args['id'] ?? null ); - if ( null !== $tree ) { + if ( ! \is_null( $tree ) ) { $args['id'] = $tree; } @@ -261,7 +261,7 @@ public function filter_orders_query( array $args ): array { */ public function add_row_actions( array $actions, \WC_Order $order ): array { $node = $this->can_see() ? dws_lowc_get_order_node( $order ) : null; - if ( null === $node ) { + if ( \is_null( $node ) ) { return $actions; } @@ -308,7 +308,7 @@ public function add_row_actions( array $actions, \WC_Order $order ): array { * @return void */ public function enqueue_assets(): void { - if ( ! $this->can_see() || null === self::get_list_type( get_current_screen() ) ) { + if ( ! $this->can_see() || \is_null( self::get_list_type( get_current_screen() ) ) ) { return; } @@ -349,7 +349,7 @@ protected function can_see(): bool { */ protected static function get_list_type( ?\WP_Screen $screen ): ?string { foreach ( dws_lowc_get_supported_order_types() as $type ) { - if ( null !== $screen && ( OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id( $type ) : "edit-$type" ) === $screen->id ) { + if ( ! \is_null( $screen ) && ( OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id( $type ) : "edit-$type" ) === $screen->id ) { return $type; } } @@ -407,7 +407,7 @@ protected static function get_depth_filter(): ?int { */ protected static function get_depth_clauses(): array { $depth = self::get_depth_filter(); - if ( null === $depth ) { + if ( \is_null( $depth ) ) { return array(); } @@ -459,7 +459,7 @@ protected static function get_tree_filter( mixed $found ): ?array { } $root = dws_lowc_get_root_order( $pivot ); - $tree = null === $root ? array() : dws_lowc_get_orders_tree( $root->get_order() ) ?? array(); + $tree = \is_null( $root ) ? array() : dws_lowc_get_orders_tree( $root->get_order() ) ?? array(); $found = \is_array( $found ) ? \filter_var( $found, \FILTER_VALIDATE_INT, \FILTER_REQUIRE_ARRAY | \FILTER_NULL_ON_FAILURE ) : null; if ( \is_array( $found ) && array() !== $found ) { $tree = \array_intersect( $tree, $found ); diff --git a/plugins/linked-orders-for-woocommerce/src/MetaBox.php b/plugins/linked-orders-for-woocommerce/src/MetaBox.php index 41e88c7..0ec43e0 100644 --- a/plugins/linked-orders-for-woocommerce/src/MetaBox.php +++ b/plugins/linked-orders-for-woocommerce/src/MetaBox.php @@ -43,7 +43,7 @@ public function register_hooks(): void { */ public function add_meta_box( string $screen_id, mixed $post_or_order ): void { $node = current_user_can( DWS_LOWC_CAP_SEE_ORDER_LINKS ) && current_user_can( DWS_LOWC_CAP_SEE_METABOX ) ? dws_lowc_get_order_node( $post_or_order ) : null; - if ( null === $node ) { + if ( \is_null( $node ) ) { return; } @@ -63,14 +63,14 @@ public function add_meta_box( string $screen_id, mixed $post_or_order ): void { */ public function render_meta_box( mixed $post_or_order ): void { $node = dws_lowc_get_order_node( $post_or_order ); - if ( null === $node ) { + if ( \is_null( $node ) ) { return; } $parent = $node->get_parent(); $can_create = $node->can_create_child(); $type_name = dws_lowc_get_singular_name( $node->get_post_type() ); - if ( null !== $parent ) { + if ( ! \is_null( $parent ) ) { ?>

: diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php index f900996..709b183 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/LinkingTest.php @@ -98,7 +98,7 @@ public function test_a_link_waits_for_another_request_to_finish_linking(): void $php = '$lock = new DeepWebSolutions\\LinkedOrders\\Scoped\\DeepWebSolutions\\Framework\\Core\\Storage\\Lock( "dws_lowc_linking", 30 ); $lock->acquire( "another-request" ); sleep( 1 ); $lock->release( "another-request" );'; \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval ' . \escapeshellarg( $php ) . ' > /dev/null 2>&1 &' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- The other request must hold the lock while this one links. - $held = static fn (): bool => null !== $wpdb->get_var( "SELECT option_value FROM {$wpdb->options} WHERE option_name = 'dws_lowc_linking'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Watches the other request's lock row past the options cache. + $held = static fn (): bool => ! \is_null( $wpdb->get_var( "SELECT option_value FROM {$wpdb->options} WHERE option_name = 'dws_lowc_linking'" ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Watches the other request's lock row past the options cache. $tries = 0; while ( ! $held() && $tries++ < 300 ) { \usleep( 50000 ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php index e131b4f..015885e 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php @@ -318,7 +318,7 @@ protected function query( ?array $found = null, array $meta_query = array() ): a if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { $args = apply_filters( 'woocommerce_order_list_table_prepare_items_query_args', - $others + ( null === $found ? array() : array( 'id' => $found ) ) + array( + $others + ( \is_null( $found ) ? array() : array( 'id' => $found ) ) + array( 'type' => 'shop_order', 'status' => 'any', 'limit' => -1, @@ -332,7 +332,7 @@ protected function query( ?array $found = null, array $meta_query = array() ): a $vars = apply_filters( 'request', - $others + ( null === $found ? array() : array( 'post__in' => $found ) ) + array( + $others + ( \is_null( $found ) ? array() : array( 'post__in' => $found ) ) + array( 'post_type' => 'shop_order', 'post_status' => 'any', 'posts_per_page' => -1, diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php index 0d653bb..3a1ff9c 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php @@ -428,7 +428,7 @@ function dws_lpmwc_check_payment_method_access_via_wc_order_meta( string $locked function dws_lpmwc_get_raw_setting( string $field_id, ?string $group = null ) { _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' ); $setting = dws_lpmwc_find_v1_setting( $field_id, $group ); - return null === $setting ? null : get_option( $setting['key'], null ); + return is_null( $setting ) ? null : get_option( $setting['key'], null ); } /** @@ -464,7 +464,7 @@ function dws_lpmwc_get_validated_setting( string $field_id, ?string $group = nul function dws_lpmwc_validate_setting( $value, string $field_id, ?string $group = null ) { _deprecated_function( __FUNCTION__, '2.0.0' ); $setting = dws_lpmwc_find_v1_setting( $field_id, $group ); - return null === $setting ? null : dws_lpmwc_validate_v1_setting( $value, $setting['key'] ); + return is_null( $setting ) ? null : dws_lpmwc_validate_v1_setting( $value, $setting['key'] ); } /** @@ -495,7 +495,7 @@ function dws_lpmwc_get_validated_general_setting( string $field_id ) { * @return array{group: string, field: string, key: string}|null */ function dws_lpmwc_find_v1_setting( string $field_id, ?string $group ): ?array { - if ( null === $group ) { + if ( is_null( $group ) ) { $parts = explode( '/', $field_id, 2 ); if ( 2 !== count( $parts ) ) { return null; @@ -504,7 +504,7 @@ function dws_lpmwc_find_v1_setting( string $field_id, ?string $group ): ?array { } $key = DWS_LPMWC_V1_SETTINGS[ $group ][ $field_id ] ?? null; - return null === $key ? null : array( + return is_null( $key ) ? null : array( 'group' => $group, 'field' => $field_id, 'key' => $key, @@ -524,7 +524,7 @@ function dws_lpmwc_find_v1_setting( string $field_id, ?string $group ): ?array { */ function dws_lpmwc_read_v1_setting( string $field_id, ?string $group ) { $setting = dws_lpmwc_find_v1_setting( $field_id, $group ); - if ( null === $group ) { + if ( is_null( $group ) ) { /** * Filters a setting's value, by its group-prefixed v1 id, before its group reads it. * @@ -535,13 +535,13 @@ function dws_lpmwc_read_v1_setting( string $field_id, ?string $group ) { * @param mixed $value Null, or the value a callback answers. * @param string $field_id v1's id of the setting, prefixed with its group and a slash. */ - $value = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/settings/get_validated_option_value', array( null, $field_id ), '2.0.0', null === $setting ? 'get_option' : 'option_' . $setting['key'] ); - if ( null !== $value ) { + $value = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/settings/get_validated_option_value', array( null, $field_id ), '2.0.0', is_null( $setting ) ? 'get_option' : 'option_' . $setting['key'] ); + if ( ! is_null( $value ) ) { return $value; } } - return null === $setting ? null : dws_lpmwc_apply_v1_setting_filter( dws_lpmwc_validate_v1_setting( get_option( $setting['key'], null ), $setting['key'] ), $setting['group'], $setting['field'], 'option_' . $setting['key'] ); + return is_null( $setting ) ? null : dws_lpmwc_apply_v1_setting_filter( dws_lpmwc_validate_v1_setting( get_option( $setting['key'], null ), $setting['key'] ), $setting['group'], $setting['field'], 'option_' . $setting['key'] ); } /** diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php index 3e3292b..649eca7 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php @@ -52,7 +52,7 @@ function dws_lpmwc_add_settings(): void { if ( is_array( $default ) ) { $value = is_array( $v1 ) ? array_values( array_filter( $v1, 'is_string' ) ) : $default; } else { - $value = wc_bool_to_string( null === $v1 ? $default : ( filter_var( $v1, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ?? $default ) ); // filter_var() reads null as false, so an absent row takes the default before it. + $value = wc_bool_to_string( is_null( $v1 ) ? $default : ( filter_var( $v1, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ?? $default ) ); // filter_var() reads null as false, so an absent row takes the default before it. } if ( ! add_option( $key, $value, '', true ) ) { @@ -151,7 +151,7 @@ function dws_lpmwc_move_order_grants(): void { foreach ( $tables as $table => $column ) { $wpdb->flush(); $listed = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- The order tables exist only once WooCommerce created them. - if ( null === $listed && '' !== $wpdb->last_error ) { + if ( is_null( $listed ) && '' !== $wpdb->last_error ) { throw new RuntimeException( 'The v1 order grants could not be read.' ); } if ( $table !== $listed ) { diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php index 5fd2e26..c1623e7 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php @@ -334,7 +334,7 @@ public function test_should_lock_judges_no_order_on_an_order_pay_page_without_th #[DataProvider( 'floors' )] public function test_the_lock_holds_and_nothing_relaxes_it_while_the_plugin_cannot_boot( ?string $header, string $expected ): void { $source = (string) \file_get_contents( DWS_LPMWC_FILE ); - if ( null !== $header ) { + if ( ! \is_null( $header ) ) { \file_put_contents( DWS_LPMWC_FILE, (string) \preg_replace( '/^(\s*\*\s*' . \preg_quote( $header, '/' ) . ':\s*).*$/m', '${1}99.0', $source ) ); } @@ -376,7 +376,7 @@ protected static function visit_order_pay( int $order_id, ?string $key ): void { $GLOBALS['wp']->query_vars['order-pay'] = (string) $order_id; set_query_var( 'order-pay', (string) $order_id ); add_filter( 'woocommerce_is_checkout', '__return_true' ); - if ( null !== $key ) { + if ( ! \is_null( $key ) ) { $_GET['key'] = $key; } } From b4839590f54ec045c0c9f92ea7219fa5c91ffd11 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:55:18 +0100 Subject: [PATCH 33/39] Qualify only PHP's own functions and constants The style guide gives only PHP's own functions and constants a leading backslash, which lets opcache compile the functions it special-cases, and leaves WordPress's unqualified, yet Internal Comments called filter_var() and its FILTER_ constants unqualified in five lines of src/ and array_fill_keys() in one test, while ten lines across the plugins called is_wp_error(), current_time(), esc_html(), esc_url() or wp_parse_url() with a backslash. Each PHP call and constant in a namespaced file takes the backslash, and each WordPress call drops it. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/src/AdminScreens.php | 4 ++-- plugins/internal-comments/src/Comments.php | 8 ++++---- plugins/internal-comments/src/Permissions.php | 2 +- plugins/internal-comments/src/RestRoutes.php | 2 +- .../tests/Integration/AdminScreensTest.php | 2 +- .../tests/Integration/DataGuardsTest.php | 4 ++-- .../internal-comments/tests/Integration/MetaBoxTest.php | 2 +- .../tests/Integration/MigrationsTest.php | 4 ++-- .../internal-comments/tests/Integration/SettingsTest.php | 2 +- .../tests/Integration/MetaBoxTest.php | 2 +- .../tests/Integration/SettingsTest.php | 2 +- .../tests/Integration/SettingsTest.php | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/internal-comments/src/AdminScreens.php b/plugins/internal-comments/src/AdminScreens.php index 8d2f573..43138ac 100644 --- a/plugins/internal-comments/src/AdminScreens.php +++ b/plugins/internal-comments/src/AdminScreens.php @@ -505,8 +505,8 @@ public function render_revisions( \WP_Comment $comment ): void { echo '
    '; foreach ( $changes as $change_id ) { $content = get_comment_meta( $comment_id, "_dws_ic_previous_content_$change_id", true ); - $user = get_userdata( filter_var( get_comment_meta( $comment_id, "_dws_ic_changed_by_$change_id", true ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? 0 ); - $time = filter_var( get_comment_meta( $comment_id, "_dws_ic_changed_time_$change_id", true ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? 0; + $user = get_userdata( \filter_var( get_comment_meta( $comment_id, "_dws_ic_changed_by_$change_id", true ), \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE ) ?? 0 ); + $time = \filter_var( get_comment_meta( $comment_id, "_dws_ic_changed_time_$change_id", true ), \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE ) ?? 0; $name = false === $user ? __( 'an unknown user', 'internal-comments' ) : $user->display_name; ?>
  • diff --git a/plugins/internal-comments/src/Comments.php b/plugins/internal-comments/src/Comments.php index ccab769..13d2188 100644 --- a/plugins/internal-comments/src/Comments.php +++ b/plugins/internal-comments/src/Comments.php @@ -155,11 +155,11 @@ public function keep_post_comments_internal( int $post_id ): void { * @return array|\WP_Error */ public function filter_update_data( array|\WP_Error $data, array $comment ): array|\WP_Error { - if ( \is_wp_error( $data ) || DWS_IC_COMMENT_TYPE !== ( $comment['comment_type'] ?? null ) ) { + if ( is_wp_error( $data ) || DWS_IC_COMMENT_TYPE !== ( $comment['comment_type'] ?? null ) ) { return $data; } - $comment_id = (int) filter_var( $comment['comment_ID'] ?? 0, FILTER_VALIDATE_INT ); + $comment_id = (int) \filter_var( $comment['comment_ID'] ?? 0, \FILTER_VALIDATE_INT ); $previous = wp_unslash( $comment['comment_content'] ?? '' ); if ( ( $data['comment_content'] ?? '' ) !== $previous ) { $change_id = \uniqid( (string) get_current_user_id(), true ); @@ -187,7 +187,7 @@ public function filter_update_data( array|\WP_Error $data, array $comment ): arr * @return void */ public function keep_replies_private( int|string $comment_id ): void { - if ( true !== dws_ic_is_internal_comment( $comment_id ) || true !== filter_var( get_comment_meta( (int) $comment_id, DWS_IC_META_PRIVATE, true ), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ) { + if ( true !== dws_ic_is_internal_comment( $comment_id ) || true !== \filter_var( get_comment_meta( (int) $comment_id, DWS_IC_META_PRIVATE, true ), \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE ) ) { return; } @@ -265,7 +265,7 @@ public function guard_privacy( mixed $check, int $comment_id, string $meta_key, } // A comment turns private only before anyone replies, since a reply's author would otherwise own the private thread once the comment above is deleted. - $turns_private = true === filter_var( $meta_value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) && true !== filter_var( get_comment_meta( $comment_id, DWS_IC_META_PRIVATE, true ), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ); + $turns_private = true === \filter_var( $meta_value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE ) && true !== \filter_var( get_comment_meta( $comment_id, DWS_IC_META_PRIVATE, true ), \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE ); if ( $turns_private && dws_ic_has_replies( $comment_id ) ) { return false; } diff --git a/plugins/internal-comments/src/Permissions.php b/plugins/internal-comments/src/Permissions.php index fcc2904..d80f109 100644 --- a/plugins/internal-comments/src/Permissions.php +++ b/plugins/internal-comments/src/Permissions.php @@ -102,7 +102,7 @@ public function guard_core_rest_comment( mixed $response, mixed $handler, \WP_RE default => null, } : null; $comment_id = \is_numeric( $request['id'] ) ? (int) $request['id'] : 0; - if ( \is_wp_error( $response ) || \is_null( $capability ) || true !== dws_ic_is_internal_comment( $comment_id ) || current_user_can( $capability, $comment_id ) ) { + if ( is_wp_error( $response ) || \is_null( $capability ) || true !== dws_ic_is_internal_comment( $comment_id ) || current_user_can( $capability, $comment_id ) ) { return $response; } diff --git a/plugins/internal-comments/src/RestRoutes.php b/plugins/internal-comments/src/RestRoutes.php index a18180a..c395aa6 100644 --- a/plugins/internal-comments/src/RestRoutes.php +++ b/plugins/internal-comments/src/RestRoutes.php @@ -351,7 +351,7 @@ public function update_comment( \WP_REST_Request $request ): \WP_REST_Response|\ do_action( 'dws_ic/internal_comment/before_update', $comment, $data ); $updated = wp_update_comment( wp_slash( $data ), true ); - if ( \is_wp_error( $updated ) || false === $updated ) { + if ( is_wp_error( $updated ) || false === $updated ) { return new \WP_Error( 'dws_ic_not_updated', __( 'The internal comment could not be updated. Please try again.', 'internal-comments' ), array( 'status' => 500 ) ); } diff --git a/plugins/internal-comments/tests/Integration/AdminScreensTest.php b/plugins/internal-comments/tests/Integration/AdminScreensTest.php index f91b348..75e1e4e 100644 --- a/plugins/internal-comments/tests/Integration/AdminScreensTest.php +++ b/plugins/internal-comments/tests/Integration/AdminScreensTest.php @@ -199,7 +199,7 @@ public function test_the_comments_screen_leaves_out_the_rows_its_reader_cannot_s public function test_the_comments_screen_offers_only_the_actions_an_internal_comment_supports(): void { $comment = get_comment( $this->comment( 'Row' ) ); - $actions = array_fill_keys( array( 'approve', 'unapprove', 'reply', 'quickedit', 'edit', 'spam', 'trash', 'untrash', 'delete' ), '' ); + $actions = \array_fill_keys( array( 'approve', 'unapprove', 'reply', 'quickedit', 'edit', 'spam', 'trash', 'untrash', 'delete' ), '' ); self::assertInstanceOf( \WP_Comment::class, $comment ); self::assertSame( array( 'edit', 'trash', 'untrash', 'delete' ), \array_keys( new AdminScreens()->filter_row_actions( $actions, $comment ) ) ); diff --git a/plugins/internal-comments/tests/Integration/DataGuardsTest.php b/plugins/internal-comments/tests/Integration/DataGuardsTest.php index 35e861a..8ed2f4b 100644 --- a/plugins/internal-comments/tests/Integration/DataGuardsTest.php +++ b/plugins/internal-comments/tests/Integration/DataGuardsTest.php @@ -276,8 +276,8 @@ public function test_v1s_approved_comments_stay_hidden_until_the_move_finishes() 'comment_type' => DWS_IC_COMMENT_TYPE, 'comment_approved' => '1', 'comment_content' => 'v1', - 'comment_date' => \current_time( 'mysql' ), - 'comment_date_gmt' => \current_time( 'mysql', true ), + 'comment_date' => current_time( 'mysql' ), + 'comment_date_gmt' => current_time( 'mysql', true ), ) ); $v1 = (int) $wpdb->insert_id; diff --git a/plugins/internal-comments/tests/Integration/MetaBoxTest.php b/plugins/internal-comments/tests/Integration/MetaBoxTest.php index 5d24678..a52b06b 100644 --- a/plugins/internal-comments/tests/Integration/MetaBoxTest.php +++ b/plugins/internal-comments/tests/Integration/MetaBoxTest.php @@ -130,7 +130,7 @@ public function test_the_markup_hooks_fire_v1s_names_first_then_v2s(): void { $comment = $this->comment( 'Hooked' ); $classes = static fn ( array $classes ): array => \array_merge( $classes, array( 'from-v2' ) ); $action = static function ( \DWS_Internal_Comment $comment ): void { - echo '' . \esc_html( (string) $comment->comment_ID ) . ''; + echo '' . esc_html( (string) $comment->comment_ID ) . ''; }; add_filter( 'dws_ic/meta_box/comment_classes', $classes ); add_action( 'dws_ic/meta_box/before_comment_actions', $action ); diff --git a/plugins/internal-comments/tests/Integration/MigrationsTest.php b/plugins/internal-comments/tests/Integration/MigrationsTest.php index 2f2a6af..c0b6dc7 100644 --- a/plugins/internal-comments/tests/Integration/MigrationsTest.php +++ b/plugins/internal-comments/tests/Integration/MigrationsTest.php @@ -116,8 +116,8 @@ public function test_the_second_step_converts_v1s_internal_comments_in_batches_a 'comment_type' => $type, 'comment_approved' => $status, 'comment_content' => "$type $status", - 'comment_date' => \current_time( 'mysql' ), - 'comment_date_gmt' => \current_time( 'mysql', true ), + 'comment_date' => current_time( 'mysql' ), + 'comment_date_gmt' => current_time( 'mysql', true ), ) ); return (int) $wpdb->insert_id; diff --git a/plugins/internal-comments/tests/Integration/SettingsTest.php b/plugins/internal-comments/tests/Integration/SettingsTest.php index 2d7050b..51fa1be 100644 --- a/plugins/internal-comments/tests/Integration/SettingsTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsTest.php @@ -171,7 +171,7 @@ public function test_the_plugin_row_links_to_the_page_and_to_the_support_forum() $links = apply_filters( "plugin_action_links_{$basename}", array( 'deactivate' => 'Deactivate' ), $basename, array(), 'all' ); $meta = apply_filters( 'plugin_row_meta', array(), $basename, array(), 'all' ); - self::assertStringContainsString( \esc_url( admin_url( 'options-general.php?page=dws-internal-comments' ) ), (string) \reset( $links ) ); + self::assertStringContainsString( esc_url( admin_url( 'options-general.php?page=dws-internal-comments' ) ), (string) \reset( $links ) ); self::assertStringContainsString( 'https://wordpress.org/support/plugin/internal-comments/', \implode( '', $meta ) ); self::assertSame( array(), apply_filters( 'plugin_row_meta', array(), 'another-plugin/another-plugin.php', array(), 'all' ) ); } diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php index 84ae866..a19d05d 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php @@ -102,7 +102,7 @@ public function test_the_box_shows_the_parent_the_children_and_the_button_to_add self::assertSame( wc_get_order( $child->get_id() )->get_edit_order_url(), $link?->getAttribute( 'href' ) ); $button = $box->querySelector( 'a.button' ); self::assertSame( 'Add new child Order', \trim( (string) $button?->textContent ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- A DOM property. - \parse_str( (string) \wp_parse_url( (string) $button?->getAttribute( 'href' ), \PHP_URL_QUERY ), $query ); + \parse_str( (string) wp_parse_url( (string) $button?->getAttribute( 'href' ), \PHP_URL_QUERY ), $query ); self::assertSame( 'dws_lowc_create_linked_child', $query['action'] ?? null ); self::assertSame( (string) $order->get_id(), $query['parent_id'] ?? null ); self::assertSame( 1, wp_verify_nonce( (string) ( $query['_wpnonce'] ?? '' ), 'dws_create_linked_child' ) ); diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php index 5ba3af5..ada996a 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php @@ -151,7 +151,7 @@ public function test_the_plugin_row_links_to_the_section_and_to_the_support_foru $links = apply_filters( "plugin_action_links_{$basename}", array( 'deactivate' => 'Deactivate' ), $basename, array(), 'all' ); $meta = apply_filters( 'plugin_row_meta', array(), $basename, array(), 'all' ); - self::assertStringContainsString( \esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=' . self::SECTION ) ), (string) \reset( $links ) ); + self::assertStringContainsString( esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=' . self::SECTION ) ), (string) \reset( $links ) ); self::assertStringContainsString( 'https://wordpress.org/support/plugin/linked-orders-for-woocommerce/', \implode( '', $meta ) ); self::assertSame( array(), apply_filters( 'plugin_row_meta', array(), 'another-plugin/another-plugin.php', array(), 'all' ) ); } diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php index 869ff0b..91ad0ce 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php @@ -159,7 +159,7 @@ public function test_the_plugin_row_links_to_the_section_and_to_the_support_foru $links = apply_filters( "plugin_action_links_{$basename}", array( 'deactivate' => 'Deactivate' ), $basename, array(), 'all' ); $meta = apply_filters( 'plugin_row_meta', array(), $basename, array(), 'all' ); - self::assertStringContainsString( \esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=' . self::SECTION ) ), (string) \reset( $links ) ); + self::assertStringContainsString( esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=' . self::SECTION ) ), (string) \reset( $links ) ); self::assertStringContainsString( 'https://wordpress.org/support/plugin/locked-payment-methods-for-woocommerce/', \implode( '', $meta ) ); self::assertSame( array(), apply_filters( 'plugin_row_meta', array(), 'another-plugin/another-plugin.php', array(), 'all' ) ); } From 09a48e7acdfa01492b3f3045bb687fac926cce53 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:56:48 +0100 Subject: [PATCH 34/39] Name the v1 boot hooks each functions.php fires Each plugin's functions.php relays its boot to v1's _initialized and _initialization_failure hooks, yet its file docblock never said so, which X232 has every functions.php name, as X220 has Internal Comments' (A439). Each file docblock gains a second paragraph naming the two hooks and that they fire before every listener of their replacements. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/functions.php | 2 ++ plugins/linked-orders-for-woocommerce/functions.php | 2 ++ plugins/locked-payment-methods-for-woocommerce/functions.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/plugins/internal-comments/functions.php b/plugins/internal-comments/functions.php index 76aa172..e59b1ab 100644 --- a/plugins/internal-comments/functions.php +++ b/plugins/internal-comments/functions.php @@ -2,6 +2,8 @@ /** * Hooks the plugin into WordPress and loads the files in includes/. * + * It also fires v1's boot hooks, dws_ic_initialized and dws_ic_initialization_failure, at the earliest priority of the v2 hooks that replace them. + * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/linked-orders-for-woocommerce/functions.php b/plugins/linked-orders-for-woocommerce/functions.php index 1808e6d..5ead112 100644 --- a/plugins/linked-orders-for-woocommerce/functions.php +++ b/plugins/linked-orders-for-woocommerce/functions.php @@ -2,6 +2,8 @@ /** * Hooks the plugin into WordPress and loads the files in includes/. * + * It also fires v1's boot hooks, dws_lowc_initialized and dws_lowc_initialization_failure, at the earliest priority of the v2 hooks that replace them. + * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/functions.php b/plugins/locked-payment-methods-for-woocommerce/functions.php index 42dec8c..e7209d3 100644 --- a/plugins/locked-payment-methods-for-woocommerce/functions.php +++ b/plugins/locked-payment-methods-for-woocommerce/functions.php @@ -2,6 +2,8 @@ /** * Hooks the plugin into WordPress and loads the files in includes/. * + * It also fires v1's boot hooks, dws_lpmwc_initialized and dws_lpmwc_initialization_failure, at the earliest priority of the v2 hooks that replace them. + * * @since 2.0.0 * @version 2.0.0 * From 179ae0beb163b81acef44b0b7eadac1a570d1aef Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:57:30 +0100 Subject: [PATCH 35/39] Drop the interpolation braces that change nothing Eight interpolations in Linked Orders' and Locked Payment Methods' code braced a variable that nothing follows but a quote or a character no name can hold, where the framework's sweep and the template drop such braces (X139's A437). Each loses its braces; a variable an underscore or a name character follows keeps them, since PHP would read the longer name. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/linked-orders-for-woocommerce/includes/settings.php | 2 +- plugins/linked-orders-for-woocommerce/src/Settings.php | 6 +++--- .../includes/migrations.php | 2 +- .../locked-payment-methods-for-woocommerce/src/Settings.php | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/linked-orders-for-woocommerce/includes/settings.php b/plugins/linked-orders-for-woocommerce/includes/settings.php index faf538f..f71f0b8 100644 --- a/plugins/linked-orders-for-woocommerce/includes/settings.php +++ b/plugins/linked-orders-for-woocommerce/includes/settings.php @@ -157,5 +157,5 @@ function dws_lowc_find_setting( string $field_id, ?string $group ): ?array { $group = substr( $group, 0, -9 ); } - return in_array( $group, DWS_LOWC_SETTINGS_GROUPS, true ) ? array( $group, $field_id, "dws-lowc_{$group}_{$field_id}" ) : null; + return in_array( $group, DWS_LOWC_SETTINGS_GROUPS, true ) ? array( $group, $field_id, "dws-lowc_{$group}_$field_id" ) : null; } diff --git a/plugins/linked-orders-for-woocommerce/src/Settings.php b/plugins/linked-orders-for-woocommerce/src/Settings.php index cdb8258..f433b96 100644 --- a/plugins/linked-orders-for-woocommerce/src/Settings.php +++ b/plugins/linked-orders-for-woocommerce/src/Settings.php @@ -71,7 +71,7 @@ public function get_section_fields( string $tab_id, string $section_id ): array $wc_fields = array(); foreach ( $group_fields as $id => $field ) { - $wc_fields[] = array( 'id' => "dws-lowc_{$group}_{$id}" ) + $field; + $wc_fields[] = array( 'id' => "dws-lowc_{$group}_$id" ) + $field; } /** @@ -89,7 +89,7 @@ public function get_section_fields( string $tab_id, string $section_id ): array array( array( 'type' => 'title', - 'id' => "dws-lowc_{$group}", + 'id' => "dws-lowc_$group", 'title' => 'general' === $group ? __( 'General Settings', 'linked-orders-for-woocommerce' ) : __( 'Plugin Settings', 'linked-orders-for-woocommerce' ), ), ), @@ -97,7 +97,7 @@ public function get_section_fields( string $tab_id, string $section_id ): array array( array( 'type' => 'sectionend', - 'id' => "dws-lowc_{$group}", + 'id' => "dws-lowc_$group", ), ) ); diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php index 649eca7..122390d 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php @@ -48,7 +48,7 @@ function dws_lpmwc_add_settings(): void { } $default = DWS_LPMWC_OPTION_DEFAULTS[ $key ]; - $v1 = get_option( "dws-wc-lpm_{$group}_{$id}", null ); + $v1 = get_option( "dws-wc-lpm_{$group}_$id", null ); if ( is_array( $default ) ) { $value = is_array( $v1 ) ? array_values( array_filter( $v1, 'is_string' ) ) : $default; } else { diff --git a/plugins/locked-payment-methods-for-woocommerce/src/Settings.php b/plugins/locked-payment-methods-for-woocommerce/src/Settings.php index 7dec0ef..86ccebc 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/Settings.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/Settings.php @@ -82,15 +82,15 @@ public function get_section_fields( string $tab_id, string $section_id ): array $fields[] = array( 'type' => 'title', - 'id' => "dws-wc-lpm_{$group}", + 'id' => "dws-wc-lpm_$group", 'title' => $titles[ $group ], ); foreach ( $group_fields as $id => $field ) { - $fields[] = array( 'id' => $keys[ $id ] ?? "dws-wc-lpm_{$group}_{$id}" ) + $field; // A field a v1 callback adds keeps the option v1 stored it under. + $fields[] = array( 'id' => $keys[ $id ] ?? "dws-wc-lpm_{$group}_$id" ) + $field; // A field a v1 callback adds keeps the option v1 stored it under. } $fields[] = array( 'type' => 'sectionend', - 'id' => "dws-wc-lpm_{$group}", + 'id' => "dws-wc-lpm_$group", ); } From b2e4e04c1249fc4ee29d1c7afb7dc4c8d4cf24c8 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:59:45 +0100 Subject: [PATCH 36/39] Tag each plugin's API Nothing in the plugins' code marked which global functions, constants and classes a peer may rely on through 2.x, and Locked Payment Methods tagged five strategy callbacks @internal, which X150 has no plugin do (A331, A394, A433). Each element of a plugin's contract carries @api in its own tag group after the description, as X150 and X362 rule: the slug, name and version getters; Linked Orders' twelve v1 helpers and DWS_Order_Node (X49); Internal Comments' v1 comment helpers and settings readers and DWS_Internal_Comment (X69); Locked Payment Methods' dws_lpmwc_is_payment_method_locked() (X95); and in each, v1's deprecated shims and constants until the next major removes them. The accessors stay untagged, and the five @internal tags go. Assisted-by: Claude Code:claude-opus-5-5 --- .../internal-comments/includes/comments.php | 14 ++++++ .../internal-comments/includes/deprecated.php | 24 ++++++++++ .../internal-comments/includes/metadata.php | 6 +++ .../internal-comments/includes/settings.php | 8 ++++ .../models/DWS_Internal_Comment.php | 2 + .../includes/deprecated.php | 24 ++++++++++ .../includes/linking.php | 10 ++++ .../includes/metadata.php | 6 +++ .../includes/post-type.php | 8 ++++ .../includes/settings.php | 6 +++ .../models/DWS_Order_Node.php | 2 + .../AGENTS.md | 2 +- .../includes/deprecated.php | 46 +++++++++++++++++++ .../includes/grants.php | 2 + .../includes/metadata.php | 6 +++ .../src/UnlockStrategies/GroupsStrategy.php | 2 - .../src/UnlockStrategies/OrderStrategy.php | 2 - .../UnlockStrategies/UserProfileStrategy.php | 2 - .../src/UnlockStrategies/UserRoleStrategy.php | 2 - .../WooCommerceMembershipsStrategy.php | 2 - 20 files changed, 165 insertions(+), 11 deletions(-) diff --git a/plugins/internal-comments/includes/comments.php b/plugins/internal-comments/includes/comments.php index 77b7ba9..cc9b3b9 100644 --- a/plugins/internal-comments/includes/comments.php +++ b/plugins/internal-comments/includes/comments.php @@ -45,6 +45,8 @@ /** * Returns the comment that an id, a comment or an array holding a comment_ID names, or null when there is none. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -65,6 +67,8 @@ function dws_ic_get_comment( $comment ): ?WP_Comment { /** * Returns whether a comment is internal, or null when there is no such comment. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -80,6 +84,8 @@ function dws_ic_is_internal_comment( $comment ): ?bool { /** * Returns whether a post's type, or an order's type in any of WooCommerce's storages, is one the settings select for internal comments. * + * @api + * * @since 1.1.0 * @version 2.0.0 * @@ -116,6 +122,8 @@ function dws_ic_get_post_type( ?int $post_id = null ): ?string { /** * Stores an internal comment by an author, the current user by default, and returns its id, or null when it could not be stored. * + * @api + * * @since 1.1.0 * @version 2.0.0 * @@ -168,6 +176,8 @@ function dws_ic_create_internal_comment( array $args = array(), ?WP_User $author /** * Returns a post's internal comments the current user can see, threaded by default, each wrapped in the plugin's model. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -201,6 +211,8 @@ function dws_ic_get_internal_comments( int $post_id, array $args = array(), bool /** * Returns the internal comments a trashed post held under a status, as WordPress recorded them when it trashed the post, which the current user can see. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -233,6 +245,8 @@ function dws_ic_get_internal_comments_from_trash_meta( int $post_id, string $sta /** * Returns whether an internal comment is private, because its author or the author of one of its ancestors marked it so, or null for a comment that is not internal. * + * @api + * * @since 1.0.0 * @version 2.0.0 * diff --git a/plugins/internal-comments/includes/deprecated.php b/plugins/internal-comments/includes/deprecated.php index 790dedf..53a7f70 100644 --- a/plugins/internal-comments/includes/deprecated.php +++ b/plugins/internal-comments/includes/deprecated.php @@ -31,6 +31,8 @@ /** * The plugin's untranslated name. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_ic_get_plugin_name(). @@ -40,6 +42,8 @@ /** * The plugin's version. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_ic_get_plugin_version(). @@ -49,6 +53,8 @@ /** * The path of the plugin's main file. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use DWS_IC_FILE. @@ -58,6 +64,8 @@ /** * The oldest PHP version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -67,6 +75,8 @@ /** * The oldest WordPress version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -105,6 +115,8 @@ /** * Returns the plugin's name, translated once init has fired. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_ic_get_plugin_name(). @@ -119,6 +131,8 @@ function dws_ic_name(): string { /** * Returns the plugin's version. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_ic_get_plugin_version(). @@ -133,6 +147,8 @@ function dws_ic_version(): string { /** * Returns the path of the plugin's main file. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -147,6 +163,8 @@ function dws_ic_path(): string { /** * Returns the oldest PHP version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -161,6 +179,8 @@ function dws_ic_min_php(): string { /** * Returns the oldest WordPress version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -175,6 +195,8 @@ function dws_ic_min_wp(): string { /** * Returns a hook name as v1 built it for the plugin. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -192,6 +214,8 @@ function dws_ic_get_hook_tag( string $name, $extra = array() ): string { /** * Returns a hook name as v1 built it for one of its components, or null for an id v1 had no component for. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 diff --git a/plugins/internal-comments/includes/metadata.php b/plugins/internal-comments/includes/metadata.php index 0ff03dd..d90691e 100644 --- a/plugins/internal-comments/includes/metadata.php +++ b/plugins/internal-comments/includes/metadata.php @@ -15,6 +15,8 @@ /** * Returns the plugin's slug, which is the text domain its header declares. * + * @api + * * @since 2.0.0 * @version 2.0.0 * @@ -29,6 +31,8 @@ function dws_ic_get_plugin_slug(): string { * * WordPress flags translations loaded earlier. * + * @api + * * @since 2.0.0 * @version 2.0.0 * @@ -42,6 +46,8 @@ function dws_ic_get_plugin_name(): string { /** * Returns the plugin's version from its header. * + * @api + * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/internal-comments/includes/settings.php b/plugins/internal-comments/includes/settings.php index d2cffa3..48058b0 100644 --- a/plugins/internal-comments/includes/settings.php +++ b/plugins/internal-comments/includes/settings.php @@ -26,6 +26,8 @@ /** * Returns the stored value of a setting, or null for a setting that is not stored or a group v1 did not have. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -47,6 +49,8 @@ function dws_ic_get_raw_setting( string $field_id, ?string $group = null ) { /** * Returns the validated value of a setting, filtered, or null for a group v1 did not have. * + * @api + * * @since 1.1.0 * @version 2.0.0 * @@ -114,6 +118,8 @@ function dws_ic_get_validated_setting( string $field_id, ?string $group = null ) /** * Returns the names of the post types selected for internal comments, as stored, so a post type that registers late stays selected. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -127,6 +133,8 @@ function dws_ic_get_selected_post_types(): array { /** * Returns whether internal comments can be marked private. * + * @api + * * @since 1.0.0 * @version 2.0.0 * diff --git a/plugins/internal-comments/models/DWS_Internal_Comment.php b/plugins/internal-comments/models/DWS_Internal_Comment.php index a841586..0387a95 100644 --- a/plugins/internal-comments/models/DWS_Internal_Comment.php +++ b/plugins/internal-comments/models/DWS_Internal_Comment.php @@ -7,6 +7,8 @@ * * It keeps v1's global name, which other plugins type-hint, and reads of the comment's fields and calls to its methods reach the wrapped comment. * + * @api + * * @since 1.0.0 * @version 2.0.0 * diff --git a/plugins/linked-orders-for-woocommerce/includes/deprecated.php b/plugins/linked-orders-for-woocommerce/includes/deprecated.php index 9675397..d128ba7 100644 --- a/plugins/linked-orders-for-woocommerce/includes/deprecated.php +++ b/plugins/linked-orders-for-woocommerce/includes/deprecated.php @@ -31,6 +31,8 @@ /** * The plugin's untranslated name. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lowc_get_plugin_name(). @@ -40,6 +42,8 @@ /** * The plugin's version. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lowc_get_plugin_version(). @@ -49,6 +53,8 @@ /** * The path of the plugin's main file. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use DWS_LOWC_FILE. @@ -58,6 +64,8 @@ /** * The oldest PHP version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -67,6 +75,8 @@ /** * The oldest WordPress version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -88,6 +98,8 @@ /** * Returns the plugin's name, translated once init has fired. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lowc_get_plugin_name(). @@ -102,6 +114,8 @@ function dws_lowc_name(): string { /** * Returns the plugin's version. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lowc_get_plugin_version(). @@ -116,6 +130,8 @@ function dws_lowc_version(): string { /** * Returns the path of the plugin's main file. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -130,6 +146,8 @@ function dws_lowc_path(): string { /** * Returns the oldest PHP version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -144,6 +162,8 @@ function dws_lowc_min_php(): string { /** * Returns the oldest WordPress version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -158,6 +178,8 @@ function dws_lowc_min_wp(): string { /** * Returns a hook name as v1 built it for the plugin. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -175,6 +197,8 @@ function dws_lowc_get_hook_tag( string $name, $extra = array() ): string { /** * Returns a hook name as v1 built it for one of its components, or null for an id v1 had no component for. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 diff --git a/plugins/linked-orders-for-woocommerce/includes/linking.php b/plugins/linked-orders-for-woocommerce/includes/linking.php index f589993..aea1f13 100644 --- a/plugins/linked-orders-for-woocommerce/includes/linking.php +++ b/plugins/linked-orders-for-woocommerce/includes/linking.php @@ -17,6 +17,8 @@ /** * Returns an order's node, or null when the value names no order of a supported type. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -35,6 +37,8 @@ function dws_lowc_get_order_node( $order ): ?DWS_Order_Node { /** * Returns whether an order is at the top of its tree, or null when it has no node. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -52,6 +56,8 @@ function dws_lowc_is_root_order( $order ): ?bool { * * A link reads and rewrites the parent's list of children, so one lock serializes links across requests, and each reads both orders' meta from the database once it holds the lock. A link holds it for milliseconds, so the next one waits for it rather than refusing at once. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -105,6 +111,8 @@ function dws_lowc_link_orders( int $parent_id, int $child_id ): ?bool { /** * Returns the node at the top of an order's tree, which is the order's own for a root or for a child whose parent was deleted, or null when the order has no node. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -124,6 +132,8 @@ function dws_lowc_get_root_order( $order ): ?DWS_Order_Node { /** * Returns the ids of an order and its descendants, each child before the next with its own descendants, or null when the order has no node. * + * @api + * * @since 1.0.0 * @version 2.0.0 * diff --git a/plugins/linked-orders-for-woocommerce/includes/metadata.php b/plugins/linked-orders-for-woocommerce/includes/metadata.php index ea31903..7c5ff15 100644 --- a/plugins/linked-orders-for-woocommerce/includes/metadata.php +++ b/plugins/linked-orders-for-woocommerce/includes/metadata.php @@ -15,6 +15,8 @@ /** * Returns the plugin's slug, which is the text domain its header declares. * + * @api + * * @since 2.0.0 * @version 2.0.0 * @@ -29,6 +31,8 @@ function dws_lowc_get_plugin_slug(): string { * * WordPress flags translations loaded earlier. * + * @api + * * @since 2.0.0 * @version 2.0.0 * @@ -42,6 +46,8 @@ function dws_lowc_get_plugin_name(): string { /** * Returns the plugin's version from its header. * + * @api + * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/linked-orders-for-woocommerce/includes/post-type.php b/plugins/linked-orders-for-woocommerce/includes/post-type.php index 4f17b87..33d3f45 100644 --- a/plugins/linked-orders-for-woocommerce/includes/post-type.php +++ b/plugins/linked-orders-for-woocommerce/includes/post-type.php @@ -15,6 +15,8 @@ /** * Returns the order types whose orders can be linked. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -61,6 +63,8 @@ function dws_lowc_get_singular_name( WP_Post_Type $post_type ): string { /** * Returns whether an order is of a type that can be linked, or null when it is not an order. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -80,6 +84,8 @@ function dws_lowc_is_supported_order( $order ): ?bool { /** * Returns the statuses, without their 'wc-' prefix, that an order must have for a child to be created from it. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -137,6 +143,8 @@ function dws_lowc_get_create_child_url( int $parent_id ): string { * * Returns null when the parent is missing or unsupported, or when the creator is not callable or creates no supported order, the creator's error when it fails, and an error when the link is refused, after deleting the child. * + * @api + * * @since 1.0.0 * @version 2.0.0 * diff --git a/plugins/linked-orders-for-woocommerce/includes/settings.php b/plugins/linked-orders-for-woocommerce/includes/settings.php index f71f0b8..4b83283 100644 --- a/plugins/linked-orders-for-woocommerce/includes/settings.php +++ b/plugins/linked-orders-for-woocommerce/includes/settings.php @@ -23,6 +23,8 @@ /** * Returns the stored value of a setting, or null for a setting that is not stored or a group v1 did not have. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -39,6 +41,8 @@ function dws_lowc_get_raw_setting( string $field_id, ?string $group = null ) { /** * Returns the validated value of a setting, filtered, or null for a group v1 did not have. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @@ -97,6 +101,8 @@ function dws_lowc_get_validated_setting( string $field_id, ?string $group = null /** * Returns a value validated as the given built-in setting stores it, or the value itself for any other field. * + * @api + * * @since 1.1.0 * @version 2.0.0 * diff --git a/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php b/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php index 3c44745..c073432 100644 --- a/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php +++ b/plugins/linked-orders-for-woocommerce/models/DWS_Order_Node.php @@ -7,6 +7,8 @@ * * It keeps v1's global name, which other plugins type-hint, and calls to it that name no method of its own reach the order. * + * @api + * * @since 1.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md index 5ca1422..7b1443d 100644 --- a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md +++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md @@ -59,7 +59,7 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on, and t ## Conventions 1. **Placement.** `src/` holds the composition root, its components and the plan tab host. Code that registers no hooks, such as keys, readers, v1's helper API, the footprint and migration steps, is `dws_lpmwc_` functions and `DWS_LPMWC_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match`, nullsafe calls or first-class callables there. Container definitions are composition and stay in `Plugin::get_definitions()`. -2. **Strategies.** Each way of unlocking is a component in `src/UnlockStrategies/`, gated on its switch, and an integration on its plugin too through a static `is_available()` that the settings read. It answers `dws_lpmwc/payment_method/should_lock` from the arguments alone, through an `@internal` static `unlocks()` that v1's per-strategy checks call, and returns `false` or the lock it was given. The safeguard passes the current user and, on an order's own payment page with its key, that order. +2. **Strategies.** Each way of unlocking is a component in `src/UnlockStrategies/`, gated on its switch, and an integration on its plugin too through a static `is_available()` that the settings read. It answers `dws_lpmwc/payment_method/should_lock` from the arguments alone, through a static `unlocks()` that v1's per-strategy checks call, and returns `false` or the lock it was given. The safeguard passes the current user and, on an order's own payment page with its key, that order. 3. **Request context.** `is_admin()` is false on REST requests and true in admin-ajax ones, so an admin-only feature decides the request context in its callbacks, never in `should_load()`. 4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. It reads a template-built base plugin's slug, name and version only through that plugin's `{prefix}_get_plugin_*()` getters, from `plugins_loaded` on, since the add-on may load first, and behind `function_exists()`, since a base plugin below its floors or with a partial build may stay active without defining them. 5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input. diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php index 3a1ff9c..01ba6ac 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php @@ -37,6 +37,8 @@ /** * The plugin's untranslated name. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_name(). @@ -46,6 +48,8 @@ /** * The plugin's version. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_version(). @@ -55,6 +59,8 @@ /** * The path of the plugin's main file. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use DWS_LPMWC_FILE. @@ -64,6 +70,8 @@ /** * The oldest PHP version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -73,6 +81,8 @@ /** * The oldest WordPress version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -107,6 +117,8 @@ /** * Returns the plugin's name, translated once init has fired. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_name(). @@ -121,6 +133,8 @@ function dws_lpmwc_name(): string { /** * Returns the plugin's version. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_version(). @@ -135,6 +149,8 @@ function dws_lpmwc_version(): string { /** * Returns the path of the plugin's main file. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -149,6 +165,8 @@ function dws_lpmwc_path(): string { /** * Returns the oldest PHP version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -163,6 +181,8 @@ function dws_lpmwc_min_php(): string { /** * Returns the oldest WordPress version the plugin runs on. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -177,6 +197,8 @@ function dws_lpmwc_min_wp(): string { /** * Returns a hook name as v1 built it for the plugin. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -194,6 +216,8 @@ function dws_lpmwc_get_hook_tag( string $name, $extra = array() ): string { /** * Returns a hook name as v1 built it for one of its components, or null for an id v1 had no component for. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -270,6 +294,8 @@ function dws_lpmwc_fire_v1_initialization_failure( Throwable $error ): void { /** * Returns whether a payment method stays locked for a user, as the plugin's strategies judge it, firing v1's filter first. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -300,6 +326,8 @@ function dws_lpmwc_check_payment_method_access_for_user( string $locked_method_i /** * Returns whether a payment method stays locked for paying an order, as the plugin's strategies judge it for the order and its customer, firing v1's filter first. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -331,6 +359,8 @@ function dws_lpmwc_check_payment_method_access_for_wc_order( string $locked_meth /** * Returns whether the user-profile strategy leaves a payment method locked for a user, true while the strategy is off. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -348,6 +378,8 @@ function dws_lpmwc_check_payment_method_access_via_user_meta( string $locked_met /** * Returns whether the role strategy leaves a payment method locked for a user, true while the strategy is off. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -365,6 +397,8 @@ function dws_lpmwc_check_payment_method_access_via_user_roles( string $locked_me /** * Returns whether the Groups strategy leaves a payment method locked for a user, true while the strategy is off. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -382,6 +416,8 @@ function dws_lpmwc_check_payment_method_access_via_groups( string $locked_method /** * Returns whether the WooCommerce Memberships strategy leaves a payment method locked for a user, true while the strategy is off. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -399,6 +435,8 @@ function dws_lpmwc_check_payment_method_access_via_wc_memberships( string $locke /** * Returns whether the order strategy leaves a payment method locked for paying an order, true while the strategy is off. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked(). @@ -416,6 +454,8 @@ function dws_lpmwc_check_payment_method_access_via_wc_order_meta( string $locked /** * Returns a setting's stored value, by v1's field id and group, or null for an id v1 had no setting for. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use get_option(). @@ -434,6 +474,8 @@ function dws_lpmwc_get_raw_setting( string $field_id, ?string $group = null ) { /** * Returns a setting's value, of the setting's kind, by v1's field id and group, through v1's value filters, or null for an id v1 had no setting for. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use get_option(). @@ -451,6 +493,8 @@ function dws_lpmwc_get_validated_setting( string $field_id, ?string $group = nul /** * Returns a value as the setting's kind, by v1's field id and group: a boolean for a switch and a list of strings for a list, falling back to the setting's default. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 @@ -470,6 +514,8 @@ function dws_lpmwc_validate_setting( $value, string $field_id, ?string $group = /** * Returns a General setting's value, of the setting's kind, by v1's field id, through v1's value filter. * + * @api + * * @since 1.0.0 * @version 2.0.0 * @deprecated 2.0.0 Use get_option(). diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php index 8f6b31c..52ed497 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php @@ -168,6 +168,8 @@ function dws_lpmwc_get_plan_unlocked_payment_method_ids( int $plan_id ): array { /** * Returns whether a payment method is locked for a user and an order: whether the locked list holds it and dws_lpmwc/payment_method/should_lock does not relax it. * + * @api + * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php b/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php index 36fff2b..3e99b32 100644 --- a/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php +++ b/plugins/locked-payment-methods-for-woocommerce/includes/metadata.php @@ -15,6 +15,8 @@ /** * Returns the plugin's slug, which is the text domain its header declares. * + * @api + * * @since 2.0.0 * @version 2.0.0 * @@ -29,6 +31,8 @@ function dws_lpmwc_get_plugin_slug(): string { * * WordPress flags translations loaded earlier. * + * @api + * * @since 2.0.0 * @version 2.0.0 * @@ -42,6 +46,8 @@ function dws_lpmwc_get_plugin_name(): string { /** * Returns the plugin's version from its header. * + * @api + * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php index 20ec9f4..b51250c 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php @@ -111,8 +111,6 @@ public static function is_available(): bool { /** * Returns whether a group of the user, or a group above one, unlocks the payment method. * - * @internal - * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php index 2dce681..04c1af7 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php @@ -109,8 +109,6 @@ public function get_object_form( string $form_id, int $object_id ): array { /** * Returns whether the order's grants list holds the payment method. * - * @internal - * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php index aea2a23..0d63575 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php @@ -119,8 +119,6 @@ public function get_object_form( string $form_id, int $object_id ): array { /** * Returns whether the user's grants list on the current site holds the payment method. * - * @internal - * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php index b10e11c..1a7eb1e 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php @@ -45,8 +45,6 @@ public function register_hooks(): void { /** * Returns whether the user holds one of the full-access roles, which unlocks every locked payment method. * - * @internal - * * @since 2.0.0 * @version 2.0.0 * diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php index 3d8607f..096a873 100644 --- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php +++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php @@ -118,8 +118,6 @@ public static function is_available(): bool { /** * Returns whether a plan the user is an active member of unlocks the payment method. * - * @internal - * * @since 2.0.0 * @version 2.0.0 * From 1754cba868d73a5486121e44ca413a713c028dd7 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:59:45 +0100 Subject: [PATCH 37/39] State each plugin's contract and supported versions No plugin's AGENTS.md said what its 2.x contract is, and no SECURITY.md said which releases receive fixes, where X150 and X360 give each AGENTS.md the template's Contract section and A767 each SECURITY.md a Supported versions section naming 2.x and 1.x. Each plugin's AGENTS.md gains the Contract section between Conventions and Style, in the template's words, and each SECURITY.md, the root's for every plugin included, says that only the latest 2.x release receives security fixes and 1.x none. Assisted-by: Claude Code:claude-opus-5-5 --- SECURITY.md | 4 ++++ plugins/internal-comments/AGENTS.md | 4 ++++ plugins/internal-comments/SECURITY.md | 4 ++++ plugins/linked-orders-for-woocommerce/AGENTS.md | 4 ++++ plugins/linked-orders-for-woocommerce/SECURITY.md | 4 ++++ plugins/locked-payment-methods-for-woocommerce/AGENTS.md | 4 ++++ plugins/locked-payment-methods-for-woocommerce/SECURITY.md | 4 ++++ 7 files changed, 28 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index d2af903..659ca06 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,10 @@ Report it privately through [GitHub's security advisory form](https://github.com/ahegyes/wordpress-plugins/security/advisories/new), never in a public issue. The first response comes within 7 days. +## Supported versions + +For each plugin, only the latest 2.x release receives security fixes. 1.x receives none. + ## Scope In scope: every plugin under `plugins/`, its prefixing configuration and its workflows, and the monorepo's own workflows. diff --git a/plugins/internal-comments/AGENTS.md b/plugins/internal-comments/AGENTS.md index c31a919..673f0ae 100644 --- a/plugins/internal-comments/AGENTS.md +++ b/plugins/internal-comments/AGENTS.md @@ -74,6 +74,10 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI ru 10. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_ic_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks. 11. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails; bulk work is a batched step, which runs only from the runner's scheduled hook. +## Contract + +The plugin's contract is the elements tagged `@api`, the hooks it fires, and its option, meta and capability names and REST routes; breaking it ships as the next major. Every other function, constant and class, `src/` included, is internal and changes in any release. + ## Style - Every PHP file opens with ` Date: Wed, 11 Nov 2026 17:01:51 +0100 Subject: [PATCH 38/39] Name the Unit suites' header and stub checks Each plugin's tests README listed the Unit suite's checks without the entry headers row, the readme's Tested up to row or the stub check this branch adds. Each Unit row names them. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/tests/README.md | 2 +- plugins/linked-orders-for-woocommerce/tests/README.md | 2 +- plugins/locked-payment-methods-for-woocommerce/tests/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/internal-comments/tests/README.md b/plugins/internal-comments/tests/README.md index 80f82ea..f0aa3a5 100644 --- a/plugins/internal-comments/tests/README.md +++ b/plugins/internal-comments/tests/README.md @@ -6,7 +6,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | -| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data and capabilities, and the readme's version and floors | +| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data and capabilities, the readme's version and floors, the entry's `WC tested up to` against the locked WooCommerce, the entry's lack of a `Tested up to`, which the readme alone holds, and the stub in every shipped directory | | Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded, and the multisite site for its `multisite` group | the boot, every component, v1's keys, settings page and API, comment API, model class, capabilities and deprecated functions and hooks, the meta capabilities and private threads, the guards that keep internal comments under their status and the reads that leave them out without the plugin, the REST routes, the meta box, the list column, Comments screen and edit screen, orders, the migrations and the batched move of v1's comments, uninstall after v1's consent, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | posting, replying to, editing and trashing comments in the meta box, on a post and on an order, an empty comment refused, the quick view and the Comments screen's view, axe on the meta box and the quick view, and the plugin with WooCommerce deactivated | diff --git a/plugins/linked-orders-for-woocommerce/tests/README.md b/plugins/linked-orders-for-woocommerce/tests/README.md index 1050a8b..5f2210c 100644 --- a/plugins/linked-orders-for-woocommerce/tests/README.md +++ b/plugins/linked-orders-for-woocommerce/tests/README.md @@ -6,7 +6,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | -| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors | +| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, the readme's version and floors, the entry's `WC tested up to` against the locked WooCommerce, the entry's lack of a `Tested up to`, which the readme alone holds, and the stub in every shipped directory | | Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded, and the multisite site for its `multisite` group | the boot, every component, v1's keys, settings API, linking API, node class, capabilities and deprecated functions and hooks, the options and their readers, the migrations, deactivation, uninstall after v1's consent, the assets, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait` | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | a child created from an order's meta box and both orders' depth in the orders list, the plugin row's link to its settings section, and the plugin with WooCommerce deactivated | diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/README.md b/plugins/locked-payment-methods-for-woocommerce/tests/README.md index cde4bcd..5189667 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/README.md +++ b/plugins/locked-payment-methods-for-woocommerce/tests/README.md @@ -6,7 +6,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | -| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors | +| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, the readme's version and floors, the entry's `WC tested up to` against the locked WooCommerce, the entry's lack of a `Tested up to`, which the readme alone holds, and the stub in every shipped directory | | Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce, Groups and order tables loaded, and the multisite site, without Groups, for its `multisite` group | the boot, the lock and its relaxation, fresh requests below the PHP and WooCommerce floors, every strategy and its form, the settings, the capabilities, v1's deprecated functions and hooks, the migrations from v1, deactivation, uninstall, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network; every row whose outcome the order storage can change runs on all four storages through `OrderStorageTrait`, the Groups rows run in the `groups` group against Groups itself, and the Memberships rows in the `wc-memberships` group against a double of its one function | | End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | a locked payment method a customer is granted on the block checkout, the Store API's refusal of a guest's checkout with a locked payment method, and the plugin with WooCommerce deactivated; the monorepo's shared environment checks a locked payment method's absence from a guest's classic and block checkout | From 6c7f05ffcd400883c8501d9cb0e9fe5271bfb709 Mon Sep 17 00:00:00 2001 From: Tony Hegyes <36231502+ahegyes@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:04:26 +0100 Subject: [PATCH 39/39] Name the class each one-class test covers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every plugin test carried #[CoversNothing], though style guide §7 gives a test that covers one class #[CoversClass], and each phpunit.dist.xml's held only src/, leaving the includes/ and models/ code most tests reach outside it (A470, A471, as X364 folds A573 into this branch). Seventeen tests whose subject is one src/ class name it: Linked Orders' six component tests, Internal Comments' five, and Locked Payment Methods' settings and five strategy tests. Tests that reach the includes/ API or several classes keep #[CoversNothing], Internal Comments' CommentsTest among them, since it covers the comment functions rather than the Comments component. Each adds includes/, and models/ where the plugin has one; no lane collects coverage. Assisted-by: Claude Code:claude-opus-5-5 --- plugins/internal-comments/phpunit.dist.xml | 2 ++ .../internal-comments/tests/Integration/AdminScreensTest.php | 4 ++-- plugins/internal-comments/tests/Integration/MetaBoxTest.php | 4 ++-- .../internal-comments/tests/Integration/PermissionsTest.php | 5 +++-- .../internal-comments/tests/Integration/RestRoutesTest.php | 5 +++-- plugins/internal-comments/tests/Integration/SettingsTest.php | 4 ++-- plugins/linked-orders-for-woocommerce/phpunit.dist.xml | 2 ++ .../tests/Integration/AutocompletionTest.php | 5 +++-- .../tests/Integration/ChildCreationTest.php | 5 +++-- .../Integration/SequentialOrderNumbersProTest.php | 4 ++-- .../tests/Integration/ListTableTest.php | 5 +++-- .../tests/Integration/MetaBoxTest.php | 5 +++-- .../tests/Integration/SettingsTest.php | 4 ++-- .../locked-payment-methods-for-woocommerce/phpunit.dist.xml | 1 + .../tests/Integration/SettingsTest.php | 4 ++-- .../Integration/UnlockStrategies/GroupsStrategyTest.php | 4 ++-- .../tests/Integration/UnlockStrategies/OrderStrategyTest.php | 4 ++-- .../Integration/UnlockStrategies/UserProfileStrategyTest.php | 4 ++-- .../Integration/UnlockStrategies/UserRoleStrategyTest.php | 4 ++-- .../UnlockStrategies/WooCommerceMembershipsStrategyTest.php | 4 ++-- 20 files changed, 45 insertions(+), 34 deletions(-) diff --git a/plugins/internal-comments/phpunit.dist.xml b/plugins/internal-comments/phpunit.dist.xml index c04a1e5..62148d3 100644 --- a/plugins/internal-comments/phpunit.dist.xml +++ b/plugins/internal-comments/phpunit.dist.xml @@ -27,6 +27,8 @@ src + includes + models diff --git a/plugins/internal-comments/tests/Integration/AdminScreensTest.php b/plugins/internal-comments/tests/Integration/AdminScreensTest.php index 75e1e4e..8df558e 100644 --- a/plugins/internal-comments/tests/Integration/AdminScreensTest.php +++ b/plugins/internal-comments/tests/Integration/AdminScreensTest.php @@ -6,10 +6,10 @@ use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( AdminScreens::class )] final class AdminScreensTest extends TestCase { use RecordsDeprecationsTrait; diff --git a/plugins/internal-comments/tests/Integration/MetaBoxTest.php b/plugins/internal-comments/tests/Integration/MetaBoxTest.php index a52b06b..f27a998 100644 --- a/plugins/internal-comments/tests/Integration/MetaBoxTest.php +++ b/plugins/internal-comments/tests/Integration/MetaBoxTest.php @@ -6,10 +6,10 @@ use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( MetaBox::class )] final class MetaBoxTest extends TestCase { use RecordsDeprecationsTrait; diff --git a/plugins/internal-comments/tests/Integration/PermissionsTest.php b/plugins/internal-comments/tests/Integration/PermissionsTest.php index d9de004..f0bfa79 100644 --- a/plugins/internal-comments/tests/Integration/PermissionsTest.php +++ b/plugins/internal-comments/tests/Integration/PermissionsTest.php @@ -2,11 +2,12 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; +use DeepWebSolutions\InternalComments\Permissions; use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( Permissions::class )] final class PermissionsTest extends TestCase { protected static array $users = array(); diff --git a/plugins/internal-comments/tests/Integration/RestRoutesTest.php b/plugins/internal-comments/tests/Integration/RestRoutesTest.php index 2d4202c..1334f4a 100644 --- a/plugins/internal-comments/tests/Integration/RestRoutesTest.php +++ b/plugins/internal-comments/tests/Integration/RestRoutesTest.php @@ -2,13 +2,14 @@ namespace DeepWebSolutions\InternalComments\Tests\Integration; +use DeepWebSolutions\InternalComments\RestRoutes; use DeepWebSolutions\InternalComments\Tests\Support\RecordsDeprecationsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( RestRoutes::class )] final class RestRoutesTest extends TestCase { use RecordsDeprecationsTrait; diff --git a/plugins/internal-comments/tests/Integration/SettingsTest.php b/plugins/internal-comments/tests/Integration/SettingsTest.php index 51fa1be..07ae2f4 100644 --- a/plugins/internal-comments/tests/Integration/SettingsTest.php +++ b/plugins/internal-comments/tests/Integration/SettingsTest.php @@ -8,11 +8,11 @@ use DeepWebSolutions\InternalComments\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( Settings::class )] final class SettingsTest extends TestCase { use RecordsDeprecationsTrait; use RestoresOptionsTrait; diff --git a/plugins/linked-orders-for-woocommerce/phpunit.dist.xml b/plugins/linked-orders-for-woocommerce/phpunit.dist.xml index c04a1e5..62148d3 100644 --- a/plugins/linked-orders-for-woocommerce/phpunit.dist.xml +++ b/plugins/linked-orders-for-woocommerce/phpunit.dist.xml @@ -27,6 +27,8 @@ src + includes + models diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php index 876a848..5f15d14 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/AutocompletionTest.php @@ -2,14 +2,15 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; +use DeepWebSolutions\LinkedOrders\Autocompletion; use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( Autocompletion::class )] final class AutocompletionTest extends TestCase { use OrderStorageTrait; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php index e9e1dbe..6dbe6dc 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ChildCreationTest.php @@ -2,6 +2,7 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; +use DeepWebSolutions\LinkedOrders\ChildCreation; use DeepWebSolutions\LinkedOrders\Plugin; use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\AdminNotices; use DeepWebSolutions\LinkedOrders\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\Notice; @@ -10,11 +11,11 @@ use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( ChildCreation::class )] final class ChildCreationTest extends TestCase { use OrderStorageTrait; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php index f4aaa57..cb71e22 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/Integration/SequentialOrderNumbersProTest.php @@ -4,11 +4,11 @@ use DeepWebSolutions\LinkedOrders\Integration\SequentialOrderNumbersPro; use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( SequentialOrderNumbersPro::class )] final class SequentialOrderNumbersProTest extends TestCase { use OrderStorageTrait; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php index 015885e..244f189 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/ListTableTest.php @@ -3,16 +3,17 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; use Automattic\WooCommerce\Utilities\OrderUtil; +use DeepWebSolutions\LinkedOrders\ListTable; use DeepWebSolutions\LinkedOrders\Plugin; use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( ListTable::class )] final class ListTableTest extends TestCase { use OrderStorageTrait; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php index a19d05d..e79f1b4 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/MetaBoxTest.php @@ -3,15 +3,16 @@ namespace DeepWebSolutions\LinkedOrders\Tests\Integration; use Automattic\WooCommerce\Utilities\OrderUtil; +use DeepWebSolutions\LinkedOrders\MetaBox; use DeepWebSolutions\LinkedOrders\Tests\Support\OrderStorageTrait; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( MetaBox::class )] final class MetaBoxTest extends TestCase { use OrderStorageTrait; diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php index ada996a..91a39ef 100644 --- a/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/linked-orders-for-woocommerce/tests/Integration/SettingsTest.php @@ -6,11 +6,11 @@ use DeepWebSolutions\LinkedOrders\Tests\Support\RecordsDeprecationsTrait; use DeepWebSolutions\LinkedOrders\Tests\Support\RestoresOptionsTrait; use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( Settings::class )] final class SettingsTest extends TestCase { use RecordsDeprecationsTrait; use RestoresOptionsTrait; diff --git a/plugins/locked-payment-methods-for-woocommerce/phpunit.dist.xml b/plugins/locked-payment-methods-for-woocommerce/phpunit.dist.xml index c04a1e5..85246b7 100644 --- a/plugins/locked-payment-methods-for-woocommerce/phpunit.dist.xml +++ b/plugins/locked-payment-methods-for-woocommerce/phpunit.dist.xml @@ -27,6 +27,7 @@ src + includes diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php index 91ad0ce..2154bf2 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php @@ -7,11 +7,11 @@ use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RestoresOptionsTrait; use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\GroupsStrategy; use PHPUnit\Framework\Attributes\After; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( Settings::class )] final class SettingsTest extends TestCase { use RecordsDeprecationsTrait; use RestoresOptionsTrait; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php index 756f1dc..53f9751 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php @@ -8,11 +8,11 @@ use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\GroupsStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( GroupsStrategy::class )] #[Group( 'groups' )] final class GroupsStrategyTest extends TestCase { use RestoresOptionsTrait; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php index 69e4b2a..7c5b4cb 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php @@ -11,11 +11,11 @@ use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\OrderStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( OrderStrategy::class )] final class OrderStrategyTest extends TestCase { use OrderStorageTrait; use RestoresOptionsTrait; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php index 2a3d864..23d8faf 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php @@ -7,10 +7,10 @@ use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserProfileStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( UserProfileStrategy::class )] final class UserProfileStrategyTest extends TestCase { use RestoresOptionsTrait; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php index 13fc6d9..4188367 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserRoleStrategyTest.php @@ -8,10 +8,10 @@ use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserRoleStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversNothing] +#[CoversClass( UserRoleStrategy::class )] final class UserRoleStrategyTest extends TestCase { use RecordsDeprecationsTrait; use RestoresOptionsTrait; diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php index 702a243..0bc42f4 100644 --- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php +++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php @@ -9,14 +9,14 @@ use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\WooCommerceMembershipsStrategy; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; require_once \dirname( __DIR__, 2 ) . '/Support/memberships-functions.php'; // Memberships is a paid plugin absent from WP Packages, so these rows run against a double of its one function and its three hooks, unverified against a licensed copy. -#[CoversNothing] +#[CoversClass( WooCommerceMembershipsStrategy::class )] #[Group( 'wc-memberships' )] final class WooCommerceMembershipsStrategyTest extends TestCase { use RestoresOptionsTrait;