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

One-to-One unidirectional association broken when only one-sided #11175

Closed withinboredom closed 8 months ago

withinboredom commented 9 months ago

Bug Report

Q A
BC Break no
Version 2.17.3

Summary

In trying to define a simple-ish denormalized mappings of some objects, I stumbled across a bug where it will fail to denormalize a unidirectional association if both sides do not define the association, multiple associations exist, and composite keys are involved.

Current behavior

Errors and warnings are returned:

Undefined array key \"locale\" in /app/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 3140
Trying to access array offset on null in /app/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 3141
Undefined array key \"\" in /app/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 3141
Call to a member function setValue() on null
/app/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php
Line: 3141

How to reproduce

The following entities are given:

namespace TranslationService\Domains;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\UniqueConstraint;

/**
 * Represents a supported locale, readonly
 */
#[Table('supported_locales')]
#[Entity(readOnly: true)]
#[UniqueConstraint('domain_support', fields: ['domain', 'locale'])]
class SupportedLocale
{
    public function __construct(
        #[Column(type: 'ascii_string', length: 12), Id]
        public string $locale,

        #[Column(type: 'string', length: 255), Id]
        #[ManyToOne(DomainSettings::class, inversedBy: 'supportedLocales')]
        public string $domain,

        #[Column(type: 'boolean')]
        public bool $enabled = true,
    ) {
    }

    public function __toString(): string
    {
        return $this->locale;
    }
}

#[Table('domain_settings')]
#[Entity]
class DomainSettings
{
    public function __construct(
        #[Id, Column(type: 'string', length: 255)]
        public readonly string $domain,

        #[Column(type: 'integer'), Version]
        private int $version,

        #[OneToMany(mappedBy: 'domain', targetEntity: SupportedLocale::class, cascade: ['persist'], orphanRemoval: true, indexBy: 'locale')]
        #[SequenceField(arrayType: SupportedLocale::class)]
        public Collection $supportedLocales,

        #[OneToOne(inversedBy: 'locale', targetEntity: SupportedLocale::class)]
        #[JoinColumn(name: 'domain', referencedColumnName: 'domain', nullable: false)]
        #[JoinColumn(name: 'defaultLocale', referencedColumnName: 'locale', nullable: false)]
        public SupportedLocale $defaultLocale,
    ) {
    }
}

Perform database migration and then execute the following to create new entities:


$locales = [
  'en' => new SupportedLocale('en', 'myDomain', true),
  'nl' => new SupportedLocale('nl', 'myDomain', true),
];
$entity = new DomainSettings('myDomain', 0, new ArrayCollection($locales), $locales['en']);
$entityManager->persist($entity);
$entityManager->flush();
$entityManager->clear();

$entity = $entityManager->find(DomainSettings::class, 'myDomain'); // crash

Expected behavior

I expected this code to not be executed if there is not an association on the other entity (maybe check if $inverseAssoc is null).

I know I am doing something unusual, but should it still work?

stof commented 8 months ago

you have #[OneToOne(inversedBy: 'locale', targetEntity: SupportedLocale::class)] in DomainSettings, but locale is not the inverse relation in SupportedLocale. So your mapping is invalid. You should not use inversedBy if there is no inverse side.

The issue in your case is that you tell DomainSettings that this is a bidirectional relation, not a unidirectional one.

stof commented 8 months ago

Note that this is something that the schema validator can detect.

In a Symfony project, you can run bin/console doctrine:schema:validate to run this validator (and I recommend running this command as one of your CI jobs)

withinboredom commented 8 months ago

Note that this is something that the schema validator can detect.

TIL. Thanks for sharing!

your mapping is invalid

Yep! I figured it out, but forgot to close the issue!