Custom Rule
First, make sure it's not covered by any existing Rectors.
Let's say we want to change method calls from set* to change*.
<?php
$user = new User();
-$user->setPassword('123456');
+$user->changePassword('123456');
1. Create a New Rector and Implement Methods
Create a class that extends Rector\Rector\AbstractRector. It will inherit useful methods e.g. to check node type and name. See the source (or type $this-> in an IDE) for a list of available methods.
<?php
namespace Utils\Rector\Rector;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Expr\MethodCall;
use Rector\Rector\AbstractRector;
final class MyFirstRector extends AbstractRector
{
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
// what node types are we looking for?
// pick from
// https://github.com/rectorphp/php-parser-nodes-docs/
return [MethodCall::class];
}
/**
* @param MethodCall $node
*/
public function refactor(Node $node): ?Node
{
$methodCallName = $this->getName($node->name);
if ($methodCallName === null) {
return null;
}
// we only care about "set*" method names
if (! str_starts_with($methodCallName, 'set')) {
// return null to skip it
return null;
}
$newMethodCallName = preg_replace(
'#^set#', 'change', $methodCallName
);
$node->name = new Identifier($newMethodCallName);
// return $node if you modified it
return $node;
}
}
File Structure
This is how the file structure for custom rule in your own project will look like:
/src
SomeCode.php
/utils
/rector
/src
/Rector
MyFirstRector.php
/tests
/Rector
/MyFirstRector
/Fixture
test_fixture.php.inc
/config
config.php
MyFirstRectorTest.php
rector.php
composer.json
Update composer.json
We also need to load Rector rules in composer.json:
{
"autoload": {
"psr-4": {
"App\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Utils\\Rector\\": "utils/rector/src",
"Utils\\Rector\\Tests\\": "utils/rector/tests"
}
}
}
After adding this to composer.json, be sure to reload Composer's class map:
composer dump-autoload
2. Register It
<?php
use Utils\Rector\Rector\MyFirstRector;
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withRules([
MyFirstRector::class
]);
3. Let Rector Refactor Your Code
The rector.php configuration is loaded by default, so we can skip it.
# see the diff first
vendor/bin/rector process src --dry-run
# if it's ok, apply
vendor/bin/rector process src
That's it!
4. Write a Test
Writing a test for your custom rule will save you a lot of time in future debugging. Rector provides a structured way of running your rule on different snippets of code, so you can validate it works as expected in a variety of cases.
There are 2 composer packages needed to run tests for your custom rule:
phpunit/phpunit: the testing frameworkrector/rector: this contains theAbstractRectorTestCaseclass to simplify test configuration
MyFirstRectorTest.php
This class handles the heavy lifting of preparing Rector & running it against your test cases. The usual structure of the test class is as follows:
<?php
declare(strict_types=1);
namespace Utils\Rector\Tests\Rector\MyFirstRector;
use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class MyFirstRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/config.php';
}
}
There are 3 methods in this test class:
public function test(string $filePath): void:- This method helps PHPUnit detect the test
- For
$filePath, we use a PHPUnit DataProvider - This triggers a run for every test file in your
Fixturedirectory
public static function provideData(): Iterator:- Using
self::yieldFilesFromDirectoryit iterates over all test cases you provided - By default this only picks up files ending on
.php.inc
- Using
public function provideConfigFilePath(): string:- Returns a
rector.php-styled file configuring the minimal set of rules needed to run the tests (includingMyFirstRector)
- Returns a
config/config.php
This is a rector.php-styled file. If your rule is not configurable, it will look like this:
use Rector\Config\RectorConfig;
use Utils\Rector\Rector\MyFirstRector;
return RectorConfig::configure()
->withRules([
MyFirstRector::class,
]);
This essentially reflects how you would use your rule in real life.
Fixture/*.php.inc
These are the snippets of code on which Rector runs your custom rule.
To prevent automated tools from picking up those snippets, add an extra suffix .inc (so example.php becomes example.php.inc).
There are two options for every test file: either the snippet should be changed by your rule, or it should stay the same.
Fixture/test_fixture.php.inc
Assuming your rule changes $user->setPassword('123456') to $user->changePassword('123456'),
this is an example snippet:
<?php
namespace Utils\Rector\Tests\Rector\MyFirstRector\Fixture;
class SomeClass
{
public function handlePasswordChange(User $user, string $password)
{
$user->setPassword($password);
}
}
?>
-----
<?php
namespace Utils\Rector\Tests\Rector\MyFirstRector\Fixture;
class SomeClass
{
public function handlePasswordChange(User $user, string $password)
{
$user->changePassword($password);
}
}
?>
This file contains a "before" and "after" situation, separated by exactly 5 dashes: -----.
The AbstractRectorTestCase detects the -----, runs the rules configured in config/config.php on the snippet before the -----
and asserts that the changed file exactly matches the snippet after the -----.
Fixture/skip_rule_test_fixture.php.inc
There are cases where you want to check that your rule is not applied.
The file structure is very similar to Fixture/test_fixture.php.inc with 1 exception: it only contains a "before" situation.
It is not necessary to prefix the fixture with skip, but doing so makes it easy to see that no changes are expected.
<?php
namespace Utils\Rector\Tests\Rector\MyFirstRector\Fixture;
class SomeClass
{
public function handleLogin(User $user, string $password)
{
return $user->isCorrectPassword($password);
}
}
?>
There is no -----, so AbstractRectorTestCase runs the rules on the snippet and asserts that no changes are applied.
Running Your Tests
vendor/bin/phpunit tests