Category: Development

Modernizing PHPDoc Array annotations with Rector

Author(s)

Kevin
Kevin
In one of my project, I’m working with Symfony Serializer, phpstan/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](https://www.php.net/manual/en/function.array-is-list.php) 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 Psalm, PHPStan 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](https://cs.symfony.com/doc/rules/phpdoc/phpdoc_array_type.html) 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.

More articles

Author(s)

Akira
Akira

Making of : “La nuit des Losers”

lanuitdeslosers.ch, built fast: paper wireframes, a bold hand-drawn identity, then Claude Code to prototype and ship. A mouse-tracking hero animation, a bowling game made of sponsor logos, and a few bugs we’re keeping.
lanuitdeslosers.ch, built fast: paper wireframes, a bold hand-drawn identity, then Claude Code to prototype and ship. A mouse-tracking hero animation, a bowling game made of sponsor logos, and a few bugs we’re keeping.
Making of : “La nuit des Losers”

Author(s)

Pierre
Pierre

Dark mode: necessity or trend

Nous faisons le tri entre idées reçues, réalités techniques et bonnes pratiques de design.
Nous faisons le tri entre idées reçues, réalités techniques et bonnes pratiques de design.
Dark mode: necessity or trend

Author(s)

Yann
Pierre
Yann and Pierre

Experiment #2 – Surway, a social experiments game

Pour notre deuxième expérimentation, nous avons relevé le défi de sortir de notre zone de confort en développant Surway, un jeu vidéo jouable directement dans le navigateur, où les actions de chaque joueur restent inscrites pour tous ceux qui suivront.
Pour notre deuxième expérimentation, nous avons relevé le défi de sortir de notre zone de confort en développant Surway, un jeu vidéo jouable directement dans le navigateur, où les actions de chaque joueur restent inscrites pour tous ceux qui suivront.
Experiment #2 – Surway, a social experiments game