Skip to content

map_private_properties: true does not propagate to PhpStan extractors, causing silent wrong mapping for promoted constructor parameters with PHPDoc generic types #337

@floxblah

Description

@floxblah

AutoMapper version: 10.1.0

PHP version: 8.4

Description

When map_private_properties: true is set in the bundle config, the AutoMapperExtension only updates the ReflectionExtractor, but the two PhpStan extractors (phpstan_source_extractor and phpstan_target_extractor) remain hardcoded with allowPrivateAccess: false.

This means that for a target class with a private promoted constructor parameter typed as a generic collection via PHPDoc, AutoMapper cannot determine the item type. The failure is silent: instead of throwing, it falls back to mapping each item with the Entity → array mapper, resulting in a runtime error deep in business code like Call to a member function getFoo() on array.

Here is the full reproduction section:


Reproduction

automapper.yaml:

automapper:
    map_private_properties: true

Source entity:

#[ORM\Entity]
#[Mapper(null, Order::class)]
class OrderEntity
{
    /** @var Collection<int, ProductEntity> */
    #[ORM\OneToMany(targetEntity: ProductEntity::class, mappedBy: 'order', cascade: ['persist', 'remove'])]
    private Collection $products;

    /** @return list<ProductEntity> */
    public function getProducts(): array
    {
        return $this->products->getValues();
    }
}

Related entity:

#[Mapper(null, Product::class)]
#[ORM\Entity]
class ProductEntity
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    public function __construct(
        #[ORM\ManyToOne(inversedBy: 'products')]
        #[ORM\JoinColumn(nullable: false)]
        private readonly OrderEntity $order,
        #[ORM\Column(length: 255)]
        private readonly string $name,
    ) {}

    public function getId(): ?int { return $this->id; }
    public function getName(): string { return $this->name; }
}

Target model:

final readonly class Order
{
    /**
     * @param list<Product> $products
     */
    public function __construct(
        private int $id,
        private array $products,
    ) {}

    /** @return list<Product> */
    public function getProducts(): array { return $this->products; }
}

Related model:

final readonly class Product
{
    public function __construct(private int $id, private string $name) {}

    public function getId(): int { return $this->id; }
    public function getName(): string { return $this->name; }
}

Mapping:

$model = $autoMapper->map($orderEntity, Order::class);
$model->getProducts()[0]->getName(); // Error: Call to a member function getName() on array

Expected behavior

Each item in $products is mapped to a Product model object.

Actual behavior

Each item in $products is a raw array, causing a fatal error when calling any method on it.

Question

Is this an intended limitation, or a bug where map_private_properties: true should also set allowPrivateAccess: true on the PhpStan extractors?

Workaround

Override the service in services.yaml to enable private access on the target PhpStan extractor:

automapper.property_info.phpstan_target_extractor:
    class: Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor
    arguments:
        $accessorPrefixes: []
        $allowPrivateAccess: true

This allows the extractor to read the @param list<Product> PHPDoc on private promoted constructor parameters, restoring correct type resolution for collection items.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions