patchlevel/coding-standard
A shared PHP coding standard configuration for the Patchlevel ecosystem, bundling rule sets and fixer presets so teams can enforce consistent code style with minimal setup. Add it as a dev dependency and extend the ready-made config in your PHP-CS-Fixer or ECS setup to keep projects aligned without...
Install it as a dev dependency — coding standards belong in require-dev, never in production autoload:
composer require --dev patchlevel/coding-standard
Know what you're installing: it's a PHP_CodeSniffer ruleset, and specifically a re-copy of doctrine/coding-standard. Release 1.3.0 is literally "re-copy doctrine standard," so you no longer need to sniff around for whether this is ECS or PHPCS. Practical consequences:
Doctrine.…, SlevomatCodingStandard.…), not a bespoke Patchlevel.* namespace. Open vendor/patchlevel/coding-standard/ and read the ruleset's <ruleset name="..."> attribute before writing exclude rules.composer.json for the type (phpcs-standard) and the PHPCS constraint, so you pair it with a compatible runner version.Install the runner if the package doesn't pull it in transitively:
composer require --dev squizlabs/php_codesniffer
Register and verify. Either point at the ruleset path directly, or register the standard:
vendor/bin/phpcs -i # lists installed standards — note the actual name, it may read "Doctrine"
vendor/bin/phpcs --config-set installed_paths vendor/patchlevel/coding-standard
vendor/bin/phpcs -i # the standard should now appear
Then run it on a single file first, using whatever name -i printed:
vendor/bin/phpcs --standard=Doctrine src/SomeClass.php
First real use case: run it against one directory of code you're actively touching, fix the cheap issues with vendor/bin/phpcbf, and commit a clean baseline before enforcing it in CI. Don't point it at a large legacy tree on day one — you'll get thousands of rows and learn nothing.
Commit a phpcs.xml.dist at the project root and reference the standard by name, rather than passing --standard= everywhere. This is the single highest-leverage move: it makes the invocation vendor/bin/phpcs with no arguments, which is what editors, hooks, and CI all expect.
<?xml version="1.0"?>
<ruleset name="Project">
<file>src</file>
<file>tests</file>
<arg name="extensions" value="php"/>
<arg name="colors"/>
<arg value="ps"/> <!-- progress + show sniff codes -->
<rule ref="Doctrine"/> <!-- use the name printed by `phpcs -i` -->
<!-- project-specific relaxations go here, not in CI flags -->
<rule ref="Doctrine">
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint"/>
</rule>
</ruleset>
Treat a version bump of this package as a ruleset change, not a library bump. It's a copy of Doctrine's standard, so 1.3.0's "re-copy" means the contents moved to a newer upstream revision. Even though it's a minor release, the effective rule set can change and turn a green branch red. Upgrade on a throwaway branch, run vendor/bin/phpcs --report=source, and skim the sniff-name diff before merging.
Because it's a re-copy, decide deliberately whether you need it at all. If you don't rely on Patchlevel-specific tweaks, depending on doctrine/coding-standard directly gets you upstream fixes sooner and one less middleman. Compare the two rulesets side by side once; if the diff is empty or trivial, take the dependency with the faster release cadence.
Enforce in CI with a cache and a summary report. Fail the build on errors, and use --report=summary so the log stays readable on a 500-file codebase:
- run: vendor/bin/phpcs --cache --report=summary
Cache the .phpcs-cache file between runs keyed on the PHPCS version and the standard's version — a ruleset re-copy invalidates the cache's usefulness.
Adopt incrementally on legacy code. Two workable strategies:
<file>src/NewModule</file> entries one at a time; leave the rest out until each area is cleaned.phpcs.xml.dist, set <arg name="warning-severity" value="0"/> and let only errors break the build while you burn down warnings with --report=source to prioritise by sniff frequency.Use phpcbf as a pre-commit step, phpcs in CI. Auto-fixing is safe for whitespace/import-order sniffs and dangerous for anything that rewrites control flow. Standard practice:
vendor/bin/phpcbf src && vendor/bin/phpcs src
Wire that into a composer script ("lint": "phpcbf src; phpcs src") and call the script from GrumPH, CaptainHook, or a husky pre-commit hook. Keeping the hook and CI on the same phpcs.xml.dist is what prevents "works in my editor, fails in CI" drift.
Editor integration is the real day-to-day win. Point PHPStorm's PHP → Quality Tools → PHP_CodeSniffer at vendor/bin/phpcs and the committed phpcs.xml.dist; in VS Code use shevaua.phpcs or bmewburn.vscode-intelephense's phpcs setting. Once configured, violations appear inline and the "run linter" step disappears from your workflow entirely.
Pair with a formatter, don't replace it. If the team already uses PHP-CS-Fixer or Pint, keep it for formatting and let the standard own the sniffable rules (naming, strict types, unused imports, dangerous constructs). Run the formatter first, then phpcs, so you don't get fix-conflicts between two auto-fixers.
Extend rather than fork. When you need to diverge from the standard, your project phpcs.xml.dist is the extension point — <exclude name="..."> to drop a sniff, <rule ref="Generic.Files.LineLength"><properties>... to retune a shared sniff, or a small local <rule ref="path/to/MySniff.php"/> for genuinely project-specific rules. Never edit files under vendor/.
This is a very low-adoption package (1 star, 0 dependents). 1.3.0 shows a maintainer is still responsive and pulled a PR from a Doctrine contributor, which is a good sign — but the release history reads as occasional catch-up rather than active development. Pin an exact version in composer.json ("patchlevel/coding-standard": "1.3.0" not "^1.3") so a silent composer update can't change your rule set, and assume you may end up forking it if upstream stops tracking PHP/PHPCS releases. Budget for that.
A ruleset that is a copy lags its source. PR #13 "re-copy doctrine standard" implies the package had drifted from Doctrine's standard for some period. If you're relying on it to match Doctrine's current rules, verify after each bump — and be aware that a long gap between Patchlevel releases means you're on an older Doctrine standard than the rest of the ecosystem.
Don't install this alongside doctrine/coding-standard. Two packages shipping a ruleset with the same internal name will collide in installed_paths, and phpcs -i may list one or the other depending on registration order. Pick one, and if you must have both, reference them by explicit filesystem path rather than by registered name.
PHPCS 3.x vs 4.x is a real compatibility cliff. Re-copying an upstream ruleset doesn't necessarily move the tool constraint. PHP_CodeSniffer 4 changed the sniff interface and some report formats. If Composer resolves an unexpected PHPCS major, constrain it explicitly in require-dev before debugging phantom sniff errors.
installed_paths does not survive a composer install on a fresh machine. --config-set writes to the global CodeSniffer.conf inside the vendor directory, which gets blown away. Either re-run it in a CI bootstrap step, commit it via a post-install-cmd script, or — better — reference the standard by its ruleset path in phpcs.xml.dist and avoid the registration problem entirely.
Silent pass-by-omission. If you typo the standard name, PHPCS may behave unexpectedly depending on version — the -i check plus a run on a single known-bad file is the fast sanity test. This matters more than usual here, because the registered name likely isn't "Patchlevel." Always confirm a deliberately broken file produces an error before trusting a green CI run.
-s is the debugging flag you want. Without it you get messages with no sniff code, which makes searching docs, GitHub issues, and your own exclude list painful. Put <arg value="s"/> (or -s) into the config permanently.
phpcbf on a dirty working tree is how people lose work. It rewrites files in place. Run it on a clean tree, review with git diff, and never run it as part of a build step that touches checked-in code.
Exclude generated code, migrations, and vendor explicitly. Vendor is excluded by default when you use a phpcs.xml.dist, but only if you don't override <file>/<exclude-pattern> badly. Add <exclude-pattern>*/migrations/*</exclude-pattern> and anything Doctrine/codegen produces — otherwise your error count is dominated by files nobody should hand-edit.
Memory and time on large trees. phpcs can blow past the default memory_limit on a few thousand files. Use --cache, run per-directory in CI matrix jobs, or php -d memory_limit=1G vendor/bin/phpcs. If runs are slow, --report=source first to find the expensive sniffs.
Exit codes matter for CI. PHPCS exits 1 on errors, 2 on fixer-needed in some report modes, and 3 on internal errors — a set -e script treats all as failure, which is usually fine, but when a sniff crashes you'll see a generic build failure. Keep -vvv handy to distinguish "your code is bad" from "the sniff is broken."
The standard encodes Doctrine's opinions, filtered through Patchlevel. Read the ruleset once, up front, and consciously accept or exclude each rule group. Blindly adopting a third-party standard on a mature codebase generates churn on code that was fine, and the resulting diff review burden is where standards adoption usually dies.
How can I help you explore Laravel packages today?