diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 244d7faf7..62f56d0e9 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -34,6 +34,8 @@ jobs: - "highest" symfony-version: - "stable" + proxy: + - "lazy-ghost" include: # Test against lowest dependencies - dependencies: "lowest" @@ -42,6 +44,7 @@ jobs: driver-version: "1.17.0" topology: "server" symfony-version: "stable" + proxy: "lazy-ghost" # Test with highest dependencies - topology: "server" php-version: "8.2" @@ -49,6 +52,7 @@ jobs: driver-version: "stable" dependencies: "highest" symfony-version: "7" + proxy: "lazy-ghost" # Test with a 5.0 replica set - topology: "replica_set" php-version: "8.2" @@ -56,6 +60,14 @@ jobs: driver-version: "stable" dependencies: "highest" symfony-version: "stable" + proxy: "lazy-ghost" + # Test with ProxyManager + - php-version: "8.2" + mongodb-version: "5.0" + driver-version: "stable" + dependencies: "highest" + symfony-version: "stable" + proxy: "proxy-manager" # Test with a 5.0 sharded cluster # Currently disabled due to a bug where MongoDB reports "sharding status unknown" # - topology: "sharded_cluster" @@ -64,6 +76,7 @@ jobs: # driver-version: "stable" # dependencies: "highest" # symfony-version: "stable" +# proxy: "lazy-ghost" steps: - name: "Checkout" @@ -111,6 +124,13 @@ jobs: composer require --no-update symfony/var-dumper:^7@dev composer require --no-update --dev symfony/cache:^7@dev + - name: "Remove proxy-manager-lts" + if: "${{ matrix.proxy != 'proxy-manager' }}" + run: | + # proxy-manager-lts is not installed by default and must not be used + # unless explicitly requested + composer remove --no-update --dev friendsofphp/proxy-manager-lts + - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v3" with: @@ -132,3 +152,4 @@ jobs: run: "vendor/bin/phpunit" env: DOCTRINE_MONGODB_SERVER: ${{ steps.setup-mongodb.outputs.cluster-uri }} + USE_LAZY_GHOST_OBJECTS: ${{ matrix.proxy == 'lazy-ghost' && '1' || '0' }}" diff --git a/composer.json b/composer.json index dfc8aa1a1..2ae0368ac 100644 --- a/composer.json +++ b/composer.json @@ -28,19 +28,20 @@ "doctrine/event-manager": "^1.0 || ^2.0", "doctrine/instantiator": "^1.1 || ^2", "doctrine/persistence": "^3.2 || ^4", - "friendsofphp/proxy-manager-lts": "^1.0", "jean85/pretty-package-versions": "^1.3.0 || ^2.0.1", "mongodb/mongodb": "^1.17.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/deprecation-contracts": "^2.2 || ^3.0", - "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0" + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.2 || ^7.0" }, "require-dev": { "ext-bcmath": "*", "doctrine/annotations": "^1.12 || ^2.0", "doctrine/coding-standard": "^12.0", "doctrine/orm": "^3.2", + "friendsofphp/proxy-manager-lts": "^1.0", "jmikola/geojson": "^1.0", "phpbench/phpbench": "^1.0.0", "phpstan/phpstan": "~1.10.67", diff --git a/lib/Doctrine/ODM/MongoDB/Configuration.php b/lib/Doctrine/ODM/MongoDB/Configuration.php index adc486991..ca61fcbfc 100644 --- a/lib/Doctrine/ODM/MongoDB/Configuration.php +++ b/lib/Doctrine/ODM/MongoDB/Configuration.php @@ -24,6 +24,7 @@ use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Persistence\ObjectRepository; use InvalidArgumentException; +use LogicException; use MongoDB\Driver\WriteConcern; use ProxyManager\Configuration as ProxyManagerConfiguration; use ProxyManager\Factory\LazyLoadingGhostFactory; @@ -33,6 +34,7 @@ use ReflectionClass; use function array_key_exists; +use function class_exists; use function interface_exists; use function trigger_deprecation; use function trim; @@ -82,12 +84,23 @@ class Configuration */ public const AUTOGENERATE_EVAL = 3; + /** + * Autogenerate the proxy class when the proxy file does not exist or + * when the proxied file changed. + * + * This strategy causes a file_exists() call whenever any proxy is used the + * first time in a request. When the proxied file is changed, the proxy will + * be updated. + */ + public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4; + /** * Array of attributes for this configuration instance. * * @phpstan-var array{ * autoGenerateHydratorClasses?: self::AUTOGENERATE_*, * autoGeneratePersistentCollectionClasses?: self::AUTOGENERATE_*, + * autoGenerateProxyClasses?: self::AUTOGENERATE_*, * classMetadataFactoryName?: class-string, * defaultCommitOptions?: CommitOptions, * defaultDocumentRepositoryClassName?: class-string>, @@ -106,6 +119,8 @@ class Configuration * persistentCollectionGenerator?: PersistentCollectionGenerator, * persistentCollectionDir?: string, * persistentCollectionNamespace?: string, + * proxyDir?: string, + * proxyNamespace?: string, * repositoryFactory?: RepositoryFactory * } */ @@ -113,17 +128,12 @@ class Configuration private ?CacheItemPoolInterface $metadataCache = null; + /** @deprecated */ private ProxyManagerConfiguration $proxyManagerConfiguration; - private int $autoGenerateProxyClasses = self::AUTOGENERATE_EVAL; - private bool $useTransactionalFlush = false; - public function __construct() - { - $this->proxyManagerConfiguration = new ProxyManagerConfiguration(); - $this->setAutoGenerateProxyClasses(self::AUTOGENERATE_FILE_NOT_EXISTS); - } + private bool $useLazyGhostObject = true; /** * Adds a namespace under a certain alias. @@ -248,14 +258,8 @@ public function setMetadataCache(CacheItemPoolInterface $cache): void */ public function setProxyDir(string $dir): void { - $this->getProxyManagerConfiguration()->setProxiesTargetDir($dir); - - // Recreate proxy generator to ensure its path was updated - if ($this->autoGenerateProxyClasses !== self::AUTOGENERATE_FILE_NOT_EXISTS) { - return; - } - - $this->setAutoGenerateProxyClasses($this->autoGenerateProxyClasses); + $this->attributes['proxyDir'] = $dir; + unset($this->proxyManagerConfiguration); } /** @@ -263,53 +267,43 @@ public function setProxyDir(string $dir): void */ public function getProxyDir(): ?string { - return $this->getProxyManagerConfiguration()->getProxiesTargetDir(); + return $this->attributes['proxyDir'] ?? null; } /** * Gets an int flag that indicates whether proxy classes should always be regenerated * during each script execution. + * + * @return self::AUTOGENERATE_* */ public function getAutoGenerateProxyClasses(): int { - return $this->autoGenerateProxyClasses; + return $this->attributes['autoGenerateProxyClasses'] ?? self::AUTOGENERATE_FILE_NOT_EXISTS; } /** * Sets an int flag that indicates whether proxy classes should always be regenerated * during each script execution. * + * @param self::AUTOGENERATE_* $mode + * * @throws InvalidArgumentException If an invalid mode was given. */ public function setAutoGenerateProxyClasses(int $mode): void { - $this->autoGenerateProxyClasses = $mode; - $proxyManagerConfig = $this->getProxyManagerConfiguration(); - - switch ($mode) { - case self::AUTOGENERATE_FILE_NOT_EXISTS: - $proxyManagerConfig->setGeneratorStrategy(new FileWriterGeneratorStrategy( - new FileLocator($proxyManagerConfig->getProxiesTargetDir()), - )); - - break; - case self::AUTOGENERATE_EVAL: - $proxyManagerConfig->setGeneratorStrategy(new EvaluatingGeneratorStrategy()); - - break; - default: - throw new InvalidArgumentException('Invalid proxy generation strategy given - only AUTOGENERATE_FILE_NOT_EXISTS and AUTOGENERATE_EVAL are supported.'); - } + $this->attributes['autoGenerateProxyClasses'] = $mode; + unset($this->proxyManagerConfiguration); } public function getProxyNamespace(): ?string { - return $this->getProxyManagerConfiguration()->getProxiesNamespace(); + return $this->attributes['proxyNamespace'] ?? null; } public function setProxyNamespace(string $ns): void { - $this->getProxyManagerConfiguration()->setProxiesNamespace($ns); + $this->attributes['proxyNamespace'] = $ns; + unset($this->proxyManagerConfiguration); } public function setHydratorDir(string $dir): void @@ -589,14 +583,39 @@ public function getPersistentCollectionGenerator(): PersistentCollectionGenerato return $this->attributes['persistentCollectionGenerator']; } + /** @deprecated */ public function buildGhostObjectFactory(): LazyLoadingGhostFactory { - return new LazyLoadingGhostFactory(clone $this->getProxyManagerConfiguration()); + return new LazyLoadingGhostFactory($this->getProxyManagerConfiguration()); } + /** @deprecated */ public function getProxyManagerConfiguration(): ProxyManagerConfiguration { - return $this->proxyManagerConfiguration; + if (isset($this->proxyManagerConfiguration)) { + return $this->proxyManagerConfiguration; + } + + $proxyManagerConfiguration = new ProxyManagerConfiguration(); + $proxyManagerConfiguration->setProxiesTargetDir($this->getProxyDir()); + $proxyManagerConfiguration->setProxiesNamespace($this->getProxyNamespace()); + + switch ($this->getAutoGenerateProxyClasses()) { + case self::AUTOGENERATE_FILE_NOT_EXISTS: + $proxyManagerConfiguration->setGeneratorStrategy(new FileWriterGeneratorStrategy( + new FileLocator($proxyManagerConfiguration->getProxiesTargetDir()), + )); + + break; + case self::AUTOGENERATE_EVAL: + $proxyManagerConfiguration->setGeneratorStrategy(new EvaluatingGeneratorStrategy()); + + break; + default: + throw new InvalidArgumentException('Invalid proxy generation strategy given - only AUTOGENERATE_FILE_NOT_EXISTS and AUTOGENERATE_EVAL are supported.'); + } + + return $this->proxyManagerConfiguration = $proxyManagerConfiguration; } public function setUseTransactionalFlush(bool $useTransactionalFlush): void @@ -608,6 +627,32 @@ public function isTransactionalFlushEnabled(): bool { return $this->useTransactionalFlush; } + + /** + * Generate proxy classes using Symfony VarExporter's LazyGhostTrait if true. + * Otherwise, use ProxyManager's LazyLoadingGhostFactory (deprecated) + */ + public function setUseLazyGhostObject(bool $flag): void + { + if ($flag === false) { + if (! class_exists(ProxyManagerConfiguration::class)) { + throw new LogicException('Package "friendsofphp/proxy-manager-lts" is required to disable LazyGhostObject.'); + } + + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.10', + 'Using "friendsofphp/proxy-manager-lts" is deprecated. Use "symfony/var-exporter" LazyGhostObjects instead.', + ); + } + + $this->useLazyGhostObject = $flag; + } + + public function isLazyGhostObjectEnabled(): bool + { + return $this->useLazyGhostObject; + } } interface_exists(MappingDriver::class); diff --git a/lib/Doctrine/ODM/MongoDB/DocumentManager.php b/lib/Doctrine/ODM/MongoDB/DocumentManager.php index e07c2ddad..ab0915c3b 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentManager.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentManager.php @@ -9,10 +9,12 @@ use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\Mapping\MappingException; +use Doctrine\ODM\MongoDB\Proxy\Factory\LazyGhostProxyFactory; use Doctrine\ODM\MongoDB\Proxy\Factory\ProxyFactory; use Doctrine\ODM\MongoDB\Proxy\Factory\StaticProxyFactory; use Doctrine\ODM\MongoDB\Proxy\Resolver\CachingClassNameResolver; use Doctrine\ODM\MongoDB\Proxy\Resolver\ClassNameResolver; +use Doctrine\ODM\MongoDB\Proxy\Resolver\LazyGhostProxyClassNameResolver; use Doctrine\ODM\MongoDB\Proxy\Resolver\ProxyManagerClassNameResolver; use Doctrine\ODM\MongoDB\Query\FilterCollection; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; @@ -157,7 +159,9 @@ protected function __construct(?Client $client = null, ?Configuration $config = ], ); - $this->classNameResolver = new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config)); + $this->classNameResolver = $config->isLazyGhostObjectEnabled() + ? new CachingClassNameResolver(new LazyGhostProxyClassNameResolver()) + : new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config)); $metadataFactoryClassName = $this->config->getClassMetadataFactoryName(); $this->metadataFactory = new $metadataFactoryClassName(); @@ -182,7 +186,9 @@ protected function __construct(?Client $client = null, ?Configuration $config = $this->unitOfWork = new UnitOfWork($this, $this->eventManager, $this->hydratorFactory); $this->schemaManager = new SchemaManager($this, $this->metadataFactory); - $this->proxyFactory = new StaticProxyFactory($this); + $this->proxyFactory = $config->isLazyGhostObjectEnabled() + ? new LazyGhostProxyFactory($this, $config->getProxyDir(), $config->getProxyNamespace(), $config->getAutoGenerateProxyClasses()) + : new StaticProxyFactory($this); $this->repositoryFactory = $this->config->getRepositoryFactory(); } diff --git a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php index 2b91220d5..1e8135b6a 100644 --- a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php @@ -11,6 +11,7 @@ use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs; use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\UnitOfWork; use ProxyManager\Proxy\GhostObjectInterface; @@ -448,6 +449,12 @@ public function hydrate(object $document, array $data, array $hints = []): array } } + if ($document instanceof InternalProxy) { + // Skip initialization to not load any object data + $document->__setInitialized(true); + } + + // Support for legacy proxy-manager-lts if ($document instanceof GhostObjectInterface && $document->getProxyInitializer() !== null) { // Inject an empty initialiser to not load any object data $document->setProxyInitializer(static function ( diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index 45cf542ae..896faf240 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -14,6 +14,7 @@ use Doctrine\ODM\MongoDB\Id\IdGenerator; use Doctrine\ODM\MongoDB\LockException; use Doctrine\ODM\MongoDB\Mapping\Annotations\TimeSeries; +use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Types\Incrementable; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\Types\Versionable; @@ -1893,9 +1894,11 @@ public function getIdentifierObject(object $document) */ public function setFieldValue(object $document, string $field, $value): void { - if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) { + if ($document instanceof InternalProxy && ! $document->__isInitialized()) { //property changes to an uninitialized proxy will not be tracked or persisted, //so the proxy needs to be loaded first. + $document->__load(); + } elseif ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) { $document->initializeProxy(); } @@ -1909,7 +1912,9 @@ public function setFieldValue(object $document, string $field, $value): void */ public function getFieldValue(object $document, string $field) { - if ($document instanceof GhostObjectInterface && $field !== $this->identifier && ! $document->isProxyInitialized()) { + if ($document instanceof InternalProxy && $field !== $this->identifier && ! $document->__isInitialized()) { + $document->__load(); + } elseif ($document instanceof GhostObjectInterface && $field !== $this->identifier && ! $document->isProxyInitialized()) { $document->initializeProxy(); } diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Autoloader.php b/lib/Doctrine/ODM/MongoDB/Proxy/Autoloader.php new file mode 100644 index 000000000..d346a0c19 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Autoloader.php @@ -0,0 +1,90 @@ +; + +/** + * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR + */ +class extends \ implements \ +{ + + + public function __isInitialized(): bool + { + return isset($this->lazyObjectState) && $this->isLazyObjectInitialized(); + } + + public function __serialize(): array + { + + } +} + +EOPHP; + + /** The UnitOfWork this factory uses to retrieve persisters */ + private readonly UnitOfWork $uow; + + /** @var Configuration::AUTOGENERATE_* */ + private int $autoGenerate; + + /** @var array */ + private array $proxyFactories = []; + + private LifecycleEventManager $lifecycleEventManager; + + /** + * Initializes a new instance of the ProxyFactory class that is + * connected to the given EntityManager. + * + * @param DocumentManager $dm The EntityManager the new factory works for. + * @param string $proxyDir The directory to use for the proxy classes. It must exist. + * @param string $proxyNs The namespace to use for the proxy classes. + * @param bool|Configuration::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes. + */ + public function __construct( + private readonly DocumentManager $dm, + private readonly string $proxyDir, + private readonly string $proxyNs, + bool|int $autoGenerate = Configuration::AUTOGENERATE_NEVER, + ) { + if (! $proxyDir) { + throw new InvalidArgumentException('You must configure a proxy directory. See docs for details'); + } + + if (! $proxyNs) { + throw new InvalidArgumentException('You must configure a proxy namespace'); + } + + if (is_int($autoGenerate) && ($autoGenerate < 0 || $autoGenerate > 4)) { + throw new InvalidArgumentException(sprintf('Invalid auto generate mode "%s" given.', is_scalar($autoGenerate) ? (string) $autoGenerate : get_debug_type($autoGenerate))); + } + + $this->uow = $dm->getUnitOfWork(); + $this->autoGenerate = (int) $autoGenerate; + $this->lifecycleEventManager = new LifecycleEventManager($dm, $this->uow, $dm->getEventManager()); + } + + /** @param mixed $identifier */ + public function getProxy(ClassMetadata $metadata, $identifier): InternalProxy + { + $className = $metadata->getName(); + + $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className); + + return $proxyFactory($identifier); + } + + /** + * Generates proxy classes for all given classes. + * + * @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies. + * @param string|null $proxyDir The target directory of the proxy classes. If not specified, the + * directory configured on the Configuration of the EntityManager used + * by this factory is used. + * + * @return int Number of generated proxies. + */ + public function generateProxyClasses(array $classes, string|null $proxyDir = null): int + { + $generated = 0; + + foreach ($classes as $class) { + if ($this->skipClass($class)) { + continue; + } + + $proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir); + $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs); + + $this->generateProxyClass($class, $proxyFileName, $proxyClassName); + + ++$generated; + } + + return $generated; + } + + protected function skipClass(ClassMetadata $metadata): bool + { + return $metadata->isMappedSuperclass + || $metadata->isEmbeddedDocument + || $metadata->getReflectionClass()->isAbstract(); + } + + /** + * Creates a closure capable of initializing a proxy + * + * @param ClassMetadata $classMetadata + * + * @return Closure(InternalProxy&T, array):void + * + * @throws DocumentNotFoundException + * + * @template T of object + */ + private function createLazyInitializer(ClassMetadata $classMetadata, DocumentPersister $persister): Closure + { + $factory = $this; + + return static function (InternalProxy $proxy, mixed $identifier) use ($persister, $classMetadata, $factory): void { + $original = $persister->load([$classMetadata->identifier => $identifier], $proxy); + + if (! $original && ! $factory->lifecycleEventManager->documentNotFound($proxy, $identifier)) { + throw DocumentNotFoundException::documentNotFound($classMetadata->getName(), $identifier); + } + + // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed + if ($proxy instanceof NotifyPropertyChanged) { + $proxy->addPropertyChangedListener($factory->uow); + } + }; + } + + private function getProxyFileName(string $className, string $baseDirectory): string + { + $baseDirectory = $baseDirectory ?: $this->proxyDir; + + return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER + . str_replace('\\', '', $className) . '.php'; + } + + /** @param class-string $className */ + private function getProxyFactory(string $className): Closure + { + $skippedProperties = []; + $class = $this->dm->getClassMetadata($className); + $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; + $reflector = $class->getReflectionClass(); + + while ($reflector) { + foreach ($reflector->getProperties($filter) as $property) { + $name = $property->name; + + if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)))) { + continue; + } + + $prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : ''); + + $skippedProperties[$prefix . $name] = true; + } + + $filter = ReflectionProperty::IS_PRIVATE; + $reflector = $reflector->getParentClass(); + } + + $className = $class->getName(); // aliases and case sensitivity + $entityPersister = $this->uow->getDocumentPersister($className); + $initializer = $this->createLazyInitializer($class, $entityPersister); + $proxyClassName = $this->loadProxyClass($class); + + $proxyFactory = Closure::bind(static function (mixed $identifier) use ($initializer, $skippedProperties, $class): InternalProxy { + /** @see LazyGhostTrait::createLazyGhost() */ + $proxy = static::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { + $initializer($object, $identifier); + }, $skippedProperties); + + $class->setIdentifierValue($proxy, $identifier); + + return $proxy; + }, null, $proxyClassName); + + return $this->proxyFactories[$className] = $proxyFactory; + } + + private function loadProxyClass(ClassMetadata $class): string + { + $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs); + + if (class_exists($proxyClassName, false)) { + return $proxyClassName; + } + + if ($this->autoGenerate === Configuration::AUTOGENERATE_EVAL) { + $this->generateProxyClass($class, null, $proxyClassName); + + return $proxyClassName; + } + + $fileName = $this->getProxyFileName($class->getName(), $this->proxyDir); + + if ( + match ($this->autoGenerate) { + Configuration::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED => ! file_exists($fileName) || filemtime($fileName) < filemtime($class->getReflectionClass()->getFileName()), + Configuration::AUTOGENERATE_FILE_NOT_EXISTS => ! file_exists($fileName), + Configuration::AUTOGENERATE_ALWAYS => true, + Configuration::AUTOGENERATE_NEVER => false, + } + ) { + $this->generateProxyClass($class, $fileName, $proxyClassName); + } + + require $fileName; + + return $proxyClassName; + } + + private function generateProxyClass(ClassMetadata $class, string|null $fileName, string $proxyClassName): void + { + $i = strrpos($proxyClassName, '\\'); + $placeholders = [ + '' => $class->getName(), + '' => substr($proxyClassName, 0, $i), + '' => substr($proxyClassName, 1 + $i), + '' => InternalProxy::class, + ]; + + preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches); + + foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) { + $placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class); + } + + $proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders); + + if (! $fileName) { + if (! class_exists($proxyClassName)) { + eval(substr($proxyCode, 5)); + } + + return; + } + + $parentDirectory = dirname($fileName); + + if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true) || ! is_writable($parentDirectory)) { + throw new InvalidArgumentException(sprintf('Your proxy directory "%s" must be writable', $this->proxyDir)); + } + + $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12)); + + file_put_contents($tmpFileName, $proxyCode); + @chmod($tmpFileName, 0664); + rename($tmpFileName, $fileName); + } + + private function generateUseLazyGhostTrait(ClassMetadata $class): string + { + $code = ProxyHelper::generateLazyGhost($class->getReflectionClass()); + $code = substr($code, 7 + (int) strpos($code, "\n{")); + $code = substr($code, 0, (int) strpos($code, "\n}")); + $code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait { + initializeLazyObject as private; + setLazyObjectAsInitialized as public __setInitialized; + isLazyObjectInitialized as private; + createLazyGhost as private; + resetLazyObject as private; + } + + public function __load(): void + { + $this->initializeLazyObject(); + } + '), $code); + + return $code; + } + + private function generateSerializeImpl(ClassMetadata $class): string + { + $reflector = $class->getReflectionClass(); + $properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this'; + + $code = '$properties = ' . $properties . '; + unset($properties["\0" . self::class . "\0lazyObjectState"]); + + '; + + if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) { + return $code . 'return $properties;'; + } + + return $code . '$data = []; + + foreach (parent::__sleep() as $name) { + $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null; + + if (null === $k) { + trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE); + } else { + $data[$k] = $value; + } + } + + return $data;'; + } + + private static function generateProxyClassName(string $className, string $proxyNamespace): string + { + return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\'); + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/ProxyFactory.php b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/ProxyFactory.php index 1540e40e9..4a8d93aee 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/ProxyFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/ProxyFactory.php @@ -5,7 +5,6 @@ namespace Doctrine\ODM\MongoDB\Proxy\Factory; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; -use ProxyManager\Proxy\GhostObjectInterface; interface ProxyFactory { @@ -19,9 +18,9 @@ public function generateProxyClasses(array $classes): int; * @param mixed $identifier * @phpstan-param ClassMetadata $metadata * - * @return T&GhostObjectInterface + * @return T * * @template T of object */ - public function getProxy(ClassMetadata $metadata, $identifier): GhostObjectInterface; + public function getProxy(ClassMetadata $metadata, $identifier): object; } diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php index e13bbfb14..d3d6d2dc1 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php @@ -22,6 +22,8 @@ /** * This factory is used to create proxy objects for documents at runtime. + * + * @deprecated since 2.10, use LazyGhostProxyFactory instead */ final class StaticProxyFactory implements ProxyFactory { diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/InternalProxy.php b/lib/Doctrine/ODM/MongoDB/Proxy/InternalProxy.php new file mode 100644 index 000000000..90af928ca --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Proxy/InternalProxy.php @@ -0,0 +1,18 @@ + + */ +interface InternalProxy extends Proxy +{ + public function __setInitialized(bool $initialized): void; +} diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/LazyGhostProxyClassNameResolver.php b/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/LazyGhostProxyClassNameResolver.php new file mode 100644 index 000000000..e9bb658cf --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/LazyGhostProxyClassNameResolver.php @@ -0,0 +1,31 @@ +resolveClassName($class); + } + + public function resolveClassName(string $className): string + { + $pos = strrpos($className, '\\' . Proxy::MARKER . '\\'); + + if ($pos === false) { + return $className; + } + + return substr($className, $pos + Proxy::MARKER_LENGTH + 2); + } +} diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index a9fd158a4..a2284b861 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -14,6 +14,7 @@ use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; use Doctrine\ODM\MongoDB\Persisters\CollectionPersister; use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder; +use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Query\Query; use Doctrine\ODM\MongoDB\Types\DateType; use Doctrine\ODM\MongoDB\Types\Type; @@ -970,6 +971,10 @@ private function computeAssociationChanges(object $parentDocument, array $assoc, $class = $this->dm->getClassMetadata($parentDocument::class); $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument); + if ($value instanceof InternalProxy && ! $value->__isInitialized()) { + return; + } + if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) { return; } @@ -2777,7 +2782,14 @@ public function getOrCreateDocument(string $className, array $data, array &$hint $document = $this->identityMap[$class->name][$serializedId]; $oid = spl_object_hash($document); if ($this->isUninitializedObject($document)) { - $document->setProxyInitializer(null); + if ($document instanceof InternalProxy) { + $document->__setInitialized(true); + } elseif ($document instanceof GhostObjectInterface) { + $document->setProxyInitializer(null); + } else { + throw new \RuntimeException(sprintf('Expected uninitialized proxy or ghost object from class "%s"', $document::name)); + } + $overrideLocalValues = true; if ($document instanceof NotifyPropertyChanged) { $document->addPropertyChangedListener($this); @@ -3057,7 +3069,9 @@ public function getScheduledCollectionUpdates(): array */ public function initializeObject(object $obj): void { - if ($obj instanceof GhostObjectInterface && $obj->isProxyInitialized() === false) { + if ($obj instanceof InternalProxy && $obj->__isInitialized() === false) { + $obj->__load(); + } elseif ($obj instanceof GhostObjectInterface && $obj->isProxyInitialized() === false) { $obj->initializeProxy(); } elseif ($obj instanceof PersistentCollectionInterface) { $obj->initialize(); @@ -3072,6 +3086,7 @@ public function initializeObject(object $obj): void public function isUninitializedObject(object $obj): bool { return match (true) { + $obj instanceof InternalProxy => $obj->__isInitialized() === false, $obj instanceof GhostObjectInterface => $obj->isProxyInitialized() === false, $obj instanceof PersistentCollectionInterface => $obj->isInitialized() === false, default => false diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e2c6ab2d8..6a889b8a5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -545,6 +545,11 @@ parameters: count: 1 path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php + - + message: "#^Call to an undefined static method Doctrine\\\\ODM\\\\MongoDB\\\\Proxy\\\\Factory\\\\LazyGhostProxyFactory\\:\\:createLazyGhost\\(\\)\\.$#" + count: 1 + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php + - message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Proxy\\\\Factory\\\\StaticProxyFactory\\:\\:createInitializer\\(\\) should return Closure\\(ProxyManager\\\\Proxy\\\\GhostObjectInterface\\&TDocument\\=, string\\=, array\\\\=, Closure\\|null\\=, array\\\\=\\)\\: bool but returns Closure\\(ProxyManager\\\\Proxy\\\\GhostObjectInterface, string, array, mixed, array\\)\\: true\\.$#" count: 1 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c16a73401..3860390bd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,5 +29,6 @@ + diff --git a/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php b/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php index b24c54c3f..67652d5c2 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; +use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Tests\Query\Filter\Filter; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\Persistence\Mapping\Driver\MappingDriver; @@ -15,6 +16,7 @@ use MongoDB\Driver\Server; use MongoDB\Model\DatabaseInfo; use PHPUnit\Framework\TestCase; +use ProxyManager\Proxy\LazyLoadingInterface; use function array_key_exists; use function array_map; @@ -87,6 +89,7 @@ protected static function getConfiguration(): Configuration $config->setPersistentCollectionNamespace('PersistentCollections'); $config->setDefaultDB(DOCTRINE_MONGODB_DATABASE); $config->setMetadataDriverImpl(static::createMetadataDriverImpl()); + $config->setUseLazyGhostObject((bool) $_ENV['USE_LAZY_GHOST_OBJECTS']); $config->addFilter('testFilter', Filter::class); $config->addFilter('testFilter2', Filter::class); @@ -114,6 +117,11 @@ public static function assertArraySubset(array $subset, array $array, bool $chec } } + public static function isLazyObject(object $document): bool + { + return $document instanceof InternalProxy || $document instanceof LazyLoadingInterface; + } + protected static function createMetadataDriverImpl(): MappingDriver { return AttributeDriver::create(__DIR__ . '/../../../../Documents'); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdentifiersTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdentifiersTest.php index 66cec0181..3994f0a29 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdentifiersTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdentifiersTest.php @@ -7,7 +7,6 @@ use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\Event; use Documents\User; -use ProxyManager\Proxy\LazyLoadingInterface; use function assert; use function get_class; @@ -30,7 +29,7 @@ public function testGetIdentifierValue(): void $userTest = $test->getUser(); self::assertEquals($user->getId(), $userTest->getId()); - self::assertInstanceOf(LazyLoadingInterface::class, $userTest); + self::assertTrue(self::isLazyObject($userTest)); self::assertTrue($this->uow->isUninitializedObject($userTest)); $this->dm->clear(); @@ -42,7 +41,7 @@ public function testGetIdentifierValue(): void $foundUser = $test->getUser(); self::assertEquals($user->getId(), $class->getIdentifierValue($user)); self::assertEquals($user->getId(), $class->getFieldValue($foundUser, 'id')); - self::assertInstanceOf(LazyLoadingInterface::class, $foundUser); + self::assertTrue(self::isLazyObject($foundUser)); self::assertTrue($this->uow->isUninitializedObject($foundUser)); self::assertEquals('jwage', $foundUser->getUsername()); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php index 00468ea54..8017b89f3 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php @@ -33,7 +33,6 @@ use InvalidArgumentException; use MongoDB\Driver\ReadPreference; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; -use ProxyManager\Proxy\GhostObjectInterface; use function assert; use function func_get_args; @@ -95,7 +94,7 @@ public function testPrimeReferencesWithDBRefObjects(): void ->field('groups')->prime(true); foreach ($qb->getQuery() as $user) { - self::assertInstanceOf(GhostObjectInterface::class, $user->getAccount()); + self::assertTrue(self::isLazyObject($user->getAccount())); self::assertFalse($this->uow->isUninitializedObject($user->getAccount())); self::assertCount(2, $user->getGroups()); @@ -104,7 +103,7 @@ public function testPrimeReferencesWithDBRefObjects(): void * initialized, they will not be hydrated as proxy objects. */ foreach ($user->getGroups() as $group) { - self::assertNotInstanceOf(GhostObjectInterface::class, $group); + self::assertFalse(self::isLazyObject($group)); self::assertInstanceOf(Group::class, $group); } } @@ -133,13 +132,13 @@ public function testPrimeReferencesWithSimpleReferences(): void ->field('users')->prime(true); foreach ($qb->getQuery() as $simpleUser) { - self::assertInstanceOf(GhostObjectInterface::class, $simpleUser->getUser()); + self::assertTrue(self::isLazyObject($simpleUser->getUser())); self::assertFalse($this->uow->isUninitializedObject($simpleUser->getUser())); self::assertCount(2, $simpleUser->getUsers()); foreach ($simpleUser->getUsers() as $user) { - self::assertNotInstanceOf(GhostObjectInterface::class, $user); + self::assertFalse(self::isLazyObject($user)); self::assertInstanceOf(User::class, $user); } } @@ -188,20 +187,20 @@ public function testPrimeReferencesNestedInNamedEmbeddedReference(): void ->field('embeddedDocs.referencedDocs')->prime(true); foreach ($qb->getQuery() as $root) { - self::assertNotInstanceOf(GhostObjectInterface::class, $root->embeddedDoc); + self::assertFalse(self::isLazyObject($root->embeddedDoc)); self::assertInstanceOf(EmbeddedWhichReferences::class, $root->embeddedDoc); self::assertCount(2, $root->embeddedDocs); foreach ($root->embeddedDocs as $embeddedDoc) { - self::assertNotInstanceOf(GhostObjectInterface::class, $embeddedDoc); + self::assertFalse(self::isLazyObject($embeddedDoc)); self::assertInstanceOf(EmbeddedWhichReferences::class, $embeddedDoc); - self::assertInstanceOf(GhostObjectInterface::class, $embeddedDoc->referencedDoc); + self::assertTrue(self::isLazyObject($embeddedDoc->referencedDoc)); self::assertFalse($this->uow->isUninitializedObject($embeddedDoc->referencedDoc)); self::assertCount(2, $embeddedDoc->referencedDocs); foreach ($embeddedDoc->referencedDocs as $referencedDoc) { - self::assertNotInstanceOf(GhostObjectInterface::class, $referencedDoc); + self::assertFalse(self::isLazyObject($referencedDoc)); self::assertInstanceOf(Reference::class, $referencedDoc); } } @@ -252,37 +251,37 @@ public function testPrimeReferencesWithDifferentStoreAsReferences(): void assert($referenceUser instanceof ReferenceUser); $user = $referenceUser->getUser(); self::assertInstanceOf(User::class, $user); - self::assertInstanceOf(GhostObjectInterface::class, $user); + self::assertTrue(self::isLazyObject($user)); self::assertFalse($this->uow->isUninitializedObject($user)); self::assertCount(1, $referenceUser->getUsers()); foreach ($referenceUser->getUsers() as $user) { - self::assertNotInstanceOf(GhostObjectInterface::class, $user); + self::assertFalse(self::isLazyObject($user)); self::assertInstanceOf(User::class, $user); } $parentUser = $referenceUser->getParentUser(); - self::assertInstanceOf(GhostObjectInterface::class, $parentUser); + self::assertTrue(self::isLazyObject($parentUser)); self::assertInstanceOf(User::class, $parentUser); self::assertFalse($this->uow->isUninitializedObject($parentUser)); self::assertCount(1, $referenceUser->getParentUsers()); foreach ($referenceUser->getParentUsers() as $user) { - self::assertNotInstanceOf(GhostObjectInterface::class, $user); + self::assertFalse(self::isLazyObject($user)); self::assertInstanceOf(User::class, $user); } $otherUser = $referenceUser->getOtherUser(); self::assertInstanceOf(User::class, $otherUser); - self::assertInstanceOf(GhostObjectInterface::class, $otherUser); + self::assertTrue(self::isLazyObject($otherUser)); self::assertFalse($this->uow->isUninitializedObject($otherUser)); self::assertCount(1, $referenceUser->getOtherUsers()); foreach ($referenceUser->getOtherUsers() as $user) { - self::assertNotInstanceOf(GhostObjectInterface::class, $user); + self::assertFalse(self::isLazyObject($user)); self::assertInstanceOf(User::class, $user); } } @@ -309,10 +308,10 @@ public function testPrimeReferencesWithDiscriminatedReferenceMany(): void foreach ($qb->getQuery() as $user) { $favorites = $user->getFavorites()->toArray(); - self::assertNotInstanceOf(GhostObjectInterface::class, $favorites[0]); + self::assertFalse(self::isLazyObject($favorites[0])); self::assertInstanceOf(Group::class, $favorites[0]); - self::assertNotInstanceOf(GhostObjectInterface::class, $favorites[1]); + self::assertFalse(self::isLazyObject($favorites[1])); self::assertInstanceOf(Project::class, $favorites[1]); } } @@ -331,7 +330,7 @@ public function testPrimeReferencesWithDiscriminatedReferenceOne(): void ->field('server')->prime(true); foreach ($qb->getQuery() as $agent) { - self::assertInstanceOf(GhostObjectInterface::class, $agent->server); + self::assertTrue(self::isLazyObject($agent->server)); self::assertFalse($this->uow->isUninitializedObject($agent->server)); } } @@ -360,7 +359,7 @@ public function testPrimeReferencesIgnoresInitializedProxyObjects(): void self::assertCount(2, $user->getGroups()); foreach ($user->getGroups() as $group) { - self::assertNotInstanceOf(GhostObjectInterface::class, $group); + self::assertFalse(self::isLazyObject($group)); self::assertInstanceOf(Group::class, $group); } } @@ -440,7 +439,7 @@ public function testPrimeReferencesInFindAndModifyResult(): void self::assertCount(1, $user->getGroups()); foreach ($user->getGroups() as $group) { - self::assertNotInstanceOf(GhostObjectInterface::class, $group); + self::assertFalse(self::isLazyObject($group)); self::assertInstanceOf(Group::class, $group); } } @@ -472,7 +471,7 @@ public function testPrimeEmbeddedReferenceOneLevelDeep(): void $phonenumber = $phonenumbers->current(); - self::assertNotInstanceOf(GhostObjectInterface::class, $phonenumber); + self::assertFalse(self::isLazyObject($phonenumber)); self::assertInstanceOf(Phonenumber::class, $phonenumber); } @@ -523,7 +522,7 @@ public function testPrimeEmbeddedReferenceTwoLevelsDeep(): void $currency = $money->getCurrency(); - self::assertInstanceOf(GhostObjectInterface::class, $currency); + self::assertTrue(self::isLazyObject($currency)); self::assertInstanceOf(Currency::class, $currency); self::assertFalse($this->uow->isUninitializedObject($currency)); } @@ -551,7 +550,7 @@ public function testPrimeReferencesInReferenceMany(): void self::assertInstanceOf(BlogPost::class, $post); $comment = $post->comments->first(); - self::assertInstanceOf(GhostObjectInterface::class, $comment->author); + self::assertTrue(self::isLazyObject($comment->author)); self::assertFalse($this->uow->isUninitializedObject($comment->author)); } @@ -578,7 +577,7 @@ public function testPrimeReferencesInReferenceManyWithRepositoryMethodEager(): v self::assertInstanceOf(BlogPost::class, $post); $comment = $post->repoCommentsWithPrimer->first(); - self::assertInstanceOf(GhostObjectInterface::class, $comment->author); + self::assertTrue(self::isLazyObject($comment->author)); self::assertFalse($this->uow->isUninitializedObject($comment->author)); } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencesTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencesTest.php index 10de47590..88cc979f0 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencesTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencesTest.php @@ -22,8 +22,6 @@ use Documents\User; use MongoDB\BSON\Binary; use MongoDB\BSON\ObjectId; -use ProxyManager\Proxy\GhostObjectInterface; -use ProxyManager\Proxy\LazyLoadingInterface; use function assert; @@ -82,7 +80,7 @@ public function testLazyLoadReference(): void assert($profile instanceof Profile); self::assertInstanceOf(Profile::class, $profile); - self::assertInstanceOf(GhostObjectInterface::class, $profile); + self::assertTrue(self::isLazyObject($profile)); $profile->getFirstName(); @@ -104,7 +102,7 @@ public function testLazyLoadedWithNotifyPropertyChanged(): void $user = $this->dm->find($user::class, $user->getId()); $profile = $user->getProfileNotify(); - self::assertInstanceOf(GhostObjectInterface::class, $profile); + self::assertTrue(self::isLazyObject($profile)); self::assertTrue($this->uow->isUninitializedObject($profile)); $user->getProfileNotify()->setLastName('Malarz'); @@ -396,13 +394,13 @@ public function testDocumentNotFoundExceptionWithArrayId(): void ); $test = $this->dm->find($test::class, $test->id); - self::assertInstanceOf(LazyLoadingInterface::class, $test->referenceOne); + self::assertTrue(self::isLazyObject($test->referenceOne)); $this->expectException(DocumentNotFoundException::class); $this->expectExceptionMessage( 'The "Doctrine\ODM\MongoDB\Tests\Functional\DocumentWithArrayId" document with identifier ' . '{"identifier":2} could not be found.', ); - $test->referenceOne->initializeProxy(); + $this->uow->initializeObject($test->referenceOne); } public function testDocumentNotFoundExceptionWithObjectId(): void @@ -429,12 +427,12 @@ public function testDocumentNotFoundExceptionWithObjectId(): void $user = $this->dm->find($user::class, $user->getId()); $profile = $user->getProfile(); - self::assertInstanceOf(LazyLoadingInterface::class, $profile); + self::assertTrue(self::isLazyObject($profile)); $this->expectException(DocumentNotFoundException::class); $this->expectExceptionMessage( 'The "Documents\Profile" document with identifier "abcdefabcdefabcdefabcdef" could not be found.', ); - $profile->initializeProxy(); + $this->uow->initializeObject($profile); } public function testDocumentNotFoundExceptionWithMongoBinDataId(): void @@ -460,13 +458,13 @@ public function testDocumentNotFoundExceptionWithMongoBinDataId(): void ); $test = $this->dm->find($test::class, $test->id); - self::assertInstanceOf(LazyLoadingInterface::class, $test->referenceOne); + self::assertTrue(self::isLazyObject($test->referenceOne)); $this->expectException(DocumentNotFoundException::class); $this->expectExceptionMessage( 'The "Doctrine\ODM\MongoDB\Tests\Functional\DocumentWithMongoBinDataId" document with identifier ' . '"testbindata" could not be found.', ); - $test->referenceOne->initializeProxy(); + $this->uow->initializeObject($test->referenceOne); } public function testDocumentNotFoundEvent(): void @@ -502,8 +500,8 @@ public function testDocumentNotFoundEvent(): void $this->dm->getEventManager()->addEventListener(Events::documentNotFound, new DocumentNotFoundListener($closure)); - self::assertInstanceOf(LazyLoadingInterface::class, $profile); - $profile->initializeProxy(); + self::assertTrue(self::isLazyObject($profile)); + $this->uow->initializeObject($profile); } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/SimpleReferencesTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/SimpleReferencesTest.php index 4d69bff53..c164321f5 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/SimpleReferencesTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/SimpleReferencesTest.php @@ -8,7 +8,6 @@ use Documents\SimpleReferenceUser; use Documents\User; use MongoDB\BSON\ObjectId; -use ProxyManager\Proxy\GhostObjectInterface; use stdClass; use function assert; @@ -84,10 +83,9 @@ public function testProxy(): void self::assertNotNull($test); $user = $test->getUser(); - assert($user instanceof User && $user instanceof GhostObjectInterface); self::assertNotNull($user); self::assertInstanceOf(User::class, $user); - self::assertInstanceOf(GhostObjectInterface::class, $user); + self::assertTrue(self::isLazyObject($user)); self::assertTrue($this->uow->isUninitializedObject($user)); self::assertEquals('jwage', $user->getUsername()); self::assertFalse($this->uow->isUninitializedObject($user)); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH520Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH520Test.php index 6ff634841..bd4406891 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH520Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH520Test.php @@ -8,7 +8,6 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; -use ProxyManager\Proxy\GhostObjectInterface; class GH520Test extends BaseTestCase { @@ -29,7 +28,7 @@ public function testPrimeWithGetSingleResult(): void $document = $query->getSingleResult(); self::assertInstanceOf(GH520Document::class, $document); - self::assertInstanceOf(GhostObjectInterface::class, $document->ref); + self::assertTrue(self::isLazyObject($document->ref)); self::assertFalse($this->uow->isUninitializedObject($document->ref)); } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH593Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH593Test.php index 0248d9fd8..b468dd2a8 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH593Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH593Test.php @@ -9,7 +9,6 @@ use Doctrine\ODM\MongoDB\DocumentNotFoundException; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; -use ProxyManager\Proxy\GhostObjectInterface; use function iterator_to_array; @@ -58,16 +57,16 @@ public function testReferenceManyOwningSidePreparesFilterCriteria(): void */ self::assertCount(2, $user1following); - self::assertInstanceOf(GhostObjectInterface::class, $user1following[0]); + self::assertTrue(self::isLazyObject($user1following[0])); self::assertFalse($this->uow->isUninitializedObject($user1following[0])); self::assertEquals($user2->getId(), $user1following[0]->getId()); - self::assertInstanceOf(GhostObjectInterface::class, $user1following[1]); + self::assertTrue(self::isLazyObject($user1following[1])); self::assertTrue($this->uow->isUninitializedObject($user1following[1])); self::assertEquals($user3->getId(), $user1following[1]->getId()); $this->expectException(DocumentNotFoundException::class); - $user1following[1]->initializeProxy(); + $this->uow->initializeObject($user1following[1]); } public function testReferenceManyInverseSidePreparesFilterCriteria(): void diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH602Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH602Test.php index ce38f44ed..f1e3cfe35 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH602Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH602Test.php @@ -9,7 +9,6 @@ use Doctrine\ODM\MongoDB\DocumentNotFoundException; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; -use ProxyManager\Proxy\GhostObjectInterface; use function iterator_to_array; @@ -49,16 +48,16 @@ public function testReferenceManyOwningSidePreparesFilterCriteriaForDifferentCla */ self::assertCount(2, $user1likes); - self::assertInstanceOf(GhostObjectInterface::class, $user1likes[0]); + self::assertTrue(self::isLazyObject($user1likes[0])); self::assertFalse($this->uow->isUninitializedObject($user1likes[0])); self::assertEquals($thing1->getId(), $user1likes[0]->getId()); - self::assertInstanceOf(GhostObjectInterface::class, $user1likes[1]); + self::assertTrue(self::isLazyObject($user1likes[1])); self::assertTrue($this->uow->isUninitializedObject($user1likes[1])); self::assertEquals($thing2->getId(), $user1likes[1]->getId()); $this->expectException(DocumentNotFoundException::class); - $user1likes[1]->initializeProxy(); + $this->uow->initializeObject($user1likes[1]); } public function testReferenceManyInverseSidePreparesFilterCriteriaForDifferentClass(): void diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php index 0b4e912e8..3895f4325 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php @@ -13,7 +13,6 @@ use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use MongoDB\BSON\Binary; use PHPUnit\Framework\Attributes\DataProvider; -use ProxyManager\Proxy\GhostObjectInterface; class GH852Test extends BaseTestCase { @@ -49,7 +48,7 @@ public function testA(Closure $idGenerator): void self::assertEquals($idGenerator('parent'), $parent->id); self::assertEquals('parent', $parent->name); - self::assertInstanceOf(GhostObjectInterface::class, $parent->refOne); + self::assertTrue(self::isLazyObject($parent->refOne)); self::assertInstanceOf(GH852Document::class, $parent->refOne); self::assertTrue($this->uow->isUninitializedObject($parent->refOne)); self::assertEquals($idGenerator('childA'), $parent->refOne->id); @@ -61,13 +60,13 @@ public function testA(Closure $idGenerator): void /* These proxies will be initialized when we first access the collection * by DocumentPersister::loadReferenceManyCollectionOwningSide(). */ - self::assertInstanceOf(GhostObjectInterface::class, $parent->refMany[0]); + self::assertTrue(self::isLazyObject($parent->refMany[0])); self::assertInstanceOf(GH852Document::class, $parent->refMany[0]); self::assertFalse($this->uow->isUninitializedObject($parent->refMany[0])); self::assertEquals($idGenerator('childB'), $parent->refMany[0]->id); self::assertEquals('childB', $parent->refMany[0]->name); - self::assertInstanceOf(GhostObjectInterface::class, $parent->refMany[1]); + self::assertTrue(self::isLazyObject($parent->refMany[1])); self::assertInstanceOf(GH852Document::class, $parent->refMany[1]); self::assertFalse($this->uow->isUninitializedObject($parent->refMany[1])); self::assertEquals($idGenerator('childC'), $parent->refMany[1]->id); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH936Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH936Test.php index b2eb71704..fd25150b8 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH936Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH936Test.php @@ -8,7 +8,6 @@ use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; -use ProxyManager\Proxy\GhostObjectInterface; class GH936Test extends BaseTestCase { @@ -27,7 +26,7 @@ public function testRemoveCascadesThroughProxyDocuments(): void $foo = $this->dm->find(GH936Document::class, $foo->id); - self::assertInstanceOf(GhostObjectInterface::class, $foo->ref); + self::assertTrue(self::isLazyObject($foo->ref)); $this->dm->remove($foo); $this->dm->flush(); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ViewTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ViewTest.php index 608fc45a4..63ab53512 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ViewTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ViewTest.php @@ -10,7 +10,6 @@ use Documents\CmsUser; use Documents\UserName; use Documents\ViewReference; -use ProxyManager\Proxy\GhostObjectInterface; use function assert; @@ -112,7 +111,7 @@ public function testViewReferences(): void $viewReference = $this->dm->find(ViewReference::class, $alcaeus->getId()); self::assertInstanceOf(ViewReference::class, $viewReference); - self::assertInstanceOf(GhostObjectInterface::class, $viewReference->getReferenceOneView()); + self::assertTrue(self::isLazyObject($viewReference->getReferenceOneView())); self::assertSame($malarzm->getId(), $viewReference->getReferenceOneView()->getId()); // No proxies for inverse referenceOne @@ -120,7 +119,7 @@ public function testViewReferences(): void self::assertSame($alcaeus->getId(), $viewReference->getReferenceOneViewMappedBy()->getId()); self::assertCount(1, $viewReference->getReferenceManyView()); - self::assertInstanceOf(GhostObjectInterface::class, $viewReference->getReferenceManyView()[0]); + self::assertTrue(self::isLazyObject($viewReference->getReferenceManyView()[0])); self::assertSame($malarzm->getId(), $viewReference->getReferenceManyView()[0]->getId()); // No proxies for inverse referenceMany diff --git a/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php b/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php index 30191a6da..0982d629b 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php @@ -11,7 +11,6 @@ use Doctrine\ODM\MongoDB\PersistentCollection; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; use Doctrine\ODM\MongoDB\Query\Query; -use ProxyManager\Proxy\GhostObjectInterface; class HydratorTest extends BaseTestCase { @@ -41,10 +40,10 @@ public function testHydrator(): void self::assertEquals('jon', $user->name); self::assertInstanceOf(DateTime::class, $user->birthdate); self::assertInstanceOf(HydrationClosureReferenceOne::class, $user->referenceOne); - self::assertInstanceOf(GhostObjectInterface::class, $user->referenceOne); + self::assertTrue(self::isLazyObject($user->referenceOne)); self::assertInstanceOf(PersistentCollection::class, $user->referenceMany); - self::assertInstanceOf(GhostObjectInterface::class, $user->referenceMany[0]); - self::assertInstanceOf(GhostObjectInterface::class, $user->referenceMany[1]); + self::assertTrue(self::isLazyObject($user->referenceMany[0])); + self::assertTrue(self::isLazyObject($user->referenceMany[1])); self::assertInstanceOf(HydrationClosureEmbedOne::class, $user->embedOne); self::assertInstanceOf(PersistentCollection::class, $user->embedMany); self::assertEquals('jon', $user->embedOne->name); @@ -54,7 +53,7 @@ public function testHydrator(): void public function testHydrateProxyWithMissingAssociations(): void { $user = $this->dm->getReference(HydrationClosureUser::class, 1); - self::assertInstanceOf(GhostObjectInterface::class, $user); + self::assertTrue(self::isLazyObject($user)); $this->dm->getHydratorFactory()->hydrate($user, [ '_id' => 1, diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php index c8f74769d..cb69ce98c 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php @@ -39,7 +39,6 @@ use InvalidArgumentException; use MongoDB\BSON\Document; use PHPUnit\Framework\Attributes\DataProvider; -use ProxyManager\Proxy\GhostObjectInterface; use ReflectionClass; use ReflectionException; use stdClass; @@ -496,7 +495,7 @@ public function testGetFieldValueInitializesProxy(): void $metadata = $this->dm->getClassMetadata(Album::class); self::assertEquals($document->getName(), $metadata->getFieldValue($proxy, 'name')); - self::assertInstanceOf(GhostObjectInterface::class, $proxy); + self::assertTrue(self::isLazyObject($proxy)); self::assertFalse($this->uow->isUninitializedObject($proxy)); } @@ -511,7 +510,7 @@ public function testGetFieldValueOfIdentifierDoesNotInitializeProxy(): void $metadata = $this->dm->getClassMetadata(Album::class); self::assertEquals($document->getId(), $metadata->getFieldValue($proxy, 'id')); - self::assertInstanceOf(GhostObjectInterface::class, $proxy); + self::assertTrue(self::isLazyObject($proxy)); self::assertTrue($this->uow->isUninitializedObject($proxy)); } @@ -533,7 +532,7 @@ public function testSetFieldValueWithProxy(): void $this->dm->clear(); $proxy = $this->dm->getReference(Album::class, $document->getId()); - self::assertInstanceOf(GhostObjectInterface::class, $proxy); + self::assertTrue(self::isLazyObject($proxy)); $metadata = $this->dm->getClassMetadata(Album::class); $metadata->setFieldValue($proxy, 'name', 'nevermind'); @@ -542,7 +541,7 @@ public function testSetFieldValueWithProxy(): void $this->dm->clear(); $proxy = $this->dm->getReference(Album::class, $document->getId()); - self::assertInstanceOf(GhostObjectInterface::class, $proxy); + self::assertTrue(self::isLazyObject($proxy)); self::assertInstanceOf(Album::class, $proxy); self::assertEquals('nevermind', $proxy->getName()); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/ProxyFactoryTest.php similarity index 81% rename from tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php rename to tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/ProxyFactoryTest.php index 5a1db88dc..16373cfe4 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/ProxyFactoryTest.php @@ -9,6 +9,7 @@ use Doctrine\ODM\MongoDB\Event\DocumentNotFoundEventArgs; use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\LockException; +use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\Cart; use Documents\DocumentWithUnmappedProperties; @@ -18,7 +19,7 @@ use PHPUnit\Framework\MockObject\MockObject; use ProxyManager\Proxy\GhostObjectInterface; -class StaticProxyFactoryTest extends BaseTestCase +class ProxyFactoryTest extends BaseTestCase { /** @var Client|MockObject */ private Client $client; @@ -45,7 +46,7 @@ public function testProxyInitializeWithException(): void $uow = $this->dm->getUnitOfWork(); $proxy = $this->dm->getReference(Cart::class, '123'); - self::assertInstanceOf(GhostObjectInterface::class, $proxy); + self::assertTrue(self::isLazyObject($proxy)); $closure = static function (DocumentNotFoundEventArgs $eventArgs) { self::fail('DocumentNotFoundListener should not be called'); @@ -53,7 +54,7 @@ public function testProxyInitializeWithException(): void $this->dm->getEventManager()->addEventListener(Events::documentNotFound, new DocumentNotFoundListener($closure)); try { - $proxy->initializeProxy(); + $this->uow->initializeObject($proxy); self::fail('An exception should have been thrown'); } catch (LockException $exception) { self::assertInstanceOf(LockException::class, $exception); @@ -61,7 +62,7 @@ public function testProxyInitializeWithException(): void $uow->computeChangeSets(); - self::assertFalse($proxy->isProxyInitialized(), 'Proxy should not be initialized'); + self::assertTrue($this->uow->isUninitializedObject($proxy), 'Proxy should not be initialized'); } public function tearDown(): void @@ -81,10 +82,14 @@ private function createMockedDocumentManager(): DocumentManager public function testCreateProxyForDocumentWithUnmappedProperties(): void { $proxy = $this->dm->getReference(DocumentWithUnmappedProperties::class, '123'); - self::assertInstanceOf(GhostObjectInterface::class, $proxy); + self::assertTrue(self::isLazyObject($proxy)); - // Disable initialiser so we can access properties without initialising the object - $proxy->setProxyInitializer(null); + // Disable initializer so we can access properties without initialising the object + if ($proxy instanceof InternalProxy) { + $proxy->__setInitialized(true); + } elseif ($proxy instanceof GhostObjectInterface) { + $proxy->setProxyInitializer(null); + } self::assertSame('bar', $proxy->foo); } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php index fead82a86..4eabffe4e 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php @@ -28,7 +28,6 @@ use MongoDB\Driver\WriteConcern; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; -use ProxyManager\Proxy\GhostObjectInterface; use ReflectionProperty; use Throwable; @@ -489,7 +488,7 @@ public function testRecomputeChangesetForUninitializedProxyDoesNotCreateChangese $user = $this->dm->find(ForumUser::class, $id); self::assertInstanceOf(ForumUser::class, $user); - self::assertInstanceOf(GhostObjectInterface::class, $user->getAvatar()); + self::assertTrue(self::isLazyObject($user->getAvatar())); $classMetadata = $this->dm->getClassMetadata(ForumAvatar::class);