matthiasnoback/symfony-service-definition-validator
Validates Symfony service definitions at build time, catching misconfigured services, arguments, and references before they cause runtime errors. Useful in CI to keep your container configuration correct as the application grows, so broken service wiring fails fast instead of at deploy time.
Install as a dev dependency (it should never run in the prod environment):
composer require --dev matthiasnoback/symfony-service-definition-validator
The package ships a Symfony bundle that registers validation compiler passes. Enable it only in dev and test:
// config/bundles.php
return [
// ...
MatthiasNoback\SymfonyServiceDefinitionValidator\SymfonyServiceDefinitionValidatorBundle::class => ['dev' => true, 'test' => true],
];
The validations run at container compile time, so the first way to invoke them is simply to warm the cache:
bin/console cache:warmup --env=dev
# or
bin/console cache:clear
If any validator fails it throws during compilation, which is how you'll see the report.
First use case: drop it into a legacy or long-lived Symfony app and warm the cache. The highest-signal validators are usually the ones for unused service definitions and unused constructor arguments — they quickly show you services nobody injects and arguments added "just in case" years ago. Start there, fix the cheap ones (delete the service / argument), then tighten the rules that are noisier for your codebase.
Where to look first in the repo:
src/DependencyInjection/ — see which compiler passes are registered and how they're toggled.src/Validator/ — the actual checks; each interface/class shows you what is inspectable.service_definition_validator: section) — that's where you enable/disable individual validators.CI gate via cache warmup. Because validation is a compile-time pass, the simplest CI job is:
APP_ENV=test bin/console cache:warmup --no-warmup
(or just cache:clear) in a test/dev environment. A failure fails the build. This is the pattern that makes the package pay for itself — validation runs on every PR without a separate command to remember.
Incremental adoption with a baseline. Large apps will produce hundreds of findings on day one. Turn validators on one at a time (via config) and land them in separate PRs: first UnusedConstructorArgument, then UnusedServiceDefinition, then stricter ones (e.g. "depend on interfaces, not concrete classes"). Don't try to enable everything at once — the signal-to-noise collapses.
Exclude noisy services with tags or config. Services that are legitimately referenced dynamically (event subscribers resolved by name, console commands, tagged iterators) will show up as "unused." Exclude them via the package's exclusion config or by tagging them, so the validator only reports things a human can actually fix.
Pair with built-in introspection. Use it alongside debug:container and debug:autowiring:
bin/console debug:container --show-private | grep SomeService
Validators tell you what's wrong; debug:container tells you why a service exists and who references it.
Custom validator (extension point). If you have house rules (e.g. "no service may be public unless it matches *Controller"), implement the package's validator interface and register it as a tagged compiler pass. This is where you encode team conventions once rather than in code review.
Diff-based review. When a validator fails, the diff between "before" and "after" the offending PR is usually where the fix belongs. Wire the check so it's part of cache:warmup in the merge pipeline — that way the failing change is caught in the same PR that introduced it.
$container->get('foo')) or resolved via a factory the container can't see will be reported as unused. This is the most common source of noise in older apps. Exclude these services explicitly.%env()% and dynamic arguments. Arguments that resolve at runtime are not statically inspectable. Don't expect the "unused argument" validator to be authoritative on configurations driven by env vars or parameter resolution.ContainerBuilder, not what your app does at runtime. It cannot know that a tagged service is consumed by a compiler pass in another bundle unless that pass declares its dependency correctly. Make sure compiler passes that iterate tags also register the services they need.prod. If a validator fires in production, cache:warmup fails and the deploy breaks. Register the bundle in dev/test only, and if you must validate in staging, run it in a dedicated build step with a dev-like env.cache:warmup. If it becomes painful in CI, run the full set of validators only on the main branch and a subset on PRs.symfony/* constraints in composer.json before assuming Symfony 7 support — you may need to pin a compatible Symfony minor or patch the package's constraint. Run composer why-not symfony/framework-bundle if install fails.@required arguments. The unused-argument validator understands ? and @required on autowired arguments, but not every construct — variadic and promoted-property edge cases can trip it. When a report looks wrong, verify by inspecting the raw definition with debug:container --show-arguments.string reference somewhere is a silent ServiceNotFoundException waiting in a code path CI doesn't exercise. Grep the codebase for the service id before deleting, even if the validator says it's safe.config/packages/dev/ so toggles never leak into the test env unless you intentionally want them there (in which case mirror the file into config/packages/test/).How can I help you explore Laravel packages today?