orm icon indicating copy to clipboard operation
orm copied to clipboard

"LogicException: Attempting to change readonly property ..." for entity's Id during proxy initialization

Open Firehed opened this issue 2 years ago • 13 comments

Bug Report

Q A
BC Break no
Version 2.12.3

Summary

When initializing a proxy by accessing a non-loaded property, if the Id is set as readonly, a LogicException gets thrown.

Probably similar to #9538, but in a different code path. I ran into this by directly using getReference(), but I suspect the behavior would be exhibited on any proxy relation.

Current behavior

LogicException: Attempting to change readonly property Firehed\Entities\Feed::$id. in /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php:48

Stack:

reader-php_fpm-1    | #0 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(2745): Doctrine\ORM\Mapping\ReflectionReadonlyProperty->setValue(Object(DoctrineProxies\__CG__\Firehed\Entities\Feed), '2')
reader-php_fpm-1    | #1 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php(156): Doctrine\ORM\UnitOfWork->createEntity('Firehed\\Entitie...', Array, Array)
reader-php_fpm-1    | #2 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php(63): Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator->hydrateRowData(Array, Array)
reader-php_fpm-1    | #3 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php(270): Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator->hydrateAllData()
reader-php_fpm-1    | #4 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php(757): Doctrine\ORM\Internal\Hydration\AbstractHydrator->hydrateAll(Object(Doctrine\DBAL\Result), Object(Doctrine\ORM\Query\ResultSetMapping), Array)
reader-php_fpm-1    | #5 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php(767): Doctrine\ORM\Persisters\Entity\BasicEntityPersister->load(Array, Object(DoctrineProxies\__CG__\Firehed\Entities\Feed))
reader-php_fpm-1    | #6 /var/www/html/vendor/doctrine/orm/lib/Doctrine/ORM/Proxy/ProxyFactory.php(132): Doctrine\ORM\Persisters\Entity\BasicEntityPersister->loadById(Array, Object(DoctrineProxies\__CG__\Firehed\Entities\Feed))
reader-php_fpm-1    | #7 /var/www/html/.generated/doctrine-proxies/__CG__FirehedEntitiesFeed.php(75): Doctrine\ORM\Proxy\ProxyFactory->Doctrine\ORM\Proxy\{closure}(Object(DoctrineProxies\__CG__\Firehed\Entities\Feed), '__get', Array)
reader-php_fpm-1    | #8 /var/www/html/.generated/doctrine-proxies/__CG__FirehedEntitiesFeed.php(75): Closure->__invoke(Object(DoctrineProxies\__CG__\Firehed\Entities\Feed), '__get', Array)
reader-php_fpm-1    | #9 /var/www/html/src/Api/MyStories.php(47): DoctrineProxies\__CG__\Firehed\Entities\Feed->__get('title')
reader-php_fpm-1    | #10 /var/www/html/public/index.php(71): Firehed\Api\MyStories->run(Array)
reader-php_fpm-1    | #11 {main}

How to reproduce

Entity:

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\{
    Column,
    Entity,
    GeneratedValue,
    Id,
    Table,
};

#[Entity]
#[Table(name: 'feeds')]
class Feed
{
    #[Id]
    #[GeneratedValue]
    #[Column(options: ['unsigned' => true], type: Types::BIGINT)]
    public readonly int $id;

    #[Column(options: ['default' => ''])]
    public string $title;
}

Table:

CREATE TABLE `feeds` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Code to cause error:

$reference = $em->getReference(Feed::class, 2);
var_dump($reference->id); // int(2)
var_dump($reference->title); // <-- crash here

Expected behavior

Data is loaded fine, var_dump shows the expected value from the db.

Firehed avatar Jun 25 '22 00:06 Firehed

I have experienced exactly same bug on 2.13.1.

In my case error showed when I changed Doctrine\ORM\Mapping\GeneratedValue strategy of my id property from default auto increment to UUID generated using Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator.

ciekals11 avatar Sep 28 '22 19:09 ciekals11

After some trying I have found a solution that works for me

In entity I had

#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\Column(type: 'uuid')]
private readonly string $id;

When trying to access any property (other than ID) I got mentioned error

Changing column type from uuid to string solved this for me.

My assumption is that this error is caused by handling of a UUID type field in Postgres. Maybe someone smarter will be able to confirm this.

ciekals11 avatar Sep 28 '22 20:09 ciekals11

Ran into the same problem, but with DBAL custom type (private readonly SomeId $id) , not autogenerated, id. This was for a OneToOne association. My fix was to replace readonly with a @psalm-immutable annotation on the id property temporarily.

gndk avatar Jan 08 '23 13:01 gndk

Ran into the same problem with $id being a custom type build around a Symfony uuid. The cause seems to be the strict comparison in ReflectionReadonlyProperty.php:45

if (parent::getValue($objectOrValue) !== $value) {
    throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));
}

When I inspect the values in xdebug console, both represent the same uuid, but having different object id's fails the comparison:

> parent::getValue($objectOrValue)
< App\Domain\Tag\TagGroupId::__set_state(array(
   'uuid' => 
  Symfony\Component\Uid\UuidV3::__set_state(array(
     'uid' => '3a705d0d-54e1-37b8-bd58-b4a188fa126a',
  )),
))

> $value
< App\Domain\Tag\TagGroupId::__set_state(array(
   'uuid' => 
  Symfony\Component\Uid\UuidV3::__set_state(array(
     'uid' => '3a705d0d-54e1-37b8-bd58-b4a188fa126a',
  )),
))

> spl_object_id(parent::getValue($objectOrValue))
< 4875

> spl_object_id($value)
< 4737

cl1ck avatar Jul 17 '23 13:07 cl1ck

I encountered the issue today with a readonly classs around an int / IDENTITY id column:

#[ORM\Entity]
readonly class Sample
{
    #[ORM\Id, ORM\GeneratedValue(strategy: 'IDENTITY')]
    #[ORM\Column()]
    private int $id;

    //...
}

juliusstoerrle avatar Nov 14 '23 19:11 juliusstoerrle

I have the same issue in the following situation

class Dog
{
    private function __construct(
        private readonly Uuid $id
    ) { 
        // ...
    }
<doctrine-mapping xmlns="https://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="https://doctrine-project.org/schemas/orm/doctrine-mapping
                      https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <entity name="App\Entity\Dog"
            table="xxx"
            repository-class="xxx">

        <id name="id" type="uuid" column="id"/>

        <field name="createdAt" column="created_at" type="datetime_immutable" />
    </entity>
</doctrine-mapping>
  • doctrine/doctrine-bundle: "^2.11" , resulting in 2.11.1

The error occurs in a frontend situation where we are fetching DogOwners, which has a many-to-one to Dog

        <many-to-one field="Dog" target-entity="App\Entity\Dog" inversed-by="userDogs">
            <join-column name="dog_id" referenced-column-name="id" />
        </many-to-one>

jeroendesloovere avatar Jan 04 '24 07:01 jeroendesloovere

Having the same issue on:

doctrine/orm: 2.18.0
doctrine/dbal: 3.8.1

User entity:

final class User extends AggregateRoot implements EntityInterface
{
    private readonly EntityId $id;
    ...
    public function __construct(
        private Email $email,
        private HashedPassword $password,
    ) {
        $this->id = new EntityId();
    }
    ...
}

ORM definition:

...
    <entity name="App\Xyz\Domain\Entity\User" table="`user`" repository-class="App\Xyz\Infrastructure\Doctrine\Repository\UserRepository">
        <id name="id" type="entity_id">
            <generator strategy="NONE"/>
        </id>
        <field name="email" type="email" unique="true"/>
        <field name="password" type="hashed_password"/>
        ...
    </entity
...

Getting an error after POST operation (insert):

request.CRITICAL: Uncaught PHP Exception LogicException: "Attempting to change readonly property App\Xyz\Domain\Entity\User::$id." at ReflectionReadonlyProperty.php line 46 {"exception":"[object] (LogicException(code: 0): Attempting to change readonly property App\Xyz\Domain\Entity\User::$id. at /app/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php:46)"} []

Perf avatar Feb 14 '24 10:02 Perf

Me too

bigfoot90 avatar Feb 15 '24 01:02 bigfoot90

Any updates about this issue?

Perf avatar Mar 18 '24 16:03 Perf

I debugged to the same point as @cl1ck did: There are two Uuid objects with the same content, but being different objects.

In my case it is an ID with Symfony Uuid type which is set inside the constructor. Seems as the object is hydrated twice from the same Uuid coming from the database. In my case it belongs to a EXTRA_LAZY fetched ManyToOne related Entity of the Entity being shown on that route. Maybe when generating the proxy object, the related Uuid-identified Entity gets only its ID hydrated and when I access its data, the whole object gets hydrated and with it its ID again and those two Uuid-objects have the dame value, but are not the same object. And thus, the if (parent::getValue($objectOrValue) !== $value) check inside ReflectionReadonlyProperty.php fails and we see that error. Changing the relation fetch mode to EAGER also fails btw., so that is not the cause. Anyhow, that !== seems to be too strict.

I have some other Entities with a readonly Uuid-based ID and do not see the error there, the only difference ist that none of those are ManyToOne related in other Entities and all other Entities that are ManyToOne related have integer based IDs or non-readonly Uuid based IDs.

spackmat avatar Apr 27 '24 15:04 spackmat

Hello, I face the same issue. Has somebody found a solution nor a workaround?

massimilianobraglia avatar Jul 19 '24 08:07 massimilianobraglia

This seems a duplicate of #9505

garak avatar Jul 19 '24 09:07 garak

Hello, I face the same issue. Has somebody found a solution nor a workaround?

I suggest using this option until the problem is resolved. I think this is the most correct option at the moment, so that it can be quickly corrected with future updates:

    /**
     * @readonly impossible to specify "readonly" attribute natively due
     *           to a Doctrine feature/bug https://github.com/doctrine/orm/issues/9863
     */
    #[ORM\Id]
    #[ORM\Column(name: 'id', type: ExampleId::class)]
    public ExampleId $id;

SerafimArts avatar Jul 19 '24 22:07 SerafimArts