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\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, null given, called in /path/to/project/vendor/doctrine/orm/src/PersistentCollection.php on line 162 #11790

Open
savemetenminutes opened this issue Jan 15, 2025 · 1 comment

Comments

@savemetenminutes
Copy link

savemetenminutes commented Jan 15, 2025

Bug Report

Q A
Version 3.3.1
Previous Version if the bug is a regression 3.3.0

Summary

A regression was introduced by this commit:
https://github.com/doctrine/orm/commit/439b4dacf415b743b0d40d65c490a4123759c520/#diff-ecc4306d602db732bf06eda6ac0fc41b23fe8a25f4a2548196f813f30dd2ac35R1277

  1. Have an entity with a one-to-many relation
  2. Define index-by in the mapping
  3. Within the same UnitOfWork perform (Edit: this probably happens every time an entity is fetched for a second time as part of a collection which has index-by defined in the mapping after enabling or disabling a filter, which causes the persister's filter hash to change):
  • $childEntity->getParent() so that it goes through vendor/doctrine/orm/src/Proxy/ProxyFactory.php:279
    $entityPersister = $this->uow->getEntityPersister($className);
  • enable a doctrine filter, which changes the $this->filterHash of the BasicEntityPersister
  • $parentEntity->getChildCollection() so that it goes through vendor/doctrine/orm/src/UnitOfWork.php:2702
    $persister = $this->getEntityPersister($assoc->targetEntity);
  1. This will cause the same instance of the BasicEntityPersister to populate its ->currentPersisterContext->rsm instance of ResultSetMapping by calling the ->addFieldResult twice for each ChildEntity column. So the ResultSetMapping->fieldMappings array in the end will hold each entity column twice, but with different alias (index). The \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getSelectColumnsSQL() mechanism for detecting whether the current instance has already calculated the select columns from a previous query
    vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php:1234
    if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) {
    is bypassed due to the new filter hash, but the ->currentPersisterContext->sqlAliasCounter is not reset and
    vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php:1243
    $columnList[] = $this->getSelectColumnSQL($field, $this->class);
    will result in
    \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getSQLColumnAlias()
    to keep incrementing the
    $this->currentPersisterContext->sqlAliasCounter++
    from the position it was left at from the previous query, thus the second column set with the greater alias indexes will eventually be fetched in the result row. Due to the entity persister's ResultSetMapping instance's ->indexByMap pointing to the alias with the lesser index the following happens:
    vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php:498
    $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
    Here $resultKey becomes null and
    $this->hints['collection']->hydrateSet($resultKey, $element);
    in turn calls
    vendor/doctrine/orm/src/PersistentCollection.php:162
    $this->unwrap()->set($key, $element);
    resulting in the exception from the title of the issue.

Current behavior

An exception is thrown.

Expected behavior

Collection indexing used to work in this case in version 3.3.0 of doctrine/orm

How to reproduce

Providing partial stack traces of the two fetches within the same UnitOfWork - with the breakpoint set at the problematic double addition of the columns in the ResultSetMapping->fieldMappings.

ResultSetMapping.php:327, Doctrine\ORM\Query\ResultSetMapping->addFieldResult()
BasicEntityPersister.php:1505, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getSelectColumnSQL()
BasicEntityPersister.php:1243, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getSelectColumnsSQL()
BasicEntityPersister.php:1119, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getSelectSQL()
BasicEntityPersister.php:734, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->load()
BasicEntityPersister.php:754, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->loadById()
ProxyFactory.php:220, Doctrine\ORM\Proxy\ProxyFactory::Doctrine\ORM\Proxy\{closure:/path/to/project/vendor/doctrine/orm/src/Proxy/ProxyFactory.php:219-242}()
ProxyFactory.php:286, Doctrine\Entities\__CG__\Devision\Auth\Context\UserClient\Domain\UserClient::Doctrine\ORM\Proxy\{closure:/path/to/project/vendor/doctrine/orm/src/Proxy/ProxyFactory.php:285-287}()
LazyObjectState.php:100, Symfony\Component\VarExporter\Internal\LazyObjectState->initialize()
LazyGhostTrait.php:178, Doctrine\Entities\__CG__\Devision\Auth\Context\UserClient\Domain\UserClient->__get()
UserClient.php:51, Devision\Auth\Context\UserClient\Domain\UserClient->getUser()
ResultSetMapping.php:327, Doctrine\ORM\Query\ResultSetMapping->addFieldResult()
BasicEntityPersister.php:1505, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getSelectColumnSQL()
BasicEntityPersister.php:1243, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getSelectColumnsSQL()
BasicEntityPersister.php:1119, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getSelectSQL()
BasicEntityPersister.php:1831, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->getOneToManyStatement()
BasicEntityPersister.php:1779, Doctrine\ORM\Persisters\Entity\BasicEntityPersister->loadOneToManyCollection()
UnitOfWork.php:2706, Doctrine\ORM\UnitOfWork->loadCollection()
PersistentCollection.php:636, Doctrine\ORM\PersistentCollection->doInitialize()
PersistentCollection.php:186, Doctrine\ORM\PersistentCollection->initialize()
AbstractLazyCollection.php:138, Doctrine\Common\Collections\AbstractLazyCollection->getValues()
User.php:287, Devision\Auth\Context\User\Domain\User->getUserClientCollection()

And a JSON formatted partial stack trace of the Exception:

            "message": "Doctrine\\Common\\Collections\\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, null given, called in /path/to/project/vendor/doctrine/orm/src/PersistentCollection.php on line 162",
            "code": 0,
            "type": "TypeError",
            "file:line": "/path/to/project/vendor/doctrine/collections/src/ArrayCollection.php:310",
            "previous": null,
            "trace": [
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/PersistentCollection.php:162",
                    "callee": "Doctrine\\Common\\Collections\\ArrayCollection->set",
                    "args": [
                        "NULL",
                        "{instanceof Devision\\Auth\\Context\\UserClient\\Domain\\UserClient}"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php:501",
                    "callee": "Doctrine\\ORM\\PersistentCollection->hydrateSet",
                    "args": [
                        "NULL",
                        "{instanceof Devision\\Auth\\Context\\UserClient\\Domain\\UserClient}"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php:143",
                    "callee": "Doctrine\\ORM\\Internal\\Hydration\\ObjectHydrator->hydrateRowData",
                    "args": [
                        "array (  'uuid_9' => '\\'<REDACTED>\\'',  'created_10' => '\\'<REDACTED>\\'',  'modified_11' => '\\'<REDACTED>\\'',  'not_archived_12' => '<REDACTED>',  'archived_13' => 'NULL',  'id_14' => '<REDACTED>',  'auth_user_id_15' => '<REDACTED>',  'auth_client_id_16' => '<REDACTED>',)",
                        "array ()"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php:168",
                    "callee": "Doctrine\\ORM\\Internal\\Hydration\\ObjectHydrator->hydrateAllData",
                    "args": []
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php:1001",
                    "callee": "Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator->hydrateAll",
                    "args": [
                        "{instanceof Doctrine\\DBAL\\Result}",
                        "{instanceof Doctrine\\ORM\\Query\\ResultSetMapping}",
                        "array (  'deferEagerLoad' => 'true',  'collection' => '{instanceof Doctrine\\\\ORM\\\\PersistentCollection}',)"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php:1781",
                    "callee": "Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister->loadCollectionFromStatement",
                    "args": [
                        "{instanceof Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping}",
                        "{instanceof Doctrine\\DBAL\\Result}",
                        "{instanceof Doctrine\\ORM\\PersistentCollection}"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/UnitOfWork.php:2706",
                    "callee": "Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister->loadOneToManyCollection",
                    "args": [
                        "{instanceof Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping}",
                        "{instanceof Devision\\Auth\\Context\\User\\Domain\\User}",
                        "{instanceof Doctrine\\ORM\\PersistentCollection}"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/PersistentCollection.php:636",
                    "callee": "Doctrine\\ORM\\UnitOfWork->loadCollection",
                    "args": [
                        "{instanceof Doctrine\\ORM\\PersistentCollection}"
                    ]
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/orm/src/PersistentCollection.php:186",
                    "callee": "Doctrine\\ORM\\PersistentCollection->doInitialize",
                    "args": []
                },
                {
                    "file:line": "/path/to/project/vendor/doctrine/collections/src/AbstractLazyCollection.php:138",
                    "callee": "Doctrine\\ORM\\PersistentCollection->initialize",
                    "args": []
                },
                {
                    "file:line": "/path/to/project/vendor/devision/devision-auth/src/Context/User/Domain/User.php:287",
                    "callee": "Doctrine\\Common\\Collections\\AbstractLazyCollection->getValues",
                    "args": []
                },
                {
                    "file:line": "/path/to/project/vendor/devision/adex-sales/src/Context/Auth/Context/User/Application/Service/UserService.php:292",
                    "callee": "Devision\\Auth\\Context\\User\\Domain\\User->getUserClientCollection",
                    "args": []
                },
@savemetenminutes savemetenminutes changed the title Doctrine\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, null given, called in /var/www/content/adex-sales/vendor/doctrine/orm/src/PersistentCollection.php on line 162 Doctrine\Common\Collections\ArrayCollection::set(): Argument #1 ($key) must be of type string|int, null given, called in /path/to/project/vendor/doctrine/orm/src/PersistentCollection.php on line 162 Jan 15, 2025
savemetenminutes referenced this issue Jan 16, 2025
…ters

CachedPersisterContext::$selectJoinSql should be clear or regenerated when sqlFilter changed
The problem reproduce when in use fetch=EAGER and use additional sql filter on this property
@greg0ire
Copy link
Member

cc @dbannik

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

2 participants