spryker/code-sniffer
Spryker Code Sniffer is a PHPCS ruleset for PHP 8.3+ that follows PSR-2 and PSR-12 with extra Spryker convention sniffs. It ships Spryker, SprykerStrict, and opt-in SprykerConventions standards, and works via phpcs/phpcbf.
This is a PHP_CodeSniffer standard, not a Laravel package. There is no service provider, no config/code-sniffer.php, no Artisan command. It ships two rulesets (Spryker and the stricter SprykerStrict) plus ~80 custom sniffs on top of PSR-2/PSR-12 and slevomat/coding-standard.
composer require --dev spryker/code-sniffer
That pulls in squizlabs/php_codesniffer and slevomat/coding-standard as transitive deps. You call the binaries directly — the console code:sniff:style shortcut mentioned in the README belongs to Spryker's Development module and does not exist in a Laravel app.
First run — sniff only, show sniff codes (-s), summary report:
vendor/bin/phpcs --standard=vendor/spryker/code-sniffer/Spryker/ruleset.xml app/ -s --report=summary
Where to look first, in this order:
vendor/spryker/code-sniffer/Spryker/ruleset.xml — the actual truth. Read it before trusting the README.docs/sniffs.md — generated list of included sniffs, one line each.docs/README.md — the parts that matter for a non-Spryker codebase: silencing, phpVersion, .license, test_files skipping.SprykerStrict/ruleset.xml — diff it against Spryker/ to see what "strict" adds.First realistic use case: do not try to adopt the whole standard on an existing Laravel codebase. A typical Laravel app will emit hundreds of violations from the file-doc-block, method-annotation and namespace sniffs that were written for Spryker's module layout. Instead, use it as a second-opinion audit on a single new module (app/Modules/Billing) or as a curated rule source you cherry-pick into your own phpcs.xml. Keep Laravel Pint for formatting; use this for the structural/semantic sniffs Pint can't express.
Never point CI at the vendor ruleset directly — you'll be at the mercy of upstream changes on composer update, and you can't exclude the Spryker-specific noise.
<?xml version="1.0"?>
<ruleset name="AppStandard">
<description>Laravel app standard, based on Spryker + Slevomat.</description>
<file>app</file>
<file>config</file>
<file>database</file>
<file>tests</file>
<rule ref="vendor/spryker/code-sniffer/Spryker/ruleset.xml">
<!-- Spryker DI/facade docblock sniffs: meaningless outside Spryker modules -->
<exclude name="Spryker.MethodAnnotation.ConfigMethodAnnotation"/>
<exclude name="Spryker.MethodAnnotation.EntityManagerMethodAnnotation"/>
<exclude name="Spryker.MethodAnnotation.FacadeMethodAnnotation"/>
<exclude name="Spryker.MethodAnnotation.FactoryMethodAnnotation"/>
<exclude name="Spryker.MethodAnnotation.QueryContainerMethodAnnotation"/>
<exclude name="Spryker.MethodAnnotation.RepositoryMethodAnnotation"/>
<exclude name="Spryker.Namespaces.SprykerNamespace"/>
<exclude name="Spryker.Internal.SprykerDisallowFunctions"/>
</rule>
<exclude-pattern>*/bootstrap/cache/*</exclude-pattern>
<exclude-pattern>*/storage/*</exclude-pattern>
<exclude-pattern>*/public/build/*</exclude-pattern>
<exclude-pattern>*/tests/fixtures/*</exclude-pattern>
</ruleset>
Then vendor/bin/phpcs with no --standard picks up phpcs.xml from the CWD.
"scripts": {
"cs": "phpcs -q --report=summary --parallel=4",
"cs:full": "phpcs -s --report=full",
"cs:fix": "phpcbf --parallel=4 || true",
"cs:changed": "phpcs -s --file-list=<(git diff --name-only --diff-filter=ACM origin/main...HEAD -- '*.php')"
}
cs:fix is deliberately || true — phpcbf exits non-zero when unfixable violations remain, which will otherwise break composer run chains. --parallel makes the difference on a large app/.
Two jobs. The blocking one only checks files touched in the MR; the advisory one produces a source report so you can see which sniffs dominate. Use --report=source before deciding what to silence — it aggregates by sniff, which is far more actionable than a 4,000-line wall of errors.
# blocking
git diff --name-only --diff-filter=ACM "$BASE"...HEAD -- '*.php' \
| xargs -r vendor/bin/phpcs -q --standard=phpcs.xml -s
The upstream migration guidance (lock to ~ patch releases, never bump the sniffer in a feature branch, land standard updates on a dedicated master-cs branch and merge forward) applies verbatim to a Laravel team with long-lived release branches. It's the single most useful thing in the docs.
Identical to the README, just swap the standard for your own ruleset:
$ProjectFileDir$/vendor/bin/phpcs, Parameters: --standard=$ProjectFileDir$/phpcs.xml -p $FilePath$, Working dir: $ProjectFileDir$$ProjectFileDir$/vendor/bin/phpcbf, Parameters: --standard=$ProjectFileDir$/phpcs.xml -v $FilePath$Bind to Ctrl+, / Ctrl+.. Prefer the single-file invocation over file watchers — file watchers running phpcbf on save will fight Pint on every keystroke.
Both tools rewrite the same bytes. Pick an order and stick to it:
phpcs runs in CI only, as an audit. If you must run phpcbf, exclude every sniff in the SlevomatCodingStandard.Whitespaces / SlevomatCodingStandard.Arrays families, or you get a reformat ping-pong.Silence a whole sniff, or one code within a sniff, without touching the source:
<!-- full silence -->
<rule ref="SlevomatCodingStandard.ControlStructures.ControlStructureSpacing">
<severity>0</severity>
</rule>
<!-- silence a single code (x.y.z.code) -->
<rule ref="SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName">
<severity>0</severity>
</rule>
The three-part form silences an entire sniff; the four/five-part form silences one violation code. Getting this wrong silently does nothing — always confirm the exact code with -s.
If you write your own (e.g. "no env() outside config/"), drop them in ./Sniffs/<Category>/ next to your ruleset, add <rule ref="./Sniffs"/>, and mirror the structure under ./tests/ — the base library auto-discovers both. Scaffold with:
php vendor/spryker/code-sniffer/tests/generate.php App.MyCategory.NoEnvOutsideConfig
bin/tokenize.php (in the package root) dumps the token stream for a file, which is the fastest way to figure out why a sniff isn't matching.
Most of the standard silently skips your code. Spryker.Namespaces.SprykerNamespace and all the MethodAnnotation sniffs only fire for namespaces in Pyz,SprykerEco,SprykerMiddleware,SprykerSdk,Spryker. On App\... code they pass vacuously. A green build does not mean "the Spryker standard passed" — it means "the subset that applies to Laravel passed." Exclude them explicitly so your report is honest, or reconfigure:
<rule ref="Spryker.Namespaces.SprykerNamespace">
<properties>
<property name="namespace" value="App"/>
<property name="isRoot" value="true"/>
</properties>
</rule>
rootDir is the escape hatch when src/ isn't your root — for Laravel that means app.
The shipped ruleset pre-excludes PHP 8 features. DisallowNamedArguments, DisallowTrailingCommaInDeclaration, DisallowConstructorPropertyPromotion and DisallowNullSafeObjectOperator are excluded upstream to avoid PHP 7.4 breakage. Because they're excluded via <exclude> inside the referenced ruleset, a plain <rule ref=".../Spryker/ruleset.xml"/> won't re-enable them. On a PHP 8.2+ Laravel app you'll want to re-add them by name:
<rule ref="SlevomatCodingStandard.Functions.DisallowNamedArguments"/>
Same story for Spryker.PHP.DeclareStrictTypesAfterFileDoc — strictTypesMandatory defaults off. Turning it on changes runtime semantics (no more implicit casts), so don't flip it in the same PR as anything else.
.license is a trap. Creating an empty .license at the repo root means "no license doc block is expected"; putting content there means every PHP file must match it byte-for-byte including the trailing newline. Add this file on a branch where you're prepared to accept either zero diff or a repo-wide diff — there is no middle ground, and the failure mode is thousands of identical file-comment errors that look like a broken sniffer.
config-set writes to the shared PHPCS config, not your project. vendor/bin/phpcs --config-set installed_paths ... mutates a config that lives with the binary — fine in CI (fresh composer install), confusing locally if you also have a global phpcs. Prefer explicit --standard=phpcs.xml and skip config-set entirely.
phpcbf needs multiple passes. Fixers conflict; a single run leaves residual violations and a non-zero exit. Loop until clean, and preview with --report=diff first:
phpcbf --report=diff app/ | head -200
while vendor/bin/phpcbf -q app/ >/dev/null 2>&1; do :; done
Stale cache after version bumps. If you pass --cache, delete the cache file whenever you change phpcs.xml or bump the sniffer — the cache key doesn't account for the standard's content, and you'll chase phantom "fixed" violations. --parallel and --cache are also mutually temperamental on old PHPCS versions.
phpVersion for the disallow-functions sniff is a separate knob from the platform requirement:
<rule ref="Spryker.Internal.SprykerDisallowFunctions">
<properties><property name="phpVersion" value="8.2"/></properties>
</rule>
Set it to off if you already depend on polyfills or shims that the sniff doesn't know about — otherwise it flags functions you've legitimately backfilled.
test_files is a magic folder name. Anything under a directory called test_files is auto-skipped. If you have fixture PHP that should be linted, don't name the directory test_files; conversely, renaming an existing fixtures dir to it is a free way to kill a whole category of noise.
Debugging sequence that actually works: -s to get codes → --report=source to rank by volume → silence the top three → re-run. Do not start with --report=full on a legacy app.
Adoption risk. 38 stars, and dependents effectively zero outside Spryker's own suite; the package is alive (releases into 2026, PHPStan level 8, CI badge) but its maintenance signal is "the vendor needs it," not "the community needs it." Pin it ("spryker/code-sniffer": "~1.2" or an exact version) and keep your real policy in your phpcs.xml. That way an upstream <exclude> or a new sniff can never turn a green branch red overnight.
How can I help you explore Laravel packages today?