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

Merge 2.19.x up into 3.2.x #11581

Merged
merged 12 commits into from
Aug 23, 2024
Merged
9 changes: 5 additions & 4 deletions docs/en/reference/working-with-objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,11 @@ Performance of different deletion strategies
Deleting an object with all its associated objects can be achieved
in multiple ways with very different performance impacts.

1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
will fetch this association. If its a Single association it will
pass this entity to
``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``.
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
fetch this association. If it's a Single association it will pass
this entity to ``EntityManager#remove()``. If the association is a
collection, Doctrine will loop over all its elements and pass them to
``EntityManager#remove()``.
In both cases the cascade remove semantics are applied recursively.
For large object graphs this removal strategy can be very costly.
2. Using a DQL ``DELETE`` statement allows you to delete multiple
Expand Down
4 changes: 2 additions & 2 deletions docs/en/tutorials/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@ step:

// Create a simple "default" Doctrine ORM configuration for Attributes
$config = ORMSetup::createAttributeMetadataConfiguration(
paths: array(__DIR__."/src"),
paths: [__DIR__ . '/src'],
isDevMode: true,
);
// or if you prefer XML
// $config = ORMSetup::createXMLMetadataConfiguration(
// paths: array(__DIR__."/config/xml"),
// paths: [__DIR__ . '/config/xml'],
// isDevMode: true,
//);

Expand Down
10 changes: 9 additions & 1 deletion src/NativeQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ protected function _doExecute(): Result|int
$types = [];

foreach ($this->getParameters() as $parameter) {
$name = $parameter->getName();
$name = $parameter->getName();

if ($parameter->typeWasSpecified()) {
$parameters[$name] = $parameter->getValue();
$types[$name] = $parameter->getType();

continue;
}

$value = $this->processParameterValue($parameter->getValue());
$type = $parameter->getValue() === $value
? $parameter->getType()
Expand Down
12 changes: 9 additions & 3 deletions src/Persisters/Collection/OneToManyPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Utility\PersisterHelper;

use function array_fill;
use function array_keys;
use function array_reverse;
use function array_values;
use function assert;
use function count;
use function implode;
use function is_int;
use function is_string;
Expand Down Expand Up @@ -174,9 +177,12 @@ private function deleteEntityCollection(PersistentCollection $collection): int

if ($targetClass->isInheritanceTypeSingleTable()) {
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
$statement .= ' AND ' . $discriminatorColumn->name . ' = ?';
$parameters[] = $targetClass->discriminatorValue;
$types[] = $discriminatorColumn->type;
$discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap);
$statement .= ' AND ' . $discriminatorColumn->name . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')';
foreach ($discriminatorValues as $discriminatorValue) {
$parameters[] = $discriminatorValue;
$types[] = $discriminatorColumn->type;
}
}

$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
Expand Down
39 changes: 32 additions & 7 deletions src/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -792,17 +792,42 @@ public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEnti

$computedIdentifier = [];

/** @var array<string,mixed>|null $sourceEntityData */
$sourceEntityData = null;

// TRICKY: since the association is specular source and target are flipped
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
throw MappingException::joinColumnMustPointToMappedField(
$sourceClass->name,
$sourceKeyColumn,
);
}
// The likely case here is that the column is a join column
// in an association mapping. However, there is no guarantee
// at this point that a corresponding (generally identifying)
// association has been mapped in the source entity. To handle
// this case we directly reference the column-keyed data used
// to initialize the source entity before throwing an exception.
$resolvedSourceData = false;
if (! isset($sourceEntityData)) {
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
}

if (isset($sourceEntityData[$sourceKeyColumn])) {
$dataValue = $sourceEntityData[$sourceKeyColumn];
if ($dataValue !== null) {
$resolvedSourceData = true;
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$dataValue;
}
}

$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
if (! $resolvedSourceData) {
throw MappingException::joinColumnMustPointToMappedField(
$sourceClass->name,
$sourceKeyColumn,
);
}
} else {
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
}
}

$targetEntity = $this->load($computedIdentifier, null, $assoc);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;

use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table;

#[Entity]
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse')]
class InverseSide
{
/** Associative id (owning identifier) */
#[Id]
#[OneToOne(targetEntity: InverseSideIdTarget::class, inversedBy: 'inverseSide')]
#[JoinColumn(nullable: false, name: 'associativeId')]
public InverseSideIdTarget $associativeId;

#[OneToOne(targetEntity: OwningSide::class, mappedBy: 'inverse')]
public OwningSide $owning;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table;

#[Entity]
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse_id_target')]
class InverseSideIdTarget
{
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public string $id;

#[OneToOne(targetEntity: InverseSide::class, mappedBy: 'associativeId')]
public InverseSide $inverseSide;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table;

#[Entity]
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_owning')]
class OwningSide
{
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public string $id;

/** Owning side */
#[OneToOne(targetEntity: InverseSide::class, inversedBy: 'owning')]
#[JoinColumn(name: 'inverse', referencedColumnName: 'associativeId')]
public InverseSide $inverse;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional;

use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSide;
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSideIdTarget;
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\OwningSide;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;

use function assert;

class OneToOneInverseSideWithAssociativeIdLoadAfterDqlQueryTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(OwningSide::class, InverseSideIdTarget::class, InverseSide::class);
}

#[Group('GH-11108')]
public function testInverseSideWithAssociativeIdOneToOneLoadedAfterDqlQuery(): void
{
$owner = new OwningSide();
$inverseId = new InverseSideIdTarget();
$inverse = new InverseSide();

$owner->id = 'owner';
$inverseId->id = 'inverseId';
$inverseId->inverseSide = $inverse;
$inverse->associativeId = $inverseId;
$owner->inverse = $inverse;
$inverse->owning = $owner;

$this->_em->persist($owner);
$this->_em->persist($inverseId);
$this->_em->persist($inverse);
$this->_em->flush();
$this->_em->clear();

$fetchedInverse = $this
->_em
->createQueryBuilder()
->select('inverse')
->from(InverseSide::class, 'inverse')
->andWhere('inverse.associativeId = :associativeId')
->setParameter('associativeId', 'inverseId')
->getQuery()
->getSingleResult();
assert($fetchedInverse instanceof InverseSide);

self::assertInstanceOf(InverseSide::class, $fetchedInverse);
self::assertInstanceOf(InverseSideIdTarget::class, $fetchedInverse->associativeId);
self::assertInstanceOf(OwningSide::class, $fetchedInverse->owning);

$this->assertSQLEquals(
'select o0_.associativeid as associativeid_0 from one_to_one_inverse_side_assoc_id_load_inverse o0_ where o0_.associativeid = ?',
$this->getLastLoggedQuery(1)['sql'],
);

$this->assertSQLEquals(
'select t0.id as id_1, t0.inverse as inverse_2 from one_to_one_inverse_side_assoc_id_load_owning t0 where t0.inverse = ?',
$this->getLastLoggedQuery()['sql'],
);
}
}
105 changes: 105 additions & 0 deletions tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

class GH11501Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->setUpEntitySchema([
GH11501AbstractTestEntity::class,
GH11501TestEntityOne::class,
GH11501TestEntityTwo::class,
GH11501TestEntityHolder::class,
]);
}

/** @throws ORMException */
public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void
{
$testEntityOne = new GH11501TestEntityOne();
$testEntityTwo = new GH11501TestEntityTwo();
$testEntityHolder = new GH11501TestEntityHolder();

$testEntityOne->testEntityHolder = $testEntityHolder;
$testEntityHolder->testEntities->add($testEntityOne);

$testEntityTwo->testEntityHolder = $testEntityHolder;
$testEntityHolder->testEntities->add($testEntityTwo);

$em = $this->getEntityManager();
$em->persist($testEntityOne);
$em->persist($testEntityTwo);
$em->persist($testEntityHolder);
$em->flush();

$testEntityHolder->testEntities = new ArrayCollection();
$em->persist($testEntityHolder);
$em->flush();
$em->refresh($testEntityHolder);

static::assertEmpty($testEntityHolder->testEntities->toArray(), 'All records should have been deleted');
}
}

#[ORM\Entity]
#[ORM\Table(name: 'one_to_many_single_table_inheritance_test_entities_parent_join')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
'test_entity_one' => 'GH11501TestEntityOne',
'test_entity_two' => 'GH11501TestEntityTwo',
])]
class GH11501AbstractTestEntity
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
public int $id;

#[ORM\ManyToOne(targetEntity: 'GH11501TestEntityHolder', inversedBy: 'testEntities')]
#[ORM\JoinColumn(name: 'test_entity_holder_id', referencedColumnName: 'id')]
public GH11501TestEntityHolder $testEntityHolder;
}


#[ORM\Entity]
class GH11501TestEntityOne extends GH11501AbstractTestEntity
{
}

#[ORM\Entity]
class GH11501TestEntityTwo extends GH11501AbstractTestEntity
{
}

#[ORM\Entity]
class GH11501TestEntityHolder
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
public int $id;

#[ORM\OneToMany(
targetEntity: 'GH11501AbstractTestEntity',
mappedBy: 'testEntityHolder',
orphanRemoval: true,
)]
public Collection $testEntities;

public function __construct()
{
$this->testEntities = new ArrayCollection();
}
}
Loading
Loading