5 Tricks to Write Better Custom Rules

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.

1. Decide What Node to be Changed before vs after that is Needed

There 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.

2. Utilize dump_node() and print_node() for Debugging During Writing

When 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

3. Return null for no change, the Node or array of Stmt on Changed

For 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,
    ];
}

4. Return NodeTraverser::REMOVE_NODE to remove the Stmt node

For 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.

5. Return 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:

  • skip below the current Node
  • on current Rector rule only

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!