doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.94k stars 2.52k forks source link

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

Open Firehed opened 2 years ago

Firehed commented 2 years ago

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.

ciekals11 commented 2 years ago

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 commented 2 years ago

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.

gndk commented 1 year ago

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.

cl1ck commented 1 year ago

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
juliusstoerrle commented 1 year ago

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;

    //...
}
jeroendesloovere commented 10 months ago

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>

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>
Perf commented 9 months ago

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)"} []

bigfoot90 commented 9 months ago

Me too

Perf commented 8 months ago

Any updates about this issue?

spackmat commented 6 months ago

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.

massimilianobraglia commented 4 months ago

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

garak commented 4 months ago

This seems a duplicate of #9505

SerafimArts commented 4 months ago

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;