Skip to content

Merge 3.4.x up into 3.5.x #12004

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

Merged
merged 9 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .doctrine-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@
"slug": "latest",
"upcoming": true
},
{
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"upcoming": true
},
{
"name": "3.4",
"branchName": "3.4.x",
"slug": "3.4",
"upcoming": true
"current": true
},
{
"name": "3.3",
"branchName": "3.3.x",
"slug": "3.3",
"current": true
"maintained": false
},
{
"name": "3.2",
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,27 @@ jobs:
- "pdo_sqlite"
deps:
- "highest"
lazy_proxy:
native_lazy:
- "0"
include:
- php-version: "8.2"
dbal-version: "4@dev"
extension: "pdo_sqlite"
lazy_proxy: "0"
native_lazy: "0"
- php-version: "8.2"
dbal-version: "4@dev"
extension: "sqlite3"
lazy_proxy: "0"
native_lazy: "0"
- php-version: "8.1"
dbal-version: "default"
deps: "lowest"
extension: "pdo_sqlite"
lazy_proxy: "0"
native_lazy: "0"
- php-version: "8.4"
dbal-version: "default"
deps: "highest"
extension: "pdo_sqlite"
lazy_proxy: "1"
native_lazy: "1"

steps:
- name: "Checkout"
Expand Down Expand Up @@ -93,18 +93,18 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage-no-cache.xml"
env:
ENABLE_SECOND_LEVEL_CACHE: 0
ENABLE_LAZY_PROXY: ${{ matrix.lazy_proxy }}
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}

- name: "Run PHPUnit with Second Level Cache"
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml"
env:
ENABLE_SECOND_LEVEL_CACHE: 1
ENABLE_LAZY_PROXY: ${{ matrix.lazy_proxy }}
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}

- name: "Upload coverage file"
uses: "actions/upload-artifact@v4"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.lazy_proxy }}-coverage"
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage"
path: "coverage*.xml"


Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.4.x][3.4] | [3.3.x][3.3] | [2.21.x][2.21] | [2.20.x][2.20] |
| [4.0.x][4.0] | [3.5.x][3.5] | [3.4.x][3.4] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.4 image]][3.4] | [![Build status][3.3 image]][3.3] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
| [![Build status][4.0 image]][4.0] | [![Build status][3.5 image]][3.5] | [![Build status][3.4 image]][3.4] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |

Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
Expand All @@ -20,14 +20,14 @@ without requiring unnecessary code duplication.
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
[3.4 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.4.x
[3.4]: https://github.com/doctrine/orm/tree/3.4.x
[3.4 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.4.x/graph/badge.svg
[3.4 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.4.x
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg
Expand Down
1 change: 1 addition & 0 deletions docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ You can hydrate an entity nested in a DTO :
// CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'}

In a Dto, if you want add all fields of an entity, you can use ``.*`` :

.. code-block:: php

<?php
Expand Down
2 changes: 1 addition & 1 deletion src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -3057,7 +3057,7 @@ public function initializeObject(object $obj): void
*/
public function isUninitializedObject(mixed $obj): bool
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection)) {
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection) && is_object($obj)) {
return $this->em->getClassMetadata($obj::class)->reflClass->isUninitializedLazyObject($obj);
}

Expand Down
16 changes: 6 additions & 10 deletions tests/Tests/ORM/EntityManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhp;
use ReflectionClass;
use ReflectionProperty;
use stdClass;
use Symfony\Component\VarExporter\LazyGhostTrait;
use TypeError;

class EntityManagerTest extends OrmTestCase
Expand Down Expand Up @@ -180,17 +181,12 @@ public function testWrapInTransactionReThrowsThrowables(): void
}

/** Resetting the EntityManager relies on lazy objects until https://github.com/doctrine/orm/issues/5933 is resolved */
#[RequiresPhp('8.4')]
public function testLazyGhostEntityManager(): void
{
$em = new class () extends EntityManager {
use LazyGhostTrait;
$reflector = new ReflectionClass(EntityManager::class);

public function __construct()
{
}
};

$em = $em::createLazyGhost(static function ($em): void {
$em = $reflector->newLazyGhost($initializer = static function (EntityManager $em): void {
$r = new ReflectionProperty(EntityManager::class, 'unitOfWork');
$r->setValue($em, new class () extends UnitOfWork {
public function __construct()
Expand All @@ -207,7 +203,7 @@ public function clear(): void
$em->close();
$this->assertFalse($em->isOpen());

$em->resetLazyObject();
$reflector->resetAsLazyGhost($em, $initializer);
$this->assertTrue($em->isOpen());
}

Expand Down
5 changes: 5 additions & 0 deletions tests/Tests/ORM/Functional/PropertyHooksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\Tests\ORM\Functional;

use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Tests\Models\PropertyHooks\MappingVirtualProperty;
use Doctrine\Tests\Models\PropertyHooks\User;
Expand All @@ -17,6 +18,10 @@ protected function setUp(): void
{
parent::setUp();

if ($this->_em->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
self::markTestSkipped('MySQL/MariaDB is case-insensitive by default, and the logic of this test relies on case sensitivity.');
}

if (! $this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$this->markTestSkipped('Property hooks require native lazy objects to be enabled.');
}
Expand Down
5 changes: 2 additions & 3 deletions tests/Tests/ORM/Hydration/ObjectHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Mocks\ArrayResultFactory;
Expand Down Expand Up @@ -1030,7 +1029,7 @@ public function testCreatesProxyForLazyLoadingWithForeignKeys(): void
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
) extends ProxyFactory {
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
TestCase::assertSame(ECommerceShipping::class, $className);
TestCase::assertSame(['id' => 42], $identifier);
Expand Down Expand Up @@ -1084,7 +1083,7 @@ public function testCreatesProxyForLazyLoadingWithForeignKeysWithAliasedProductE
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
) extends ProxyFactory {
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
TestCase::assertSame(ECommerceShipping::class, $className);
TestCase::assertSame(['id' => 42], $identifier);
Expand Down
12 changes: 12 additions & 0 deletions tests/Tests/ORM/Proxy/ProxyFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ public function testSkipAbstractClassesOnGeneration(): void
#[Group('DDC-2432')]
public function testFailedProxyLoadingDoesNotMarkTheProxyAsInitialized(): void
{
if ($this->emMock->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('This test is not relevant when native lazy objects are enabled');
}

$persister = $this->getMockBuilder(BasicEntityPersister::class)
->onlyMethods(['load'])
->disableOriginalConstructor()
Expand All @@ -146,6 +150,10 @@ public function testFailedProxyLoadingDoesNotMarkTheProxyAsInitialized(): void
#[Group('DDC-2432')]
public function testFailedProxyCloningDoesNotMarkTheProxyAsInitialized(): void
{
if ($this->emMock->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('This test is not relevant when native lazy objects are enabled');
}

$persister = $this->getMockBuilder(BasicEntityPersister::class)
->onlyMethods(['load', 'getClassMetadata'])
->disableOriginalConstructor()
Expand All @@ -172,6 +180,10 @@ public function testFailedProxyCloningDoesNotMarkTheProxyAsInitialized(): void

public function testProxyClonesParentFields(): void
{
if ($this->emMock->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('This test is not relevant when native lazy objects are enabled');
}

$companyEmployee = new CompanyEmployee();
$companyEmployee->setSalary(1000); // A property on the CompanyEmployee
$companyEmployee->setName('Bob'); // A property on the parent class, CompanyPerson
Expand Down
16 changes: 15 additions & 1 deletion tests/Tests/ORM/UnitOfWorkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\Version;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMInvalidArgumentException;
Expand All @@ -40,6 +41,7 @@
use stdClass;

use function enum_exists;
use function is_object;
use function random_int;
use function uniqid;

Expand Down Expand Up @@ -278,7 +280,19 @@ public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationV
$user->username = 'John';
$user->avatar = $invalidValue;

$this->expectException(ORMInvalidArgumentException::class);
if (
is_object($invalidValue) &&
! $invalidValue instanceof ArrayCollection &&
$this->_emMock->getConfiguration()->isNativeLazyObjectsEnabled()
) {
// in the case of stdClass, the changeset is rejected because
// stdClass is not a valid entity
// when using native lazy objects, this happens because UnitOfWork::isUninitializedObject()
// needs to load the class metadata to do its job
$this->expectException(MappingException::class);
} else {
$this->expectException(ORMInvalidArgumentException::class);
}

$this->_unitOfWork->computeChangeSet($metadata, $user);
}
Expand Down
7 changes: 7 additions & 0 deletions tests/Tests/OrmFunctionalTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,13 @@ protected function getEntityManager(

$enableNativeLazyObjects = getenv('ENABLE_NATIVE_LAZY_OBJECTS');

if ($enableNativeLazyObjects === false) {
// If the environment variable is not set, we default to true.
// This is OK because environment variables are always strings, and
// we are comparing it to a boolean.
$enableNativeLazyObjects = true;
}

if (PHP_VERSION_ID >= 80400 && $enableNativeLazyObjects) {
$config->enableNativeLazyObjects(true);
}
Expand Down
17 changes: 17 additions & 0 deletions tests/Tests/TestUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
use function explode;
use function fwrite;
use function get_debug_type;
use function getenv;
use function in_array;
use function sprintf;
use function str_starts_with;
use function strlen;
use function substr;

use const PHP_VERSION_ID;
use const STDERR;

/**
Expand Down Expand Up @@ -89,8 +91,23 @@ public static function getPrivilegedConnection(): DbalExtensions\Connection

public static function configureProxies(Configuration $configuration): void
{
$enableNativeLazyObjects = getenv('ENABLE_NATIVE_LAZY_OBJECTS');

if ($enableNativeLazyObjects === false) {
// If the environment variable is not set, we default to true.
// This is OK because environment variables are always strings, and
// we are comparing it to a boolean.
$enableNativeLazyObjects = true;
}

$configuration->setProxyDir(__DIR__ . '/Proxies');
$configuration->setProxyNamespace('Doctrine\Tests\Proxies');

if (PHP_VERSION_ID >= 80400 && $enableNativeLazyObjects) {
$configuration->enableNativeLazyObjects(true);

return;
}
}

private static function initializeDatabase(): void
Expand Down