diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..de3425c --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,62 @@ +name: "QA" + +on: + workflow_call: + inputs: + php-versions: + description: "JSON array of PHP versions to run the matrix against." + type: string + required: false + default: '["8.2", "8.3", "8.4", "8.5"]' + run-integration: + description: "Run the integration test suite in addition to the unit suite." + type: boolean + required: false + default: false + composer-options: + description: "Extra flags passed to composer install." + type: string + required: false + default: "" + +jobs: + qa: + name: "PHP ${{ matrix.php }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ${{ fromJSON(inputs.php-versions) }} + 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 + + - name: Install dependencies + run: composer install --no-interaction --no-progress ${{ inputs.composer-options }} + + # Overrides php-version from the consumer's mago.toml for each matrix leg + # without mutating the committed config file. + - name: Pin Mago PHP version + run: echo "MAGO_PHP_VERSION=${{ matrix.php }}" >> "$GITHUB_ENV" + + - name: Check coding standards + run: composer cs-check + + - name: Static analysis + if: success() || failure() + run: composer static-analysis + + - name: Unit tests + if: success() || failure() + run: composer test + + - name: Integration tests + if: inputs.run-integration && (success() || failure()) + run: composer test-integration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f46acd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +composer.lock +.DS_Store +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb6fa87 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, php-db + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 0e9ebbc..7063cb5 100644 --- a/README.md +++ b/README.md @@ -92,14 +92,13 @@ name: CI on: push: - branches: ["main"] pull_request: jobs: qa: uses: php-db/phpdb-qa-tools/.github/workflows/qa.yml@main with: - php-versions: '["8.2", "8.3", "8.4"]' + php-versions: '["8.2", "8.3", "8.4", "8.5"]' run-integration: false ``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..15a44fc --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "php-db/phpdb-qa-tools", + "description": "Shared QA toolchain for php-db components: Mago formatting, linting and static analysis, plus the PHPUnit baseline and CI workflow.", + "license": "BSD-3-Clause", + "type": "library", + "keywords": [ + "phpdb", + "qa", + "coding-standards", + "mago", + "linter", + "formatter", + "static-analysis" + ], + "homepage": "https://github.com/php-db/phpdb-qa-tools", + "support": { + "issues": "https://github.com/php-db/phpdb-qa-tools/issues", + "source": "https://github.com/php-db/phpdb-qa-tools", + "forum": "https://github.com/php-db/phpdb-qa-tools/discussions" + }, + "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)" + } +} diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..1d85cd0 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,76 @@ +# Migrating from laminas-coding-standard + +Repository-by-repository, in dependency order, using `phpdb-validator` as the template. + +## Prerequisites + +Install the Mago binary once per machine (see the [README](../README.md#prerequisite-install-mago)). + +## Steps + +### 1. Swap dev dependencies + +```sh +composer remove --dev laminas/laminas-coding-standard +# also remove psalm/psalm, vimeo/psalm, or phpstan/phpstan if present +composer require --dev php-db/phpdb-qa-tools +``` + +### 2. Remove phpcs artifacts + +```sh +rm -f phpcs.xml phpcs.xml.dist .phpcs-cache +``` + +Also remove any `psalm.xml` / `phpstan.neon` and their baselines. + +### 3. Add the minimal mago.toml + +```toml +extends = "vendor/php-db/phpdb-qa-tools/mago.toml" +php-version = "8.2.0" + +[source] +paths = ["src", "test"] +includes = ["vendor"] +``` + +Append repository-specific `[analyzer]` settings (e.g. `class-initializers`) as needed. + +### 4. Adopt the PHPUnit baseline + +```sh +cp vendor/php-db/phpdb-qa-tools/templates/phpunit.xml.dist . +``` + +Adjust test-suite paths only if the repository cannot follow the standard +`test/unit` / `test/integration` layout. + +### 5. Update composer scripts + +Adopt the standard `check` / `cs-check` / `cs-fix` / `static-analysis` / `test` +scripts (see the README). + +### 6. Reformat in a single isolated commit + +```sh +composer cs-fix +git add -A +git commit -m "Apply mago formatting" +git rev-parse HEAD >> .git-blame-ignore-revs +git add .git-blame-ignore-revs +git commit -m "Ignore mago reformat in git blame" +``` + +Keeping the mechanical reformat isolated (and recorded in `.git-blame-ignore-revs`) +keeps `git blame` useful. + +### 7. Triage remaining findings + +Fix or explicitly suppress remaining lint/analyzer findings in follow-up commits. +Large repositories may temporarily relax specific rules in their local `mago.toml` +(child settings override the base) — open a tracking issue to remove the relaxation. + +### 8. Update CI + +Replace the phpcs/psalm workflow steps with the standard QA workflow (see the README). diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 0000000..4adbeb8 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,66 @@ +# Rule rationale + +Why the non-default choices in the shared `mago.toml` are what they are. The +configuration was extracted from the `phpdb-validator` refactor — the first +php-db component migrated to Mago — and reflects what survived contact with a +real codebase. + +## Formatter + +- **`print-width = 120`, 4-space indentation** — carried over from the + laminas-coding-standard era; matches the existing codebase style. +- **`preserve-breaking-*` family** — intentional line breaks (broken argument + lists, member-access chains, arrays, conditionals) survive reformatting + instead of being collapsed when they fit the print width. This dramatically + reduces diff churn on already-formatted code. +- **`align-assignment-like` / `sort-class-methods`** — opinionated readability + choices ratified during the validator refactor. `sort-class-methods` makes + method order deterministic, eliminating "where does this method go" review + comments. +- **`space-after-logical-not-unary-prefix-operator`** — `! $foo` over `!$foo`, + matching the laminas-coding-standard convention. + +## Linter + +- **`minimum-fail-level = "Help"`** — everything the linter reports fails the + build. Rules relegated to `level = "Help"` are still enforced; the level only + affects display severity. +- **`integrations = ["laminas", "phpunit"]`** — enables framework-aware rules + for the two ecosystems every php-db component sits in. +- **Naming rules** (`class-name`, `interface-name`, `trait-name` with + `psr = true`; camelCase methods/variables) — direct continuation of the + PSR-12/laminas-coding-standard naming policy. +- **`yoda-conditions`** — ratified during the refactor; guards against + accidental assignment in conditions. +- **`function-name` excludes `src/functions/*`** — free functions in the + conventional functions directory may use snake_case, matching common + PHP library practice. +- **`too-many-methods` excludes `test/`** — test cases legitimately accumulate + many test methods. + +## Analyzer + +- **`excludes = ["test"]`** — analysis focuses on shipped code; tests are + covered by the linter (including the `phpunit` integration) and by running. +- **`check-throws` + shared `unchecked-exceptions`** — enforces documented + `@throws` for checked exceptions. `Error`, `LogicException` (programmer + errors), and PHPUnit's internal exceptions are unchecked, as is + `Laminas\Validator\Exception\RuntimeException` which laminas-validator + throws pervasively. +- **`enforce-class-finality` / `require-api-or-internal`** — every class is + final unless deliberately opened; every symbol declares whether it is public + API. These are the strictest choices in the base and the most likely + migration friction; repositories may relax them locally with a tracking + issue. +- **`class-initializers`** — the base lists only the one initializer every + php-db component shares, `PHPUnit\Framework\TestCase::setUp`. Because arrays + concatenate on merge, repositories append their own (e.g. Laminas' + `Element::init` / `BaseInputFilter::init`, ...) without repeating the base + list. + +## Versioning policy + +Following laminas-coding-standard's precedent: enabling new rules (or +tightening existing ones) lands only in **major** releases of this package. +Minor and patch releases may fix documentation, templates, or relax/adjust +rules in a backwards-compatible direction. diff --git a/mago.toml b/mago.toml new file mode 100644 index 0000000..00b28b6 --- /dev/null +++ b/mago.toml @@ -0,0 +1,132 @@ +#:schema https://mago.carthage.software/1.43.0/schema.json +# php-db/phpdb-qa-tools — shared base configuration. +# +# Consuming repositories extend this file and supply the project-specific +# facts (php-version, source paths, class-initializers, etc.): +# +# extends = "vendor/php-db/phpdb-qa-tools/mago.toml" +# php-version = "8.2.0" +# +# [source] +# paths = ["src", "test"] +# includes = ["vendor"] +version = "1" + +[formatter] +preset = "default" +print-width = 120 +tab-width = 4 +use-tabs = false +preserve-breaking-member-access-chain = true +preserve-breaking-member-access-chain-first-method-on-same-line = true +preserve-breaking-argument-list = true +preserve-breaking-array-like = true +preserve-breaking-parameter-list = true +preserve-breaking-attribute-list = true +preserve-breaking-conditional-expression = true +preserve-breaking-condition-expression = true +preserve-redundant-logical-binary-expression-parentheses = true +preserve-breaking-binary-expression = true +space-after-logical-not-unary-prefix-operator = true +always-break-named-arguments-list = true +align-assignment-like = true +sort-class-methods = true + +[linter] +minimum-fail-level = "Help" +integrations = ["laminas", "phpunit"] + +[linter.rules] +literal-named-argument = { enabled = true } +halstead = { effort-threshold = 7000 } +valid-docblock = { enabled = true } +no-negated-ternary = { enabled = true } +no-short-bool-cast = { enabled = true } +no-inline = { enabled = true } +no-array-accumulation-in-loop = { enabled = true } +no-literal-namespace-string = { enabled = true } +no-parameter-shadowing = { enabled = true } +no-side-effects-with-declarations = { enabled = true } +prefer-array-spread = { enabled = true } +prefer-explode-over-preg-split = { enabled = true } +prefer-first-class-callable = { check-functions = true } +prefer-test-attribute = { enabled = true } +require-namespace = { enabled = true } +prefer-pre-increment = { enabled = true } +prefer-self-return-type = { enabled = true } +sorted-integer-keys = { enabled = true } +yoda-conditions = { enabled = true } +file-name = { enabled = true } +array-style = { level = "Help" } +block-statement = { level = "Help" } +braced-string-interpolation = { enabled = true, level = "Help" } +no-alias-function = { level = "Help" } +no-php-tag-terminator = { level = "Help" } +no-trailing-space = { level = "Help" } +string-style = { enabled = true, level = "Help" } +ambiguous-constant-access = { enabled = true } +ambiguous-function-call = { enabled = true } +class-name = { psr = true } +function-name = { camel = true, exclude = ["src/functions/*"] } +interface-name = { psr = true } +method-name = { enabled = true, camel = true } +no-fully-qualified-global-class-like = { enabled = true } +no-fully-qualified-global-constant = { enabled = true } +no-fully-qualified-global-function = { enabled = true } +property-name = { enabled = true } +trait-name = { psr = true } +variable-name = { enabled = true, camel = true, either = false } +too-many-methods = { exclude = ["test/"] } +no-iterator-to-array-in-foreach = { enabled = true } +no-duplicate-match-arm = { level = "Help" } +no-empty-comment = { level = "Help" } +no-empty-loop = { level = "Help" } +no-is-null = { enabled = true, level = "Help" } +no-null-property-init = { enabled = true } +no-redundant-else = { enabled = true } +disallowed-type-instantiation = { enabled = true } +no-debug-symbols = { level = "Help" } +no-assign-in-argument = { enabled = true } +no-dead-store = { enabled = true } +no-redundant-variable = { enabled = true } +no-unused-closure-capture = { enabled = true } +no-unused-global = { enabled = true } +no-unused-static = { enabled = true } +switch-continue-to-break = { enabled = true } +invalid-open-tag = { level = "Help" } + +[analyzer] +excludes = ["test"] +plugins = ["psl", "psr-container"] +find-unused-definitions = true +find-unused-expressions = true +find-overly-wide-return-types = true +analyze-dead-code = true +memoize-properties = true +check-throws = true +unchecked-exceptions = [ + "Error", + "LogicException", + "Laminas\\Validator\\Exception\\RuntimeException", + "PHPUnit\\Framework\\Exception", + "PHPUnit\\Framework\\MockObject\\Exception", + "PHPUnit\\Event\\NoPreviousThrowableException", +] +unchecked-exception-classes = [] +check-missing-override = true +find-unused-parameters = true +strict-list-index-checks = true +strict-array-index-existence = true +allow-array-truthy-operand = false +no-boolean-literal-comparison = true +check-missing-type-hints = true +check-arrow-function-missing-type-hints = true +register-super-globals = false +check-property-initialization = true +check-use-statements = true +check-name-casing = true +enforce-class-finality = true +require-api-or-internal = true +class-initializers = [ + "PHPUnit\\Framework\\TestCase::setUp", +] diff --git a/templates/phpunit.xml.dist b/templates/phpunit.xml.dist new file mode 100644 index 0000000..0e7051d --- /dev/null +++ b/templates/phpunit.xml.dist @@ -0,0 +1,37 @@ + + + + + ./test/unit + + + ./test/integration + + + + + + + + + ./src + + +