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

Add support for PHP 8.4 Lazy Objects RFC with configuration flag #11853

Open
wants to merge 15 commits into
base: 3.4.x
Choose a base branch
from
Open
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
14 changes: 13 additions & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,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 @@ -85,16 +95,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
40 changes: 34 additions & 6 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,9 +76,26 @@ Configuration Options
The following sections describe all the configuration options
available on a ``Doctrine\ORM\Configuration`` instance.

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***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

.. code-block:: php

<?php
Expand All @@ -88,6 +110,9 @@ down.
Proxy Namespace (***REQUIRED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

.. code-block:: php

<?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::setLazyProxyEnabled(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) 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
Loading