Unable to use readonly modifier for Collection properties #7139

Closed
opened 2026-01-22 15:45:28 +01:00 by admin · 3 comments
Owner

Originally created by @janedbal on GitHub (Apr 27, 2023).

Bug Report

Doctrine fails to replace ArrayCollection with PersistentCollection for readonly properties. I assume this should be documented or bypassed (cannot imagine how).

Current docs states it should be possible.:

An entity class must not be final nor read-only but it may contain final methods or read-only properties.

Q A
BC Break no
Version 2.14.2

Current behavior

It throws exception, callstack:

LogicException : Attempting to change readonly property App\User::$accounts.
 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php:46
 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:697
 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:528
 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:863
 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:377
 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:403

How to reproduce

Test:

<?php declare(strict_types = 1);

namespace App;

use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;

class UseIndexSqlWalkerTest extends TestCase
{
    public function createEntityManager(): EntityManagerInterface
    {
        $config = new Configuration();
        $config->setProxyDir(sys_get_temp_dir());
        $config->setProxyNamespace('MyProxies');
        $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver([__DIR__], false));

        return EntityManager::create(['driver' => 'pdo_sqlite', 'memory' => true], $config);
    }

    public function testReadonlyCollection(): void {
        $account = new Account();
        $user = new User($account);
        $em = $this->createEntityManager();
        $em->persist($account);
        $em->persist($user);
        $em->flush();
    }

}

Entities:

<?php declare(strict_types = 1);

namespace App;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class User
{

    /**
     * @ORM\Id
     * @ORM\Column(type="string", nullable=false)
     * @ORM\GeneratedValue
     */
    public ?int $id;

    /**
     * @ORM\ManyToMany(targetEntity=Account::class)
     */
    private readonly Collection $accounts;

    public function __construct(Account $account)
    {
        $this->accounts = new ArrayCollection([$account]);
    }

}
<?php declare(strict_types = 1);

namespace App;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Account
{

    /**
     * @ORM\Id
     * @ORM\Column(type="string", nullable=false)
     * @ORM\GeneratedValue
     */
    public ?int $id;

}

Expected behavior

No failure

Originally created by @janedbal on GitHub (Apr 27, 2023). ### Bug Report Doctrine fails to **replace ArrayCollection with PersistentCollection for readonly properties**. I assume this should be documented or bypassed (cannot imagine how). [Current docs states it should be possible.](https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/architecture.html#entities): > An entity class must not be final nor read-only but it may contain final methods or read-only properties. | Q | A |------------ | ------ | BC Break | no | Version | 2.14.2 #### Current behavior It throws exception, callstack: ``` LogicException : Attempting to change readonly property App\User::$accounts. /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php:46 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:697 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:528 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:863 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:377 /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:403 ``` #### How to reproduce Test: ```php <?php declare(strict_types = 1); namespace App; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; class UseIndexSqlWalkerTest extends TestCase { public function createEntityManager(): EntityManagerInterface { $config = new Configuration(); $config->setProxyDir(sys_get_temp_dir()); $config->setProxyNamespace('MyProxies'); $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver([__DIR__], false)); return EntityManager::create(['driver' => 'pdo_sqlite', 'memory' => true], $config); } public function testReadonlyCollection(): void { $account = new Account(); $user = new User($account); $em = $this->createEntityManager(); $em->persist($account); $em->persist($user); $em->flush(); } } ``` Entities: ```php <?php declare(strict_types = 1); namespace App; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class User { /** * @ORM\Id * @ORM\Column(type="string", nullable=false) * @ORM\GeneratedValue */ public ?int $id; /** * @ORM\ManyToMany(targetEntity=Account::class) */ private readonly Collection $accounts; public function __construct(Account $account) { $this->accounts = new ArrayCollection([$account]); } } ``` ```php <?php declare(strict_types = 1); namespace App; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Account { /** * @ORM\Id * @ORM\Column(type="string", nullable=false) * @ORM\GeneratedValue */ public ?int $id; } ``` #### Expected behavior No failure
admin closed this issue 2026-01-22 15:45:29 +01:00
Author
Owner

@mpdude commented on GitHub (May 31, 2023):

When a new entity instance is created – and not yet persisted – you need to initialize the collection attribute with ArrayCollection, like in your example given.

At least when the entity is persisted for the first time, this ArrayCollection needs to be replaced by a PersistentCollection.

I don't see a way in PHP's reflection API to modify readonly properties.

So, my (personal) conclusion would be that this is a technical limitation that we cannot overcome, and it should be documented as such.

I don't know if it is worthwhile to add runtime checks catching this and giving a more explicit error message; users are going to notice anyway their code does not work.

@mpdude commented on GitHub (May 31, 2023): When a new entity instance is created – and not yet persisted – you need to initialize the collection attribute with `ArrayCollection`, like in your example given. At least when the entity is persisted for the first time, this `ArrayCollection` needs to be replaced by a `PersistentCollection`. I don't see a way in PHP's reflection API to modify `readonly` properties. So, my (personal) conclusion would be that this is a technical limitation that we cannot overcome, and it should be documented as such. I don't know if it is worthwhile to add runtime checks catching this and giving a more explicit error message; users are going to notice anyway their code does not work.
Author
Owner

@vnivuahc commented on GitHub (Aug 23, 2025):

I encountered the same issue with a OneToMany relationship.

Changing

    #[ORM\OneToMany(targetEntity: Account::class]
    private readonly Collection $accounts;

to

    #[ORM\OneToMany(targetEntity: Account::class]
    public private(set) Collection $accounts;

seems to avoid the error, with PHP8.4.

This is not as explicit and handy than readonly but it is worth than nothing.

EDITED after the issue was closed : I fixed my code sample. I meant public private(set), not private private(set)

@vnivuahc commented on GitHub (Aug 23, 2025): I encountered the same issue with a **OneToMany** relationship. Changing ```php #[ORM\OneToMany(targetEntity: Account::class] private readonly Collection $accounts; ``` to ```php #[ORM\OneToMany(targetEntity: Account::class] public private(set) Collection $accounts; ``` seems to avoid the error, with **PHP8.4**. This is not as explicit and handy than `readonly` but it is worth than nothing. EDITED after the issue was closed : I fixed my code sample. I meant `public private(set)`, not `private private(set)`
Author
Owner

@derrabus commented on GitHub (Aug 23, 2025):

#[ORM\OneToMany(targetEntity: Account::class]
private private(set) Collection $accounts;

seems to avoid the error, with PHP8.4.

Yes because it's the same as simply removing the readonly flag.

#[ORM\OneToMany(targetEntity: Account::class]
private Collection $accounts;

I'd like to close the issue as won't fix. The ORM needs to be able to replace the collection when persisting a new entity. Therefore, readonly does not make sense here. If somebody feels like this should be documented better, please send a PR.

@derrabus commented on GitHub (Aug 23, 2025): > ```php > #[ORM\OneToMany(targetEntity: Account::class] > private private(set) Collection $accounts; > ``` > > seems to avoid the error, with **PHP8.4**. Yes because it's the same as simply removing the `readonly` flag. ```php #[ORM\OneToMany(targetEntity: Account::class] private Collection $accounts; ``` --- I'd like to close the issue as won't fix. The ORM needs to be able to replace the collection when persisting a new entity. Therefore, `readonly` does not make sense here. If somebody feels like this should be documented better, please send a PR.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7139