Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b9973a7
Introduce PHP 8.4 lazy proxy/ghost API.
beberlei Oct 9, 2024
77754a3
Call setRawValueWithoutLazyInitialization for support with lazy proxy.
beberlei Feb 18, 2025
14bb33a
Merge branch '3.4.x' into ReflectionLazyGhost-3.4.x
beberlei Feb 27, 2025
e2079f9
Refactorings
beberlei Feb 27, 2025
db4cede
Revert test change partially and skip with lazy objects.
beberlei Feb 27, 2025
325beaa
Houskeeping: phpcs
beberlei Feb 27, 2025
f362704
Run with ENABLE_LAZY_PROXY=1 in php 8.4 matrix.
beberlei Feb 27, 2025
dc0f3ac
Fix ci
beberlei Feb 27, 2025
0d54881
Transient properties are not skipping lazy initialization anymore, to…
beberlei Feb 27, 2025
4007d4a
Restore behavior preventing property hook use in 8.4 in unsupported c…
beberlei Feb 27, 2025
65165f7
Add \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE
beberlei Feb 28, 2025
dea73d9
Rename isNativeLazyObjectsEnabled/enableNativeLazyObjects.
beberlei Feb 28, 2025
db057a2
Housekeeping: phpcs
beberlei Feb 28, 2025
6b7a492
Update advanced-configuration docs and make proxy config variables no…
beberlei Mar 1, 2025
e32fc2f
Move code around
beberlei Mar 7, 2025
f957ef3
Merge branch '3.4.x' into ReflectionLazyGhost-3.4.x
beberlei Mar 24, 2025
7fdc4e5
Apply suggestions from code review
beberlei Mar 24, 2025
70f7ac5
Merge remote-tracking branch 'origin/3.4.x' into ReflectionLazyGhost-…
beberlei Mar 24, 2025
df41fa2
Merge remote-tracking branch 'beberlei/ReflectionLazyGhost-3.4.x' int…
beberlei Mar 24, 2025
d2c4729
Merge 3.4.x
beberlei Mar 28, 2025
17a1d87
Pick suggestions
beberlei Mar 28, 2025
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
14 changes: 13 additions & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,27 @@ jobs:
- "pdo_sqlite"
deps:
- "highest"
lazy_proxy:
- "0"
include:
- php-version: "8.2"
dbal-version: "4@dev"
extension: "pdo_sqlite"
lazy_proxy: "0"
- php-version: "8.2"
dbal-version: "4@dev"
extension: "sqlite3"
lazy_proxy: "0"
- php-version: "8.1"
dbal-version: "default"
deps: "lowest"
extension: "pdo_sqlite"
lazy_proxy: "0"
- php-version: "8.4"
dbal-version: "default"
deps: "highest"
extension: "pdo_sqlite"
lazy_proxy: "1"

steps:
- name: "Checkout"
Expand Down Expand Up @@ -83,16 +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 }}

- 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 }}

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


Expand Down
48 changes: 38 additions & 10 deletions docs/en/reference/advanced-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ steps of configuration.

// ...

if ($applicationMode == "development") {
if ($applicationMode === "development") {
$queryCache = new ArrayAdapter();
$metadataCache = new ArrayAdapter();
} else {
Expand All @@ -32,13 +32,18 @@ steps of configuration.
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
$config->setProxyNamespace('MyProject\Proxies');

if ($applicationMode == "development") {
$config->setAutoGenerateProxyClasses(true);
if (PHP_VERSION_ID > 80400) {
$config->enableNativeLazyObjects(true);
} else {
$config->setAutoGenerateProxyClasses(false);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
$config->setProxyNamespace('MyProject\Proxies');

if ($applicationMode === "development") {
$config->setAutoGenerateProxyClasses(true);
} else {
$config->setAutoGenerateProxyClasses(false);
}
}

$connection = DriverManager::getConnection([
Expand Down Expand Up @@ -71,8 +76,25 @@ Configuration Options
The following sections describe all the configuration options
available on a ``Doctrine\ORM\Configuration`` instance.

Proxy Directory (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Native Lazy Objects (**OPTIONAL**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

With PHP 8.4 we recommend that you use native lazy objects instead of
the code generation approach using the ``symfony/var-exporter`` Ghost trait.

With Doctrine 4, the minimal requirement will become PHP 8.4 and native lazy objects
will become the only approach to lazy loading.

.. code-block:: php

<?php
$config->enableNativeLazyObjects(true);

Proxy Directory
~~~~~~~~~~~~~~~

Required except if you use native lazy objects with PHP 8.4.
This setting will be removed in the future.

.. code-block:: php

Expand All @@ -85,8 +107,11 @@ classes. For a detailed explanation on proxy classes and how they
are used in Doctrine, refer to the "Proxy Objects" section further
down.

Proxy Namespace (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxy Namespace
~~~~~~~~~~~~~~~

Required except if you use native lazy objects with PHP 8.4.
This setting will be removed in the future.

.. code-block:: php

Expand Down Expand Up @@ -200,6 +225,9 @@ deprecated ``Doctrine\DBAL\Logging\SQLLogger`` interface.
Auto-generating Proxy Classes (**OPTIONAL**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This setting is not required if you use native lazy objects with PHP 8.4
and will be removed in the future.

Proxy classes can either be generated manually through the Doctrine
Console or automatically at runtime by Doctrine. The configuration
option that controls this behavior is:
Expand Down
8 changes: 1 addition & 7 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ parameters:
path: src/EntityManager.php

-
message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns Doctrine\\ORM\\Proxy\\InternalProxy\.$#'
message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns object\.$#'
identifier: return.type
count: 1
path: src/EntityManager.php
Expand Down Expand Up @@ -2322,12 +2322,6 @@ parameters:
count: 1
path: src/Proxy/ProxyFactory.php

-
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:getProxy\(\) return type with generic interface Doctrine\\ORM\\Proxy\\InternalProxy does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Proxy/ProxyFactory.php

-
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:loadProxyClass\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
Expand Down
16 changes: 16 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
use function is_a;
use function strtolower;

use const PHP_VERSION_ID;

/**
* Configuration container for all configuration options of Doctrine.
* It combines all configuration options from DBAL & ORM.
Expand Down Expand Up @@ -593,6 +595,20 @@
$this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses;
}

public function isNativeLazyObjectsEnabled(): bool
{
return $this->attributes['nativeLazyObjects'] ?? false;
}

public function enableNativeLazyObjects(bool $nativeLazyObjects): void

Check warning on line 603 in src/Configuration.php

View check run for this annotation

Codecov / codecov/patch

src/Configuration.php#L603

Added line #L603 was not covered by tests
{
if (PHP_VERSION_ID < 80400) {
throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.');

Check warning on line 606 in src/Configuration.php

View check run for this annotation

Codecov / codecov/patch

src/Configuration.php#L605-L606

Added lines #L605 - L606 were not covered by tests
}

$this->attributes['nativeLazyObjects'] = $nativeLazyObjects;

Check warning on line 609 in src/Configuration.php

View check run for this annotation

Codecov / codecov/patch

src/Configuration.php#L609

Added line #L609 was not covered by tests
}

/**
* To be deprecated in 3.1.0
*
Expand Down
11 changes: 11 additions & 0 deletions src/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\ReflectionService;
use LogicException;
use ReflectionClass;
use ReflectionException;

Expand All @@ -41,6 +42,8 @@
use function strtolower;
use function substr;

use const PHP_VERSION_ID;

/**
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
* metadata mapping information of a class which describes how a class should be mapped
Expand Down Expand Up @@ -296,6 +299,14 @@
// second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
throw MappingException::noInheritanceOnMappedSuperClass($class->name);
}

foreach ($class->propertyAccessors as $propertyAccessor) {
$property = $propertyAccessor->getUnderlyingReflector();

if (PHP_VERSION_ID >= 80400 && count($property->getHooks()) > 0) {
throw new LogicException('Doctrine ORM does not support property hooks without also enabling Configuration::enableNativeLazyObjects(true). Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.');

Check warning on line 307 in src/Mapping/ClassMetadataFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Mapping/ClassMetadataFactory.php#L307

Added line #L307 was not covered by tests
}
}
}

protected function newClassMetadataInstance(string $className): ClassMetadata
Expand Down
8 changes: 7 additions & 1 deletion src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
namespace Doctrine\ORM\Mapping\PropertyAccessors;

use Doctrine\ORM\Proxy\InternalProxy;
use LogicException;
use ReflectionProperty;

use function ltrim;

use const PHP_VERSION_ID;

/**
* This is a PHP 8.4 and up only class and replaces ObjectCastPropertyAccessor.
*
Expand All @@ -28,12 +31,15 @@

private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
{
if (PHP_VERSION_ID < 80400) {
throw new LogicException('This class requires PHP 8.4 or higher.');

Check warning on line 35 in src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php

View check run for this annotation

Codecov / codecov/patch

src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php#L35

Added line #L35 was not covered by tests
}
}

public function setValue(object $object, mixed $value): void
{
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
$this->reflectionProperty->setRawValue($object, $value);
$this->reflectionProperty->setRawValueWithoutLazyInitialization($object, $value);

return;
}
Expand Down
26 changes: 23 additions & 3 deletions src/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;

Expand Down Expand Up @@ -142,11 +143,11 @@
private readonly string $proxyNs,
bool|int $autoGenerate = self::AUTOGENERATE_NEVER,
) {
if (! $proxyDir) {
if (! $proxyDir && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
throw ORMInvalidArgumentException::proxyDirectoryRequired();
}

if (! $proxyNs) {
if (! $proxyNs && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
throw ORMInvalidArgumentException::proxyNamespaceRequired();
}

Expand All @@ -163,8 +164,23 @@
* @param class-string $className
* @param array<mixed> $identifier
*/
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$classMetadata = $this->em->getClassMetadata($className);
$entityPersister = $this->uow->getEntityPersister($className);

Check warning on line 171 in src/Proxy/ProxyFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Proxy/ProxyFactory.php#L170-L171

Added lines #L170 - L171 were not covered by tests

$proxy = $classMetadata->reflClass->newLazyGhost(static function (object $object) use ($identifier, $entityPersister): void {
$entityPersister->loadById($identifier, $object);
}, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE);

Check warning on line 175 in src/Proxy/ProxyFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Proxy/ProxyFactory.php#L173-L175

Added lines #L173 - L175 were not covered by tests

foreach ($identifier as $idField => $value) {
$classMetadata->propertyAccessors[$idField]->setValue($proxy, $value);

Check warning on line 178 in src/Proxy/ProxyFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Proxy/ProxyFactory.php#L177-L178

Added lines #L177 - L178 were not covered by tests
}

return $proxy;

Check warning on line 181 in src/Proxy/ProxyFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Proxy/ProxyFactory.php#L181

Added line #L181 was not covered by tests
}

$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);

return $proxyFactory($identifier);
Expand All @@ -182,6 +198,10 @@
*/
public function generateProxyClasses(array $classes, string|null $proxyDir = null): int
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
return 0;

Check warning on line 202 in src/Proxy/ProxyFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Proxy/ProxyFactory.php#L202

Added line #L202 was not covered by tests
}

$generated = 0;

foreach ($classes as $class) {
Expand Down
17 changes: 16 additions & 1 deletion src/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -2378,7 +2378,11 @@
}

if ($this->isUninitializedObject($entity)) {
$entity->__setInitialized(true);
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$class->reflClass->markLazyObjectAsInitialized($entity);

Check warning on line 2382 in src/UnitOfWork.php

View check run for this annotation

Codecov / codecov/patch

src/UnitOfWork.php#L2382

Added line #L2382 was not covered by tests
} else {
$entity->__setInitialized(true);
}
} else {
if (
! isset($hints[Query::HINT_REFRESH])
Expand Down Expand Up @@ -3033,6 +3037,13 @@

if ($obj instanceof PersistentCollection) {
$obj->initialize();

return;
}

if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$reflection = $this->em->getClassMetadata($obj::class)->getReflectionClass();
$reflection->initializeLazyObject($obj);

Check warning on line 3046 in src/UnitOfWork.php

View check run for this annotation

Codecov / codecov/patch

src/UnitOfWork.php#L3045-L3046

Added lines #L3045 - L3046 were not covered by tests
}
}

Expand All @@ -3043,6 +3054,10 @@
*/
public function isUninitializedObject(mixed $obj): bool
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection)) {
return $this->em->getClassMetadata($obj::class)->reflClass->isUninitializedLazyObject($obj);

Check warning on line 3058 in src/UnitOfWork.php

View check run for this annotation

Codecov / codecov/patch

src/UnitOfWork.php#L3058

Added line #L3058 was not covered by tests
}

return $obj instanceof InternalProxy && ! $obj->__isInitialized();
}

Expand Down
8 changes: 3 additions & 5 deletions tests/Tests/ORM/Functional/BasicFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\IterableTester;
Expand Down Expand Up @@ -557,7 +556,7 @@ public function testSetToOneAssociationWithGetReference(): void
$this->_em->persist($article);
$this->_em->flush();

self::assertFalse($userRef->__isInitialized());
self::assertTrue($this->isUninitializedObject($userRef));

$this->_em->clear();

Expand Down Expand Up @@ -592,7 +591,7 @@ public function testAddToToManyAssociationWithGetReference(): void
$this->_em->persist($user);
$this->_em->flush();

self::assertFalse($groupRef->__isInitialized());
self::assertTrue($this->isUninitializedObject($groupRef));

$this->_em->clear();

Expand Down Expand Up @@ -940,8 +939,7 @@ public function testManyToOneFetchModeQuery(): void
->setParameter(1, $article->id)
->setFetchMode(CmsArticle::class, 'user', ClassMetadata::FETCH_EAGER)
->getSingleResult();
self::assertInstanceOf(InternalProxy::class, $article->user, 'It IS a proxy, ...');
self::assertFalse($this->isUninitializedObject($article->user), '...but its initialized!');
self::assertFalse($this->isUninitializedObject($article->user));
$this->assertQueryCount(2);
}

Expand Down
Loading