A Symfony application is full of strings that mean something: route names,
template paths, translation keys, service ids, bundle configuration keys. To
PHP, they are strings like any others. Your CI pipeline verifies types with
PHPStan or Psalm, style with PHP-CS-Fixer and behavior with PHPUnit, but
nothing alerts you if you make a typo like
redirectToRoute('order_confirmaton').
The latest version of the Symfony Language Tools closes that gap. The new
check command runs a headless version of the Symfony-aware diagnostics from
the editor integrations: against saved files, from the command line, in CI. It
works for every Symfony application, whether or not anyone on the team uses an
editor integration.
Your CI Doesn't Know Symfony
Symfony reports these mistakes late, or never: mistyped route names, missing templates, missing translation keys. They survive static analysis and code review because, as far as types are concerned, everything is fine. Even the PHPStan Symfony extension, which makes PHP analysis container-aware, works at the type level and inside PHP files; templates, translations, routes and configuration files stay out of its reach.
The editor integrations catch these mistakes while you type, but an editor does not review a pull request and is not attached when a script or a coding agent modifies the application. These checks belong in CI, next to the tools you already run.
One Command in Symfony CLI
If you use the Symfony CLI (version 5.20.0 or newer), you already have the checker. Run it from the root of your workspace:
1 2 3 4 5 6 7
$ symfony lsp:check
Project .: runtime metadata, environment dev, complete
.:src/Controller/CheckoutController.php:14:40: error [route.not_found] Route "order_confirmaton" does not exist in the selected environment.
.:templates/checkout/confirmation.html.twig:1:8: error [translation.not_found] Translation "checkout.confirmed" does not exist in domain "messages".
.:templates/checkout/confirmation.html.twig:2:13: error [template.not_found] Template "checkout/summary.html.twig" does not exist in the selected environment.
Summary: 3 diagnostics, 3 active, 0 baseline matches, 0 stale baseline entries, 3 blocking
Symfony CLI downloads the latest stable Symfony Language Tools automatically or
you can download it from GitHub Releases and run symfony-lsp check; both
invocations accept the same options.
Without arguments, the command discovers Symfony applications and checks all supported project files; pass files, directories or patterns to focus the analysis:
1 2
$ symfony lsp:check src/ templates/
$ symfony lsp:check 'config/**/*.yaml'
The command reports Symfony-specific problems only, 30 diagnostic codes today
(symfony lsp:check --list-codes):
- unknown routes and missing required route parameters;
- missing templates and Twig components;
- unknown arguments of Twig functions and filters;
- missing translation keys, domains and message placeholders;
- unknown services and parameters;
- unknown console argument and option names;
- invalid bundle configuration keys, types and enums;
- unknown or incompatible environment variable processors;
- unknown Messenger buses and transports, and invalid handler signatures;
- unknown validation constraint options;
- unknown Stimulus controllers and importmap entrypoints;
- unknown security firewalls and user providers;
- unknown form options and invalid event listener methods.
Runtime analysis is enabled by default: the checker boots each application in
the selected Symfony environment and loads the same routes, services, bundle
configuration and other runtime metadata as the editor integrations, so
diagnostics reflect your real application, not a guess based on conventions.
Through Symfony CLI, it also inherits the project-aware symfony php
selection of PHP version and configuration. When a CI job must not execute
application code, use source-only mode:
1
$ symfony lsp:check --source-only
Reports say which analysis mode was used, and a runtime or indexing failure marks the result as incomplete instead of silently falling back to source-only analysis.
Reports Built for CI
The default report is a deterministic, human-readable list of projects, diagnostics and summary counts; three structured formats target other tools:
1 2 3
$ symfony lsp:check --format=json > diagnostics.json
$ symfony lsp:check --format=github
$ symfony lsp:check --format=sarif > symfony-lsp.sarif
The GitHub format emits workflow annotations directly on files and lines. A minimal GitHub Actions job:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# .github/workflows/symfony-diagnostics.yaml
name: Symfony diagnostics
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: symfony-cli
- run: composer install --no-progress
- run: symfony lsp:check --format=github
The SARIF 2.1 report can be uploaded to code-scanning systems and includes stable fingerprints, so findings can be tracked between runs.
Exit statuses distinguish findings from failures in the checker itself:
0: analysis completed without blocking diagnostics;10: analysis completed with blocking diagnostics;11: invocation, configuration, selection, code policy or baseline is invalid;12: analysis is incomplete because of an indexing, timeout, process or internal failure.
A failing diagnostic provider does not discard the other providers' findings:
the partial report remains available, but the incomplete status and exit code
12 prevent CI from treating it as clean.
Adopt the Checker Gradually
An existing application may already contain known diagnostics. An occurrence-specific baseline enforces new findings without hiding the existing ones:
1 2
$ symfony lsp:check --generate-baseline
$ symfony lsp:check --baseline=.symfony-lsp-baseline.json
Matches remain visible but do not block, survive unrelated line movement and
distinguish repeated occurrences of the same diagnostic. Refresh the baseline
explicitly with --refresh-baseline, or add --strict-baseline to
require stale entries to be removed.
You can also choose which diagnostic codes block CI without filtering the report:
1
$ symfony lsp:check --fail-on=route.not_found,translation.not_found
Unknown codes are rejected, so a renamed or removed code cannot silently weaken the policy.
The checker and the editor integrations share the .symfony-lsp.json project
configuration. Path exclusions are useful for embedded fixtures and generated
sources, and missing-translation diagnostics are opt-in, as in the editor:
1 2 3 4 5 6 7 8
{
"version": 1,
"translationDiagnostics": true,
"excludePaths": [
"tests/Fixtures/**",
"generated/"
]
}
See the headless diagnostics guide for output schemas, baseline behavior, privacy guarantees, caching and the complete command-line reference, and the project configuration guide for the shared settings.
Built for Agents Too
Coding agents write a growing share of your Symfony code, and they work the
way CI does: no editor attached, validating their changes with command-line
tools. Give them this one. An agent that runs symfony lsp:check after each
change catches its own invented route names, stale template references and
broken configuration before a human reviews the diff.
The command is designed for that loop: deterministic reports, stable exit codes, machine-readable formats with precise positions and a discoverable diagnostic contract. A human and an agent running the same check see exactly the same findings.
Add It to Your Pipeline
Start with a local symfony lsp:check, add a baseline if the application
needs one, then choose the output format and blocking policy that fit your
pipeline. Editor users keep the diagnostics they already have; now the rest of
the team, the CI and your agents get them too.
If the checker reports something it should not, misses a Symfony pattern used by your application or cannot boot the project reliably, report it on the issue tracker.