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

Doctrine ORM 2.17.x Update Causes TypeError with indexBy on Doctrine Type (custom UUID) #11149

Open
mkarolczyk opened this issue Jan 2, 2024 · 10 comments · Fixed by #11380
Open

Comments

@mkarolczyk
Copy link

mkarolczyk commented Jan 2, 2024

BC Break Report

Q A
BC Break yes
Version 2.17.x

Summary

After updating doctrine/orm to version 2.17.x, the index-by for doctrine type stopped working even though the object (custom UUID) has the __toString() method. Up to version 2.16.x everything worked fine.

TypeError: Doctrine\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, App\Common\Domain\Uuid given, called in /home/xxx/vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php on line 191

Previous behavior

Until version 2.16.x, indexBy for doctrine type objects (custom UUID) with the __toString() method worked fine.

<one-to-many field="subaccountRecipients" target-entity="App\Message\Preparation\Domain\SubaccountRecipient" mapped-by="message" fetch="EAGER" orphan-removal="true" index-by="subaccountRecipientId">
            <cascade>
                <cascade-persist />
                <cascade-remove />
            </cascade>
        </one-to-many>

Current behavior

Currently, Doctrine is unable to read such an object and shows an error. Before creating an ArrayCollection, there is no cast to a simple type, in this case string.

TypeError: Doctrine\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, App\Common\Domain\Uuid given, called in /home/xxx/vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php on line 191

How to reproduce

  • Have an existing Symfony application using Doctrine 2.17.x
  • Add in the indexBy field, which is an object mapped by doctrine type
  • Use findOneBy method
@greg0ire
Copy link
Member

greg0ire commented Jan 4, 2024

@mkarolczyk hey 👋

I tried finding what may have caused this, and failed, so instead, I wrote this blog post especially for you. Can you please try to apply what you read here to this issue, and let me know if anything is unclear? Thanks.

@gordinskiy
Copy link
Contributor

Got same error after trying to use Optimized eager fetch instead of Multi-step hydration

Doctrine\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, {MyCustomUlidValueObject} given, called in /opt/project/vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php on line 191

@greg0ire
Copy link
Member

greg0ire commented Jan 6, 2024

OK. @beberlei can you please look into this?

@rik702
Copy link

rik702 commented Jan 7, 2024

Did the git bisect thing (thanks @greg0ire working link to blog post) ... was introduced in 76fd34f76607b1b96f381377c1c51df292c759aa

@greg0ire
Copy link
Member

greg0ire commented Jan 7, 2024

Nice job! The link did change since I published the blog post 😅

@greg0ire
Copy link
Member

greg0ire commented Jan 7, 2024

The other thing that might help would be a stack trace. Here is a documentation article (that I also authored) about that

@rik702
Copy link

rik702 commented Jan 8, 2024

TypeError:
Doctrine\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, Symfony\Component\Uid\UuidV7 given, called in .../vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php on line 191

  at vendor/doctrine/collections/src/ArrayCollection.php:311
  at Doctrine\Common\Collections\ArrayCollection->set(object(UuidV7), object(***))
     (vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php:191)
  at Doctrine\ORM\PersistentCollection->hydrateSet(object(UuidV7), object(***))
     (vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:3195)
  at Doctrine\ORM\UnitOfWork->eagerLoadCollections(array('018cab11-c97b-xxxx-xxxx-cdfcb3042045' => object(PersistentCollection)), array('fieldName' => '***', 'mappedBy' => '***', 'targetEntity' => '******', 'cascade' => array('persist', 'remove'), 'indexBy' => '***Uuid', 'orphanRemoval' => true, 'fetch' => 3, 'type' => 4, 'inversedBy' => null, 'isOwningSide' => false, 'sourceEntity' => '******', 'isCascadeRemove' => true, 'isCascadePersist' => true, 'isCascadeRefresh' => false, 'isCascadeMerge' => false, 'isCascadeDetach' => false))
     (vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:3156)
  at Doctrine\ORM\UnitOfWork->triggerEagerLoads()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php:126)
  at Doctrine\ORM\Internal\Hydration\ObjectHydrator->cleanup()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php:272)
  at Doctrine\ORM\Internal\Hydration\AbstractHydrator->hydrateAll(object(Result), object(ResultSetMapping), array('deferEagerLoad' => true))
     (vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:943)
  at Doctrine\ORM\Persisters\Entity\BasicEntityPersister->loadAll(array('User' => array(object(User))))
     (vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:3182)
  at Doctrine\ORM\UnitOfWork->eagerLoadCollections(array('018cab11-cb22-xxxx-xxxx-4b2239340e9c' => object(PersistentCollection)), array('fieldName' => '******s', 'mappedBy' => 'User', 'targetEntity' => '******', 'cascade' => array('persist', 'remove'), 'indexBy' => '***Uuid', 'orphanRemoval' => true, 'fetch' => 3, 'type' => 4, 'inversedBy' => null, 'isOwningSide' => false, 'sourceEntity' => '******', 'isCascadeRemove' => true, 'isCascadePersist' => true, 'isCascadeRefresh' => false, 'isCascadeMerge' => false, 'isCascadeDetach' => false))
     (vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:3156)
  at Doctrine\ORM\UnitOfWork->triggerEagerLoads()
     (vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php:68)
  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(object(Result), object(ResultSetMapping), array())
     (vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:779)
  at Doctrine\ORM\Persisters\Entity\BasicEntityPersister->load(array('UserId' => '******'), null, null, array(), null, 1, null)
     (vendor/doctrine/orm/lib/Doctrine/ORM/EntityRepository.php:241)
  at Doctrine\ORM\EntityRepository->findOneBy(array('UserId' => '******'))
     (vendor/symfony/doctrine-bridge/Security/User/EntityUserProvider.php:54)
  at Symfony\Bridge\Doctrine\Security\User\EntityUserProvider->loadUserByIdentifier('******')
     (vendor/symfony/security-http/Authenticator/Passport/Badge/UserBadge.php:87)
  at Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge->getUser()
     (vendor/symfony/security-http/Authenticator/Passport/Passport.php:56)
  at Symfony\Component\Security\Http\Authenticator\Passport\Passport->getUser()
     (vendor/symfony/security-http/EventListener/UserCheckerListener.php:42)
  at Symfony\Component\Security\Http\EventListener\UserCheckerListener->preCheckCredentials(object(CheckPassportEvent), 'Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/Debug/WrappedListener.php:116)
  at Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(object(CheckPassportEvent), 'Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:206)
  at Symfony\Component\EventDispatcher\EventDispatcher->callListeners(array(object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent', object(CheckPassportEvent))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:56)
  at Symfony\Component\EventDispatcher\EventDispatcher->dispatch(object(CheckPassportEvent), 'Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent')
     (vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:127)
  at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(object(CheckPassportEvent))
     (vendor/symfony/security-http/Authentication/AuthenticatorManager.php:180)
  at Symfony\Component\Security\Http\Authentication\AuthenticatorManager->executeAuthenticator(object(TraceableAuthenticator), object(Request))
     (vendor/symfony/security-http/Authentication/AuthenticatorManager.php:158)
  at Symfony\Component\Security\Http\Authentication\AuthenticatorManager->executeAuthenticators(array(object(TraceableAuthenticator)), object(Request))
     (vendor/symfony/security-http/Authentication/AuthenticatorManager.php:140)
  at Symfony\Component\Security\Http\Authentication\AuthenticatorManager->authenticateRequest(object(Request))
     (vendor/symfony/security-http/Firewall/AuthenticatorManagerListener.php:40)
  at Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener->authenticate(object(RequestEvent))
     (vendor/symfony/security-http/Authenticator/Debug/TraceableAuthenticatorManagerListener.php:68)
  at Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener->authenticate(object(RequestEvent))
     (vendor/symfony/security-bundle/Debug/WrappedLazyListener.php:46)
  at Symfony\Bundle\SecurityBundle\Debug\WrappedLazyListener->authenticate(object(RequestEvent))
     (vendor/symfony/security-http/Firewall/AbstractListener.php:26)
  at Symfony\Component\Security\Http\Firewall\AbstractListener->__invoke(object(RequestEvent))
     (vendor/symfony/security-bundle/Security/LazyFirewallContext.php:60)
  at Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext->__invoke(object(RequestEvent))
     (vendor/symfony/security-bundle/Debug/TraceableFirewallListener.php:77)
  at Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener->callListeners(object(RequestEvent), object(Generator))
     (vendor/symfony/security-http/Firewall.php:95)
  at Symfony\Component\Security\Http\Firewall->onKernelRequest(object(RequestEvent), 'kernel.request', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/Debug/WrappedListener.php:116)
  at Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(object(RequestEvent), 'kernel.request', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:206)
  at Symfony\Component\EventDispatcher\EventDispatcher->callListeners(array(object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.request', object(RequestEvent))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:56)
  at Symfony\Component\EventDispatcher\EventDispatcher->dispatch(object(RequestEvent), 'kernel.request')
     (vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:127)
  at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(object(RequestEvent), 'kernel.request')
     (vendor/symfony/http-kernel/HttpKernel.php:154)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/http-kernel/HttpKernel.php:76)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/http-kernel/Kernel.php:185)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php:35)
  at Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner->run()
     (vendor/autoload_runtime.php:29)
  at require_once('.../vendor/autoload_runtime.php')
     (public/index.php:4)                

Here's a stack trace, It's happening at logon, with a chain of fetch='EAGER' relationships on OneToMany, ManytoOne then another OneToMany. Each entity has Symfony UUID PKs as below:

  #[ORM\Column(type: 'uuid')]
  #[ORM\GeneratedValue(strategy: 'CUSTOM')]
  #[ORM\CustomIdGenerator('doctrine.uuid_generator')]
  #[ORM\Id]
  private ?Uuid $uuid = null;

@wmouwen
Copy link

wmouwen commented Jan 25, 2024

Related to #11097.

The changes in this commit started injecting entities and backed enumerations as keys for collections.

if ($mapping['indexBy']) {
$indexByProperty = $targetClass->getReflectionProperty($mapping['indexBy']);
$collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
} else {
$collectionBatch[$idHash]->add($targetValue);
}

In the example of #11097, it expects the scalar value behind the database column language_id, but gets the Language entity instead.

In the example by @rik702 above, it expects the scalar value behind the Uuid class, but gets the Uuid class/entity instead.

In my beliefs the mapper in UnitOfWork should cast the result of $indexByProperty->getValue($targetValue) to the primary identifier of the object if an object is returned. Note that it should also cast BackedEnums to their scalar value to maintain the behavior of 2.16.x.

@greg0ire any thoughts on this?

@greg0ire
Copy link
Member

No thoughts, this sounds good.
Possible next steps for anybody willing to work on this:

  • build a failing test case from the examples above
  • try the solution proposed by @wmouwen , see if it breaks any tests in the test suite

@pajon
Copy link

pajon commented Feb 27, 2024

Hi everyone,

I've been debugging, and I believe I've identified the issue. It appears that Doctrine ORM does not support arrays with custom types.

You can find more information about this in the Doctrine ORM source code here: https://github.com/doctrine/orm/blob/b187bc85881cc85d36b28e7ed9dbe2071d472e30/src/Persisters/Entity/BasicEntityPersister.php#L1891

While the input contains the correct type, the output always seems to be translated to DBAL ArrayParameterType[STRING|INTEGER|ASCII].

In my case, the custom type (UuidType) is being translated to ArrayParameterType::STRING, which invokes Uuid::__toString() instead of using UuidType::convertToDatabaseValue()

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

Successfully merging a pull request may close this issue.

6 participants