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:
- Coding style — covered by phpcs/phpcbf via laminas-coding-standard.
- Static analysis — not covered; requires a separate tool (Psalm/PHPStan) with its own configuration, baseline handling, and CI step.
- 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:
- Tag and release
php-db/phpdb-coding-standards 1.0 (config extracted from phpdb-validator).
- Switch
phpdb-validator itself to consume the package (replacing its standalone mago.toml with the extends form) to validate the inheritance mechanism.
- 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.
- 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
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:mago.toml(formatter, linter, and analyzer configuration) that downstream repositories inherit via Mago'sextendsdirective;phpunit.xml.disttemplate and shared PHPUnit conventions (strictness flags, test-suite layout, cache directory);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'sversionpin keeps everyone consistent regardless of how the binary got onto the machine.Background
laminas-coding-standardis a PHP_CodeSniffer ruleset extending PSR-12. It addresses only one of the three concerns a modern PHP project needs to cover:In practice each php-db repository carries its own drift-prone configuration of
phpcs.xml, analysis config, andphpunit.xml.dist.Mago consolidates all three concerns into a single, fast, Rust-based binary:
mago lint,mago format, andmago analyzeare all configured from a singlemago.toml. There is no phpcs + php-cs-fixer + Psalm/PHPStan matrix to keep aligned.laminasandphpunitintegrations (both already enabled in the reference implementation), which matters given our components are laminas-validator/servicemanager adjacent.mago.tomlsupports anextendsdirective — exactly the mechanism a shared standards package needs.Considerations
laminas/laminas-coding-standard(and any Psalm/PHPStan setup) forphp-db/phpdb-coding-standardsinrequire-dev, replacesphpcs.xmlwith a minimalmago.toml, and updates its CI workflow..git-blame-ignore-revskeepsgit blameuseful, but open PRs at migration time will need rebasing.versionpin in the shared config warns immediately on a stale or too-new binary rather than producing subtly different formatting.align-assignment-like,sort-class-methods,yoda-conditions,print-width = 120) are opinionated choices the team should explicitly ratify.Proposal(s)
1. Create
php-db/phpdb-coding-standardsA new repository and Composer package:
{ "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:
2. Base
mago.tomlFormatter:
print-width = 120, 4-space indentation, PER-CS defaults, thepreserve-breaking-*family enabled so intentional line breaks survive reformatting, plusalign-assignment-likeandsort-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-throwswith a shared unchecked-exceptions list,find-unused-definitions,analyze-dead-code,check-missing-type-hints,check-property-initialization,enforce-class-finality, andrequire-api-or-internal.3. Consuming repositories
Each repository's
mago.tomlshrinks to the project-specific facts: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 shipstemplates/phpunit.xml.distwithfailOnWarning,failOnDeprecation,failOnNotice,beStrictAboutOutputDuringTests,beStrictAboutTestsThatDoNotTestAnything, alldisplayDetailsOn*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
phpunitlinter integration additionally enforces test conventions (e.g.prefer-test-attribute) at lint time.5. CI
6. Migration plan
Repository-by-repository, in dependency order, using
phpdb-validatoras the template:php-db/phpdb-coding-standards1.0 (config extracted fromphpdb-validator).phpdb-validatoritself to consume the package (replacing its standalonemago.tomlwith theextendsform) to validate the inheritance mechanism.phpcs.xml(.dist)and.phpcs-cache; add the minimalmago.toml; copy the PHPUnit template; runcomposer cs-fixand 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.Version pinning: the base
mago.tomlsetsversionso all repositories run the same Mago release, and Renovate (already in use) manages upgrades of bothphpdb-coding-standardsand the pinned Mago version as ordinary PRs.Appendix/Additional Info
extends— https://mago.carthage.software/main/en/guide/configuration/