Compare commits

...

24 Commits
3.4.1 ... 3.4.4

Author SHA1 Message Date
Grégoire Paris
e67fa5388b Merge pull request #12043 from beberlei/Bugfix-DisableNativeLazyLogicException
Only throw PHP 8.4 requirement exception when enabling native lazy objects.
2025-06-30 23:43:15 +02:00
Grégoire Paris
dddcc507ef Merge pull request #12039 from xabbuh/pr-12036
do not register the legacy proxy class name resolver with enabled native lazy ghost
2025-06-30 20:38:34 +02:00
Christian Flothmann
b41d9da88d do not register the legacy proxy class name resolver with enabled native lazy ghost 2025-06-30 19:14:11 +02:00
Benjamin Eberlei
c04bfb78b7 Only throw PHP 8.4 requirement exception when enabling native lazy objects. 2025-06-30 19:01:49 +02:00
Grégoire Paris
ee919d6231 Merge pull request #12030 from greg0ire/test-w-lazy-o
Rework tests and benchmarks to work with lazy objects
2025-06-27 18:13:24 +02:00
Grégoire Paris
04c390693a Merge pull request #12033 from greg0ire/remove-assert
Remove wrong assertion
2025-06-27 15:55:40 +02:00
Gregoire PARIS
8d9e2e7d4e Remove wrong assertion
When using native lazy objects, it is plain wrong.
2025-06-27 14:42:37 +02:00
Grégoire Paris
ef607f26c2 Merge pull request #12031 from doctrine/stof-patch-1
Clean the handling of proxy initialization in the UnitOfWork
2025-06-27 14:14:15 +02:00
Gregoire PARIS
ed543a205c Rework tests and benchmarks to work with lazy objects
These tests and benchmarks are still relevant with lazy objects.
I am not setting up an extra job to test phpbench without native lazy
objects. Instead, I'm bumping the PHP version to 8.4 so that native lazy
objects are in use.
2025-06-27 14:12:58 +02:00
Christophe Coevoet
de1c28bb16 Clean the handling of proxy initialization in the UnitOfWork
Using the VarExporter Hydrator to assign default values of properties when marking an entity as initialized is needed only when using var-exporter proxies.
For lazy objects, this behavior is already provided by `ReflectionClass::markLazyObjectAsInitialized`
2025-06-27 13:58:03 +02:00
Grégoire Paris
b4ca0cd5fb Merge pull request #12024 from greg0ire/3.4.x
Merge 2.20.x up into 3.4.x
2025-06-26 20:51:01 +02:00
Grégoire Paris
a49c1beb93 Merge remote-tracking branch 'origin/2.20.x' into 3.4.x 2025-06-26 20:38:31 +02:00
Grégoire Paris
6307b4fa7d Merge pull request #8012 from sgehrig/bug/#8011-ordering-with-arithmetic-expression
Bug/#8011 ordering with arithmetic expression
2025-06-24 19:50:46 +02:00
Stefan Gehrig
067ad51b3f fixes sqlite sql inconsistency 2025-03-17 08:48:30 +01:00
Stefan Gehrig
00c77213fb fixes codesniffer violation 2025-03-15 09:42:21 +01:00
Stefan Gehrig
c68b8f90b3 adds a test for postgres that uses a HIDDEN result variable for ordering based on arithmetic expression 2025-03-05 09:28:52 +01:00
Stefan Gehrig
aa4f9ce9e9 CS fix based on PHP_CodeSniffer report 2025-03-05 09:22:57 +01:00
Stefan Gehrig
d96fc23327 skips tests when running on postgres 2025-02-27 10:30:21 +01:00
Stefan Gehrig
ec6d1b9f72 fixes whitespace
Signed-off-by: Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
2025-01-07 08:51:19 +01:00
Stefan Gehrig
d809fed52a fixes code sniffer complaints
Signed-off-by: Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
2025-01-07 08:48:42 +01:00
Stefan Gehrig
0e4786dfa8 adds testcases for order by items enclosed in ((...)) (double brackets - just one bracket does not work)
just one bracket (...) gives

Exception : [Doctrine\ORM\Query\QueryException] [Syntax Error] line 0, col xx: Error: Expected Doctrine\ORM\Query\Lexer::T_IDENTIFIER, got '('
2025-01-03 10:45:08 +01:00
Stefan Gehrig
c429262f02 adds detection of literals/result variables at the beginning of an order by item with arithmetic expression
Not sure whether this covers the whole problem regarding complex expressions in order by items but it fixes the provided test cases
2025-01-03 10:45:07 +01:00
Stefan Gehrig
f4fdcbcdcb adds more test cases 2025-01-03 10:44:17 +01:00
Stefan Gehrig
b0806469d5 adds test case for GH issue #8011 2025-01-03 10:44:17 +01:00
9 changed files with 274 additions and 44 deletions

View File

@@ -3450,12 +3450,6 @@ parameters:
count: 1
path: src/UnitOfWork.php
-
message: '#^PHPDoc tag @phpstan\-assert\-if\-true for \$obj contains generic interface Doctrine\\ORM\\Proxy\\InternalProxy but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/UnitOfWork.php
-
message: '#^Parameter \#2 \$assoc of method Doctrine\\ORM\\PersistentCollection\<\(int\|string\),mixed\>\:\:setOwner\(\) expects Doctrine\\ORM\\Mapping\\AssociationMapping&Doctrine\\ORM\\Mapping\\ToManyAssociationMapping, Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping given\.$#'
identifier: argument.type
@@ -3498,6 +3492,12 @@ parameters:
count: 1
path: src/UnitOfWork.php
-
message: '#^Unable to resolve the template type T in call to method static method Symfony\\Component\\VarExporter\\Hydrator\:\:hydrate\(\)$#'
identifier: argument.templateType
count: 1
path: src/UnitOfWork.php
-
message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\:\:\$name\.$#'
identifier: property.notFound

View File

@@ -602,7 +602,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
public function enableNativeLazyObjects(bool $nativeLazyObjects): void
{
if (PHP_VERSION_ID < 80400) {
if (PHP_VERSION_ID < 80400 && $nativeLazyObjects) {
throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.');
}

View File

@@ -66,7 +66,9 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
public function setEntityManager(EntityManagerInterface $em): void
{
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
if (! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
}
$this->em = $em;
}

View File

@@ -1464,7 +1464,7 @@ final class Parser
assert($this->lexer->lookahead !== null);
$expr = match (true) {
$this->isMathOperator($peek) => $this->SimpleArithmeticExpression(),
$this->isMathOperator($peek) || $this->isMathOperator($glimpse) => $this->SimpleArithmeticExpression(),
$glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(),
$this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(),
$this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(),

View File

@@ -2383,9 +2383,9 @@ class UnitOfWork implements PropertyChangedListener
$class->reflClass->markLazyObjectAsInitialized($entity);
} else {
$entity->__setInitialized(true);
}
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
}
} else {
if (
! isset($hints[Query::HINT_REFRESH])
@@ -3050,11 +3050,7 @@ class UnitOfWork implements PropertyChangedListener
}
}
/**
* Tests if a value is an uninitialized entity.
*
* @phpstan-assert-if-true InternalProxy $obj
*/
/** Tests if a value is an uninitialized entity. */
public function isUninitializedObject(mixed $obj): bool
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection) && is_object($obj)) {

View File

@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\Tests\Models\Company\CompanyEmployee;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function count;
/**
* Functional tests for ordering with arithmetic expression.
*/
#[Group('GH8011')]
class GH8011Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('company');
parent::setUp();
$this->generateFixture();
}
private function skipIfPostgres(string $test): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if ($platform instanceof PostgreSQLPlatform) {
self::markTestSkipped(
'The ' . $test . ' test does not work on postgresql (see https://github.com/doctrine/orm/pull/8012).',
);
}
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpression(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + p.id ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY 1 + p.id ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression2(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((1 + p.id)) ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndLiteral(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + 1 ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY s + 1 DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((s + 1)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY 1 + s DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((1 + s)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY s + p.id DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((s + p.id)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndResultVariable(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + s DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariableUsingHiddenResultVariable(): void
{
$dql = 'SELECT p, 1 + p.salary AS HIDDEN _order ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY _order DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function generateFixture(): void
{
$person1 = new CompanyEmployee();
$person1->setName('Benjamin E.');
$person1->setDepartment('IT');
$person1->setSalary(200000);
$person2 = new CompanyEmployee();
$person2->setName('Guilherme B.');
$person2->setDepartment('IT2');
$person2->setSalary(400000);
$this->_em->persist($person1);
$this->_em->persist($person2);
$this->_em->flush();
$this->_em->clear();
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Performance\LazyLoading;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Proxy\InternalProxy as Proxy;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Performance\Mock\NonProxyLoadingEntityManager;
@@ -25,9 +26,11 @@ final class ProxyInitializationTimeBench
/** @var Proxy[] */
private array|null $initializedEmployees = null;
private EntityManager $em;
public function init(): void
{
$proxyFactory = (new NonProxyLoadingEntityManager(EntityManagerFactory::getEntityManager([])))
$proxyFactory = (new NonProxyLoadingEntityManager($this->em = EntityManagerFactory::getEntityManager([])))
->getProxyFactory();
for ($i = 0; $i < 10000; ++$i) {
@@ -36,36 +39,36 @@ final class ProxyInitializationTimeBench
$this->initializedUsers[$i] = $proxyFactory->getProxy(CmsUser::class, ['id' => $i]);
$this->initializedEmployees[$i] = $proxyFactory->getProxy(CmsEmployee::class, ['id' => $i]);
$this->initializedUsers[$i]->__load();
$this->initializedEmployees[$i]->__load();
$this->em->getUnitOfWork()->initializeObject($this->initializedUsers[$i]);
$this->em->getUnitOfWork()->initializeObject($this->initializedEmployees[$i]);
}
}
public function benchCmsUserInitialization(): void
{
foreach ($this->cmsUsers as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
public function benchCmsEmployeeInitialization(): void
{
foreach ($this->cmsEmployees as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
public function benchInitializationOfAlreadyInitializedCmsUsers(): void
{
foreach ($this->initializedUsers as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
public function benchInitializationOfAlreadyInitializedCmsEmployees(): void
{
foreach ($this->initializedEmployees as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
}

View File

@@ -14,8 +14,6 @@ use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\Proxies\__CG__\Doctrine\Tests\Models\CMS\CmsUser as CmsUserProxy;
use function assert;
/**
* Test that Doctrine ORM correctly works with proxy instances exactly like with ordinary Entities
*
@@ -34,10 +32,6 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
{
parent::setUp();
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('This test is not applicable when lazy proxy is enabled.');
}
$this->createSchemaForModels(
CmsUser::class,
CmsTag::class,
@@ -83,8 +77,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
{
$userId = $this->user->getId();
$uninitializedProxy = $this->_em->getReference(CmsUser::class, $userId);
assert($uninitializedProxy instanceof CmsUserProxy);
self::assertInstanceOf(CmsUserProxy::class, $uninitializedProxy);
$this->assertTrue($this->isUninitializedObject($uninitializedProxy));
$this->_em->persist($uninitializedProxy);
$this->_em->flush();
@@ -116,6 +109,10 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
*/
public function testFindWithProxyName(): void
{
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('There is no such thing as a proxy class name when native lazy objects are enabled.');
}
$result = $this->_em->find(CmsUserProxy::class, $this->user->getId());
self::assertSame($this->user->getId(), $result->getId());
$this->_em->clear();

View File

@@ -14,8 +14,7 @@ use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function get_class;
use ReflectionClass;
#[Group('GH10808')]
class GH10808Test extends OrmFunctionalTestCase
@@ -32,10 +31,6 @@ class GH10808Test extends OrmFunctionalTestCase
public function testDQLDeferredEagerLoad(): void
{
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('Test requires lazy loading to be disabled');
}
$appointment = new GH10808Appointment();
$this->_em->persist($appointment);
@@ -55,14 +50,13 @@ class GH10808Test extends OrmFunctionalTestCase
$eagerLoadResult = $query->setHint(UnitOfWork::HINT_DEFEREAGERLOAD, false)->getSingleResult();
self::assertNotEquals(
GH10808AppointmentChild::class,
get_class($deferredLoadResult->child),
'$deferredLoadResult->child should be a proxy',
$reflector = new ReflectionClass(GH10808AppointmentChild::class);
self::assertFalse(
$this->isUninitializedObject($deferredLoadResult->child),
'$deferredLoadResult->child should be a native lazy object',
);
self::assertEquals(
GH10808AppointmentChild::class,
get_class($eagerLoadResult->child),
self::assertFalse(
$this->isUninitializedObject($deferredLoadResult->child),
'$eagerLoadResult->child should not be a proxy',
);
}