Skip to content

[RFC]: Replace laminas-coding-standard with a Mago-based phpdb-coding-standards package #157

Description

@simon-mundy

Proposed Version

0.6.x

Basic Information

This RFC proposes that the php-db organization cease using laminas/laminas-coding-standard (a PHP_CodeSniffer ruleset) and standardize on Mago, a linter, formatter, and static analyzer for PHP written in Rust.

To keep configuration consistent across all php-db repositories, a new Composer-installable package, php-db/phpdb-coding-standards, would provide:

  • a shared base mago.toml (formatter, linter, and analyzer configuration) that downstream repositories inherit via Mago's extends directive;
  • a baseline phpunit.xml.dist template and shared PHPUnit conventions (strictness flags, test-suite layout, cache directory);
  • standard Composer script definitions (check, cs-check, cs-fix, static-analysis, test) so every repository is driven the same way locally and in CI.

The Mago binary itself will be installed by developers into their local environment using the standard installation instructions. The shared mago.toml's version pin keeps everyone consistent regardless of how the binary got onto the machine.

Background

laminas-coding-standard is a PHP_CodeSniffer ruleset extending PSR-12. It addresses only one of the three concerns a modern PHP project needs to cover:

  1. Coding style — covered by phpcs/phpcbf via laminas-coding-standard.
  2. Static analysisnot covered; requires a separate tool (Psalm/PHPStan) with its own configuration, baseline handling, and CI step.
  3. Formatting — phpcbf can fix sniff violations, but it is not a deterministic formatter; two developers can produce differently formatted code that both pass the sniffs.

In practice each php-db repository carries its own drift-prone configuration of phpcs.xml, analysis config, and phpunit.xml.dist.

Mago consolidates all three concerns into a single, fast, Rust-based binary:

  • mago lint, mago format, and mago analyze are all configured from a single mago.toml. There is no phpcs + php-cs-fixer + Psalm/PHPStan matrix to keep aligned.
  • The formatter follows PER-CS (the successor to PSR-12) by default and produces deterministic output.
  • The analyzer performs type-checking and dead-code detection and understands existing Psalm/PHPStan annotations, so we gain analysis coverage we currently do not have, without rewriting docblocks.
  • The linter ships laminas and phpunit integrations (both already enabled in the reference implementation), which matters given our components are laminas-validator/servicemanager adjacent.
  • Since Mago 1.25, mago.toml supports an extends directive — exactly the mechanism a shared standards package needs.
  • Mago is a single static binary with no Composer or PHP dependency of its own.

Considerations

  • Each repo swaps laminas/laminas-coding-standard (and any Psalm/PHPStan setup) for php-db/phpdb-coding-standards in require-dev, replaces phpcs.xml with a minimal mago.toml, and updates its CI workflow.
  • The initial reformat touches many lines. Committing it as a single, isolated commit recorded in .git-blame-ignore-revs keeps git blame useful, but open PRs at migration time will need rebasing.
  • Contributors need the Mago binary installed (one-liner, documented in the package README). The version pin in the shared config warns immediately on a stale or too-new binary rather than producing subtly different formatting.
  • Mago's linter is curated rather than sniff-compatible with laminas-coding-standard. Some sniffs may have no direct equivalent, and Mago will flag things phpcs never did (especially the analyzer). Repos can temporarily relax specific rules locally with a tracking issue to remove the relaxation, but expect triage effort on the first few migrations.
  • PER-CS supersedes PSR-12, but our chosen settings (align-assignment-like, sort-class-methods, yoda-conditions, print-width = 120) are opinionated choices the team should explicitly ratify.
  • Mago reached 1.0 in December 2025, primarily maintained by one developer (backed by Carthage Software), versus PHP_CodeSniffer's 15+ years. The version pin keeps upgrades deliberate; a static binary keeps working even if upstream stalls; the migration is reversible since phpcs configs are trivially recreated.

Proposal(s)

1. Create php-db/phpdb-coding-standards

A new repository and Composer package:

phpdb-coding-standards/
├── ci.yml                  # github CI workflow
├── composer.json           # config-only package; suggests phpunit
├── mago.toml               # the shared base configuration (formatter/linter/analyzer)
├── templates/
│   └── phpunit.xml.dist    # baseline PHPUnit config to copy into consuming repos
├── docs/
│   ├── migration.md        # step-by-step migration guide from laminas-coding-standard
│   └── rules.md            # rationale for non-default rule choices
└── README.md
{
    "name": "php-db/phpdb-coding-standards",
    "description": "Shared Mago + PHPUnit configuration for php-db components.",
    "license": "BSD-3-Clause",
    "require": {
        "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
    },
    "suggest": {
        "phpunit/phpunit": "To run the shared test configuration (^11.5 || ^12.0)"
    }
}

The package is configuration-only. Installing the Mago binary itself is a one-time developer prerequisite, documented in the package README:

curl --proto '=https' --tlsv1.2 -sSf https://carthage.software/mago.sh | bash
# or: brew install mago  /  cargo install mago

2. Base mago.toml

Formatter: print-width = 120, 4-space indentation, PER-CS defaults, the preserve-breaking-* family enabled so intentional line breaks survive reformatting, plus align-assignment-like and sort-class-methods.

Linter: minimum-fail-level = "Help" (everything reported fails the build), integrations = ["laminas", "phpunit"], and ~50 curated rules covering naming (PSR class/interface/trait names, camelCase methods and variables), safety (no-parameter-shadowing, no-assign-in-argument, no-dead-store, no-unused-*), and modernization (prefer-array-spread, prefer-first-class-callable, prefer-test-attribute).

Analyzer: strict settings including check-throws with a shared unchecked-exceptions list, find-unused-definitions, analyze-dead-code, check-missing-type-hints, check-property-initialization, enforce-class-finality, and require-api-or-internal.

3. Consuming repositories

Each repository's mago.toml shrinks to the project-specific facts:

extends = "vendor/php-db/phpdb-coding-standards/mago.toml"
php-version = "8.2.0"

[source]
paths = ["src", "test"]
includes = ["vendor"]

[analyzer]
class-initializers = [
    "PHPUnit\\Framework\\TestCase::setUp",
]

Mago's merge semantics (nested tables merge deeply, arrays concatenate, child scalars win) let repositories tighten or relax individual rules locally without forking the whole standard.

Standard Composer scripts:

{
    "require-dev": {
        "php-db/phpdb-coding-standards": "^1.0",
        "phpunit/phpunit": "^11.5"
    },
    "scripts": {
        "check": ["@cs-check", "@static-analysis", "@test"],
        "cs-check": ["mago format --check", "mago lint"],
        "cs-fix": ["mago format", "mago lint --fix"],
        "static-analysis": "mago analyze",
        "test": "phpunit --colors=always --testsuite \"unit test\"",
        "test-integration": "phpunit --colors=always --testsuite \"integration test\"",
        "test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
    }
}

4. PHPUnit configuration

PHPUnit has no extends, so the package ships templates/phpunit.xml.dist with failOnWarning, failOnDeprecation, failOnNotice, beStrictAboutOutputDuringTests, beStrictAboutTestsThatDoNotTestAnything, all displayDetailsOn* flags, cacheDirectory=".phpunit.cache", UTC timezone) together with the standard test-suite layout (test/unit, test/integration, test/asset).

Repositories copy the template once at migration; the migration guide documents the flags so future changes are a deliberate diff rather than drift. Mago's phpunit linter integration additionally enforces test conventions (e.g. prefer-test-attribute) at lint time.

5. CI

name: "CI"

on:
  push:
  pull_request:

jobs:
  mago:
    name: "PHP ${{ matrix.php }} | ${{ matrix.dependencies }}"
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.2', '8.3', '8.4', '8.5']
        dependencies: ['lowest', 'locked', 'latest']
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up PHP and Mago
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none
          tools: mago

      # Resolve dependencies at the configured strategy level.
      # "locked" uses the committed composer.lock as-is.
      - name: Install dependencies (lowest)
        if: matrix.dependencies == 'lowest'
        run: composer update --prefer-lowest --no-interaction --no-progress

      - name: Install dependencies (locked)
        if: matrix.dependencies == 'locked'
        run: composer install --no-interaction --no-progress

      - name: Install dependencies (latest)
        if: matrix.dependencies == 'latest'
        run: composer update --no-interaction --no-progress

      # Set the Mago PHP version via env var so it overrides mago.toml
      # for each matrix PHP version without mutating the config file.
      - name: Pin Mago PHP version
        run: echo "MAGO_PHP_VERSION=${{ matrix.php }}" >> "$GITHUB_ENV"

      - name: Check formatting
        run: mago format --check

      - name: Lint
        if: success() || failure()
        run: mago lint

      - name: Analyze
        if: success() || failure()
        run: mago analyze

      - name: Test
        if: success() || failure()
        run: ./vendor/bin/phpunit --colors=always

6. Migration plan

Repository-by-repository, in dependency order, using phpdb-validator as the template:

  1. Tag and release php-db/phpdb-coding-standards 1.0 (config extracted from phpdb-validator).
  2. Switch phpdb-validator itself to consume the package (replacing its standalone mago.toml with the extends form) to validate the inheritance mechanism.
  3. For each remaining repository: ensure Mago is installed locally; swap the dev dependencies; delete phpcs.xml(.dist) and .phpcs-cache; add the minimal mago.toml; copy the PHPUnit template; run composer cs-fix and commit the mechanical reformat as a single isolated commit recorded in .git-blame-ignore-revs; fix or explicitly suppress remaining findings in follow-up commits; update CI.
  4. Once all repositories are migrated, document the standard in the org-level README/contributing guide.

Version pinning: the base mago.toml sets version so all repositories run the same Mago release, and Renovate (already in use) manages upgrades of both phpdb-coding-standards and the pinned Mago version as ordinary PRs.

Appendix/Additional Info

Metadata

Metadata

Assignees

Labels

Platform migrationChanges related to platform migrationsdocumentationImprovements or additions to documentationnext minorTarget next minor release.qaImprovements in quality assurance of the project

Projects

Status
Todo
Status
Todo
Status
Todo

Relationships

None yet

Development

No branches or pull requests

Issue actions