Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/2.20.x' into 3.3.x
Browse files Browse the repository at this point in the history
  • Loading branch information
greg0ire committed Nov 23, 2024
2 parents 4b03ec7 + 82e2c98 commit 2ff998d
Show file tree
Hide file tree
Showing 12 changed files with 361 additions and 10 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:
- "default"
- "3.7"
postgres-version:
- "15"
- "17"
extension:
- pdo_pgsql
- pgsql
Expand Down Expand Up @@ -341,7 +341,7 @@ jobs:
path: "reports"

- name: "Upload to Codecov"
uses: "codecov/codecov-action@v4"
uses: "codecov/codecov-action@v5"
with:
directory: reports
env:
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/unitofwork.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ How Doctrine Detects Changes
----------------------------

Doctrine is a data-mapper that tries to achieve persistence-ignorance (PI).
This means you map php objects into a relational database that don't
This means you map PHP objects into a relational database that don't
necessarily know about the database at all. A natural question would now be,
"how does Doctrine even detect objects have changed?".

Expand Down
1 change: 1 addition & 0 deletions docs/en/tutorials/extra-lazy-associations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ can be called without triggering a full load of the collection:
- ``Collection#containsKey($key)``
- ``Collection#count()``
- ``Collection#get($key)``
- ``Collection#isEmpty()``
- ``Collection#slice($offset, $length = null)``

For each of the above methods the following semantics apply:
Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<referencedClass name="Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand"/>
<referencedClass name="Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper"/>
<referencedClass name="Doctrine\ORM\Tools\Console\EntityManagerProvider\HelperSetManagerProvider"/>
<referencedClass name="Doctrine\Persistence\Mapping\StaticReflectionService"/>
</errorLevel>
</DeprecatedClass>
<DeprecatedMethod>
Expand Down
6 changes: 2 additions & 4 deletions src/Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,14 @@ private function __construct()
* The onFlush event occurs when the EntityManager#flush() operation is invoked,
* after any changes to managed entities have been determined but before any
* actual database operations are executed. The event is only raised if there is
* actually something to do for the underlying UnitOfWork. If nothing needs to be done,
* the onFlush event is not raised.
* actually something to do for the underlying UnitOfWork.
*/
public const onFlush = 'onFlush';

/**
* The postFlush event occurs when the EntityManager#flush() operation is invoked and
* after all actual database operations are executed successfully. The event is only raised if there is
* actually something to do for the underlying UnitOfWork. If nothing needs to be done,
* the postFlush event is not raised. The event won't be raised if an error occurs during the
* actually something to do for the underlying UnitOfWork. The event won't be raised if an error occurs during the
* flush operation.
*/
public const postFlush = 'postFlush';
Expand Down
5 changes: 4 additions & 1 deletion src/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ class BasicEntityPersister implements EntityPersister
private readonly CachedPersisterContext $limitsHandlingContext;
private readonly CachedPersisterContext $noLimitsContext;

private string|null $filterHash = null;

/**
* Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
* and persists instances of the class described by the given ClassMetadata descriptor.
Expand Down Expand Up @@ -1229,7 +1231,7 @@ final protected function getOrderBySQL(array $orderBy, string $baseTableAlias):
*/
protected function getSelectColumnsSQL(): string
{
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) {
return $this->currentPersisterContext->selectColumnListSql;
}

Expand Down Expand Up @@ -1339,6 +1341,7 @@ protected function getSelectColumnsSQL(): string
}

$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->filterHash = $this->em->getFilters()->getHash();

return $this->currentPersisterContext->selectColumnListSql;
}
Expand Down
4 changes: 2 additions & 2 deletions src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -2003,13 +2003,13 @@ private function doRefresh(object $entity, array &$visited, LockMode|int|null $l
throw ORMInvalidArgumentException::entityNotManaged($entity);
}

$this->cascadeRefresh($entity, $visited, $lockMode);

$this->getEntityPersister($class->name)->refresh(
array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
$entity,
$lockMode,
);

$this->cascadeRefresh($entity, $visited, $lockMode);
}

/**
Expand Down
128 changes: 128 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;

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

$this->createSchemaForModels(
LazyEagerCollectionUser::class,
LazyEagerCollectionAddress::class,
LazyEagerCollectionPhone::class,
);
}

public function testRefreshRefreshesBothLazyAndEagerCollections(): void
{
$user = new LazyEagerCollectionUser();
$user->data = 'Guilherme';

$ph = new LazyEagerCollectionPhone();
$ph->data = '12345';
$user->addPhone($ph);

$ad = new LazyEagerCollectionAddress();
$ad->data = '6789';
$user->addAddress($ad);

$this->_em->persist($user);
$this->_em->persist($ad);
$this->_em->persist($ph);
$this->_em->flush();
$this->_em->clear();

$user = $this->_em->find(LazyEagerCollectionUser::class, $user->id);
$ph = $user->phones[0];
$ad = $user->addresses[0];

$ph->data = 'abc';
$ad->data = 'def';

$this->_em->refresh($user);

self::assertSame('12345', $ph->data);
self::assertSame('6789', $ad->data);
}
}

#[Entity]
class LazyEagerCollectionUser
{
#[Id]
#[Column(type: 'integer')]
#[GeneratedValue(strategy: 'AUTO')]
public int $id;

#[Column(type: 'string', length: 255)]
public string $data;

/** @var Collection<LazyEagerCollectionPhone> */
#[ORM\OneToMany(targetEntity: 'LazyEagerCollectionPhone', cascade: ['refresh'], fetch: 'EAGER', mappedBy: 'user')]
public Collection $phones;

/** @var Collection<LazyEagerCollectionAddress> */
#[ORM\OneToMany(targetEntity: 'LazyEagerCollectionAddress', cascade: ['refresh'], mappedBy: 'user')]
public Collection $addresses;

public function __construct()
{
$this->addresses = new ArrayCollection();
$this->phones = new ArrayCollection();
}

public function addPhone(LazyEagerCollectionPhone $phone): void
{
$phone->user = $this;
$this->phones[] = $phone;
}

public function addAddress(LazyEagerCollectionAddress $address): void
{
$address->user = $this;
$this->addresses[] = $address;
}
}

#[Entity]
class LazyEagerCollectionPhone
{
#[Id]
#[Column(type: 'integer')]
#[GeneratedValue(strategy: 'AUTO')]
public int $id;

#[Column(type: 'string', length: 255)]
public string $data;

#[ORM\ManyToOne(targetEntity: 'LazyEagerCollectionUser', inversedBy: 'phones')]
public LazyEagerCollectionUser $user;
}

#[Entity]
class LazyEagerCollectionAddress
{
#[Id]
#[Column(type: 'integer')]
#[GeneratedValue(strategy: 'AUTO')]
public int $id;

#[Column(type: 'string', length: 255)]
public string $data;

#[ORM\ManyToOne(targetEntity: 'LazyEagerCollectionUser', inversedBy: 'addresses')]
public LazyEagerCollectionUser $user;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;

use Doctrine\Tests\OrmFunctionalTestCase;

use function sprintf;
use function str_replace;

final class ChangeFiltersTest extends OrmFunctionalTestCase
{
private const COMPANY_A = 'A';
private const COMPANY_B = 'B';

public function setUp(): void
{
parent::setUp();

$this->setUpEntitySchema([
Order::class,
User::class,
]);
}

/** @return non-empty-array<"companyA"|"companyB", array{orderId: int, userId: int}> */
private function prepareData(): array
{
$user1 = new User(self::COMPANY_A);
$order1 = new Order($user1);
$user2 = new User(self::COMPANY_B);
$order2 = new Order($user2);

$this->_em->persist($user1);
$this->_em->persist($order1);
$this->_em->persist($user2);
$this->_em->persist($order2);
$this->_em->flush();
$this->_em->clear();

return [
'companyA' => ['orderId' => $order1->id, 'userId' => $user1->id],
'companyB' => ['orderId' => $order2->id, 'userId' => $user2->id],
];
}

public function testUseEnableDisableFilter(): void
{
$this->_em->getConfiguration()->addFilter(CompanySQLFilter::class, CompanySQLFilter::class);
$this->_em->getFilters()->enable(CompanySQLFilter::class)->setParameter('company', self::COMPANY_A);

['companyA' => $companyA, 'companyB' => $companyB] = $this->prepareData();

$order1 = $this->_em->find(Order::class, $companyA['orderId']);

self::assertNotNull($order1->user, $this->generateMessage('Order1->User1 not found'));
self::assertEquals($companyA['userId'], $order1->user->id, $this->generateMessage('Order1->User1 != User1'));

$this->_em->getFilters()->disable(CompanySQLFilter::class);
$this->_em->getFilters()->enable(CompanySQLFilter::class)->setParameter('company', self::COMPANY_B);

$order2 = $this->_em->find(Order::class, $companyB['orderId']);

self::assertNotNull($order2->user, $this->generateMessage('Order2->User2 not found'));
self::assertEquals($companyB['userId'], $order2->user->id, $this->generateMessage('Order2->User2 != User2'));
}

public function testUseChangeFilterParameters(): void
{
$this->_em->getConfiguration()->addFilter(CompanySQLFilter::class, CompanySQLFilter::class);
$filter = $this->_em->getFilters()->enable(CompanySQLFilter::class);

['companyA' => $companyA, 'companyB' => $companyB] = $this->prepareData();

$filter->setParameter('company', self::COMPANY_A);

$order1 = $this->_em->find(Order::class, $companyA['orderId']);

self::assertNotNull($order1->user, $this->generateMessage('Order1->User1 not found'));
self::assertEquals($companyA['userId'], $order1->user->id, $this->generateMessage('Order1->User1 != User1'));

$filter->setParameter('company', self::COMPANY_B);

$order2 = $this->_em->find(Order::class, $companyB['orderId']);

self::assertNotNull($order2->user, $this->generateMessage('Order2->User2 not found'));
self::assertEquals($companyB['userId'], $order2->user->id, $this->generateMessage('Order2->User2 != User2'));
}

public function testUseQueryBuilder(): void
{
$this->_em->getConfiguration()->addFilter(CompanySQLFilter::class, CompanySQLFilter::class);
$filter = $this->_em->getFilters()->enable(CompanySQLFilter::class);

['companyA' => $companyA, 'companyB' => $companyB] = $this->prepareData();

$getOrderByIdCache = function (int $orderId): Order|null {
return $this->_em->createQueryBuilder()
->select('orderMaster, user')
->from(Order::class, 'orderMaster')
->innerJoin('orderMaster.user', 'user')
->where('orderMaster.id = :orderId')
->setParameter('orderId', $orderId)
->setCacheable(true)
->getQuery()
->setQueryCacheLifetime(10)
->getOneOrNullResult();
};

$filter->setParameter('company', self::COMPANY_A);

$order = $getOrderByIdCache($companyB['orderId']);
self::assertNull($order);

$order = $getOrderByIdCache($companyA['orderId']);

self::assertInstanceOf(Order::class, $order);
self::assertInstanceOf(User::class, $order->user);
self::assertEquals($companyA['userId'], $order->user->id);

$filter->setParameter('company', self::COMPANY_B);

$order = $getOrderByIdCache($companyA['orderId']);
self::assertNull($order);

$order = $getOrderByIdCache($companyB['orderId']);

self::assertInstanceOf(Order::class, $order);
self::assertInstanceOf(User::class, $order->user);
self::assertEquals($companyB['userId'], $order->user->id);
}

private function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();

return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
}
Loading

0 comments on commit 2ff998d

Please sign in to comment.