jin2chen/phpcs-phar
A Composer-installable PHAR distribution of PHP_CodeSniffer (PHPCS), making it easy to run coding-standard checks in local development and CI without managing a separate PHPCS installation.
The package is a thin Composer wrapper around the phpcs/phpcbf PHAR — it does not change how PHP_CodeSniffer behaves, so the upstream docs and wiki (linked from the squizlabs repo) are your reference. Everything you learn about PHPCS applies verbatim.
1. Install (dev only):
composer require --dev jin2chen/phpcs-phar
2. Verify what you got — do this first, because the PHAR's PHPCS version is fixed at build time (~3.6/3.7, package last released 2023-02-22):
vendor/bin/phpcs --version
vendor/bin/phpcs -i # lists bundled standards: PEAR, PSR1, PSR2, PSR12, Squiz, Zend, Generic, MySource
vendor/bin/phpcs --help | head -40
3. First use case — lint the app against bundled PSR-12 with zero config:
vendor/bin/phpcs --standard=PSR12 --extensions=php app/ routes/ config/
4. Immediately create a phpcs.xml.dist so you stop passing flags:
<?xml version="1.0"?>
<ruleset name="Laravel">
<description>Project coding standard</description>
<file>app</file>
<file>routes</file>
<file>config</file>
<file>database</file>
<file>tests</file>
<arg name="extensions" value="php"/>
<arg name="colors"/>
<arg name="parallel" value="8"/>
<arg name="cache" value=".phpcs.cache"/>
<arg name="basepath" value="."/>
<exclude-pattern>vendor/*</exclude-pattern>
<exclude-pattern>storage/*</exclude-pattern>
<exclude-pattern>bootstrap/cache/*</exclude-pattern>
<exclude-pattern>_ide_helper*.php</exclude-pattern>
<exclude-pattern>tests/Fixtures/*</exclude-pattern>
<rule ref="PSR12"/>
</ruleset>
5. Wire into composer.json scripts so it's a one-word command for the whole team:
"scripts": {
"lint": "phpcs",
"lint:fix": "phpcbf",
"lint:report": "phpcs --report=summary -s"
}
Run composer lint. That's the whole onboarding. From there, the natural progression is: -s to see sniff codes → targeted // phpcs:ignore suppressions → external standards via installed_paths → CI gate.
CI gate with GitHub Actions annotations. Use checkstyle output + cs2pr, which turns phpcs findings into inline PR comments:
- uses: shivammathur/setup-php@v2
with: { php-version: '8.2', tools: composer, coverage: none }
- run: composer install --prefer-dist --no-interaction
- run: vendor/bin/phpcs -q --report=checkstyle --no-colors | cs2pr
Keep --no-colors in CI so logs stay parseable; keep colors locally.
Cache-aware pipelines. --cache writes .phpcs.cache. Cache it in CI keyed by the ruleset hash, or you'll spend more time restoring a stale cache than linting:
- uses: actions/cache@v4
with:
path: .phpcs.cache
key: phpcs-${{ hashFiles('phpcs.xml.dist') }}
Locally, when results look wrong or missing, the first reflex is rm .phpcs.cache (or --no-cache) — a ruleset edit without a cache invalidation causes confusing "why is this still reported / not reported".
Pre-commit on changed files only. Full-repo PHPCS is slow on a large Laravel app; scope it to staged PHP files:
"lint:staged": "git diff --name-only --cached --diff-filter=ACMR -- '*.php' | xargs -r vendor/bin/phpcs -s"
Add || true guarding is not recommended for a gate — but note the exit codes (below) before writing wrapper scripts.
Coexistence with Laravel Pint. Pint wraps PHP-CS-Fixer; this package wraps PHP_CodeSniffer. They are different engines with overlapping rulesets and will fight if both auto-format in the same loop. Pick one of two clean splits:
composer pint) owns stylistic fixes; PHPCS runs only sniffs Pint cannot express — Generic.PHP.ForbiddenFunctions, SlevomatCodingStandard.Namespaces.*, PHPCompatibility, custom architecture sniffs — with PSR12 removed from your ruleset to avoid duplicate/conflicting messages.phpcbf as the fixer and drop Pint. Then set your editor to run phpcbf on save, not both.Never run phpcbf and pint in the same pre-commit hook; you get churn diffs as each undoes the other's spacing choices.
Add external standards without polluting the app. The PHAR only ships the built-in standards. Standard practice (Slevomat, PHPCompatibility, WordPress) is:
composer require --dev slevomat/coding-standard
<config name="installed_paths" value="vendor/slevomat/coding-standard"/>
<rule ref="SlevomatCodingStandard"/>
installed_paths set inside a ruleset is resolved relative to that ruleset file, so the path above is correct only because phpcs.xml.dist sits next to vendor/. This is the single most common "standard not installed" failure.
Progressive adoption / suppressing instead of rewrites. For a legacy Laravel app, don't start with PSR12 across app/. Start with one directory (app/Http/Controllers), then widen. For individual lines:
// phpcs:ignore SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint
public function __invoke($request) { ... }
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
function handler($event, $payload) { ... }
// phpcs:enable
Use -s on every run when authoring suppressions — you need the exact sniff code, and PHPCS does not tell you it by default.
Editor integration. Point PHP Intelephense / vscode-phpcs / PhpStorm's Code Sniffer configuration at vendor/bin/phpcs (the Composer bin proxy, not the .phar inside vendor/jin2chen/...). Set "standard" to the absolute path of phpcs.xml.dist so editors and CI agree. Because both binaries are shipped as PHAR-based proxies, no include_path or global PHPCS install is needed — this is exactly the isolation benefit: your app's composer.lock never gains squizlabs/php_codesniffer's dependency tree.
Use it as a diff review tool, not just a gate. vendor/bin/phpcs --report=source -s groups findings by sniff, which is the fastest way to decide which rules to disable project-wide versus fix. --report=summary gives per-file counts suitable for a "tech debt burndown" metric before you enable a rule for everyone.
Version staleness is the headline risk. The package was last released 2023-02-22, so the embedded PHPCS is roughly 3.7.x. Tokenizer support for newer PHP syntax (readonly classes, DNF types, 8.4 asymmetric visibility, hooks) is missing. Symptoms are not clear errors — they are phantom findings (indentation, spacing, "unexpected token") on files that are perfectly valid. Before blaming your code, run phpcs --version and check the token in question; add a targeted phpcs:ignore with a comment, or exclude the file.
Exit codes will break naive shell scripts. PHPCS returns 0 (clean), 1 (errors found), 2 (fixer failure), 3 (processing error — e.g. a parse error or missing standard). phpcbf is different again: 0 (nothing to fix) and 1 (fixes applied) are both effectively "success". So phpcbf || exit 1 is wrong — it fails precisely when it did its job. Check for > 1 instead.
Composer bin name collisions. If anything in your tree already pulls in squizlabs/php_codesniffer (a dev tool, a transitive dev dependency), both packages want vendor/bin/phpcs. Composer silently skips one and prints a "Skipped installation of bin" warning that is easy to miss — you then run a different PHPCS than you think. Grep composer install output for it, and composer why squizlabs/php_codesniffer if the version reported by phpcs --version doesn't match the package.
require-dev means "absent in production-shaped installs". Any CI job or Docker stage that runs composer install --no-dev has no vendor/bin/phpcs at all. Run lint in a dedicated job / build stage, or keep it in the dev stage image.
No built-in baseline. PHPCS has no --generate-baseline like PHPStan. You either suppress inline, exclude patterns, or adopt a community baseline tool. Plan for it — it's the main reason teams abandon a strict ruleset on legacy code.
phpcbf is not safe everywhere. It rewrites <?php blocks inside templates and can mangle heredocs/nowdocs and unusual string interpolation. Exclude Blade and mixed-HTML views entirely (<exclude-pattern>*resources/views/*</exclude-pattern>, *mail/*) — PHPCS cannot meaningfully lint Blade anyway. Always run git diff after a phpcbf pass and commit fixes separately from functional changes.
--parallel + custom sniffs. Parallel workers are separate processes; sniffs that write to files, mutate globals, or depend on run order can behave inconsistently. If results differ between --parallel=8 and --parallel=1, that's your culprit — disable parallel in the ruleset for that project.
Warning/error severity is a footgun. Many sniffs emit warnings, which still cause exit code 1 but don't fail builds if you've configured --warning-severity=0. Set severity deliberately in the ruleset rather than relying on default behavior, and remember --warning-severity=0 --error-severity=1 is the usual "only fail on errors" combination.
--runtime-set and <config> are different mechanisms. CLI --runtime-set installed_paths ... overrides the ruleset; useful in CI where paths differ. Forgetting which one applies leads to "works locally, external standard missing in Docker".
PHAR environments. Reading a PHAR does not require phar.readonly=0, but the phar extension must be present. Some hardened production/minimal Docker images and certain CI sandboxes disable it or restrict phar:// stream wrappers. Also note some security scanners flag vendored PHARs — expect an allowlist entry, and document why the dependency exists.
Path and file-discovery surprises. --basepath=. keeps report paths relative (nicer in CI annotations). exclude-pattern uses glob-like matching relative to the ruleset, so vendor/* will not exclude a nested packages/foo/vendor/* — use */vendor/*. Same for storage/framework/views.
Debugging checklist, in order: phpcs -i (is the standard there?) → --version (is it the PHAR or a stray system install?) → --no-cache (stale cache?) → --parallel=1 (process weirdness?) → -s (which sniff?) → run the sniff alone with --sniffs=Vendor.Standard.Sniff (isolate false positives) → confirm php -l passes (PHPCS reports parse errors as processing failures, not findings).
Extension point reminder. You can't add dependencies to the PHAR. If you need a sniff that requires a runtime library, it must be installed via Composer and referenced with installed_paths — and it will autoload with your Composer autoloader, not the PHAR's. Keep such sniffs' classes free of assumptions about PHP_CODESNIFFER_CBF/PHP_CODESNIFFER_VERBOSITY being defined by the PHAR's entry point.
How can I help you explore Laravel packages today?