Skip to content

Commit 336a70e

Browse files
authored
Switch proxies to LazyGhostTrait (#2700)
* Generate proxy classes using symfony/var-exporter * Leverage UOW::initializeObject in tests * Trigger lifecycleEventManager from lazy ghost object * Run the full test suite with proxy manager * Fix assert not lazy object * Fix deprecation version and refactor autoregenerate condition * Fix testCreateProxyForDocumentWithUnmappedProperties * Keep a single ProxyManagerConfiguration instance
1 parent d726f05 commit 336a70e

30 files changed

+753
-129
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
- "highest"
3535
symfony-version:
3636
- "stable"
37+
proxy:
38+
- "lazy-ghost"
3739
include:
3840
# Test against lowest dependencies
3941
- dependencies: "lowest"
@@ -42,20 +44,30 @@ jobs:
4244
driver-version: "1.17.0"
4345
topology: "server"
4446
symfony-version: "stable"
47+
proxy: "lazy-ghost"
4548
# Test with highest dependencies
4649
- topology: "server"
4750
php-version: "8.2"
4851
mongodb-version: "7.0"
4952
driver-version: "stable"
5053
dependencies: "highest"
5154
symfony-version: "7"
55+
proxy: "lazy-ghost"
5256
# Test with a 5.0 replica set
5357
- topology: "replica_set"
5458
php-version: "8.2"
5559
mongodb-version: "5.0"
5660
driver-version: "stable"
5761
dependencies: "highest"
5862
symfony-version: "stable"
63+
proxy: "lazy-ghost"
64+
# Test with ProxyManager
65+
- php-version: "8.2"
66+
mongodb-version: "5.0"
67+
driver-version: "stable"
68+
dependencies: "highest"
69+
symfony-version: "stable"
70+
proxy: "proxy-manager"
5971
# Test with a 5.0 sharded cluster
6072
# Currently disabled due to a bug where MongoDB reports "sharding status unknown"
6173
# - topology: "sharded_cluster"
@@ -64,6 +76,7 @@ jobs:
6476
# driver-version: "stable"
6577
# dependencies: "highest"
6678
# symfony-version: "stable"
79+
# proxy: "lazy-ghost"
6780

6881
steps:
6982
- name: "Checkout"
@@ -111,6 +124,13 @@ jobs:
111124
composer require --no-update symfony/var-dumper:^7@dev
112125
composer require --no-update --dev symfony/cache:^7@dev
113126
127+
- name: "Remove proxy-manager-lts"
128+
if: "${{ matrix.proxy != 'proxy-manager' }}"
129+
run: |
130+
# proxy-manager-lts is not installed by default and must not be used
131+
# unless explicitly requested
132+
composer remove --no-update --dev friendsofphp/proxy-manager-lts
133+
114134
- name: "Install dependencies with Composer"
115135
uses: "ramsey/composer-install@v3"
116136
with:
@@ -132,3 +152,4 @@ jobs:
132152
run: "vendor/bin/phpunit"
133153
env:
134154
DOCTRINE_MONGODB_SERVER: ${{ steps.setup-mongodb.outputs.cluster-uri }}
155+
USE_LAZY_GHOST_OBJECTS: ${{ matrix.proxy == 'lazy-ghost' && '1' || '0' }}"

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,20 @@
2828
"doctrine/event-manager": "^1.0 || ^2.0",
2929
"doctrine/instantiator": "^1.1 || ^2",
3030
"doctrine/persistence": "^3.2 || ^4",
31-
"friendsofphp/proxy-manager-lts": "^1.0",
3231
"jean85/pretty-package-versions": "^1.3.0 || ^2.0.1",
3332
"mongodb/mongodb": "^1.17.0",
3433
"psr/cache": "^1.0 || ^2.0 || ^3.0",
3534
"symfony/console": "^5.4 || ^6.0 || ^7.0",
3635
"symfony/deprecation-contracts": "^2.2 || ^3.0",
37-
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0"
36+
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0",
37+
"symfony/var-exporter": "^6.2 || ^7.0"
3838
},
3939
"require-dev": {
4040
"ext-bcmath": "*",
4141
"doctrine/annotations": "^1.12 || ^2.0",
4242
"doctrine/coding-standard": "^12.0",
4343
"doctrine/orm": "^3.2",
44+
"friendsofphp/proxy-manager-lts": "^1.0",
4445
"jmikola/geojson": "^1.0",
4546
"phpbench/phpbench": "^1.0.0",
4647
"phpstan/phpstan": "~1.10.67",

lib/Doctrine/ODM/MongoDB/Configuration.php

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
2525
use Doctrine\Persistence\ObjectRepository;
2626
use InvalidArgumentException;
27+
use LogicException;
2728
use MongoDB\Driver\WriteConcern;
2829
use ProxyManager\Configuration as ProxyManagerConfiguration;
2930
use ProxyManager\Factory\LazyLoadingGhostFactory;
@@ -33,6 +34,7 @@
3334
use ReflectionClass;
3435

3536
use function array_key_exists;
37+
use function class_exists;
3638
use function interface_exists;
3739
use function trigger_deprecation;
3840
use function trim;
@@ -82,12 +84,23 @@ class Configuration
8284
*/
8385
public const AUTOGENERATE_EVAL = 3;
8486

87+
/**
88+
* Autogenerate the proxy class when the proxy file does not exist or
89+
* when the proxied file changed.
90+
*
91+
* This strategy causes a file_exists() call whenever any proxy is used the
92+
* first time in a request. When the proxied file is changed, the proxy will
93+
* be updated.
94+
*/
95+
public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4;
96+
8597
/**
8698
* Array of attributes for this configuration instance.
8799
*
88100
* @phpstan-var array{
89101
* autoGenerateHydratorClasses?: self::AUTOGENERATE_*,
90102
* autoGeneratePersistentCollectionClasses?: self::AUTOGENERATE_*,
103+
* autoGenerateProxyClasses?: self::AUTOGENERATE_*,
91104
* classMetadataFactoryName?: class-string<ClassMetadataFactoryInterface>,
92105
* defaultCommitOptions?: CommitOptions,
93106
* defaultDocumentRepositoryClassName?: class-string<ObjectRepository<object>>,
@@ -106,24 +119,21 @@ class Configuration
106119
* persistentCollectionGenerator?: PersistentCollectionGenerator,
107120
* persistentCollectionDir?: string,
108121
* persistentCollectionNamespace?: string,
122+
* proxyDir?: string,
123+
* proxyNamespace?: string,
109124
* repositoryFactory?: RepositoryFactory
110125
* }
111126
*/
112127
private array $attributes = [];
113128

114129
private ?CacheItemPoolInterface $metadataCache = null;
115130

131+
/** @deprecated */
116132
private ProxyManagerConfiguration $proxyManagerConfiguration;
117133

118-
private int $autoGenerateProxyClasses = self::AUTOGENERATE_EVAL;
119-
120134
private bool $useTransactionalFlush = false;
121135

122-
public function __construct()
123-
{
124-
$this->proxyManagerConfiguration = new ProxyManagerConfiguration();
125-
$this->setAutoGenerateProxyClasses(self::AUTOGENERATE_FILE_NOT_EXISTS);
126-
}
136+
private bool $useLazyGhostObject = true;
127137

128138
/**
129139
* Adds a namespace under a certain alias.
@@ -248,68 +258,52 @@ public function setMetadataCache(CacheItemPoolInterface $cache): void
248258
*/
249259
public function setProxyDir(string $dir): void
250260
{
251-
$this->getProxyManagerConfiguration()->setProxiesTargetDir($dir);
252-
253-
// Recreate proxy generator to ensure its path was updated
254-
if ($this->autoGenerateProxyClasses !== self::AUTOGENERATE_FILE_NOT_EXISTS) {
255-
return;
256-
}
257-
258-
$this->setAutoGenerateProxyClasses($this->autoGenerateProxyClasses);
261+
$this->attributes['proxyDir'] = $dir;
262+
unset($this->proxyManagerConfiguration);
259263
}
260264

261265
/**
262266
* Gets the directory where Doctrine generates any necessary proxy class files.
263267
*/
264268
public function getProxyDir(): ?string
265269
{
266-
return $this->getProxyManagerConfiguration()->getProxiesTargetDir();
270+
return $this->attributes['proxyDir'] ?? null;
267271
}
268272

269273
/**
270274
* Gets an int flag that indicates whether proxy classes should always be regenerated
271275
* during each script execution.
276+
*
277+
* @return self::AUTOGENERATE_*
272278
*/
273279
public function getAutoGenerateProxyClasses(): int
274280
{
275-
return $this->autoGenerateProxyClasses;
281+
return $this->attributes['autoGenerateProxyClasses'] ?? self::AUTOGENERATE_FILE_NOT_EXISTS;
276282
}
277283

278284
/**
279285
* Sets an int flag that indicates whether proxy classes should always be regenerated
280286
* during each script execution.
281287
*
288+
* @param self::AUTOGENERATE_* $mode
289+
*
282290
* @throws InvalidArgumentException If an invalid mode was given.
283291
*/
284292
public function setAutoGenerateProxyClasses(int $mode): void
285293
{
286-
$this->autoGenerateProxyClasses = $mode;
287-
$proxyManagerConfig = $this->getProxyManagerConfiguration();
288-
289-
switch ($mode) {
290-
case self::AUTOGENERATE_FILE_NOT_EXISTS:
291-
$proxyManagerConfig->setGeneratorStrategy(new FileWriterGeneratorStrategy(
292-
new FileLocator($proxyManagerConfig->getProxiesTargetDir()),
293-
));
294-
295-
break;
296-
case self::AUTOGENERATE_EVAL:
297-
$proxyManagerConfig->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
298-
299-
break;
300-
default:
301-
throw new InvalidArgumentException('Invalid proxy generation strategy given - only AUTOGENERATE_FILE_NOT_EXISTS and AUTOGENERATE_EVAL are supported.');
302-
}
294+
$this->attributes['autoGenerateProxyClasses'] = $mode;
295+
unset($this->proxyManagerConfiguration);
303296
}
304297

305298
public function getProxyNamespace(): ?string
306299
{
307-
return $this->getProxyManagerConfiguration()->getProxiesNamespace();
300+
return $this->attributes['proxyNamespace'] ?? null;
308301
}
309302

310303
public function setProxyNamespace(string $ns): void
311304
{
312-
$this->getProxyManagerConfiguration()->setProxiesNamespace($ns);
305+
$this->attributes['proxyNamespace'] = $ns;
306+
unset($this->proxyManagerConfiguration);
313307
}
314308

315309
public function setHydratorDir(string $dir): void
@@ -589,14 +583,39 @@ public function getPersistentCollectionGenerator(): PersistentCollectionGenerato
589583
return $this->attributes['persistentCollectionGenerator'];
590584
}
591585

586+
/** @deprecated */
592587
public function buildGhostObjectFactory(): LazyLoadingGhostFactory
593588
{
594-
return new LazyLoadingGhostFactory(clone $this->getProxyManagerConfiguration());
589+
return new LazyLoadingGhostFactory($this->getProxyManagerConfiguration());
595590
}
596591

592+
/** @deprecated */
597593
public function getProxyManagerConfiguration(): ProxyManagerConfiguration
598594
{
599-
return $this->proxyManagerConfiguration;
595+
if (isset($this->proxyManagerConfiguration)) {
596+
return $this->proxyManagerConfiguration;
597+
}
598+
599+
$proxyManagerConfiguration = new ProxyManagerConfiguration();
600+
$proxyManagerConfiguration->setProxiesTargetDir($this->getProxyDir());
601+
$proxyManagerConfiguration->setProxiesNamespace($this->getProxyNamespace());
602+
603+
switch ($this->getAutoGenerateProxyClasses()) {
604+
case self::AUTOGENERATE_FILE_NOT_EXISTS:
605+
$proxyManagerConfiguration->setGeneratorStrategy(new FileWriterGeneratorStrategy(
606+
new FileLocator($proxyManagerConfiguration->getProxiesTargetDir()),
607+
));
608+
609+
break;
610+
case self::AUTOGENERATE_EVAL:
611+
$proxyManagerConfiguration->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
612+
613+
break;
614+
default:
615+
throw new InvalidArgumentException('Invalid proxy generation strategy given - only AUTOGENERATE_FILE_NOT_EXISTS and AUTOGENERATE_EVAL are supported.');
616+
}
617+
618+
return $this->proxyManagerConfiguration = $proxyManagerConfiguration;
600619
}
601620

602621
public function setUseTransactionalFlush(bool $useTransactionalFlush): void
@@ -608,6 +627,32 @@ public function isTransactionalFlushEnabled(): bool
608627
{
609628
return $this->useTransactionalFlush;
610629
}
630+
631+
/**
632+
* Generate proxy classes using Symfony VarExporter's LazyGhostTrait if true.
633+
* Otherwise, use ProxyManager's LazyLoadingGhostFactory (deprecated)
634+
*/
635+
public function setUseLazyGhostObject(bool $flag): void
636+
{
637+
if ($flag === false) {
638+
if (! class_exists(ProxyManagerConfiguration::class)) {
639+
throw new LogicException('Package "friendsofphp/proxy-manager-lts" is required to disable LazyGhostObject.');
640+
}
641+
642+
trigger_deprecation(
643+
'doctrine/mongodb-odm',
644+
'2.10',
645+
'Using "friendsofphp/proxy-manager-lts" is deprecated. Use "symfony/var-exporter" LazyGhostObjects instead.',
646+
);
647+
}
648+
649+
$this->useLazyGhostObject = $flag;
650+
}
651+
652+
public function isLazyGhostObjectEnabled(): bool
653+
{
654+
return $this->useLazyGhostObject;
655+
}
611656
}
612657

613658
interface_exists(MappingDriver::class);

lib/Doctrine/ODM/MongoDB/DocumentManager.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
1010
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface;
1111
use Doctrine\ODM\MongoDB\Mapping\MappingException;
12+
use Doctrine\ODM\MongoDB\Proxy\Factory\LazyGhostProxyFactory;
1213
use Doctrine\ODM\MongoDB\Proxy\Factory\ProxyFactory;
1314
use Doctrine\ODM\MongoDB\Proxy\Factory\StaticProxyFactory;
1415
use Doctrine\ODM\MongoDB\Proxy\Resolver\CachingClassNameResolver;
1516
use Doctrine\ODM\MongoDB\Proxy\Resolver\ClassNameResolver;
17+
use Doctrine\ODM\MongoDB\Proxy\Resolver\LazyGhostProxyClassNameResolver;
1618
use Doctrine\ODM\MongoDB\Proxy\Resolver\ProxyManagerClassNameResolver;
1719
use Doctrine\ODM\MongoDB\Query\FilterCollection;
1820
use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
@@ -157,7 +159,9 @@ protected function __construct(?Client $client = null, ?Configuration $config =
157159
],
158160
);
159161

160-
$this->classNameResolver = new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config));
162+
$this->classNameResolver = $config->isLazyGhostObjectEnabled()
163+
? new CachingClassNameResolver(new LazyGhostProxyClassNameResolver())
164+
: new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config));
161165

162166
$metadataFactoryClassName = $this->config->getClassMetadataFactoryName();
163167
$this->metadataFactory = new $metadataFactoryClassName();
@@ -182,7 +186,9 @@ protected function __construct(?Client $client = null, ?Configuration $config =
182186

183187
$this->unitOfWork = new UnitOfWork($this, $this->eventManager, $this->hydratorFactory);
184188
$this->schemaManager = new SchemaManager($this, $this->metadataFactory);
185-
$this->proxyFactory = new StaticProxyFactory($this);
189+
$this->proxyFactory = $config->isLazyGhostObjectEnabled()
190+
? new LazyGhostProxyFactory($this, $config->getProxyDir(), $config->getProxyNamespace(), $config->getAutoGenerateProxyClasses())
191+
: new StaticProxyFactory($this);
186192
$this->repositoryFactory = $this->config->getRepositoryFactory();
187193
}
188194

lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs;
1212
use Doctrine\ODM\MongoDB\Events;
1313
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14+
use Doctrine\ODM\MongoDB\Proxy\InternalProxy;
1415
use Doctrine\ODM\MongoDB\Types\Type;
1516
use Doctrine\ODM\MongoDB\UnitOfWork;
1617
use ProxyManager\Proxy\GhostObjectInterface;
@@ -448,6 +449,12 @@ public function hydrate(object $document, array $data, array $hints = []): array
448449
}
449450
}
450451

452+
if ($document instanceof InternalProxy) {
453+
// Skip initialization to not load any object data
454+
$document->__setInitialized(true);
455+
}
456+
457+
// Support for legacy proxy-manager-lts
451458
if ($document instanceof GhostObjectInterface && $document->getProxyInitializer() !== null) {
452459
// Inject an empty initialiser to not load any object data
453460
$document->setProxyInitializer(static function (

0 commit comments

Comments
 (0)