Skip to content

Commit 59c6f0c

Browse files
authored
Ensure proxy-manager is not used when native lazy objects are enabled (#2884)
Fix initialization of proxies when hydrating collection with ProxyManager (bug fix). Fix unsetting native lazy object config to not enable symfony lazy ghost objects (bug fix) Add a CI job without optional dependencies to ensure they are not required. Create test with read-only properties with lazy-objects. Not supported by ProxyManager
2 parents 134756f + 673e6dc commit 59c6f0c

File tree

12 files changed

+132
-29
lines changed

12 files changed

+132
-29
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ jobs:
9292
dependencies: "highest"
9393
symfony-version: "stable"
9494
proxy: "lazy-ghost"
95+
# Test removing optional dependencies
96+
- topology: "server"
97+
php-version: "8.4"
98+
mongodb-version: "8.0"
99+
driver-version: "stable"
100+
dependencies: "highest"
101+
symfony-version: "stable"
102+
proxy: "native"
103+
remove-optional-dependencies: true
95104
# Test with a sharded cluster
96105
# Currently disabled due to a bug where MongoDB reports "sharding status unknown"
97106
# - topology: "sharded_cluster"
@@ -148,6 +157,13 @@ jobs:
148157
composer require --no-update symfony/var-dumper:^${{ matrix.symfony-version }}
149158
composer require --no-update --dev symfony/cache:^${{ matrix.symfony-version }}
150159
160+
- name: "Remove optional dependencies"
161+
if: "${{ matrix.remove-optional-dependencies }}"
162+
run: |
163+
composer remove --no-update friendsofphp/proxy-manager-lts symfony/var-exporter
164+
composer remove --no-update --dev symfony/cache doctrine/orm doctrine/annotations
165+
composer remove --no-update --dev doctrine/coding-standard phpstan/phpstan phpstan/phpstan-deprecation-rule phpstan/phpstan-phpunit
166+
151167
- name: "Install dependencies with Composer"
152168
uses: "ramsey/composer-install@v3"
153169
with:

src/Configuration.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,8 @@ public function setUseLazyGhostObject(bool $flag): void
708708

709709
public function isLazyGhostObjectEnabled(): bool
710710
{
711-
return $this->lazyGhostObject;
711+
// Always false if native lazy objects are enabled
712+
return $this->lazyGhostObject && ! $this->nativeLazyObject;
712713
}
713714

714715
public function setUseNativeLazyObject(bool $nativeLazyObject): void
@@ -718,7 +719,6 @@ public function setUseNativeLazyObject(bool $nativeLazyObject): void
718719
}
719720

720721
$this->nativeLazyObject = $nativeLazyObject;
721-
$this->lazyGhostObject = ! $nativeLazyObject || $this->lazyGhostObject;
722722
}
723723

724724
public function isNativeLazyObjectEnabled(): bool

src/DocumentManager.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,23 @@ protected function __construct(?Client $client = null, ?Configuration $config =
154154
$this->config->getDriverOptions(),
155155
);
156156

157-
$this->classNameResolver = $this->config->isLazyGhostObjectEnabled()
158-
? new CachingClassNameResolver(new LazyGhostProxyClassNameResolver())
159-
: new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config));
157+
if ($this->config->isNativeLazyObjectEnabled()) {
158+
$this->classNameResolver = new class implements ClassNameResolver, ProxyClassNameResolver {
159+
public function getRealClass(string $class): string
160+
{
161+
return $class;
162+
}
163+
164+
public function resolveClassName(string $className): string
165+
{
166+
return $className;
167+
}
168+
};
169+
} elseif ($this->config->isLazyGhostObjectEnabled()) {
170+
$this->classNameResolver = new CachingClassNameResolver(new LazyGhostProxyClassNameResolver());
171+
} else {
172+
$this->classNameResolver = new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config));
173+
}
160174

161175
$metadataFactoryClassName = $this->config->getClassMetadataFactoryName();
162176
$this->metadataFactory = new $metadataFactoryClassName();

src/Hydrator/HydratorFactory.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -451,29 +451,18 @@ public function hydrate(object $document, array $data, array $hints = []): array
451451
}
452452
}
453453

454+
// Skip initialization to not load any object data
454455
if (PHP_VERSION_ID >= 80400) {
455456
$metadata->reflClass->markLazyObjectAsInitialized($document);
456457
}
457458

458459
if ($document instanceof InternalProxy) {
459-
// Skip initialization to not load any object data
460460
$document->__setInitialized(true);
461461
}
462462

463463
// Support for legacy proxy-manager-lts
464-
if ($document instanceof GhostObjectInterface && $document->getProxyInitializer() !== null) {
465-
// Inject an empty initialiser to not load any object data
466-
$document->setProxyInitializer(static function (
467-
GhostObjectInterface $ghostObject,
468-
string $method, // we don't care
469-
array $parameters, // we don't care
470-
&$initializer,
471-
array $properties, // we currently do not use this
472-
): bool {
473-
$initializer = null;
474-
475-
return true;
476-
});
464+
if ($document instanceof GhostObjectInterface) {
465+
$document->setProxyInitializer(null);
477466
}
478467

479468
$data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);

src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function setValue(object $object, mixed $value): void
5252
$object->__setInitialized(false);
5353
} elseif ($object instanceof GhostObjectInterface && ! $object->isProxyInitialized()) {
5454
$initializer = $object->getProxyInitializer();
55-
$object->setProxyInitializer();
55+
$object->setProxyInitializer(null);
5656
$this->reflectionProperty->setValue($object, $value);
5757
$object->setProxyInitializer($initializer);
5858
} else {

src/Proxy/Factory/StaticProxyFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private function skippedFieldsFqns(ClassMetadata $metadata): array
146146
$skippedFieldsFqns = [];
147147

148148
foreach ($metadata->getIdentifierFieldNames() as $idField) {
149-
$skippedFieldsFqns[] = $this->propertyFqcn($metadata->getReflectionProperty($idField));
149+
$skippedFieldsFqns[] = $this->propertyFqcn($metadata->getPropertyAccessor($idField)->getUnderlyingReflector());
150150
}
151151

152152
foreach ($metadata->getReflectionClass()->getProperties() as $property) {

tests/Documents/Tag.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Tag
1414
public ?string $id;
1515

1616
#[ODM\Field]
17-
public readonly string $name;
17+
public string $name;
1818

1919
/** @var Collection<int, BlogPost> */
2020
#[ODM\ReferenceMany(targetDocument: BlogPost::class, mappedBy: 'tags')]

tests/Tests/BaseTestCase.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,13 @@ protected static function getConfiguration(): Configuration
105105
$config->setPersistentCollectionNamespace('PersistentCollections');
106106
$config->setDefaultDB(DOCTRINE_MONGODB_DATABASE);
107107
$config->setMetadataDriverImpl(static::createMetadataDriverImpl());
108-
$config->setUseLazyGhostObject((bool) $_ENV['USE_LAZY_GHOST_OBJECT']);
109-
$config->setUseNativeLazyObject((bool) $_ENV['USE_NATIVE_LAZY_OBJECT']);
108+
if ($_ENV['USE_LAZY_GHOST_OBJECT']) {
109+
$config->setUseLazyGhostObject(true);
110+
}
111+
112+
if ($_ENV['USE_NATIVE_LAZY_OBJECT']) {
113+
$config->setUseNativeLazyObject(true);
114+
}
110115

111116
if ($config->isNativeLazyObjectEnabled()) {
112117
NativeLazyObjectFactory::enableTracking();

tests/Tests/ConfigurationTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
use PHPUnit\Framework\Attributes\RequiresPhp;
1414
use PHPUnit\Framework\Attributes\TestWith;
1515
use PHPUnit\Framework\TestCase;
16+
use ProxyManager\Configuration as ProxyManagerConfiguration;
1617
use stdClass;
1718

1819
use function base64_encode;
20+
use function class_exists;
1921
use function str_repeat;
2022

2123
class ConfigurationTest extends TestCase
@@ -38,6 +40,12 @@ public function testUseLazyGhostObject(): void
3840
self::assertFalse($c->isLazyGhostObjectEnabled());
3941
$c->setUseLazyGhostObject(true);
4042
self::assertTrue($c->isLazyGhostObjectEnabled());
43+
44+
if (! class_exists(ProxyManagerConfiguration::class)) {
45+
$this->expectException(LogicException::class);
46+
$this->expectExceptionMessage('Package "friendsofphp/proxy-manager-lts" is required to disable LazyGhostObject.');
47+
}
48+
4149
$c->setUseLazyGhostObject(false);
4250
self::assertFalse($c->isLazyGhostObjectEnabled());
4351
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Functional;
6+
7+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
8+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
9+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
10+
use Doctrine\ODM\MongoDB\Mapping\Annotations\ReferenceOne;
11+
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
12+
13+
class ReadOnlyPropertiesTest extends BaseTestCase
14+
{
15+
public function testReadOnlyDocument(): void
16+
{
17+
$configuration = $this->dm->getConfiguration();
18+
if (! $configuration->isNativeLazyObjectEnabled() && ! $configuration->isLazyGhostObjectEnabled()) {
19+
$this->markTestSkipped('Read-only properties are not supported by the legacy Proxy Manager. https://github.com/FriendsOfPHP/proxy-manager-lts/issues/26');
20+
}
21+
22+
$document = new ReadOnlyProperties('Test Name');
23+
$document->onlyRead = new ReadOnlyProperties('Nested Name');
24+
$this->dm->persist($document);
25+
$this->dm->persist($document->onlyRead);
26+
$this->dm->flush();
27+
$this->dm->clear();
28+
29+
$document = $this->dm->getRepository(ReadOnlyProperties::class)->find($document->id);
30+
$this->assertEquals('Test Name', $document->name);
31+
$this->assertEquals('Nested Name', $document->onlyRead->name);
32+
}
33+
}
34+
35+
#[Document]
36+
class ReadOnlyProperties
37+
{
38+
#[Id]
39+
public readonly string $id; // @phpstan-ignore property.uninitializedReadonly (initialized by reflection)
40+
41+
#[Field]
42+
public readonly string $name;
43+
44+
#[ReferenceOne(targetDocument: self::class)]
45+
public ?self $onlyRead;
46+
47+
public function __construct(string $name)
48+
{
49+
$this->name = $name;
50+
}
51+
}

0 commit comments

Comments
 (0)