by cabbey (he/him)
22 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.
Main sources: github.com/rectorphp/rector-src
Other rules: github.com/rectorphp/rector-*
Sources are all php8.2, they refactor it down to 7.4 for the published code in rectorphp/rector... with rector!
interface RectorInterface extends NodeVisitor
{
/**
* List of nodes this class checks, classes
* that implements \PhpParser\Node
* See beautiful map of all nodes
* https://github.com/rectorphp/php-parser-nodes-docs#node-overview
*
* @return array<class-string<Node>>
*/
public function getNodeTypes() : array;
/**
* Process Node of matched type
* @return Node|Node[]|null|NodeTraverser::*
*/
public function refactor(Node $node);
}
Replace all use of the trait YeOldeLib\Log\LogAware
with Shiny\NewThing\Logging\LoggerAwareTrait
.
class UsesLogging {
- use \YeOldeLib\Log\LogAware;
+ use \Shiny\NewThing\Logging\LoggerAwareTrait;
vendor/bin/rector custom-rule
What is the name of the rule class (e.g. "LegacyCallToDbalMethodCall")?:
> Demo
Generated files
===============
* utils/rector/tests/Rector/DemoRector/Fixture/some_class.php.inc
* utils/rector/tests/Rector/DemoRector/config/configured_rule.php
* utils/rector/src/Rector/DemoRector.php
* utils/rector/tests/Rector/DemoRector/DemoRectorTest.php
[OK] Base for the "DemoRector" rule was created. Now you can fill the missing
parts
[OK] We also update /Volumes/Git/RectorExamples/phpunit.xml, to
add a rector test suite.
You can run the rector tests by running: phpunit --testsuite rector
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
}
private function refactor(Node $node)
{
}
}
GetRector.com/AST
class foo {
use YeOldeLib\Log\LogAware;
}
PhpParser\Node\Stmt\Class_(
attrGroups: []
flags: 0
name: PhpParser\Node\Identifier( name: "foo" )
extends: null
implements: []
stmts: [
0: PhpParser\Node\Stmt\TraitUse(
traits: [
0: PhpParser\Node\Name\FullyQualified(
parts: ["YeOldeLib","Log","LogAware"]
)
]
adaptations: []
)
]
)
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
}
private function refactor(Node $node)
{
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [
Node\Stmt\TraitUse::class,
];
}
private function refactor(Node $node)
{
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [
Node\Stmt\TraitUse::class,
];
}
private function refactor(Node $node)
{
foreach ($node->traits as $idx => $trait) {
}
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [
Node\Stmt\TraitUse::class,
];
}
private function refactor(Node $node)
{
foreach ($node->traits as $idx => $trait) {
if ('YeOldeLib\Log\LogAware' === $trait->toString()) {
}
}
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [
Node\Stmt\TraitUse::class,
];
}
private function refactor(Node $node)
{
foreach ($node->traits as $idx => $trait) {
if ('YeOldeLib\Log\LogAware' === $trait->toString()) {
$node->traits[$idx] = new Node\Name\FullyQualified(
'Shiny\NewThing\Logging\LoggerAwareTrait'
);
}
}
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [
Node\Stmt\TraitUse::class,
];
}
private function refactor(Node $node)
{
foreach ($node->traits as $idx => $trait) {
if ('YeOldeLib\Log\LogAware' === $trait->toString()) {
$node->traits[$idx] = new Node\Name\FullyQualified(
'Shiny\NewThing\Logging\LoggerAwareTrait'
);
}
}
return $node;
}
}
refactor()
null
Node
Node[]
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [
Node\Stmt\TraitUse::class,
];
}
private function refactor(Node $node)
{
$madeChanges = false;
foreach ($node->traits as $idx => $trait) {
if ('YeOldeLib\Log\LogAware' === $trait->toString()) {
$madeChanges = true;
$node->traits[$idx] = new Node\Name\FullyQualified(
'Shiny\NewThing\Logging\LoggerAwareTrait'
);
}
}
if ($madeChanges) {
return $node;
}
return null;
}
}
$ 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
Rewrite getlogger()
and log()
calls to modern interfaces.
-self::getLogger()
+self::getStaticLogger()
self::getLogger();
PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\StaticCall(
class: PhpParser\Node\Name( parts: ["self"] )
name: PhpParser\Node\Identifier( name: "getLogger" )
args: []
)
)
self::getLogger()->log('foo');
PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\MethodCall(
var: PhpParser\Node\Expr\StaticCall(
class: PhpParser\Node\Name( parts: ["self"] )
name: PhpParser\Node\Identifier( name: "getLogger" )
args: []
)
name: PhpParser\Node\Identifier( name: "log" )
args: [
0: PhpParser\Node\Arg(
name: null
value: PhpParser\Node\Scalar\String_( value: "foo" )
byRef: false
unpack: false
)
]
)
)
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
return null;
}
}
[ERROR] Could not process
"example/UsesLogging.php"
file, due to:
System error: "Call
to undefined method
PhpParser\Node\Expr
\StaticCall::toString()"
--clear-cache
--xdebug
--debug
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
return null;
}
}
-self::log('message', 'level');
+self::getStaticLogger()->level('message');
self::log('message', 'level');
self::getStaticLogger()->level('message');
[
0: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\StaticCall(
class: PhpParser\Node\Name( parts: ["self"] )
name: PhpParser\Node\Identifier( name: "log" )
args: [
0: PhpParser\Node\Arg(
name: null
value: PhpParser\Node\Scalar\String_( value: "message" )
byRef: false
unpack: false
)
1: PhpParser\Node\Arg(
name: null
value: PhpParser\Node\Scalar\String_( value: "level" )
byRef: false
unpack: false
)
]
)
)
1: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\MethodCall(
var: PhpParser\Node\Expr\StaticCall(
class: PhpParser\Node\Name( parts: ["self"] )
name: PhpParser\Node\Identifier( name: "getStaticLogger" )
args: []
)
name: PhpParser\Node\Identifier( name: "level" )
args: [
0: PhpParser\Node\Arg(
name: null
value: PhpParser\Node\Scalar\String_( value: "message" )
byRef: false
unpack: false
)
]
)
)
]
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
if ('log' === $node->name->toString()) {
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name('self'),
new Node\Identifier('getStaticLogger')
),
$logLevel,
[
$message
]
);
}
return null;
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
if ('log' === $node->name->toString()) {
$message = $node->getArgs()[0];
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name('self'),
new Node\Identifier('getStaticLogger')
),
$logLevel,
[
$message
]
);
}
return null;
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
if ('log' === $node->name->toString()) {
$args = $node->getArgs();
$message = $args[0];
$logLevel = $args[1]->value;
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name('self'),
new Node\Identifier('getStaticLogger')
),
new Node\Identifier($logLevel->toString()),
[
$message
]
);
}
return null;
}
}
'level';
$level;
CONST_LEVEL;
self::LEVEL;
[
0: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Scalar\String_( value: "level" )
)
1: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\Variable( name: "level" )
)
2: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\ConstFetch(
name: PhpParser\Node\Name\FullyQualified( parts: ["CONST_LEVEL"] )
)
)
3: PhpParser\Node\Stmt\Expression(
expr: PhpParser\Node\Expr\ClassConstFetch(
class: PhpParser\Node\Name( parts: ["self"] )
name: PhpParser\Node\Identifier( name: "LEVEL" )
)
)
]
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
if ('log' === $node->name->toString()) {
$args = $node->getArgs();
$message = $args[0];
$logLevel = self::LogLevelExprToIdentifier($args[1]->value);
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name('self'),
new Node\Identifier('getStaticLogger')
),
$logLevel,
[
$message
]
);
}
return null;
}
}
private static function LogLevelExprToIdentifier(
Node\Expr $input
): Node\Identifier
{
if ($input instanceof Node\Scalar\String_) {
$logLevel = $input->value;
} else if ($input instanceof Node\Expr\Variable) {
$loglevel = 'unknown_manual_cleanup_needed';
} else {
// ConstFetch or ClassConstFetch
$constValue = $input->name->toLowerString()
$logLevel = match ($constValue) {
'log_level_info', 'level_info' => 'info',
'log_level_notice', 'level_notice' => 'notice',
'log_level_warning', 'level_warning' => 'warning',
'log_level_error', 'level_error' => 'error',
'log_level_deprecated', 'level_deprecated',
'log_level_depreciated' => 'deprecated',
'log_level_strict', 'level_strict' => 'strict',
default => 'unknown_manual_cleanup_needed'
};
}
return new Node\Identifier($logLevel);
}
[ERROR] Could not process "example/UsesLogging.php" file, due to: System error: "Call to a member function toLowerString() on null"
[ERROR] Could not process "example/UsesLogging.php" file, due to: System error: "Example::LogLevelExprToIdentifier(): Argument #1 ($input) must be of type PhpParser\Node\Expr, null given..."
public static function log(
string $message,
string $level = LogLevel::NOTICE
);
private static function LogLevelExprToIdentifier(
?Node\Expr $input
): Node\Identifier
{
if (null === $input) {
$logLevel = 'notice';
} else if ($input instanceof Node\Scalar\String_) {
$logLevel = $input->value;
} else if ($input instanceof Node\Expr\Variable) {
$loglevel = 'unknown_manual_cleanup_needed';
} else {
// ConstFetch or ClassConstFetch
$constValue = $input->name->toLowerString()
$logLevel = match ($constValue) {
'log_level_info', 'level_info' => 'info',
'log_level_notice', 'level_notice' => 'notice',
'log_level_warning', 'level_warning' => 'warning',
'log_level_error', 'level_error' => 'error',
'log_level_deprecated', 'level_deprecated',
'log_level_depreciated' => 'deprecated',
'log_level_strict', 'level_strict' => 'strict',
default => 'unknown_manual_cleanup_needed'
};
}
return new Node\Identifier($logLevel);
}
[ERROR] Could not process "example/UsesLogging.php" file, due to: System error: "Call to a member function toLowerString() on null"
$input
Scalar\Int_
Profiler::log('activity', 0, 42, $progress);
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
$classToString = $node->class->toString();
if (
!(
$classToString == 'self'
|| $classToString == 'static'
|| $classToString == 'parent'
)
) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
if ('log' === $node->name->toString()) {
$args = $node->getArgs();
$message = $args[0];
$logLevel = self::LogLevelExprToIdentifier($args[1]->value);
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name($classToString),
new Node\Identifier('getStaticLogger')
),
$logLevel,
[
$message
]
);
}
return null;
}
}
[ERROR] Could not process "example/UsesLogging.php" file, due to: System error: "Call to a member function toLowerString() on null"
self::log($activity, 0, 99, $progress);
$scope = ScopeFetcher::fetch($node);
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
$classToString = $node->class->toString();
if (
!(
$classToString == 'self'
|| $classToString == 'static'
|| $classToString == 'parent'
)
) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
$scope = ScopeFetcher::fetch($node);
if (
'log' === $node->name->toString()
&& null !== $scope->getClassReflection()
->getAncestorWithClassName('YeOldeLib\Base')
) {
$args = $node->getArgs();
$message = $args[0];
$logLevel = self::LogLevelExprToIdentifier($args[1]->value);
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name($classToString),
new Node\Identifier('getStaticLogger')
),
$logLevel,
[
$message
]
);
}
return null;
}
}
use PhpParser\Node;
use Rector\Rector\AbstractRector;
class MigrateToNewLogging extends AbstractRector
{
public function getNodeTypes(): array
{
return [ Node\Expr\StaticCall::class ];
}
private function refactor(Node $node)
{
if ($node->name instanceof Expr) {
return null;
}
$classToString = $node->class->toString();
if (
!(
$classToString == 'self'
|| $classToString == 'static'
|| $classToString == 'parent'
)
) {
return null;
}
if ('getLogger' === $node->name->toString()) {
$node->name = new Node\Identifier('getStaticLogger');
return $node;
}
$scope = ScopeFetcher::fetch($node);
if (
'log' === $node->name->toString()
&& 'YeOldeLib\Log\LogAware' === $scope->getTraitReflection()
->getName()
) {
$args = $node->getArgs();
$message = $args[0];
$logLevel = self::LogLevelExprToIdentifier($args[1]->value);
return new Expr\MethodCall(
new Expr\StaticCall(
new Node\Name($classToString),
new Node\Identifier('getStaticLogger')
),
$logLevel,
[
$message
]
);
}
return null;
}
}
implements MinPhpVersionInterface
public function provideMinPhpVersion() : int;
return \Rector\ValueObject\PhpVersion::PHP_80;
return \Rector\ValueObject\PhpVersionFeature::NEVER_TYPE;
public function getRuleDefinition(): RuleDefinition
rector-src/CONTRIBUTING.md
for moreTest cases for rules are in Rector-src/rules-tests
.
Tests extend from AbstractRectorTestCase
.
Expected infrastructure around Config and Fixtures.
<?php
declare(strict_types=1);
namespace Rector\Tests\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector;
use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ArrayFirstLastRectorTest 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/configured_rule.php';
}
}
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector;
use Rector\ValueObject\PhpVersion;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(ArrayFirstLastRector::class);
$rectorConfig->phpVersion(PhpVersion::PHP_85);
};
<?php
declare(strict_types=1);
namespace Rector\Tests\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector\Fixture;
final class Fixture
{
public function run(array $array)
{
echo $array[array_key_first($array)];
echo $array[array_key_last($array)];
}
}
?>
-----
<?php
declare(strict_types=1);
namespace Rector\Tests\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector\Fixture;
final class Fixture
{
public function run(array $array)
{
echo array_first($array);
echo array_last($array);
}
}
?>
|