ManyToOne inversedBy an Entity with a Derived Identity : Cannot assign int to App\Entity\B::$a of type ?App\Entity\A #7091

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

Originally created by @DaedalusDev on GitHub (Jan 13, 2023).

Bug Report

Q A
BC Break no
Version 2.14.0 2.13.5
PHP 8.1

Summary

In multiple cases in an application, we use Entity as identifier to make it easier, in particular for cases of OneToOne relationships Identity through foreign Entities. Unfortunately, we are facing an issue when we try to create a child association mapping for the child entities.
It works as expected in parent to child fetching. But it failed at child to parent fetching.

Current behavior

Fetching child association field produce an error in hydratation process.

How to reproduce

<?php
// src/Entity/A.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class A
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected ?int $id = null;

    /** @ORM\OneToOne(targetEntity=B::class, mappedBy="a") */
    protected ?B $b = null;
}
<?php
// src/Entity/B.php

namespace App\Entity;

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

/**
 * @ORM\Entity
 */
class B
{
    /**
     * @ORM\Id
     * @ORM\OneToOne(
     *     targetEntity=A::class,
     *     inversedBy="b"
     * )
     */
    protected ?A $a = null;

    /**
     * @var ?Collection<C>
     * @ORM\OneToMany(targetEntity=C::class, mappedBy="b")
     */
    protected ?Collection $cs = null;
}
<?php
// src/Entity/C.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class C
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected ?int $id = null;

    /**
     * @ORM\ManyToOne(targetEntity=B::class, inversedBy="cs")
     * @ORM\JoinColumn(referencedColumnName="a_id")
     */
    protected ?B $b = null;
}
// src/Tests/FetchTest.php
<?php

namespace Tests;

use App\Entity\A;
use App\Entity\B;
use App\Entity\C;

class FetchTest
{
    public function testFetchA(): void
    {
        try {
            $el = $this->entityManager
                ->getRepository(A::class)
                ->find(1)
            ;
            self::assertInstanceOf(A::class, $el);
        } catch (Exception $e) {
            self::fail('Must not throw');
        }
    }

    public function testFetchB(): void
    {
        try {
            $el = $this->entityManager
                ->getRepository(B::class)
                ->find(1)
            ;
            self::assertInstanceOf(B::class, $el);
        } catch (Exception $e) {
            self::fail('Must not throw');
        }
    }

    public function testFetchC(): void
    {
        try {
            $el = $this->entityManager
                ->getRepository(C::class)
                ->find(1)
            ;
            self::assertInstanceOf(C::class, $el);
        } catch (Exception $e) {
            self::fail('Must not throw');
        }
    }
}
# dump-sql.sql
CREATE TABLE b (a_id INT NOT NULL, PRIMARY KEY(a_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
CREATE TABLE a (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
CREATE TABLE c (id INT AUTO_INCREMENT NOT NULL, b_id INT DEFAULT NULL, INDEX IDX_6B9DF6F296BFCB6 (b_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
ALTER TABLE b ADD CONSTRAINT FK_71BEEFF93BDE5358 FOREIGN KEY (a_id) REFERENCES a (id);
ALTER TABLE c ADD CONSTRAINT FK_6B9DF6F296BFCB6 FOREIGN KEY (b_id) REFERENCES b (a_id);

# test data
INSERT INTO `a` VALUES (1);
INSERT INTO `b` VALUES (1);
INSERT INTO `c` VALUES (1,1);

Error

testFetchC fail with the following error :

TypeError:
Cannot assign int to property App\Entity\B::$a of type ?App\Entity\A

  at vendor/doctrine/common/lib/Doctrine/Common/Proxy/AbstractProxyFactory.php:118
  at ReflectionProperty->setValue()
     (vendor/doctrine/common/lib/Doctrine/Common/Proxy/AbstractProxyFactory.php:118)
  at Doctrine\Common\Proxy\AbstractProxyFactory->getProxy()
     (vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:2893)
  at Doctrine\ORM\UnitOfWork->createEntity()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php:156)
  at Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator->hydrateRowData()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php:63)
  at Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator->hydrateAllData()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php:270)
  at Doctrine\ORM\Internal\Hydration\AbstractHydrator->hydrateAll()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:758)
  at Doctrine\ORM\Persisters\Entity\BasicEntityPersister->load()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:768)
  at Doctrine\ORM\Persisters\Entity\BasicEntityPersister->loadById()
     (vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:521)
  at Doctrine\ORM\EntityManager->find()
     (vendor/doctrine/orm/lib/Doctrine/ORM/EntityRepository.php:197)
  at Doctrine\ORM\EntityRepository->find()
     (src/Tests/FetchTest:41)

Expected behavior

Doctrine didn't analyse B::$a mapping, and tried to allocate an integer instead of A to B::$a (instance of A or AProxy class).
This is a very very particular case, but i didn't find an option to cast ORM\JoinColumn to another type than *ToOne::targetEntity.

Just tell me if this is an interesting feature for a future version of Doctrine or if I missed something in the doc 🤐

I've found this issue #3093
It seems to relate to a similar problem even if the schema is not exactly the same. Unfortunately, it was classified as "Can't Fix"

Workaround

The most simple workaround are :

  • to use a "conventionnal" @ORM\GeneratedValue on B
  • to join C directly to A with @ORM\ManyToOne(targetEntity=A::class, inversedBy="cs")

Solution proposal

Solution 1 (declarative)

Add an optionnal extra argument targetIdentifierEntity attribute for ManyToOne.

    /** @ORM\ManyToOne(targetEntity=B::class, targetIdentifierEntity=A::class, inversedBy="cs") */
    private ?B $b = null;
Solution 2 (automatic)

Ensure identifier is integer before setValue call of the entity ReflexionClass (Can come with performance consideration ?)


Just tell me if this is an interesting feature for a future version of Doctrine or if I missed something in the doc 🤐

Thanks for your great work !

Originally created by @DaedalusDev on GitHub (Jan 13, 2023). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | ~2.14.0~ 2.13.5 | PHP | 8.1 #### Summary <!-- Provide a summary describing the problem you are experiencing. --> In multiple cases in an application, we use `Entity` as identifier to make it easier, in particular for cases of OneToOne relationships [Identity through foreign Entities](https://www.doctrine-project.org/projects/doctrine-orm/en/2.13/tutorials/composite-primary-keys.html#identity-through-foreign-entities). Unfortunately, we are facing an issue when we try to create a child association mapping for the child entities. It works as expected in parent to child fetching. But it failed at child to parent fetching. #### Current behavior <!-- What is the current (buggy) behavior? --> Fetching child association field produce an error in hydratation process. #### How to reproduce <!-- Provide steps to reproduce the bug. If possible, also add a code snippet with relevant configuration, entity mappings, DQL etc. Adding a failing Unit or Functional Test would help us a lot - you can submit one in a Pull Request separately, referencing this bug report. --> ```php <?php // src/Entity/A.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class A { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ protected ?int $id = null; /** @ORM\OneToOne(targetEntity=B::class, mappedBy="a") */ protected ?B $b = null; } ``` ```php <?php // src/Entity/B.php namespace App\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class B { /** * @ORM\Id * @ORM\OneToOne( * targetEntity=A::class, * inversedBy="b" * ) */ protected ?A $a = null; /** * @var ?Collection<C> * @ORM\OneToMany(targetEntity=C::class, mappedBy="b") */ protected ?Collection $cs = null; } ``` ```php <?php // src/Entity/C.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class C { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ protected ?int $id = null; /** * @ORM\ManyToOne(targetEntity=B::class, inversedBy="cs") * @ORM\JoinColumn(referencedColumnName="a_id") */ protected ?B $b = null; } ``` ```php // src/Tests/FetchTest.php <?php namespace Tests; use App\Entity\A; use App\Entity\B; use App\Entity\C; class FetchTest { public function testFetchA(): void { try { $el = $this->entityManager ->getRepository(A::class) ->find(1) ; self::assertInstanceOf(A::class, $el); } catch (Exception $e) { self::fail('Must not throw'); } } public function testFetchB(): void { try { $el = $this->entityManager ->getRepository(B::class) ->find(1) ; self::assertInstanceOf(B::class, $el); } catch (Exception $e) { self::fail('Must not throw'); } } public function testFetchC(): void { try { $el = $this->entityManager ->getRepository(C::class) ->find(1) ; self::assertInstanceOf(C::class, $el); } catch (Exception $e) { self::fail('Must not throw'); } } } ``` ```sql # dump-sql.sql CREATE TABLE b (a_id INT NOT NULL, PRIMARY KEY(a_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; CREATE TABLE a (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; CREATE TABLE c (id INT AUTO_INCREMENT NOT NULL, b_id INT DEFAULT NULL, INDEX IDX_6B9DF6F296BFCB6 (b_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; ALTER TABLE b ADD CONSTRAINT FK_71BEEFF93BDE5358 FOREIGN KEY (a_id) REFERENCES a (id); ALTER TABLE c ADD CONSTRAINT FK_6B9DF6F296BFCB6 FOREIGN KEY (b_id) REFERENCES b (a_id); # test data INSERT INTO `a` VALUES (1); INSERT INTO `b` VALUES (1); INSERT INTO `c` VALUES (1,1); ``` ### Error `testFetchC` fail with the following error : ``` TypeError: Cannot assign int to property App\Entity\B::$a of type ?App\Entity\A at vendor/doctrine/common/lib/Doctrine/Common/Proxy/AbstractProxyFactory.php:118 at ReflectionProperty->setValue() (vendor/doctrine/common/lib/Doctrine/Common/Proxy/AbstractProxyFactory.php:118) at Doctrine\Common\Proxy\AbstractProxyFactory->getProxy() (vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:2893) at Doctrine\ORM\UnitOfWork->createEntity() (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php:156) at Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator->hydrateRowData() (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php:63) at Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator->hydrateAllData() (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php:270) at Doctrine\ORM\Internal\Hydration\AbstractHydrator->hydrateAll() (vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:758) at Doctrine\ORM\Persisters\Entity\BasicEntityPersister->load() (vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:768) at Doctrine\ORM\Persisters\Entity\BasicEntityPersister->loadById() (vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:521) at Doctrine\ORM\EntityManager->find() (vendor/doctrine/orm/lib/Doctrine/ORM/EntityRepository.php:197) at Doctrine\ORM\EntityRepository->find() (src/Tests/FetchTest:41) ``` #### Expected behavior <!-- What was the expected (correct) behavior? --> Doctrine didn't analyse `B::$a` mapping, and tried to allocate an integer instead of `A` to `B::$a` (instance of `A` or `AProxy` class). This is a very very particular case, but i didn't find an option to cast `ORM\JoinColumn` to another type than `*ToOne::targetEntity`. Just tell me if this is an interesting feature for a future version of Doctrine or if I missed something in the doc :zipper_mouth_face: #### Related issue I've found this issue #3093 It seems to relate to a similar problem even if the schema is not exactly the same. Unfortunately, it was classified as "Can't Fix" #### Workaround The most simple workaround are : - to use a "conventionnal" `@ORM\GeneratedValue` on `B` - to join `C` directly to `A` with `@ORM\ManyToOne(targetEntity=A::class, inversedBy="cs")` #### Solution proposal ##### Solution 1 (declarative) Add an optionnal extra argument `targetIdentifierEntity` attribute for `ManyToOne`. ```php /** @ORM\ManyToOne(targetEntity=B::class, targetIdentifierEntity=A::class, inversedBy="cs") */ private ?B $b = null; ``` ##### Solution 2 (automatic) Ensure identifier is `integer` before `setValue` call of the entity `ReflexionClass` (Can come with performance consideration ?) ___ Just tell me if this is an interesting feature for a future version of Doctrine or if I missed something in the doc :zipper_mouth_face: Thanks for your great work !
admin closed this issue 2026-01-22 15:44:29 +01:00
Author
Owner

@mpdude commented on GitHub (Jan 16, 2023):

You're using B::$a as the @Id column and as an @OneToOne relationship at the same time?

I cannot see how this is supposed to work. To give just one reason, the $a column would have to contain an object and the identifier value at the same time?

@mpdude commented on GitHub (Jan 16, 2023): You're using `B::$a` as the `@Id` column and as an `@OneToOne` relationship at the same time? I cannot see how this is supposed to work. To give just one reason, the `$a` column would have to contain an object and the identifier value at the same time?
Author
Owner

@DaedalusDev commented on GitHub (Jan 17, 2023):

You're using B::$a as the @Id column and as an @OneToOne relationship at the same time?

I cannot see how this is supposed to work. To give just one reason, the $a column would have to contain an object and the identifier value at the same time?

@mpdude Yes, it works very well and we have several entities that use this mode of operation in order to compartmentalize the data. Concerning B::$a, this value doesn't contain the identifier value but an instance of A or a proxy class of A exactly as expected. Current version of doctrine/ORM manage perfecly this use case.

To functionally illustrate the use case, here is a substitution example :
For an Agency (A in previous exemple) entity that represents a customer, we have the possibility to define an AgencySettingsSubscription (B in previous exemple) entity which contains the billing settings of the Agency entity.

For the AgencySettingsSubscription entity, generating an identifier is superflux and not necessary because all agencies have unique billing settings. So the id is directly the id of the Agency entity.

Now we want to add several PaymentMethod (C in previous exemple) entities that contain the agency's payment information. However we want to attach this information to the AgencySettingsSubscription entity rather than the Agency entity.

Here is a JSON that represents the expected data format :

{
  "id": 0,
  "name": "string",
  "agencySettingsSubscription": {
    "monthlyPrice": 0,
    "paymentMethods": [
      {
        "id": 0,
        "type": "string"
      }
    ]
  }
}
@DaedalusDev commented on GitHub (Jan 17, 2023): > You're using `B::$a` as the `@Id` column and as an `@OneToOne` relationship at the same time? > > I cannot see how this is supposed to work. To give just one reason, the `$a` column would have to contain an object and the identifier value at the same time? @mpdude Yes, it works very well and we have several entities that use this mode of operation in order to compartmentalize the data. Concerning `B::$a`, this value doesn't contain the identifier value but an instance of `A` or a proxy class of `A` exactly as expected. Current version of doctrine/ORM manage perfecly this use case. To functionally illustrate the use case, here is a substitution example : For an `Agency` (`A` in previous exemple) entity that represents a customer, we have the possibility to define an `AgencySettingsSubscription` (`B` in previous exemple) entity which contains the billing settings of the `Agency` entity. For the `AgencySettingsSubscription` entity, generating an identifier is superflux and not necessary because all agencies have unique billing settings. So the `id` is directly the `id` of the Agency entity. Now we want to add several `PaymentMethod` (`C` in previous exemple) entities that contain the agency's payment information. However we want to attach this information to the `AgencySettingsSubscription` entity rather than the `Agency` entity. Here is a JSON that represents the expected data format : ```json { "id": 0, "name": "string", "agencySettingsSubscription": { "monthlyPrice": 0, "paymentMethods": [ { "id": 0, "type": "string" } ] } } ```
Author
Owner

@mpdude commented on GitHub (Jan 17, 2023):

I am not in a position to judge, but it seems to me you're overstressing what the ORM was built to do.

I'd say the @Id may be of scalar types and possibly a composite key, but definetly not a column that is a relation at the same time.

Let's wait what others have to say. Possibly a mapping check (Schema Tool? Runtime validation?) should reject this kind of configuration.

@mpdude commented on GitHub (Jan 17, 2023): I am not in a position to judge, but it seems to me you're overstressing what the ORM was built to do. I'd say the `@Id` may be of scalar types and possibly a composite key, but definetly not a column that is a relation at the same time. Let's wait what others have to say. Possibly a mapping check (Schema Tool? Runtime validation?) should reject this kind of configuration.
Author
Owner

@DaedalusDev commented on GitHub (Jan 17, 2023):

I am not in a position to judge, but it seems to me you're overstressing what the ORM was built to do.

I'd say the @Id may be of scalar types and possibly a composite key, but definetly not a column that is a relation at the same time.

Let's wait what others have to say. Possibly a mapping check (Schema Tool? Runtime validation?) should reject this kind of configuration.

See Identity through foreign Entities for more details about using an entity as identifier (primary or composite) 👍

I've update bug's description and initial issue to be clearer about our intentions 😉

@DaedalusDev commented on GitHub (Jan 17, 2023): > I am not in a position to judge, but it seems to me you're overstressing what the ORM was built to do. > > I'd say the `@Id` may be of scalar types and possibly a composite key, but definetly not a column that is a relation at the same time. > > Let's wait what others have to say. Possibly a mapping check (Schema Tool? Runtime validation?) should reject this kind of configuration. See [Identity through foreign Entities](https://www.doctrine-project.org/projects/doctrine-orm/en/2.13/tutorials/composite-primary-keys.html#identity-through-foreign-entities) for more details about using an entity as identifier (primary or composite) :+1: I've update bug's description and initial issue to be clearer about our intentions :wink:
Author
Owner

@DaedalusDev commented on GitHub (Jan 17, 2023):

Ok... My bad ! My issue runs on 2.13.5 and is fixed in 2.14.x !

Closed !

@DaedalusDev commented on GitHub (Jan 17, 2023): Ok... My bad ! My issue runs on 2.13.5 and is fixed in 2.14.x ! Closed !
Author
Owner

@toby-griffiths commented on GitHub (Sep 7, 2023):

Ok... My bad ! My issue runs on 2.13.5 and is fixed in 2.14.x !

Closed !

@DaedalusDev Thank you so much for you sharing that fix. We've ust spent a couple of hours trying to get to the bottom of this exact same issue.

@toby-griffiths commented on GitHub (Sep 7, 2023): > Ok... My bad ! My issue runs on 2.13.5 and is fixed in 2.14.x ! > > Closed ! @DaedalusDev Thank you so much for you sharing that fix. We've ust spent a couple of hours trying to get to the bottom of this exact same issue.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7091