Modernizing PHPDoc Array annotations with Rector

Auteur(s) de l'article

In one of my project, I’m working with Symfony Serializerphpstan/phpdoc-parser and phpdocumentor/reflection-docblock to handle serialization of complex objects.
These tools rely on PHPDoc annotations to understand data types, particularly for arrays of objects.
In PHPDocs, there are (at least) 2 possible syntaxes for array values:
/**
 * @param Foo[] $arrayOfFoos
 */
/**
 * @param array<Foo> $arrayOfFoos
 */
Since 8.1 we have the concept of list in PHP.
That makes Foo[] ambiguous because is it could be:
  • a list
  • an array where the key is important
    list<Foo> is almost the same as array<Foo> (implicit int keys) or array<int, Foo> with a slight difference you can be sure that integers in keys consecutive numbers from 0 up.
    But here’s where things get interesting: with recent updates to tools like PsalmPHPStan and Symfony’s Reflection-based Serializer handler, this Type[]|array syntax started causing issues. These tools began struggling with the ambiguity - they couldn't reliably determine whether we meant a strict list or a more flexible array structure.

    Exploring Options

    I therefore had many solution to resolve my issue:
    Faced with hundreds of annotations needing updates across my codebase, I had three potential paths:
    1. Manual transformation — Copy-paste and find-replace across hundreds of files. Let’s be honest, this would be error-prone and mind-numbing work. Hard pass.
      1. AI-assisted refactoring — Ask Claude or another AI to parse my entire codebase. While tempting, my project has thousands of files that would exceed token limits, and I wanted a repeatable, version-controllable solution.
        1. Rector rule — Write once, run everywhere, with the confidence that comes from automated refactoring tools. This felt like the best approach.
          The choice was obvious. Plus, creating a Rector rule means other developers facing the same challenge can benefit from the work.
          Plot twist: While writing this article, I discovered that PHP CS Fixer already provides a similar rule called phpdoc_array_type that handles these transformations! Sometimes reinventing the wheel teaches you how wheels work ✨

          A custom Rector Rule

          So, instead of manually transforming hundreds of annotations in my project, I created a custom Rector rule that automates this task.
          Here’s how it works:
          The TransformArrayTypeAnnotationRector Rule
          final class TransformArrayTypeAnnotationRector extends AbstractRector
          {
              public function getRuleDefinition(): RuleDefinition
              {
                  return new RuleDefinition(
                      'Transform array type annotations from "Type[]|array" to "array<int, Type>"',
                      [
                          new CodeSample(
                              '/**
                               * @var \App\Entity\Environment[]|array
                               */
                              private $environments;',
                              '/**
                               * @var array<int, \App\Entity\Environment>
                               */
                              private $environments;'
                          ),
                      ]
                  );
              }
          
              public function getNodeTypes(): array
              {
                  return [Property::class, ClassMethod::class, Class_::class];
              }
          
              public function refactor(Node $node): ?Node
              {
                  $docComment = $node->getDocComment();
                  if (null === $docComment) {
                      return null;
                  }
                  $originalText = $docComment->getText();
                  $transformedText = $this->transformDocComment($originalText);
                  if ($originalText === $transformedText) {
                      return null;
                  }
                  $node->setDocComment(new Doc($transformedText));
                  return $node;
              }
          }

          Configuration and Usage

          In my rector.php file, I registered my custom rule:
          use App\Utils\Rector\TransformArrayTypeAnnotationRector;
          use Rector\Config\RectorConfig;
          
          return static function (RectorConfig $rectorConfig): void {
              $rectorConfig->paths([
                  __DIR__.'/src',
                  __DIR__.'/tests',
              ]);
              $rectorConfig->rule(TransformArrayTypeAnnotationRector::class);
          };

          Results

          Running this rule on my project automatically transformed 243 files in seconds:
          ./vendor/bin/rector
          243/243 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
          
          243 files with changes
          Applied rules:
           * TransformArrayTypeAnnotationRector
          [OK] 243 files have been changed by Rector
          Each transformation follows this pattern:
          /**
          - * @var \App\Dto\Output\Category[]|array
          + * @var array<int, \App\Dto\Output\Category>
           */
          ❤️ Love on your keyboards.

          Sources

          PHPStan (Sept 2025 ) PHPDoc Types.
          https://phpstan.org/writing-php-code/phpdoc-types
          PHP Coding Standards Fixer (Feb 2019) PHPDoc array<T> type must be used instead of T[].
          https://cs.symfony.com/doc/rules/phpdoc/phpdoc_array_type.html

          Resources

          Claude AI https://claude.ai
          Helped with writing and text refinement.
          Imagen, https://deepmind.google/models/imagen/
          Generated the very accurate article’s cover illustration.