by cabbey (he/him)
20 May 2025
cabbey@phpc.social
cabbey
I'm not a member of the RectorPHP project.
I've submitted 1 patch to the project. (so far)
Building a better world through
the power of photography.
A
tool
for the
automated
transformation
of
source code
via
configurable
rules.
A tool for the
automated
transformation of source code
via
configurable rules.
composer require rector/rector --dev
Make a separate repo just to
keep all your Rector files in.
Composer install in that
repo just like normal.
vendor/bin/rector process path/to/code --dry-run
vendor/bin/rector process path/to/code
use Rector\Config\RectorConfig;
use Rector\Php83\Rector\FuncCall\RemoveGetClassGetParentClassNoArgsRector;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withRules([
RemoveGetClassGetParentClassNoArgsRector::class,
]);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withIndent(indentChar: ' ', indentSize: 4);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/vendor/something/package',
])
->withDowngradeSets(php80: true);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withComposerBased(
twig: true,
doctrine: true,
phpunit: true,
symfony: true
);
vendor/bin/rector --config pathTo/YourConfig.php ...
$ vendor/bin/rector process \
--config example/uselogging-config.php \
--dry-run
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1 file with changes
===================
1) example/UsesLogging.php:3
---------- begin diff ----------
@@ @@
declare(strict_types=1);
class UsesLogging {
- use \YeOldeLib\Log\LogAware;
+ use \Shiny\NewThing\Logging\LoggerAwareTrait;
}
----------- end diff -----------
Applied rules:
* MigrateToModernLogging
[OK] 1 file would have been changed (dry-run) by Rector
A tool for the
automated
transformation of source code
via
configurable rules.
A tool for the
automated
transformation of source code
via
configurable rules.
Abstract Symbol Trees
graph TD A[Constant: 42]
42
graph TD A[Variable: $item]
$item
graph TD A(Operator: return) -- value --> B[Variable: $results]
return $results;
graph TD A(Operator: +) -- Left --> B[Variable: $item] A(Operator: +) -- Right --> C[Constant: 42]
$item + 42
graph TD A(Operator: Assignment) -- Left --> E[Variable: $item] A(Operator: Assignment) -- Right --> B(Operator: +) B(Operator: +) -- Left --> C[Variable: $item] B(Operator: +) -- Right --> D[Constant: 42]
$item = $item + 42
graph TD A(Branch) -- Conditional --> B((Comp: ≥)) B((Comp: ≥)) -- Left --> C[Variable: $count] B((Comp: ≥)) -- Right --> D[Constant: 10] A(Branch) -- True --> E(MethodCall) A(Branch) -- False --> F(StaticCall) E(MethodCall) -- Variable --> G[Variable: $this] E(MethodCall) -- Method --> H[Identifier: doSomething] E(MethodCall) -- Argument --> I[Variable: $count] F(StaticCall) -- Class --> J[Class: Upsell] F(StaticCall) -- Method --> K[Identifier: begForMore] F(StaticCall) -- Argument --> L[empty]
if ($count >= 10) {
$this->doSomething($count);
} else {
Upsell::begForMore();
}
PhpParser\Node\Stmt\If_(
cond: PhpParser\Node\Expr\BinaryOp\GreaterOrEqual(
left: PhpParser\Node\Expr\Variable( name: "count" )
right: PhpParser\Node\Scalar\Int_( value: 10 )
)
stmts: [
0: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\MethodCall(
var: PhpParser\Node\Expr\Variable( name: "this" )
name: PhpParser\Node\Identifier( name: "doSomething" )
args: [
0: PhpParser\Node\Arg(
name: null
value: PhpParser\Node\Expr\Variable( name: "count" )
byRef: false
unpack: false
)
]
)
)
]
elseifs: []
else: PhpParser\Node\Stmt\Else_(
stmts: [
0: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\StaticCall(
class: PhpParser\Node\Name\FullyQualified( parts: ["Upsell"] )
name: PhpParser\Node\Identifier( name: "begForMore" )
args: []
)
)
]
)
)
That seems like a lot of code to maintain just for this project, surely it's going to be buggy!
Nope! They're leveraging the same library that PHPStan uses: PhpParser by nikic!
There are 128 OTHER contributors in the GH repo, with the #2 spot being one of the leads of RectorPHP, #3 being a php-src contributor, #4 being a lead from PHPStan, etc...
(a hypothetical example)
if ($count >= 10) {
$this->doSomething($count);
} else {
Upsell::begForMore();
}
if ($count >= 10) {
$this->doSomething($count);
} else {
$this->handleSmallCarts();
}
graph TD A(Branch) -- Conditional --> B((Comp: ≥)) B((Comp: ≥)) -- Left --> C[Variable: $count] B((Comp: ≥)) -- Right --> D[Constant: 10] A(Branch) -- True --> E(MethodCall) A(Branch) -- False --> F(StaticCall) E(MethodCall) -- Variable --> G[Variable: $this] E(MethodCall) -- Method --> H[Identifier: doSomething] E(MethodCall) -- Argument --> I[Variable: $count] F(StaticCall) -- Class --> J[Class: Upsell] F(StaticCall) -- Method --> K[Identifier: begForMore] F(StaticCall) -- Argument --> L[empty]
graph TD A(Branch) -- Conditional --> B((Comp: ≥)) B((Comp: ≥)) -- Left --> C[Variable: $count] B((Comp: ≥)) -- Right --> D[Constant: 10] A(Branch) -- True --> E(MethodCall) A(Branch) -- False --> F(StaticCall) E(MethodCall) -- Variable --> G[Variable: $this] E(MethodCall) -- Method --> H[Identifier: doSomething] E(MethodCall) -- Argument --> I[Variable: $count] F(StaticCall) -- Class --> J[Class: Upsell] F(StaticCall) -- Method --> K[Identifier: begForMore] F(StaticCall) -- Argument --> L[empty] classDef ignored fill:#eee8d5,stroke:#eee8d5,edgeLabelBackground:#eee8d5; class A,B,C,D,E,G,H,I ignored;
graph TD F(StaticCall) -- Class --> J[Class: Upsell] F(StaticCall) -- Method --> K[Identifier: begForMore] F(StaticCall) -- Argument --> L[empty]
graph TD F(MethodCall) -- Class --> J[Class: Upsell] F(MethodCall) -- Method --> K[Identifier: begForMore] F(MethodCall) -- Argument --> L[empty] classDef error fill:#dc322f class J error
graph TD F(MethodCall) -- Variable --> J[Variable: $this] F(MethodCall) -- Method --> K[Identifier: handleSmallCarts] F(MethodCall) -- Argument --> L[empty]
graph TD A(Branch) -- Conditional --> B((Comp: ≥)) B((Comp: ≥)) -- Left --> C[Variable: $count] B((Comp: ≥)) -- Right --> D[Constant: 10] A(Branch) -- True --> E(MethodCall) A(Branch) -- False --> F(MethodCall) E(MethodCall) -- Variable --> G[Variable: $this] E(MethodCall) -- Method --> H[Identifier: doSomething] E(MethodCall) -- Argument --> I[Variable: $count] F(MethodCall) -- Variable --> J[Variable: $this] F(MethodCall) -- Method --> K[Identifier: handleSmallCarts] F(MethodCall) -- Argument --> L[empty] classDef ignored fill:#eee8d5,stroke:#eee8d5,edgeLabelBackground:#eee8d5; class A,B,C,D,E,G,H,I ignored;
graph TD A(Branch) -- Conditional --> B((Comp: ≥)) B((Comp: ≥)) -- Left --> C[Variable: $count] B((Comp: ≥)) -- Right --> D[Constant: 10] A(Branch) -- True --> E(MethodCall) A(Branch) -- False --> F(MethodCall) E(MethodCall) -- Variable --> G[Variable: $this] E(MethodCall) -- Method --> H[Identifier: doSomething] E(MethodCall) -- Argument --> I[Variable: $count] F(MethodCall) -- Variable --> J[Variable: $this] F(MethodCall) -- Method --> K[Identifier: handleSmallCarts] F(MethodCall) -- Argument --> L[empty]
if ($count >= 10) {
$this->doSomething($count);
} else {
$this->handleSmallCarts();
}
A tool for the
automated
transformation of source code
via
configurable rules.
Calling get_class()
and get_parent_class()
without arguments is now deprecated.
class Example extends StdClass {
public function whoAreYou() {
- return get_class() . ' daughter of ' . get_parent_class();
+ return self::class . ' daughter of ' . parent::class;
}
}
use Rector\Config\RectorConfig;
use Rector\Php83\Rector\FuncCall\RemoveGetClassGetParentClassNoArgsRector;
return RectorConfig::configure()
->withRules([
RemoveGetClassGetParentClassNoArgsRector::class,
]);
Occasionally you'll have a rule change that needs some input to define or control what it does.
-$value = SomeClass::OLD_CONSTANT;
-$value = SomeClass::OTHER_OLD_CONSTANT;
+$value = SomeClass::NEW_CONSTANT;
+$value = DifferentClass::NEW_CONSTANT;
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\ClassConstFetch\RenameClassConstFetchRector;
use Rector\Renaming\ValueObject\RenameClassConstFetch;
use Rector\Renaming\ValueObject\RenameClassAndConstFetch;
return RectorConfig::configure()
->withConfiguredRule(
RenameClassConstFetchRector::class,
[
new RenameClassConstFetch(
'SomeClass', 'OLD_CONSTANT', 'NEW_CONSTANT'
),
new RenameClassAndConstFetch(
'SomeClass', 'OTHER_OLD_CONSTANT',
'DifferentClass', 'NEW_CONSTANT'
),
]
);
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\ClassConstFetch\RenameClassConstFetchRector;
use Rector\Renaming\ValueObject\RenameClassConstFetch;
use Rector\Renaming\ValueObject\RenameClassAndConstFetch;
return RectorConfig::configure()
->withConfiguredRule(
RenameClassConstFetchRector::class,
[
new RenameClassConstFetch(
SomeClass::class, 'OLD_CONSTANT', 'NEW_CONSTANT'
),
new RenameClassAndConstFetch(
SomeClass::class, 'OTHER_OLD_CONSTANT',
DifferentClass::class, 'NEW_CONSTANT'
),
]
);
$todo = [];
$reflector = new ReflectionClass(\YeOlde\Lib\Thing::class);
foreach ($reflector->getConstants() as $const) {
$newHotness = new ReflectionClassConstant(
\Shiny\New\Thing::class,
$const->getName(),
);
$todo[] = new RenameClassAndConstFetch(
$const->getDeclaringClass()->getName(),
$const->getName(),
$newHotness->getDeclaringClass()->getName(),
$newHotness->getName()
);
}
return RectorConfig::configure()
->withConfiguredRule(
RenameClassConstFetchRector::class,
$todo
);
$todo = [];
$reflector = new ReflectionClass(\YeOlde\Lib\Thing::class);
$classes = [
\Shiny\New\Thing::class,
\Shiny\New\OtherThing::class,
\Shiny\New\ThirdThing::class,
];
foreach ($reflector->getConstants() as $const) {
$constString = $const->getName();
$possibleConsts = [$constString];
while ($start = strpos($constString, '_')) {
$constString = substr($constString, $start + 1);
$possibleConsts[] = $constString;
};
foreach ($classes as $class) {
foreach ($possibleConsts as $possibleConst) {
$newHotness = new ReflectionClassConstant(
$class,
$possibleConst
);
if (null !== $newHotness) {
break 2;
}
}
}
$todo[] = new RenameClassAndConstFetch(
$const->getDeclaringClass()->getName(),
$const->getName(),
$newHotness->getDeclaringClass()->getName(),
$newHotness->getName()
);
}
return RectorConfig::configure()
->withConfiguredRule(
RenameClassConstFetchRector::class,
$todo
);
Rector has pre-defined sets of rules for common tasks.
For example, the PHP_83
set includes all the rules for deprecations and new features in PHP 8.3.
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withSets([SetList::PHP_83]);
Rector has pre-defined sets of rules for common tasks.
For example, the PHP_83
set includes all the rules for deprecations and new features in PHP 8.3.
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withPhpSets(php83: true);
Rector ships with rules for basic PHP version migration, and for some frameworks and tools.
For example: Twig, Symfony, Doctrine, PHPUnit.
Rector also ships with rules for code quality cleanups.
use Rector\Config\RectorConfig;
use Rector\PHPUnit\Set\PHPUnitSetList;
return RectorConfig::configure()
->withSets([
PHPUnitSetList::PHPUNIT_90,
]);
use Rector\Config\RectorConfig;
use Rector\PHPUnit\PHPUnit100\Rector\StmtsAwareInterface\WithConsecutiveRector;
use Rector\PHPUnit\PHPUnit90\Rector\Class_\TestListenerToHooksRector;
use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ExplicitPhpErrorApiRector;
use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\SpecificAssertContainsWithoutIdentityRector;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\ValueObject\MethodCallRename;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rules([
TestListenerToHooksRector::class,
ExplicitPhpErrorApiRector::class,
SpecificAssertContainsWithoutIdentityRector::class,
WithConsecutiveRector::class,
]);
$rectorConfig->ruleWithConfiguration(
RenameMethodRector::class,
[
new MethodCallRename(
PHPUnit\Framework\TestCase::class,
'expectExceptionMessageRegExp',
'expectExceptionMessageMatches'
),
]
);
};
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES
]);
$rectorConfig->rules([
StaticDataProviderClassMethodRector::class,
PublicDataProviderClassMethodRector::class,
AddProphecyTraitRector::class,
WithConsecutiveRector::class,
RemoveSetMethodsMethodCallRector::class,
PropertyExistsWithoutAssertRector::class,
ParentTestClassConstructorRector::class,
]);
$rectorConfig->ruleWithConfiguration(
RenameMethodRector::class,
[
// https://github.com/sebastianbergmann/phpunit/issues/4087
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertRegExp',
'assertMatchesRegularExpression'
),
// https://github.com/sebastianbergmann/phpunit/issues/5220
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertObjectHasAttribute',
'assertObjectHasProperty'
),
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertObjectNotHasAttribute',
'assertObjectNotHasProperty'
),
new MethodCallRename(
'PHPUnit\Framework\MockObject\Rule\InvocationOrder',
'getInvocationCount',
'numberOfInvocations'
),
// https://github.com/sebastianbergmann/phpunit/issues/4090
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertNotRegExp',
'assertDoesNotMatchRegularExpression'
),
// https://github.com/sebastianbergmann/phpunit/issues/4078
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertFileNotExists',
'assertFileDoesNotExist'
),
// https://github.com/sebastianbergmann/phpunit/issues/4081
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertFileNotIsReadable',
'assertFileIsNotReadable'
),
// https://github.com/sebastianbergmann/phpunit/issues/4072
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertDirectoryNotIsReadable',
'assertDirectoryIsNotReadable'
),
// https://github.com/sebastianbergmann/phpunit/issues/4075
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertDirectoryNotIsWritable',
'assertDirectoryIsNotWritable'
),
// https://github.com/sebastianbergmann/phpunit/issues/4069
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertDirectoryNotExists',
'assertDirectoryDoesNotExist'
),
// https://github.com/sebastianbergmann/phpunit/issues/4066
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertNotIsWritable',
'assertIsNotWritable'
),
// https://github.com/sebastianbergmann/phpunit/issues/4063
new MethodCallRename(
'PHPUnit\Framework\Assert',
'assertNotIsReadable',
'assertIsNotReadable'
),
// https://github.com/sebastianbergmann/phpunit/pull/3687
new MethodCallRename(
'PHPUnit\Framework\MockObject\MockBuilder',
'setMethods',
'onlyMethods'
),
//https://github.com/sebastianbergmann/phpunit/issues/5062
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectDeprecationMessage',
'expectExceptionMessage'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectDeprecationMessageMatches',
'expectExceptionMessageMatches'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectNoticeMessage',
'expectExceptionMessage'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectNoticeMessageMatches',
'expectExceptionMessageMatches'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectWarningMessage',
'expectExceptionMessage'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectWarningMessageMatches',
'expectExceptionMessageMatches'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectErrorMessage',
'expectExceptionMessage'
),
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectErrorMessageMatches',
'expectExceptionMessageMatches'
),
]);
};
Many of the sets have hundreds of changes in them.
Levels let you gradually apply those gradually.
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(
typeDeclarations: true
);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
//->withPreparedSets(typeDeclarations: true)
->withTypeCoverageLevel(0);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
//->withPreparedSets(typeDeclarations: true)
->withTypeCoverageLevel(1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
//->withPreparedSets(typeDeclarations: true)
->withTypeCoverageLevel(2);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
//->withPreparedSets(typeDeclarations: true)
->withTypeCoverageLevel(3);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
//->withPreparedSets(typeDeclarations: true)
->withTypeCoverageLevel(getenv('LEVEL'));
Lots of community packages and projects have rector rule sets and even custom rules specifically for their users. Ex: Drupal, cakephp, CraftCMS, Laminas...
A tool for the
automated
transformation of source code
via
configurable rules.
Use a tool to auto format your code. All of it.
Do this before and after using rector.
And probably in your commit flow.
Anything with old school mix of php and
html is going to be a headache.
You can either have full names everywhere (default) or you can have everything as short names with use.
Workaround is to leave rector set to full names and use something else to refactor them more intelligently.
I suggest PHPStorm's import refactor methods.
This may not be in their control without a fair amount of work on PHP-Parser... and whatever is being used for codegen.
namespace Project\MyComponent\SomePortion;
use Project\OtherComponent;
use Project\ThirdThing;
...
class ComponentObject {
private OtherComponent\ComponentObject $otherThingOne;
private ThirdThing\ComponentObject $otherThingTwo;
...
public function doAllTheThings() {
$this->otherThingOne->doSomething();
$this->otherThingTwo->doSomething();
}
public function doAllTheThings() {
- $this->otherThingOne->doSomething();
- $this->otherThingTwo->doSomething();
+ $this->otherThingOne->doSomethingElse();
+ $this->otherThingTwo->doSomethingElse();
}
namespace Project\MyComponent\SomePortion;
use Project\OtherComponent;
use Project\ThirdThing;
...
class ComponentObject {
private \Project\OtherComponent\ComponentObject $otherThingOne;
private \Project\ThirdThing\ComponentObject $otherThingTwo;
...
public function doAllTheThings() {
$this->otherThingOne->doSomethingElse();
$this->otherThingTwo->doSomethingElse();
}
namespace Project\MyComponent\SomePortion;
use Project\OtherComponent\ComponentObject;
use Project\ThirdThing\ComponentObject;
...
class ComponentObject {
private ComponentObject $otherThingOne;
private ComponentObject $otherThingTwo;
...
public function doAllTheThings() {
$this->otherThingOne->doSomethingElse();
$this->otherThingTwo->doSomethingElse();
}
namespace Project\MyComponent\SomePortion;
use Project\OtherComponent\ComponentObject as ProjectOtherComponentComponentObject;
use Project\ThirdThing\ComponentObject as ProjectThirdThingComponentObject;
...
class ComponentObject {
private ProjectOtherComponentComponentObject $otherThingOne;
private ProjectThirdThingComponentObject $otherThingTwo;
...
public function doAllTheThings() {
$this->otherThingOne->doSomethingElse();
$this->otherThingTwo->doSomethingElse();
}
namespace Project\MyComponent\SomePortion;
use Project\OtherComponent;
use Project\ThirdThing;
...
class ComponentObject {
private OtherComponent\ComponentObject $otherThingOne;
private ThirdThing\ComponentObject $otherThingTwo;
...
public function doAllTheThings() {
$this->otherThingOne->doSomethingElse();
$this->otherThingTwo->doSomethingElse();
}
/**
* @var string $noSoupForYou yada yada yada
*/
private string $noSoupForYou;
public function __construct($noSoupForYou) {
$this->noSoupForYou = $noSoupForYou;
}
/**
* @param string $noSoupForYou yada yada yada
*/
public function __construct(
private string $noSoupForYou,
) {
public function __construct(
/**
* yada yada yada
*/
private string $noSoupForYou,
) {
Unit tests, Functional tests, Regression Tests, Acceptance Tests, End-to-end Tests
The more test coverage the better!
Try to separate formatting changes
from actual code logic changes.
Just because you're not doing the grunt work of making all the changes doesn't mean you can ignore the cognitive load of understanding what the tools are doing!
You still need to be able to think about and reason around the changes.
Otherwise, there is little point to using small steps.
This is just ONE tool in the toolbox...
remember you still have all the others.
But to try to keep manual and
automated changes separated!
And document how to make the
change again in the commit message.
The whole point of the tests is to confirm you aren't breaking anything. So periodically run them again and confirm the results are the same.
|