RectorPHP and Laravel: The Automated Refactoring Tool We Should Have Been Using Years Ago.
12 minutes
TL;DR
Install:
1composer require rector/rector --dev2composer require rector/rector-laravel --dev
Run dry run:
1vendor/bin/rector process --dry-run
Apply changes, run tests, commit:
1vendor/bin/rector process2php 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.
Looking to start your next Laravel project?
We’re a passionate, happy team and we care about creating top-quality web applications that stand out from the crowd. Let our skilled development team work with you to bring your ideas to life! Get in touch today and let’s get started!
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:
1composer require rector/rector --dev2composer require rector/rector-laravel --dev
Create a rector.php file in your project root.
1<?php 2 3declare(strict_types=1); 4 5use Rector\Config\RectorConfig; 6use RectorLaravel\Set\LaravelSetList; 7use RectorLaravel\Set\LaravelLevelSetList; 8 9return RectorConfig::configure()10 ->withPaths([11 __DIR__ . '/app',12 __DIR__ . '/config',13 __DIR__ . '/database',14 __DIR__ . '/routes',15 __DIR__ . '/tests',16 ])17 ->withSkip([18 __DIR__ . '/bootstrap',19 __DIR__ . '/vendor',20 ])21 ->withSets([22 LaravelLevelSetList::UP_TO_LARAVEL_110,23 LaravelSetList::LARAVEL_CODE_QUALITY,24 ]);
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:
1vendor/bin/rector process --dry-run
To then apply these changes:
1vendor/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:
1<?phpI 2// Before 3public function store(Request $request) 4{ 5 // ... validation and saving logic 6 7 return redirect()->route('users.index') 8 ->with('success', 'User created successfully'); 9}10 11// After - Rector applies RedirectRouteToToRouteHelperRector12public function store(Request $request)13{14 // ... validation and saving logic15 16 return to_route('users.index')17 ->with('success', 'User created successfully');18}
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:
1<?php 2 3// Before 4public function rules(): array 5{ 6 return [ 7 'email' => 'required|email|unique:users,email', 8 'name' => 'required|string|max:255', 9 'age' => 'nullable|integer|min:18',10 ];11}12 13// After14public function rules(): array15{16 return [17 'email' => ['required', 'email', 'unique:users,email'],18 'name' => ['required', 'string', 'max:255'],19 'age' => ['nullable', 'integer', 'min:18'],20 ];21}
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:
1<?php 2 3// database/factories/ModelFactory.php - Before 4$factory->define(User::class, function (Faker $faker) { 5 return [ 6 'name' => $faker->name, 7 'email' => $faker->unique()->safeEmail, 8 'password' => bcrypt('password'), 9 ];10});11 12// database/factories/UserFactory.php - After13namespace Database\Factories;14 15use App\Models\User;16use Illuminate\Database\Eloquent\Factories\Factory;17 18class UserFactory extends Factory19{20 protected $model = User::class;21 22 public function definition(): array23 {24 return [25 'name' => $this->faker->name,26 'email' => $this->faker->unique()->safeEmail,27 'password' => bcrypt('password'),28 ];29 }30}
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:
1<?php 2 3// Before 4public function show(User $user) 5{ 6 if (! $user->isActive()) { 7 abort(403, 'User account is not active'); 8 } 9 10 if ($user->isBlocked()) {11 abort(403, 'User is blocked');12 }13 14 return view('users.show', compact('user'));15}16 17// After - Rector applies AbortIfRector18public function show(User $user)19{20 abort_if(! $user->isActive(), 403, 'User account is not active');21 abort_if($user->isBlocked(), 403, 'User is blocked');22 23 return view('users.show', compact('user'));24}
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:
1<?php 2 3// Before - No type information 4class UserService 5{ 6 private $repository; 7 8 public function __construct($repository) 9 {10 $this->repository = $repository;11 }12 13 public function findByEmail($email)14 {15 return $this->repository->findWhere(['email' => $email]);16 }17 18 public function getActiveUsers()19 {20 return $this->repository->scopeQuery(function($query) {21 return $query->where('active', true);22 })->all();23 }24}25 26// After - Rector adds type hints where safe27class UserService28{29 private UserRepository $repository;30 31 public function __construct(UserRepository $repository)32 {33 $this->repository = $repository;34 }35 36 public function findByEmail(string $email): ?User37 {38 return $this->repository->findWhere(['email' => $email]);39 }40 41 public function getActiveUsers(): Collection42 {43 return $this->repository->scopeQuery(function($query) {44 return $query->where('active', true);45 })->all();46 }47}
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:
1<?php 2 3// Before 4class Post extends Model 5{ 6 public function author() 7 { 8 return $this->belongsTo(User::class); 9 }10 11 public function comments()12 {13 return $this->hasMany(Comment::class);14 }15 16 public function tags()17 {18 return $this->belongsToMany(Tag::class);19 }20}21 22// After - Rector adds generic types23use Illuminate\Database\Eloquent\Relations\BelongsTo;24use Illuminate\Database\Eloquent\Relations\BelongsToMany;25use Illuminate\Database\Eloquent\Relations\HasMany;26 27class Post extends Model28{29 /**30 * @return BelongsTo<User>31 */32 public function author(): BelongsTo33 {34 return $this->belongsTo(User::class);35 }36 37 /**38 * @return HasMany<Comment>39 */40 public function comments(): HasMany41 {42 return $this->hasMany(Comment::class);43 }44 45 /**46 * @return BelongsToMany<Tag>47 */48 public function tags(): BelongsToMany49 {50 return $this->belongsToMany(Tag::class);51 }52}
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:
1<?php 2 3// Before 4namespace App\Services; 5 6use App\Models\User; 7 8class NotificationService 9{10 // class implementation11}12 13// After - Rector adds declare statement14<?php15 16declare(strict_types=1);17 18namespace App\Services;19 20use App\Models\User;21 22class NotificationService23{24 // class implementation25}
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:
1<?php 2 3// Configure in rector.php 4use RectorLaravel\Rector\FuncCall\RemoveDumpDataDeadCodeRector; 5 6return RectorConfig::configure() 7 ->withConfiguredRule(RemoveDumpDataDeadCodeRector::class, [ 8 'dd', 'dump', 'var_dump', 'ddd' 9 ]);10 11// Before12public function calculateTotal(Order $order): float13{14 $subtotal = $order->items->sum('price');15 dd($subtotal); // Oops!16 17 $tax = $subtotal * 0.20;18 $total = $subtotal + $tax;19 20 return $total;21}22 23// After24public function calculateTotal(Order $order): float25{26 $subtotal = $order->items->sum('price');27 28 $tax = $subtotal * 0.20;29 $total = $subtotal + $tax;30 31 return $total;32}
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:
1<?php 2 3// Before 4$activeUsers = $users->filter(function ($user) { 5 return $user->active === true; 6}); 7 8$userNames = $activeUsers->map(function ($user) { 9 return $user->name;10});11 12// After - Rector simplifies where possible13$activeUsers = $users->filter(fn ($user) => $user->active);14$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:
1<?php 2 3// Before 4public function getStatusLabel($status) 5{ 6 switch ($status) { 7 case 'pending': 8 return 'Awaiting Review'; 9 case 'approved':10 return 'Approved';11 case 'rejected':12 return 'Rejected';13 default:14 return 'Unknown';15 }16}17 18// After - Rector converts to match expression19public function getStatusLabel(string $status): string20{21 return match ($status) {22 'pending' => 'Awaiting Review',23 'approved' => 'Approved',24 'rejected' => 'Rejected',25 default => 'Unknown',26 };27}
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.
1<?php 2 3// Week 1: Just upgrade rules 4return RectorConfig::configure() 5 ->withSets([ 6 LaravelLevelSetList::UP_TO_LARAVEL_110, 7 ]); 8 9// Week 2: Add code quality improvements10return RectorConfig::configure()11 ->withSets([12 LaravelLevelSetList::UP_TO_LARAVEL_110,13 LaravelSetList::LARAVEL_CODE_QUALITY,14 ]);15 16// Week 3: Add type declarations17return RectorConfig::configure()18 ->withSets([19 LaravelLevelSetList::UP_TO_LARAVEL_110,20 LaravelSetList::LARAVEL_CODE_QUALITY,21 LaravelSetList::LARAVEL_TYPE_DECLARATIONS,22 ]);
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:
1<?php 2 3use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; 4use RectorLaravel\Rector\MethodCall\RedirectRouteToToRouteHelperRector; 5 6return RectorConfig::configure() 7 ->withSets([ 8 LaravelSetList::LARAVEL_CODE_QUALITY, 9 ])10 ->withSkip([11 // Keep redirect()->route() syntax (we prefer it)12 RedirectRouteToToRouteHelperRector::class,13 14 // Don't remove promoted properties even if unused15 RemoveUnusedPromotedPropertyRector::class,16 17 // Skip specific files18 __DIR__ . '/app/Legacy',19 ]);
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:
1<?php 2 3namespace App\Rector; 4 5use PhpParser\Node; 6use PhpParser\Node\Expr\FuncCall; 7use PhpParser\Node\Name; 8use Rector\Rector\AbstractRector; 9use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;10 11final class OldHelperToNewHelperRector extends AbstractRector12{13 public function getRuleDefinition(): RuleDefinition14 {15 return new RuleDefinition(16 'Convert old get_user_meta() to new getUserMeta()',17 []18 );19 }20 21 public function getNodeTypes(): array22 {23 return [FuncCall::class];24 }25 26 public function refactor(Node $node): ?Node27 {28 if (! $node instanceof FuncCall) {29 return null;30 }31 32 if (! $this->isName($node, 'get_user_meta')) {33 return null;34 }35 36 $node->name = new Name('getUserMeta');37 38 return $node;39 }40}
Then register it in your rector.php:
1<?php2 3use App\Rector\OldHelperToNewHelperRector;4 5return RectorConfig::configure()6 ->withRules([7 OldHelperToNewHelperRector::class,8 ]);
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:
1name: Rector 2 3on: [pull_request] 4 5jobs: 6 rector: 7 runs-on: ubuntu-latest 8 steps: 9 - uses: actions/checkout@v31011 - name: Setup PHP12 uses: shivammathur/setup-php@v213 with:14 php-version: 8.315 coverage: none1617 - name: Install Dependencies18 run: composer install --no-interaction --prefer-dist1920 - name: Run Rector21 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:
1#!/bin/sh2vendor/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:
1<?php 2 3// Weekly: Gradually add strict types 4return RectorConfig::configure() 5 ->withPaths([ 6 // Only process files modified in last week 7 // (detected via git diff) 8 ]) 9 ->withPreparedSets(10 typeDeclarations: true,11 deadCode: true12 );
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.