dealerdirect/phpcodesniffer-composer-installer
Composer plugin that automatically finds PHP_CodeSniffer coding standards installed via Composer and configures phpcs's installed_paths for you, so you can skip manual symlinking, repository checkouts, and config edits.
This plugin solves one problem: PHP_CodeSniffer (phpcs) doesn't know where your third-party coding standards live. Normally you'd hand-edit installed_paths, or symlink standards into vendor/squizlabs/php_codesniffer/src/Standards/. This plugin removes that step by detecting phpcodesniffer-standard type packages and calling phpcs --config-set installed_paths ... for you after every Composer operation.
Minimum steps:
composer require --dev dealerdirect/phpcodesniffer-composer-installer:"^1.0"
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev wp-coding-standards/wpcs phpcompatibility/php-compatibility
Then verify what registered:
./vendor/bin/phpcs -i
You should see your standards appended to the built-in list (PEAR, PSR1, PSR2, PSR12, Squiz, Zend, plus WordPress, PHPCompatibility, etc.). That output is the plugin's success signal—nothing else to configure.
Where to look first when something is off:
composer.json → config.allow-plugins (the plugin is silently skipped if this isn't true)vendor/squizlabs/php_codesniffer/CodeSniffer.conf (or the installed_paths in your global phpcs config)—this is the file the plugin mutatesphpcodesniffer-standard package's own composer.json → its type and directory layoutYour first real use case is almost always: install a community standard (WordPress, PHPCompatibility, Slevomat, Doctrine), write a phpcs.xml.dist referencing it by name, and run vendor/bin/phpcs. First run:
<?xml version="1.0"?>
<ruleset name="Project">
<rule ref="WordPress"/>
</ruleset>
That's it. No installed_paths line in your config, and no path juggling in CI or on colleagues' machines. That's the entire day-to-day value: a new contributor runs composer install and their phpcs is already correct.
Standard dev-dependency install. In most projects this is a require-dev entry alongside squizlabs/php_codesniffer and the standards you actually use. Commit the allow-plugins block to composer.json so no interactive prompt blocks CI:
{
"require-dev": {
"squizlabs/php_codesniffer": "^3.9",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"wp-coding-standards/wpcs": "^3.0",
"phpcompatibility/php-compatibility": "^9.3"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
Multi-standard ruleset. A very common pattern is layering standards and excluding opinions you disagree with. Because the plugin registers every installed standard, you reference each by its registered name in one ruleset:
<ruleset name="Acme">
<rule ref="PSR12"/>
<rule ref="PHPCompatibility"/>
<config name="testVersion" value="8.1-"/>
<rule ref="WordPress-Docs">
<exclude name="Squiz.Commenting.FileComment.Missing"/>
</rule>
<file>src/</file>
</ruleset>
CI wiring. The plugin fires on composer install/update, so CI needs nothing special beyond a normal install. If you cache vendor/ between runs, be aware installed_paths lives in a file, not in your lockfile—so if you restore a stale vendor cache you may restore stale paths. Simplest robust approach: warm the cache with composer install, then run composer run-script install-codestandards (see below) before linting. That's idempotent and cheap.
Explicit script invocation. For odd environments (Docker layers, Drupal/Magento scaffolding, --no-plugins runs) you can call the plugin directly and hook it into Composer's lifecycle:
{
"scripts": {
"install-codestandards": [
"PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run"
],
"post-install-cmd": ["@install-codestandards"],
"post-update-cmd": ["@install-codestandards"]
}
}
Note the namespace changed from Dealerdirect to PHPCSStandards in v1.0.0—copy-pasted snippets from older blog posts will silently fail.
As a standard author. If you maintain a package that depends on other standards, add this plugin to your require (not require-dev) with a loose constraint, e.g. "*" or "^0.4 || ^0.5 || ^0.6 || ^0.7 || ^1.0". Composer treats ^0.7 and ^1.0 as mutually exclusive, so a tight constraint here is the classic way to break consumer installs.
Search depth tuning. If your standards live deeper than three directories (common in monorepos or when a standard is nested under vendor/vendor-name/pkg/Standards/Sub/Deep/), raise the scan depth:
{
"extra": {
"phpcodesniffer-search-depth": 5
}
}
Team onboarding as a script. Pair this plugin with a composer lint/composer fix script so nobody memorizes arguments:
{
"scripts": {
"lint": "phpcs",
"lint:fix": "phpcbf"
}
}
phpcs with no arguments picks up phpcs.xml.dist automatically, and standards resolve via the plugin. New laptop → composer install && composer lint.
The plugin is disabled by default and fails quietly. On Composer 2.2+ the plugin won't run until allow-plugins grants it. Symptom: phpcs -i shows only the built-ins. Fix: composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true. In CI, use composer install --no-interaction and make sure the flag is committed, or pass --no-plugins deliberately and invoke the script yourself instead.
Global vs. local installs. If the plugin is installed globally, Composer loads the global copy rather than the project's. You can end up debugging a version of the plugin that your project never installed. Check composer global show dealerdirect/phpcodesniffer-composer-installer when behavior doesn't match composer.lock.
installed_paths is state, not code. The plugin rewrites PHP_CodeSniffer's config file (CodeSniffer.conf for a local install, ~/.config/phpcs/ or similar for global). Because it's outside version control, stale entries can accumulate—deleted packages leave dangling paths. If phpcs starts throwing "referenced standard couldn't be found", run phpcs --config-delete installed_paths followed by a fresh composer install (or the install-codestandards script) to let the plugin rebuild the list cleanly.
Package type is the trigger. A standard packaged with "type": "library" (or no type at all) will never be detected, no matter how correct the directory layout is. When a newly installed standard doesn't appear in phpcs -i, check its composer.json type before anything else.
Directory depth matters twice. The plugin searches a limited depth (default 3) for ruleset files, and a standard's own directory must be ≤3 levels from the package root. Both are configurable-ish: the search is (extra.phpcodesniffer-search-depth), the package layout is not. If you author a standard and it "doesn't register", flatten the directory structure.
Version constraint collisions. In a multi-standard dependency graph, two packages requiring this plugin at incompatible majors will deadlock resolution. The maintainers' guidance is to use a permissive constraint (* or an explicit || chain across ^0.4–^1.0). If you hit "your requirements could not be resolved", this is a likely culprit—inspect with composer why dealerdirect/phpcodesniffer-composer-installer.
Legacy namespace. Pre-1.0 it was Dealerdirect\Composer\Plugin\Installers\PHPCodeSniffer\Plugin. The current class is PHPCSStandards\.... Scripts and composer.json entries copied from older docs will error at runtime.
--no-plugins and --no-scripts. Both suppress the plugin (the second suppresses the script-based workaround too). Docker builds that optimize with composer install --no-scripts need to run the script afterward or the standard paths won't be set.
PHPCS 4 vs. 3. Both are supported, but PHPCS 4 dropped PHPCS 2 standard compatibility, so older third-party standards may install fine yet fail to load. If a standard registers in phpcs -i but errors on run, drop to PHPCS 3.x or upgrade the standard.
open_basedir contexts. Fixed in v1.2.1, but relevant if you're pinned below it—composer install could error under open_basedir restrictions. Upgrading is the fix.
Debugging approach. The plugin is deliberately silent on success. To see what it's actually doing, run composer install -vvv and grep for phpcodesniffer; then confirm the resulting paths with phpcs --config-show installed_paths. Comparing that list against phpcs -i is the fastest way to distinguish "not scanned" from "scanned but broken ruleset".
Extension point. The public surface is essentially just Plugin::run—there's no events or hooks API. If you need behavior beyond script invocation and the search-depth setting, the practical extension point is your own Composer script wrapping it, not subclassing.
It's not a linter. Easy to forget when skimming the README: this package installs nothing functional on its own. You still need squizlabs/php_codesniffer and at least one standards package. On its own it registers the built-in PEAR/PSR/Squiz/Zend standards—useful, but not why you installed it.
How can I help you explore Laravel packages today?