suin/phpcs-psr4-sniff
suin/phpcs-psr4-sniff is a PHP_CodeSniffer sniff that validates PSR-4 autoloading rules, flagging namespace, directory, and filename mismatches so Composer autoloading stays reliable in Laravel and other PHP projects.
suin/phpcs-psr4-sniff is a PHP_CodeSniffer standard that compares the class name and namespace declared in a file against the PSR-4 path it lives at, using your project's autoload mapping. It catches the class of bugs that Composer silently tolerates: a file src/Order/CreateOrder.php that declares namespace App\Orders; or class CreateOrderHandler. Composer will simply fail to autoload it at runtime; this sniff fails the build instead.
Install it as a dev dependency:
composer require --dev suin/phpcs-psr4-sniff
If you don't already use dealerdirect/phpcodesniffer-composer-installer, register the standard manually (PHPCS 3.x):
vendor/bin/phpcs --config-set installed_paths vendor/suin/phpcs-psr4-sniff
vendor/bin/phpcs -i # confirm the "PSR4" standard is listed
Then run it against your source tree from the project root (it needs to find composer.json):
vendor/bin/phpcs --standard=PSR4 src
Where to look first:
README.md — the canonical run command and any runtime config.PSR4/ruleset.xml) — tells you the sniff's registered name and where it will apply.First use case: run it against a directory you've just reorganized. Deliberately rename a class or change a namespace prefix in one file and confirm the sniff reports it. That one-minute experiment tells you whether it's wired up correctly before you add it to CI.
Put it in a project ruleset rather than invoking it ad hoc. This is the pattern that actually survives:
<?xml version="1.0"?>
<ruleset name="MyProject">
<file>app</file>
<file>src</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/storage/*</exclude-pattern>
<exclude-pattern>*/database/migrations/*</exclude-pattern>
<exclude-pattern>*/tests/Fixtures/*</exclude-pattern>
<rule ref="PSR4"/>
<rule ref="PSR12"/>
</ruleset>
Then vendor/bin/phpcs picks it up automatically, and editors/CI share one definition.
In a Laravel app, app/ maps to App\, so the standard works out of the box — but scope the ruleset carefully. Exclude config/, routes/, database/migrations/, resources/, and bootstrap/cache/, where files are intentionally not one-class-per-file PSR-4 artifacts. If you exclude too little you'll drown in false positives and disable the sniff within a week.
Chain it into Composer scripts so it runs wherever your tests run:
"scripts": {
"lint:psr4": "phpcs --standard=PSR4 app src",
"check": ["@lint:psr4", "@test"]
}
Run it in CI as a distinct step from your style checks. A PSR-4 violation is a correctness bug (the class won't autoload), not a formatting preference, so treat its failure differently from a PSR-12 failure. Keeping it separate also makes it easier to adopt: you can allow style drift while blocking autoload breakage.
Adopt gradually on legacy code. If the codebase has hundreds of mismatches, start with a report-only job (phpcs ... || true) or only target the directories that are already clean. New code goes through the gate immediately; old code gets cleaned up opportunistically when files are touched.
Check only changed files in pre-commit hooks. Full-tree runs get slow on large repos; feed git diff --name-only --diff-filter=ACM -- '*.php' into phpcs --stdin-path=... (or xargs) for the hook, and reserve the full scan for CI.
Pair it with composer dump-autoload --optimize --strict-psr. Since Composer 2.x, --strict-psr also reports PSR-4 path/namespace mismatches; running both gives you coverage from the Composer side and from the editor/CI side.
composer update pull in PHP_CodeSniffer 4.x without testing — the internal APIs changed and this sniff will likely break. Pin squizlabs/php_codesniffer to ^3.x for as long as you depend on it.composer.json to know the namespace-to-directory mapping. Run it from the directory containing the correct composer.json. In a monorepo, running from the root may resolve the wrong autoload block — invoke per package, or from a working directory where the mapping is unambiguous."App\\": "app/" and "App\\Tests\\": "tests/" needs both to be discovered; if the sniff only recognizes one, you'll get spurious failures in tests/.createOrder.php declaring CreateOrder will pass locally and fail on Linux CI (or fail to autoload in production). Run the sniff in a Linux CI job — that's where it earns its keep.1 means violations found; 2 means a configuration/execution error (e.g., standard not found). Treating 2 as "just violations" is how people ship with a silently disabled sniff.-s. vendor/bin/phpcs --standard=PSR4 -s src prints the sniff code on each message, which is what you need if you're excluding individual sniffs or writing phpcs:ignore comments.phpcs:ignore comments are the escape hatch, but use them sparingly and with the code name (// phpcs:ignore PSR4.…). A blanket ignore is a future autoload bug.composer dump-autoload --strict-psr and a smoke test that references the class.How can I help you explore Laravel packages today?