Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Firehed opened this issue Jun 25, 2022 · 13 comments

Comments

@Firehed
Copy link

Firehed commented Jun 25, 2022

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
Copy link

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
Copy link

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
Copy link

gndk commented Jan 8, 2023

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
Copy link

cl1ck commented Jul 17, 2023

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
Copy link

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
Copy link

jeroendesloovere commented Jan 4, 2024

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>

@Perf
Copy link

Perf commented Feb 14, 2024

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
Copy link

Me too

@Perf
Copy link

Perf commented Mar 18, 2024

Any updates about this issue?

@spackmat
Copy link

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
Copy link

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

@garak
Copy link

garak commented Jul 19, 2024

This seems a duplicate of #9505

@SerafimArts
Copy link

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;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests