RectorPHP and Laravel: The Automated Refactoring Tool We Should Have Been Using Years Ago
28th October 2025This post was originally published on the Jump24 blog, where I serve as Technical Director. I've syndicated it here because I wrote it, I'm proud of the work, and I want my personal site to reflect the full scope of what I'm building - both professionally and personally. If you're here for Laravel insights, you'll find them alongside golf breakdowns and life updates. If you found this helpful, you might enjoy Jump24's other content too.
TL;DR
Install:
composer require rector/rector --devcomposer require rector/rector-laravel --dev
Run dry run:
vendor/bin/rector process --dry-run
Apply changes, run tests, commit:
vendor/bin/rector processphp artisan test
Cuts Laravel upgrade time by 40–60 %.
✅ Applies framework upgrade rules, type hints, and modern PHP syntax automatically.
✅ Integrates easily into CI / pre-commit hooks.
The Pain We All Know
Hands up — how many of you have a Laravel 9 or 10 application that should be upgraded but the idea of refactoring thousands of lines makes you want to switch careers? We’ve been there. More than once.
Over the years we've worked on projects we've used things like laravelshift to help us with our Laravel upgrades and this has worked flawlessly. But there are times we want to do more than just upgrade we might want to harden our current codebase.
While many of us were still manually updating redirect()->route() calls to to_route() and adding type hints one controller at a time, a tool called RectorPHP was quietly doing this for us — automatically.
After spending a bit of time integrating Rector into one of our recent upgrade projects at Jump24, our main thought was: why didn’t we do this years ago?
Rector isn’t just a formatter. It understands your code’s structure (AST-level parsing) and safely transforms it using defined rules. The Laravel community maintains a first-class package — rector/rector-laravel — packed with upgrade and quality rules tailor-made for Laravel projects.
Let’s see how it changes everything.
One other thing before we dive in, one of the team behind Rector a very clever Tomas Votruba also wrote another package we're a big fan of which is Easy Coding Standards, you should give it a look sometime trust me you wont be disappointed.
The Problem with Laravel Upgrades We've All Faced
You know the drill. A new Laravel version drops with exciting features, breaking changes, and deprecations. You read through the upgrade guide, mentally calculate the hours required, and... postpone it for another sprint. Then another. Before you know it, you're three versions behind and the technical debt is mounting. Or worse still your just not been given the time to upgrade due to other workloads so the application stays outdated.
We've upgraded dozens of Laravel applications over the years, and the pattern goes something like this:
Run composer update and watch everything explode
Manually find and fix deprecated method calls
Update route definitions to new syntax
Add type hints that newer PHP versions support
Refactor validation arrays that used to be strings
Update test syntax that changed between versions
Fix all the things you missed in testing
Repeat for the next version
This is made a little bit easier if the application you're updating has tests as this is a great way to know if the changes you've made break anything.
For a medium-sized application, this could easily be 20-40 hours of tedious, error-prone work. For larger applications? We've had upgrade projects run for weeks.
The worst part? Most of these changes are mechanical. Converting array_get() to Arr::get(), updating Blade component syntax, changing validation rule formats - these aren't creative problem-solving tasks. They're just grunt work that machines should be doing for us.
Enter RectorPHP: Your Automated Refactoring Assistant
Rector is a command-line tool that parses your PHP code into an Abstract Syntax Tree (AST), applies transformation rules, and outputs the refactored code. Sounds complex, but using it is dead simple.
The Laravel community maintains rector/rector-laravel, a package with over 100 Laravel-specific rules that handle common upgrade patterns, code quality improvements, and type safety enhancements. It understands Laravel conventions and can safely refactor your code without breaking functionality.
Here's what Rector can do for your Laravel application:
Version Upgrades: Automatically apply breaking changes between Laravel versions Type Safety: Add type hints and return types where they can be inferred Code Quality: Remove dead code, simplify conditionals, modernise syntax Framework Patterns: Convert to newer Laravel patterns (factories, helpers, validation) Testing Updates: Update test syntax for newer PHPUnit/Pest versions
The magic is that Rector doesn't just find-and-replace text - it understands the semantic meaning of your code. It knows that where('status', '=', 'active') should become where('status', 'active'), but where('created_at', '>', $date) should stay as-is because the operator matters.
Getting Started: Installation and Basic Configuration
Let's get Rector installed and configured for a Laravel project. First, require it as a dev dependency:
composer require rector/rector --devcomposer require rector/rector-laravel --dev
Create a rector.php file in your project root.
<?phpdeclare(strict_types=1);use Rector\Config\RectorConfig;use RectorLaravel\Set\LaravelSetList;use RectorLaravel\Set\LaravelLevelSetList;return RectorConfig::configure() ->withPaths([ __DIR__ . '/app', __DIR__ . '/config', __DIR__ . '/database', __DIR__ . '/routes', __DIR__ . '/tests', ]) ->withSkip([ __DIR__ . '/bootstrap', __DIR__ . '/vendor', ]) ->withSets([ LaravelLevelSetList::UP_TO_LARAVEL_110, LaravelSetList::LARAVEL_CODE_QUALITY, ]);
This configuration tells Rector to:
Scan your app, config, database, routes, and tests directories
Skip bootstrap and vendor (obviously)
Apply all Laravel upgrade rules up to version 11
Apply Laravel code quality improvements
To see what Rector would change without actually modifying files:
vendor/bin/rector process --dry-run
To then apply these changes:
vendor/bin/rector process
Rector will show you a nice diff of every change it makes, so you can review before committing.
Examples: Upgrading Laravel Applications Safely
Let's walk through some transformations we can see that people would use when they need to upgrade their Laravel application.
Upgrading Route Syntax
Laravel 9 introduced the to_route() helper as a cleaner alternative to redirect()->route(). Manually finding and updating every occurrence across a large application is tedious. Rector handles it automatically with the help of the RedirectRouteToToRouteHelperRector Rule:
<?php// Beforepublic function store(Request $request){ // ... validation and saving logic return redirect()->route('users.index') ->with('success', 'User created successfully');}// After - Rector applies RedirectRouteToToRouteHelperRectorpublic function store(Request $request){ // ... validation and saving logic return to_route('users.index') ->with('success', 'User created successfully');}
This applies across your entire codebase in seconds.
Modernising Validation Rules
Laravel encourages using array-based validation rules rather than pipe-separated strings. Rector converts these automatically with ValidationRuleArrayStringValueToArrayRector:
<?php// Beforepublic function rules(): array{ return [ 'email' => 'required|email|unique:users,email', 'name' => 'required|string|max:255', 'age' => 'nullable|integer|min:18', ];}// Afterpublic function rules(): array{ return [ 'email' => ['required', 'email', 'unique:users,email'], 'name' => ['required', 'string', 'max:255'], 'age' => ['nullable', 'integer', 'min:18'], ];}
This makes validation rules easier to read, test, and maintain. Your IDE can now autocomplete individual rules when you're editing them.
Converting Legacy Factories to Classes
If you're upgrading from Laravel 7 or earlier, you'll need to convert old closure-based factories to the new class-based syntax. This is one of those changes that's incredibly tedious to do manually. Rector's FactoryDefinitionRector handles it:
<?php// database/factories/ModelFactory.php - Before$factory->define(User::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'password' => bcrypt('password'), ];});// database/factories/UserFactory.php - Afternamespace Database\Factories;use App\Models\User;use Illuminate\Database\Eloquent\Factories\Factory;class UserFactory extends Factory{ protected $model = User::class; public function definition(): array { return [ 'name' => $this->faker->name, 'email' => $this->faker->unique()->safeEmail, 'password' => bcrypt('password'), ]; }}
Improving Conditional Patterns
Laravel provides abort_if() and abort_unless() helpers that make conditional abort patterns cleaner. Rector identifies these patterns and refactors them automatically using the AbortIfRector rule:
<?php// Beforepublic function show(User $user){ if (! $user->isActive()) { abort(403, 'User account is not active'); } if ($user->isBlocked()) { abort(403, 'User is blocked'); } return view('users.show', compact('user'));}// After - Rector applies AbortIfRectorpublic function show(User $user){ abort_if(! $user->isActive(), 403, 'User account is not active'); abort_if($user->isBlocked(), 403, 'User is blocked'); return view('users.show', compact('user'));}
More concise, more readable, and the intent is clearer at a glance.
The Type Safety Revolution: Adding Type Hints Automatically
This is where Rector becomes genuinely game-changing. Modern PHP's type system is brilliant for catching bugs early, but adding types to an existing codebase feels like climbing Everest. Rector can do most of this work for you.
The LARAVEL_TYPE_DECLARATIONS set adds type hints and return types where they can be safely inferred:
<?php// Before - No type informationclass UserService{ private $repository; public function __construct($repository) { $this->repository = $repository; } public function findByEmail($email) { return $this->repository->findWhere(['email' => $email]); } public function getActiveUsers() { return $this->repository->scopeQuery(function($query) { return $query->where('active', true); })->all(); }}// After - Rector adds type hints where safeclass UserService{ private UserRepository $repository; public function __construct(UserRepository $repository) { $this->repository = $repository; } public function findByEmail(string $email): ?User { return $this->repository->findWhere(['email' => $email]); } public function getActiveUsers(): Collection { return $this->repository->scopeQuery(function($query) { return $query->where('active', true); })->all(); }}
Rector is clever here - it only adds types where it can be absolutely certain. If there's ambiguity, it leaves the code alone rather than risk breaking something.
Adding Generic Types to Eloquent Relationships
One area where Laravel's type system traditionally falls down is relationship methods. Rector can add proper generic return types with AddGenericReturnTypeToRelationsRector Rule:
<?php// Beforeclass Post extends Model{ public function author() { return $this->belongsTo(User::class); } public function comments() { return $this->hasMany(Comment::class); } public function tags() { return $this->belongsToMany(Tag::class); }}// After - Rector adds generic typesuse Illuminate\Database\Eloquent\Relations\BelongsTo;use Illuminate\Database\Eloquent\Relations\BelongsToMany;use Illuminate\Database\Eloquent\Relations\HasMany;class Post extends Model{ /** * @return BelongsTo<User> */ public function author(): BelongsTo { return $this->belongsTo(User::class); } /** * @return HasMany<Comment> */ public function comments(): HasMany { return $this->hasMany(Comment::class); } /** * @return BelongsToMany<Tag> */ public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); }}
This makes your IDE's autocomplete significantly smarter and helps static analysis tools like PHPStan understand your code better.
Enabling Strict Types Across Your Codebase
Once you've got type hints in place, the next step is enabling strict type checking with declare(strict_types=1). Doing this manually across hundreds of files is soul-destroying. Rector can add it automatically with DeclareStrictTypesRector:
<?php// Beforenamespace App\Services;use App\Models\User;class NotificationService{ // class implementation}// After - Rector adds declare statement<?phpdeclare(strict_types=1);namespace App\Services;use App\Models\User;class NotificationService{ // class implementation}
You can use Rector to gradually roll out strict types across our applications, starting with new code and progressively adding it to older code as we refactor. Combined with PHPStan, this catches type errors before they reach production.
Code Quality Improvements: Beyond Upgrades
Rector isn't just for version upgrades - it can improve your code quality too. The LARAVEL_CODE_QUALITY set includes rules that modernise your code and remove common anti-patterns.
Removing Debug Code
We've all done it - left a dd() or dump() in production code. Rector can find and remove these automatically with RemoveDumpDataDeadCodeRector:
<?php// Configure in rector.phpuse RectorLaravel\Rector\FuncCall\RemoveDumpDataDeadCodeRector;return RectorConfig::configure() ->withConfiguredRule(RemoveDumpDataDeadCodeRector::class, [ 'dd', 'dump', 'var_dump', 'ddd' ]);// Beforepublic function calculateTotal(Order $order): float{ $subtotal = $order->items->sum('price'); dd($subtotal); // Oops! $tax = $subtotal * 0.20; $total = $subtotal + $tax; return $total;}// Afterpublic function calculateTotal(Order $order): float{ $subtotal = $order->items->sum('price'); $tax = $subtotal * 0.20; $total = $subtotal + $tax; return $total;}
This is brilliant for pre-deployment checks. Add it to your CI pipeline and Rector will catch any debug statements before they reach production.
Simplifying Collection Operations
Laravel's collections are powerful, but sometimes we write overly verbose code. Rector identifies opportunities to simplify:
<?php// Before$activeUsers = $users->filter(function ($user) { return $user->active === true;});$userNames = $activeUsers->map(function ($user) { return $user->name;});// After - Rector simplifies where possible$activeUsers = $users->filter(fn ($user) => $user->active);$userNames = $activeUsers->pluck('name');
These are small wins, but they add up across a large codebase.
Converting to Modern PHP Syntax
Rector keeps your code using modern PHP features. For example, it'll convert old array syntax to short array syntax, use nullsafe operators where appropriate, and leverage match expressions:
<?php// Beforepublic function getStatusLabel($status){ switch ($status) { case 'pending': return 'Awaiting Review'; case 'approved': return 'Approved'; case 'rejected': return 'Rejected'; default: return 'Unknown'; }}// After - Rector converts to match expressionpublic function getStatusLabel(string $status): string{ return match ($status) { 'pending' => 'Awaiting Review', 'approved' => 'Approved', 'rejected' => 'Rejected', default => 'Unknown', };}
Modern PHP is more expressive and often safer - match expressions throw errors on unhandled cases, whereas switch statements silently fall through.
Advanced Configuration: Tailoring Rector to Your Needs
Rector's real power comes from customisation. You can create fine-grained control over exactly which rules apply to which parts of your codebase.
Applying Rules Incrementally
Don't try to apply all rules at once - that way lies madness and merge conflicts.
<?php// Week 1: Just upgrade rulesreturn RectorConfig::configure() ->withSets([ LaravelLevelSetList::UP_TO_LARAVEL_110, ]);// Week 2: Add code quality improvementsreturn RectorConfig::configure() ->withSets([ LaravelLevelSetList::UP_TO_LARAVEL_110, LaravelSetList::LARAVEL_CODE_QUALITY, ]);// Week 3: Add type declarationsreturn RectorConfig::configure() ->withSets([ LaravelLevelSetList::UP_TO_LARAVEL_110, LaravelSetList::LARAVEL_CODE_QUALITY, LaravelSetList::LARAVEL_TYPE_DECLARATIONS, ]);
This makes code review manageable and reduces the risk of introducing bugs.
Excluding Specific Rules
Sometimes Rector's changes aren't appropriate for your codebase. You can exclude specific rules:
<?phpuse Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;use RectorLaravel\Rector\MethodCall\RedirectRouteToToRouteHelperRector;return RectorConfig::configure() ->withSets([ LaravelSetList::LARAVEL_CODE_QUALITY, ]) ->withSkip([ // Keep redirect()->route() syntax (we prefer it) RedirectRouteToToRouteHelperRector::class, // Don't remove promoted properties even if unused RemoveUnusedPromotedPropertyRector::class, // Skip specific files __DIR__ . '/app/Legacy', ]);
This gives you surgical control over what changes.
Creating Custom Rules
For patterns specific to your application, you can write custom Rector rules. Here's a simple example that converts a deprecated helper we built to its replacement:
<?phpnamespace App\Rector;use PhpParser\Node;use PhpParser\Node\Expr\FuncCall;use PhpParser\Node\Name;use Rector\Rector\AbstractRector;use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;final class OldHelperToNewHelperRector extends AbstractRector{ public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( 'Convert old get_user_meta() to new getUserMeta()', [] ); } public function getNodeTypes(): array { return [FuncCall::class]; } public function refactor(Node $node): ?Node { if (! $node instanceof FuncCall) { return null; } if (! $this->isName($node, 'get_user_meta')) { return null; } $node->name = new Name('getUserMeta'); return $node; }}
Then register it in your rector.php:
<?phpuse App\Rector\OldHelperToNewHelperRector;return RectorConfig::configure() ->withRules([ OldHelperToNewHelperRector::class, ]);
We've used custom rules to modernise application-specific patterns across legacy codebases. It's incredibly powerful.
Integrating Rector into Your Workflow
Rector is most valuable when it runs automatically as part of your development workflow.
GitHub Actions Integration
We run Rector on every pull request to catch issues early:
name: Rectoron: [pull_request]jobs: rector: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.3 coverage: none - name: Install Dependencies run: composer install --no-interaction --prefer-dist - name: Run Rector run: vendor/bin/rector process --dry-run --output-format=github
Pre-commit Hooks
For smaller projects, you could use Husky or similar to run Rector on staged files before committing:
#!/bin/shvendor/bin/rector process --dry-run $(git diff --cached --name-only --diff-filter=ACM | grep '.php$')
This ensures new code follows current patterns without requiring CI time.
Continuous Refactoring
On long-running projects, you could schedule Rector runs weekly to gradually improve the codebase:
<?php// Weekly: Gradually add strict typesreturn RectorConfig::configure() ->withPaths([ // Only process files modified in last week // (detected via git diff) ]) ->withPreparedSets( typeDeclarations: true, deadCode: true );
This "continuous refactoring" approach keeps technical debt from accumulating.
The Reality Check
Look, Rector isn't perfect, here are the gotchas we've encountered.
False Positives Happen
Rector occasionally suggests changes that break things. We've seen it:
Remove seemingly dead code that was actually used via magic methods
Add strict types that break edge cases in tests
Refactor dynamic property access in ways that don't account for Laravel's magic
Solution: Always review Rector's changes. Run your test suite. Don't blindly accept every suggestion.
Type Inference Has Limits
Rector can only add types where it's completely certain. This means:
Complex dynamic code stays untyped
Methods with union types often stay as-is
Generic collection types might not be inferred correctly
Solution: Use Rector as a first pass, then manually add remaining types with PHPStan's help.
Configuration Can Be Overwhelming
With 100+ Laravel rules and hundreds more from core Rector, figuring out which to use is daunting. We've spent days tweaking configs before finding the right balance.
Solution: Start with Laravel's level sets (they're sensible defaults), then gradually add specific rules as you need them.
Performance on Large Codebases
Rector can be slow on massive applications.
Solution: Use Rector's parallel processing (--parallel flag) and configure caching properly. Only run it on changed files in CI, not the entire codebase.
Not a Replacement for Understanding
Rector can apply mechanical changes, but it can't understand your business logic or make architectural decisions. You still need to know Laravel.
Solution: Use Rector to handle grunt work, but invest time in understanding what changes it's making and why.
The Bottom Line
After using Rector on a number of projects, it's become an indispensable part of our Laravel toolkit. Is it perfect? No. Will it solve all your refactoring needs? Also no. But for the mechanical, tedious work of upgrading Laravel applications and maintaining code quality, it's genuinely transformative.
The time we used to spend manually updating method calls and adding type hints is now spent on actual problem-solving - building features, improving architecture, optimising performance. Rector handles the grunt work so we don't have to.
If you're sitting on a legacy Laravel application dreading the upgrade process, or you just want to modernise your codebase incrementally, give Rector a try. Start small with a single rule set, see what it can do, and gradually expand from there.
Your future self (and your team) will thank you when that next Laravel version drops and you're not facing weeks of manual refactoring.
Your Turn
Have you used Rector on your Laravel projects? Any horror stories or success stories to share? We'd love to hear about your experiences. Are there specific refactoring patterns you wish could be automated? Drop us a line or share your thoughts.
And if you're staring down the barrel of a major Laravel upgrade and thinking "this sounds brilliant but I don't have time to figure it out," get in touch. We've now upgraded dozens of Laravel applications with Rector, and we'd be happy to help you modernise your codebase safely and efficiently. As a Laravel Partner with over 11 years of experience, we've seen (and fixed) just about every upgrade scenario imaginable.
Want to stay updated on Laravel tooling and PHP features? Follow us for more deep dives into the tools and techniques we use every day to build better Laravel applications.