Advanced RectorShenanigans

php[tek] 2025 əwesome

by cabbey (he/him)
22 May 2025

Mastodon cabbey@phpc.social

github: cabbey

intro cabbey

Intro to cabbey

cabbey Photo by Chris MacAskill

Disclaimers

I'm not a member of the RectorPHP project.

I've submitted 1 patch to the project. (so far)

intro awesome

Intro to Awesome

Building a better world through
the power of photography.

awesome.co
awesome
SmugMug
Flickr

SmugMug Films

Flickr Foundation
TWiP

More to come...

backstory

Why?

Our codebases are now old enough to drink.

It makes some
of us want to.

RectorPHP to the Rescue!

Automate it all.

Automate it all
as much as is practical.

Confession

dev-mode

Switch to
Dev Mode

Main sources: github github.com/rectorphp/rector-src

Other rules: github 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!

intro Rule class

The Rule Class

The Rule Class
Rector Interface

              
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);
}
              
            

Example

Replace all use of the trait YeOldeLib\Log\LogAware with Shiny\NewThing\Logging\LoggerAwareTrait.

Example

              
 class UsesLogging {
-    use \YeOldeLib\Log\LogAware;
+    use \Shiny\NewThing\Logging\LoggerAwareTrait;
              
            

Bootstrap the Class

              
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)
  {
  }
}
              
            

AST Dumping

GetRector.com/AST

AST Dumping

getrector-ast screen shot
              
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;
  }
}
              
            

Valid returns from refactor()

null
no modifications.
Node
replace the visited one.
Node[]
inject more code than you started with.
              
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;
  }
}
              
            

Let's try it out

							
          $ 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

							
						
re-coding calls

Turn it up a notch

Next Step

Rewrite getlogger() and log() calls to modern interfaces.

Next Step

              
-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()"

xdebug

Command Line Options

  • --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;
  }
}
              
            

Next Next Step

              
-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"

xdebug

$input
is of type
Scalar\Int_

uh... huh? 😵‍💫

              
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);
              
            

Introducing Context

              
$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;
  }
}
              
            
advanced functionality

Getting (optionally) Fancy

Conditional PHP versions?

  1. implements MinPhpVersionInterface
  2. public function provideMinPhpVersion() : int;
  3. return \Rector\ValueObject\PhpVersion::PHP_80;
  4. return \Rector\ValueObject\PhpVersionFeature::NEVER_TYPE;

Submitting upstream?

Additional Work Needed:

  1. Test Cases
  2. public function getRuleDefinition(): RuleDefinition
    • Summary of what Rector does
    • Before & after code samples
  3. One thing at a time
  4. See github: rector-src/CONTRIBUTING.md for more

Test Infrastructure

Test 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);
    }
}

?>
              
            
Thanks

Thanks to:

Questions?

Thoughts?

Mastodon: cabbey@phpc.social