<?php declare(strict_types=1); namespace Doctrine\ODM\MongoDB\Proxy\Factory; use Closure; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\DocumentNotFoundException; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager; use Doctrine\Persistence\NotifyPropertyChanged; use ProxyManager\Factory\LazyLoadingGhostFactory; use ProxyManager\Proxy\GhostObjectInterface; use ReflectionProperty; use Throwable; use function array_filter; use function count; /** * This factory is used to create proxy objects for documents at runtime. */ final class StaticProxyFactory implements ProxyFactory { private UnitOfWork $uow; private LifecycleEventManager $lifecycleEventManager; private LazyLoadingGhostFactory $proxyFactory; public function __construct(DocumentManager $documentManager) { $this->uow = $documentManager->getUnitOfWork(); $this->lifecycleEventManager = new LifecycleEventManager($documentManager, $this->uow, $documentManager->getEventManager()); $this->proxyFactory = $documentManager->getConfiguration()->buildGhostObjectFactory(); } /** * @param mixed $identifier * @phpstan-param ClassMetadata<T> $metadata * * @return T&GhostObjectInterface<T> * * @template T of object */ public function getProxy(ClassMetadata $metadata, $identifier): GhostObjectInterface { $documentPersister = $this->uow->getDocumentPersister($metadata->getName()); $ghostObject = $this ->proxyFactory ->createProxy( $metadata->getName(), $this->createInitializer($metadata, $documentPersister), [ 'skippedProperties' => $this->skippedFieldsFqns($metadata), ], ); $metadata->setIdentifierValue($ghostObject, $identifier); return $ghostObject; } public function generateProxyClasses(array $classes): int { $concreteClasses = array_filter($classes, static fn (ClassMetadata $metadata): bool => ! ($metadata->isMappedSuperclass || $metadata->isQueryResultDocument || $metadata->getReflectionClass()->isAbstract())); foreach ($concreteClasses as $metadata) { $this ->proxyFactory ->createProxy( $metadata->getName(), static fn (): bool => true, // empty closure, serves its purpose, for now [ 'skippedProperties' => $this->skippedFieldsFqns($metadata), ], ); } return count($concreteClasses); } /** * @param ClassMetadata<TDocument> $metadata * @param DocumentPersister<TDocument> $documentPersister * * @phpstan-return Closure( * TDocument&GhostObjectInterface<TDocument>=, * string=, * array<string, mixed>=, * ?Closure=, * array<string, mixed>= * ) : bool * * @template TDocument of object */ private function createInitializer( ClassMetadata $metadata, DocumentPersister $documentPersister, ): Closure { return function ( GhostObjectInterface $ghostObject, string $method, // we don't care array $parameters, // we don't care &$initializer, array $properties, // we currently do not use this ) use ( $metadata, $documentPersister, ): bool { $originalInitializer = $initializer; $initializer = null; $identifier = $metadata->getIdentifierValue($ghostObject); try { $document = $documentPersister->load(['_id' => $identifier], $ghostObject); } catch (Throwable $exception) { $initializer = $originalInitializer; throw $exception; } if (! $document) { $initializer = $originalInitializer; if (! $this->lifecycleEventManager->documentNotFound($ghostObject, $identifier)) { throw DocumentNotFoundException::documentNotFound($metadata->getName(), $identifier); } } if ($ghostObject instanceof NotifyPropertyChanged) { $ghostObject->addPropertyChangedListener($this->uow); } return true; }; } /** * @param ClassMetadata<object> $metadata * * @return array<int, string> */ private function skippedFieldsFqns(ClassMetadata $metadata): array { $idFieldFqcns = []; foreach ($metadata->getIdentifierFieldNames() as $idField) { $idFieldFqcns[] = $this->propertyFqcn($metadata->getReflectionProperty($idField)); } return $idFieldFqcns; } private function propertyFqcn(ReflectionProperty $property): string { if ($property->isPrivate()) { return "\0" . $property->getDeclaringClass()->getName() . "\0" . $property->getName(); } if ($property->isProtected()) { return "\0*\0" . $property->getName(); } return $property->getName(); } }