rector/rector-symfony
Rector rules for Symfony that automate framework upgrades and code modernization. Apply automated refactorings across Symfony projects, including deprecation fixes and version migrations, using the Rector tool.
Reality check first: this package is a set of Rector rules for upgrading and modernising Symfony code. In a pure Laravel app it will find very little to do, because Laravel's own conventions (facades, app(), Eloquent, Blade) aren't Symfony idioms. It becomes genuinely useful in four situations:
composer require --dev rector/rector rector/rector-symfony
<?php
// rector.php
use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withSets([
SymfonySetList::SYMFONY_CODE_QUALITY,
]);
Then, always:
vendor/bin/rector process --dry-run # read the diff first
vendor/bin/rector process # apply
vendor/rector/rector-symfony/config/sets/ — every set is a plain PHP array of rule classes. Reading the set file is faster than reading docs and tells you exactly what will change.SymfonySetList constants — the canonical list of what you can opt into. Upgrade sets are versioned (SYMFONY_60, SYMFONY_64, SYMFONY_70, SYMFONY_71…); there are also non-versioned sets like SYMFONY_CODE_QUALITY and SYMFONY_CONSTRUCTOR_INJECTION.SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES if you have any Doctrine annotation code.Start with SYMFONY_CODE_QUALITY only. It is a safe, mostly-semantics-preserving cleanup set that removes dead container calls, simplifies getParameter() usage, and normalises a few Symfony idioms. If that produces zero diff on your Laravel codebase — which is the likely outcome — you've confirmed the package's cost/benefit for the project in about two minutes, and you can keep it in rector.php purely for the day a Symfony-flavoured dependency lands in src/.
Don't dump everything into one withSets(). Order and separation matter, because a later set can rewrite code an earlier one just produced.
return RectorConfig::configure()
->withPaths([__DIR__ . '/src'])
->withSets([
// 1. Symfony idioms / dead code — safe
SymfonySetList::SYMFONY_CODE_QUALITY,
// 2. Constructor injection — opinionated, review the diff
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
// 3. Version upgrades — only after 1 & 2 are clean
SymfonySetList::SYMFONY_70,
]);
Run stage 1, commit. Run stage 2, commit. Run stage 3, commit. Three small PRs beat one 400-file PR every time.
For a Laravel-first repo, the highest-value use is a non-blocking CI job that fails when new Symfony-idiom debt appears:
- run: vendor/bin/rector process --dry-run --no-progress-bar
If it's noisy on day one (--dry-run exits non-zero on any diff), gate it to the directories that actually contain Symfony-adjacent code:
->withPaths([__DIR__ . '/src/Support', __DIR__ . '/packages'])
When you're modernising one specific thing — say, converting $container->get() to constructor injection in a single service class — skip the set and name the rule:
->withRules([
\Rector\Symfony\Rector\Class_\ConstructorInjectionRector::class, // illustrative
])
and combine with path scoping:
vendor/bin/rector process src/Support/Http --dry-run
This is how you make the package earn its keep in a Laravel repo: a dozen narrow, deliberate invocations rather than one broad sweep.
rector-laravelThe two packages are complements, not competitors. In a hybrid or package repo:
->withSets([
\Rector\Laravel\Set\LaravelSetList::LARAVEL_110,
SymfonySetList::SYMFONY_CODE_QUALITY,
])
Rector's rule engine walks each file once per set; having both registered means a single rector process run covers both halves of a shared codebase. Keep the PHP level consistent across both (->withPhpSets()), because Symfony upgrade rules sometimes emit code that needs ->withPhp82() (readonly classes, never return types) to stay valid.
If you're porting Symfony code into Laravel:
Doing it in this order keeps the port diff readable — reviewers see "Symfony 5.4 → 6.4" and "Symfony → Laravel" as two distinct changes instead of one tangled mess.
for pkg in packages/*/; do
vendor/bin/rector process "$pkg/src" --dry-run --clear-cache || echo "diff in $pkg"
done
--clear-cache between runs is cheap insurance; Rector's cache is keyed on file hashes and config, and stale entries after a config change are a classic source of "why isn't my rule firing."
rector-symfony majors track Rector majors. Rector 1.x → rector-symfony 1.x; Rector 2.x → rector-symfony 2.x. If you see rules silently doing nothing or a Class not found on a set constant, check this pairing before anything else. Pin both in composer.json and bump them together.
This is the biggest foot-gun for a Laravel developer. SYMFONY_60 is not "make my code nicer" — it's "I no longer support Symfony 5.x, so remove the compatibility shims." It will happily delete branches guarded by method_exists() or Kernel::VERSION checks that you still need.
Laravel pins Symfony components at whatever the framework's composer.json allows. You are not the one deciding the Symfony version — Laravel is. So the version-gated upgrade sets are largely inert or actively wrong in an app; reserve them for packages where you control the constraint.
Practical check before running any SYMFONY_7x set:
composer show symfony/http-foundation | grep versions
Rector rules are type-aware. If a class isn't autoloadable, or a return type is mixed, the rule quietly skips it. Symptoms: "the rule I enabled did nothing."
Fixes, in order:
--debug to see per-file, per-rule decisions.--autoload-file (or the default vendor/autoload.php) resolves your classes.vendor/bin/rector process src/Foo.php --dry-run --debug.--only-rule for surgical debuggingWhen a large set produces one weird diff, isolate it:
vendor/bin/rector process src/Foo.php --dry-run --only-rule='Rector\Symfony\...\SomeRector'
Nine times out of ten the culprit is a rule that misreads a dynamic call as a static one.
withSkip() beats revertingRather than hand-reverting a diff after every run, encode the exception:
->withSkip([
\Rector\Symfony\Rector\Class_\SomeRector::class => [
__DIR__ . '/src/Legacy/Bridge.php',
],
// or skip by rule everywhere
\Rector\Symfony\Rector\MethodCall\WhateverRector::class,
])
This survives rebases and keeps CI honest. Document why in a comment — future-you will not remember.
Rector adds FQCNs inline unless ->withImportNames() is set. Always finish with Pint or PHP-CS-Fixer:
vendor/bin/rector process && vendor/bin/pint
Otherwise you ship diffs full of \Symfony\Component\... in the middle of method bodies and your reviewers will hate you.
SYMFONY_CONSTRUCTOR_INJECTION will move $this->get() / $this->container->get() into constructor properties. That's correct Symfony practice and largely wrong for Laravel's container (app(), facades, resolve()). If a file is Laravel-shaped, exclude it — don't let the rule "fix" a facade call into a constructor dependency that breaks lazy resolution or makes tests harder to write.
Both rewrite files. Run Rector → commit-check → CS fixer → commit-check, in that order. In a single lint-staged pass they can fight, and git add captures whichever wrote last.
The package's real long-term value for a Laravel dev isn't its rule list — it's that it teaches you the shape of a Rector rule. When you find yourself doing the same mechanical refactor across 30 files (say, replacing a facade with an injected dependency, or migrating $dates to $casts), write a rule in rector-rules/ in your own repo. Rector's AbstractRector + a node-type pair is genuinely 40 lines of code. The Symfony package is the best available reference implementation of a domain-specific rule collection.
Adopt Rector immediately. Adopt rector-symfony only if you own code with a Symfony composer.json constraint — a dual-framework package, a bundle, or a component-heavy shared library. For a vanilla Laravel app, installing it costs nothing and does nothing; installing it and then enabling SYMFONY_7x sets because "newer is better" is how you get a 300-file diff that breaks your test suite on a Tuesday afternoon.
How can I help you explore Laravel packages today?