Rector and its extensions already consist of many rules for PHP and Framework upgrades, improving code quality and type coverage. However, you may have your own needs - that's when you need to write your own custom rules.
There is documentation for how to write custom rules, but the following tricks can help you more.
Node
to be Changed before vs after that is NeededThere are usually 2 kinds of Node
instances that you can use:
PhpParser\Node\Expr
PhpParser\Node\Stmt
That will ensure the node and its structure are always correctly refreshed after refactored to avoid errors:
Complete parent node of "PhpParser\Node\Attribute" be a stmt.
In some cases, it may be ok not to refresh the node, e.g.:
PhpParser\Node\Name
PhpParser\Node\Identifier
PhpParser\Node\Param
PhpParser\Node\Arg
PhpParser\Node\Expr\Variable
The list is in ScopeAnalyzer::NON_REFRESHABLE_NODES
constant.
To know what Node
we need to change, you can see the visual documentation of PHP Parser nodes. You can also use Play with AST Page with visual and interactive code. We have a blog post at Introducing with AST Page.
dump_node()
and print_node()
for Debugging During WritingWhen you're on deep Node
checking, you can directly get the Node
structure or printed Node
via Node utility:
dump_node($node); // show AST structure
print_node($node); // print content of Node
null
for no change, the Node
or array of Stmt
on ChangedFor example:
public function refactor(Node $node): null|Node|array
{
if ( /* some condition */ ) {
return null;
}
if (/* some condition */) {
// make a change to Node
return $node;
}
// return array of nodes, which should be an array of Stmt[],
//e.g., insert a new line before existing stmt
return [
new \PhpParser\Node\Stmt\Nop(),
$node,
];
}
NodeTraverser::REMOVE_NODE
to remove the Stmt
nodeFor example, you want to remove If_
stmt:
-if (false === true) {
- echo 'dead code';
-}
You can return \PhpParser\NodeTraverser::REMOVE_NODE
, eg:
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\NodeTraverser;
use PhpParser\Node\Stmt\If_;
use Rector\PhpParser\Node\Value\ValueResolver;
public function __construct(private readonly ValueResolver $valueResolver)
{
}
public function getNodeTypes(): array
{
return [If_::class];
}
/**
* @param If_ $node
*/
public function refactor(Node $node): ?int
{
if ($node->cond instanceof Identical) {
return null;
}
if (! $this->valueResolver->isFalse($node->cond->left)) {
return null;
}
if (! $this->valueResolver->isTrue($node->cond->right)) {
return null;
}
return NodeTraverser::REMOVE_NODE;
}
Then, the If_
node will be removed.
NodeTraverser::DONT_TRAVERSE_CHILDREN
to skip Node
below target Node
For example, if you need to check the Array_
node but don't want to check if the Array_
is inside Property
or ClassConst
Node
, you can return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN
.
That way, Rector skips below target Node
on current Rector
rule.
so you have the following target node types:
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Expr\Array_;
// ...
public function getNodeTypes(): array
{
return [
Property::class,
ClassConst::class,
Array_::class
];
}
Then, you can check the following:
use PhpParser\NodeTraverser;
// ...
/**
* @param Property|ClassConst|Array_ $node
*/
public function refactor(Node $node): null|int|Node
{
if ($node instanceof Property || $node instanceof ClassConst) {
// Array_ below Property and ClassConst won't be processed
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
// process Array_ node that is not below Property and ClassConst
}
So, it will:
Node
Otherwise, it will be processed.
We hope these tips will give you confidence to experiment with more advanced rules and save even more time with automated work.
Happy coding!