PHPackages                             matchory/coding-style - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. matchory/coding-style

ActivePhpstan-extension

matchory/coding-style
=====================

Shared code style configuration for Matchory projects: Pint, PHPStan and Rector presets.

v0.1.2(today)012↑2650%[7 issues](https://github.com/matchory/coding-style/issues)MITJavaScriptPHP ^8.5CI passing

Since Jul 30Pushed todayCompare

[ Source](https://github.com/matchory/coding-style)[ Packagist](https://packagist.org/packages/matchory/coding-style)[ RSS](/packages/matchory-coding-style/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (8)Versions (6)Used By (0)

matchory/coding-style
=====================

[](#matchorycoding-style)

One source of truth for code style across every Matchory repository: Pint, PHPStan and Rector for PHP; oxlint, oxfmt, ESLint and TypeScript for JavaScript; ruff for Python; and the canonical `.editorconfig` shared by all three.

Edit configuration here. Never in a consumer.

Why this exists
---------------

[](#why-this-exists)

Configuration was copied between repositories and then drifted. A survey across our repositories in July 2026 found, for the same organisation and the same conventions:

DriftDetailJS formattingTwo indentation widths and two quote styles across JavaScript repositoriesJS lint coverageOne repository enforced ~73 oxlint rules; another shipped an empty rule setJS config formatSome repositories used `oxlint.config.ts`, others `.oxlintrc.json`Python line lengthTwo different limits in active servicesPython rule setsOne service selected ~46 ruff rule groups, another 8Python coverageSeveral services had no ruff configuration at allNone of this was anyone's fault. Copying a config file is the path of least resistance, and there was nothing to copy *from* that stayed current.

Extend or sync
--------------

[](#extend-or-sync)

Only half of these tools can read configuration out of a package. That distinction drives the whole design, so it is worth stating plainly:

ToolMechanismExtends?PHPStan`includes:` a vendor path, plus auto-discoveryYes, nativelyRectorA PHP call: `Preset::laravel(RectorConfig::configure())`Yes, nativelyESLintFlat config `import`Yes, nativelyTypeScript`extends` a package pathYes, nativelyoxlint / oxfmtSpread the exported object in a `.config.ts`Yes, when config is TS**Pint**`--config vendor/…`; accepts `preset` and `rules` only**No merge****ruff**`extend` takes a path, and site-packages paths are unstable**No, copied****EditorConfig**`root = false` only walks up the directory tree**No, copied**For the bottom three the package is a *transport for files*, refreshed by a sync command and verified in CI. There is no way around it, and pretending otherwise is how configuration goes stale without anyone noticing.

Repository layout
-----------------

[](#repository-layout)

Each ecosystem's location is forced by its packaging rules, which is why the tree looks asymmetric:

```
composer.json               PHP package. Composer has no `#subdirectory`, and Repman reads
                            composer.json from the repository root.
pyproject.toml              Python package. `pip install git+…` and pre-commit both expect
src/matchory_coding_style/   project metadata at the root.
.pre-commit-hooks.yaml
js/                         npm package. The only one that can live in a subdirectory, so it does.
php/                        Pint, PHPStan and Rector presets, plus the PSR-4 source.
.editorconfig               Canonical copy. Mirrored into the npm and Python packages by CI-verified
                            generators.

```

One consequence: all three packages share a single version tag. A change touching only oxlint still bumps the version PHP consumers see. That is deliberate — "every repository is on style v3" is a useful thing to be able to say — but expect Renovate noise and group the updates.

---

PHP
---

[](#php)

```
composer require --dev matchory/coding-style
```

Requires PHP 8.5. The package requires Pint, PHPStan, Rector and `tomasvotruba/cognitive-complexity`, so it owns those versions centrally. Install `larastan/larastan`, `driftingly/rector-laravel` and `pestphp/pest-plugin-rector` alongside it for the Laravel and Pest presets.

### Pint

[](#pint)

Pint does not merge configuration: `--config` selects exactly one file. Point your `fmt` script at a preset:

```
{
    "scripts": {
        "fmt": "pint --config vendor/matchory/coding-style/php/pint/base.json --parallel --cache-file=./.cache/pint.json",
        "fmt:test": "@fmt --test",
        "fmt:pre-commit": "@fmt --dirty --repair"
    }
}
```

PresetContents`base.json`38 rules on top of the `per` preset. The target for every repository.`relaxed.json``base` minus the 14 docblock-rewriting rules. An adoption ramp, not a home.`relaxed` exists because the docblock rules produce by far the largest diff when a repository first adopts the shared style, and that diff is what stalls adoption. It is generated from `base.json` by `php php/bin/generate-pint-presets.php`, so the rules have one source.

Because `--config` replaces rather than merges, a repository needing one local deviation has no escape hatch. If that comes up, merge at invoke time instead of forking the preset:

```
{
    "scripts": {
        "fmt:config": "@php -r \"file_put_contents('.cache/pint.merged.json', json_encode(array_replace_recursive(json_decode(file_get_contents('vendor/matchory/coding-style/php/pint/base.json'), true), file_exists('pint.json') ? json_decode(file_get_contents('pint.json'), true) : [])));\"",
        "fmt": ["@fmt:config", "pint --config .cache/pint.merged.json --parallel"]
    }
}
```

### PHPStan

[](#phpstan)

`php/phpstan/base.neon` is applied **automatically** to every consumer: the package declares `type: phpstan-extension` and `extra.phpstan.includes`, which `phpstan/extension-installer` discovers on install. It carries only settings that are safe in any PHP codebase — editor URLs, `treatPhpDocTypesAsCertain`, `reportUnmatchedIgnoredErrors`.

It deliberately does **not** set `level`, `paths` or `tmpDir`. A level applied invisibly from vendor is a debugging trap, and a cache directory a consumer has not gitignored would start committing analysis caches.

Everything else is opt-in:

```
includes:
    - vendor/matchory/coding-style/php/phpstan/laravel.neon
    - vendor/matchory/coding-style/php/phpstan/pest.neon
    - vendor/matchory/coding-style/php/phpstan/complexity.neon

parameters:
    level: 5
    paths:
        - app/
        - tests/

    # Must be declared here; see the merge-order note below.
    cognitive_complexity:
        class: 50
        function: 15
```

PresetContents`base.neon`Auto-applied. Universal settings only.`pest.neon`The `TestCall`/`Expectation` ignores, `universalObjectCratesClasses`, stubs.`laravel.neon``disableMigrationScan`/`disableSchemaScan`, the `HasEvents` ignore.`complexity.neon`Complexity exemptions for `*/tests/*` and `*/migrations/*`.`strict.neon``level: 9` and `treatPhpDocTypesAsCertain: true`.Three caveats, all verified against a real consumer rather than assumed:

- **A package can only set a parameter that no other auto-discovered extension sets.** PHPStan merges extension configs after every `includes:` but before the root config's own `parameters`, so the extension that sorts last wins and only your own `phpstan.neon` beats all of them. This is why the complexity thresholds are documented rather than shipped: `tomasvotruba/cognitive-complexity`declares its own defaults and sorts after `matchory/coding-style`, so any value this package set would be silently discarded. Check with `phpstan dump-parameters`.
- `ignoreErrors` from an include is **appended**, never replaced. There is no way to un-ignore locally, which is why the shared entries are limited to things that are safe unconditionally. This is also why the preset files can carry ignores but not thresholds.
- `level` in `strict.neon` only applies if your own `phpstan.neon` does not set one. A local `level: 5` silently wins.

Stays local: your `paths`, `excludePaths`, baseline, complexity thresholds, and any ignore that names a specific file.

### Rector

[](#rector)

```
