Development

RectorPHP and Laravel: The Automated Refactoring Tool We Should Have Been Using Years Ago.

12 minutes

TL;DR

Install:

shell
1composer require rector/rector --dev
2composer require rector/rector-laravel --dev

Run dry run:

shell
1vendor/bin/rector process --dry-run

Apply changes, run tests, commit:

shell
1vendor/bin/rector process
2php 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!

Get in touch

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:

  1. Run composer update and watch everything explode

  2. Manually find and fix deprecated method calls

  3. Update route definitions to new syntax

  4. Add type hints that newer PHP versions support

  5. Refactor validation arrays that used to be strings

  6. Update test syntax that changed between versions

  7. Fix all the things you missed in testing

  8. 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:

shell
1composer require rector/rector --dev
2composer require rector/rector-laravel --dev

Create a rector.php file in your project root.

php
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:

shell
1vendor/bin/rector process --dry-run

To then apply these changes:

php
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:

php
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 RedirectRouteToToRouteHelperRector
12public function store(Request $request)
13{
14 // ... validation and saving logic
15 
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:

php
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// After
14public function rules(): array
15{
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:

php
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 - After
13namespace Database\Factories;
14 
15use App\Models\User;
16use Illuminate\Database\Eloquent\Factories\Factory;
17 
18class UserFactory extends Factory
19{
20 protected $model = User::class;
21 
22 public function definition(): array
23 {
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:

php
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 AbortIfRector
18public 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:

php
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 safe
27class UserService
28{
29 private UserRepository $repository;
30 
31 public function __construct(UserRepository $repository)
32 {
33 $this->repository = $repository;
34 }
35 
36 public function findByEmail(string $email): ?User
37 {
38 return $this->repository->findWhere(['email' => $email]);
39 }
40 
41 public function getActiveUsers(): Collection
42 {
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:

php
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 types
23use Illuminate\Database\Eloquent\Relations\BelongsTo;
24use Illuminate\Database\Eloquent\Relations\BelongsToMany;
25use Illuminate\Database\Eloquent\Relations\HasMany;
26 
27class Post extends Model
28{
29 /**
30 * @return BelongsTo<User>
31 */
32 public function author(): BelongsTo
33 {
34 return $this->belongsTo(User::class);
35 }
36 
37 /**
38 * @return HasMany<Comment>
39 */
40 public function comments(): HasMany
41 {
42 return $this->hasMany(Comment::class);
43 }
44 
45 /**
46 * @return BelongsToMany<Tag>
47 */
48 public function tags(): BelongsToMany
49 {
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:

php
1<?php
2 
3// Before
4namespace App\Services;
5 
6use App\Models\User;
7 
8class NotificationService
9{
10 // class implementation
11}
12 
13// After - Rector adds declare statement
14<?php
15 
16declare(strict_types=1);
17 
18namespace App\Services;
19 
20use App\Models\User;
21 
22class NotificationService
23{
24 // class implementation
25}

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
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// Before
12public function calculateTotal(Order $order): float
13{
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// After
24public function calculateTotal(Order $order): float
25{
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:

php
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 possible
13$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:

php
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 expression
19public function getStatusLabel(string $status): string
20{
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.

php
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 improvements
10return RectorConfig::configure()
11 ->withSets([
12 LaravelLevelSetList::UP_TO_LARAVEL_110,
13 LaravelSetList::LARAVEL_CODE_QUALITY,
14 ]);
15 
16// Week 3: Add type declarations
17return 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:

php
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 unused
15 RemoveUnusedPromotedPropertyRector::class,
16 
17 // Skip specific files
18 __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:

php
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 AbstractRector
12{
13 public function getRuleDefinition(): RuleDefinition
14 {
15 return new RuleDefinition(
16 'Convert old get_user_meta() to new getUserMeta()',
17 []
18 );
19 }
20 
21 public function getNodeTypes(): array
22 {
23 return [FuncCall::class];
24 }
25 
26 public function refactor(Node $node): ?Node
27 {
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:

php
1<?php
2 
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:

yaml-frontmatter
1name: Rector
2
3on: [pull_request]
4
5jobs:
6 rector:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v3
10
11 - name: Setup PHP
12 uses: shivammathur/setup-php@v2
13 with:
14 php-version: 8.3
15 coverage: none
16
17 - name: Install Dependencies
18 run: composer install --no-interaction --prefer-dist
19
20 - name: Run Rector
21 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:

shell
1#!/bin/sh
2vendor/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
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: true
12 );

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.