<?php 
 
declare(strict_types=1); 
 
namespace Doctrine\ORM\Proxy; 
 
use Closure; 
use Doctrine\Common\Proxy\AbstractProxyFactory; 
use Doctrine\Common\Proxy\Proxy as CommonProxy; 
use Doctrine\Common\Proxy\ProxyDefinition; 
use Doctrine\Common\Proxy\ProxyGenerator; 
use Doctrine\Deprecations\Deprecation; 
use Doctrine\ORM\EntityManagerInterface; 
use Doctrine\ORM\EntityNotFoundException; 
use Doctrine\ORM\ORMInvalidArgumentException; 
use Doctrine\ORM\Persisters\Entity\EntityPersister; 
use Doctrine\ORM\Proxy\Proxy as LegacyProxy; 
use Doctrine\ORM\UnitOfWork; 
use Doctrine\ORM\Utility\IdentifierFlattener; 
use Doctrine\Persistence\Mapping\ClassMetadata; 
use Doctrine\Persistence\Proxy; 
use ReflectionProperty; 
use Symfony\Component\VarExporter\ProxyHelper; 
use Throwable; 
 
use function array_combine; 
use function array_flip; 
use function array_intersect_key; 
use function bin2hex; 
use function chmod; 
use function class_exists; 
use function dirname; 
use function file_exists; 
use function file_put_contents; 
use function filemtime; 
use function is_bool; 
use function is_dir; 
use function is_int; 
use function is_writable; 
use function ltrim; 
use function mkdir; 
use function preg_match_all; 
use function random_bytes; 
use function rename; 
use function rtrim; 
use function str_replace; 
use function strpos; 
use function strrpos; 
use function strtr; 
use function substr; 
use function ucfirst; 
 
use const DIRECTORY_SEPARATOR; 
use const PHP_VERSION_ID; 
 
/** 
 * This factory is used to create proxy objects for entities at runtime. 
 */ 
class ProxyFactory extends AbstractProxyFactory 
{ 
    /** 
     * Never autogenerate a proxy and rely that it was generated by some 
     * process before deployment. 
     */ 
    public const AUTOGENERATE_NEVER = 0; 
 
    /** 
     * Always generates a new proxy in every request. 
     * 
     * This is only sane during development. 
     */ 
    public const AUTOGENERATE_ALWAYS = 1; 
 
    /** 
     * Autogenerate the proxy class when the proxy file does not exist. 
     * 
     * This strategy causes a file_exists() call whenever any proxy is used the 
     * first time in a request. 
     */ 
    public const AUTOGENERATE_FILE_NOT_EXISTS = 2; 
 
    /** 
     * Generate the proxy classes using eval(). 
     * 
     * This strategy is only sane for development, and even then it gives me 
     * the creeps a little. 
     */ 
    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; 
 
    private const PROXY_CLASS_TEMPLATE = <<<'EOPHP' 
<?php 
 
namespace <namespace>; 
 
/** 
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR 
 */ 
class <proxyShortClassName> extends \<className> implements \<baseProxyInterface> 
{ 
    <useLazyGhostTrait> 
 
    public function __isInitialized(): bool 
    { 
        return isset($this->lazyObjectState) && $this->isLazyObjectInitialized(); 
    } 
 
    public function __serialize(): array 
    { 
        <serializeImpl> 
    } 
} 
 
EOPHP; 
 
    /** @var EntityManagerInterface The EntityManager this factory is bound to. */ 
    private $em; 
 
    /** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */ 
    private $uow; 
 
    /** @var string */ 
    private $proxyDir; 
 
    /** @var string */ 
    private $proxyNs; 
 
    /** @var self::AUTOGENERATE_* */ 
    private $autoGenerate; 
 
    /** 
     * The IdentifierFlattener used for manipulating identifiers 
     * 
     * @var IdentifierFlattener 
     */ 
    private $identifierFlattener; 
 
    /** @var array<class-string, Closure> */ 
    private $proxyFactories = []; 
 
    /** @var bool */ 
    private $isLazyGhostObjectEnabled = true; 
 
    /** 
     * Initializes a new instance of the <tt>ProxyFactory</tt> class that is 
     * connected to the given <tt>EntityManager</tt>. 
     * 
     * @param EntityManagerInterface    $em           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|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes. 
     */ 
    public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = self::AUTOGENERATE_NEVER) 
    { 
        if (! $em->getConfiguration()->isLazyGhostObjectEnabled()) { 
            if (PHP_VERSION_ID >= 80100) { 
                Deprecation::trigger( 
                    'doctrine/orm', 
                    'https://github.com/doctrine/orm/pull/10837/', 
                    'Not enabling lazy ghost objects is deprecated and will not be supported in Doctrine ORM 3.0. Ensure Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true) is called to enable them.' 
                ); 
            } 
 
            $this->isLazyGhostObjectEnabled = false; 
 
            $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs); 
            // @phpstan-ignore classConstant.deprecatedInterface 
            $proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class); 
 
            parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate); 
        } 
 
        if (! $proxyDir) { 
            throw ORMInvalidArgumentException::proxyDirectoryRequired(); 
        } 
 
        if (! $proxyNs) { 
            throw ORMInvalidArgumentException::proxyNamespaceRequired(); 
        } 
 
        if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) { 
            throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate); 
        } 
 
        $this->em                  = $em; 
        $this->uow                 = $em->getUnitOfWork(); 
        $this->proxyDir            = $proxyDir; 
        $this->proxyNs             = $proxyNs; 
        $this->autoGenerate        = (int) $autoGenerate; 
        $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory()); 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getProxy($className, array $identifier) 
    { 
        if (! $this->isLazyGhostObjectEnabled) { 
            return parent::getProxy($className, $identifier); 
        } 
 
        $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, $proxyDir = null) 
    { 
        if (! $this->isLazyGhostObjectEnabled) { 
            return parent::generateProxyClasses($classes, $proxyDir); 
        } 
 
        $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; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @deprecated ProxyFactory::resetUninitializedProxy() is deprecated and will be removed in version 3.0 of doctrine/orm. 
     */ 
    public function resetUninitializedProxy(CommonProxy $proxy) 
    { 
        return parent::resetUninitializedProxy($proxy); 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    protected function skipClass(ClassMetadata $metadata) 
    { 
        return $metadata->isMappedSuperclass 
            || $metadata->isEmbeddedClass 
            || $metadata->getReflectionClass()->isAbstract(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @deprecated ProxyFactory::createProxyDefinition() is deprecated and will be removed in version 3.0 of doctrine/orm. 
     */ 
    protected function createProxyDefinition($className) 
    { 
        $classMetadata   = $this->em->getClassMetadata($className); 
        $entityPersister = $this->uow->getEntityPersister($className); 
 
        $initializer = $this->createInitializer($classMetadata, $entityPersister); 
        $cloner      = $this->createCloner($classMetadata, $entityPersister); 
 
        return new ProxyDefinition( 
            self::generateProxyClassName($className, $this->proxyNs), 
            $classMetadata->getIdentifierFieldNames(), 
            $classMetadata->getReflectionProperties(), 
            $initializer, 
            $cloner 
        ); 
    } 
 
    /** 
     * Creates a closure capable of initializing a proxy 
     * 
     * @deprecated ProxyFactory::createInitializer() is deprecated and will be removed in version 3.0 of doctrine/orm. 
     * 
     * @phpstan-return Closure(CommonProxy):void 
     * 
     * @throws EntityNotFoundException 
     */ 
    private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure 
    { 
        $wakeupProxy = $classMetadata->getReflectionClass()->hasMethod('__wakeup'); 
 
        return function (CommonProxy $proxy) use ($entityPersister, $classMetadata, $wakeupProxy): void { 
            $initializer = $proxy->__getInitializer(); 
            $cloner      = $proxy->__getCloner(); 
 
            $proxy->__setInitializer(null); 
            $proxy->__setCloner(null); 
 
            if ($proxy->__isInitialized()) { 
                return; 
            } 
 
            $properties = $proxy->__getLazyProperties(); 
 
            foreach ($properties as $propertyName => $property) { 
                if (! isset($proxy->$propertyName)) { 
                    $proxy->$propertyName = $properties[$propertyName]; 
                } 
            } 
 
            $proxy->__setInitialized(true); 
 
            if ($wakeupProxy) { 
                $proxy->__wakeup(); 
            } 
 
            $identifier = $classMetadata->getIdentifierValues($proxy); 
 
            try { 
                $entity = $entityPersister->loadById($identifier, $proxy); 
            } catch (Throwable $exception) { 
                $proxy->__setInitializer($initializer); 
                $proxy->__setCloner($cloner); 
                $proxy->__setInitialized(false); 
 
                throw $exception; 
            } 
 
            if ($entity === null) { 
                $proxy->__setInitializer($initializer); 
                $proxy->__setCloner($cloner); 
                $proxy->__setInitialized(false); 
 
                throw EntityNotFoundException::fromClassNameAndIdentifier( 
                    $classMetadata->getName(), 
                    $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier) 
                ); 
            } 
        }; 
    } 
 
    /** 
     * Creates a closure capable of initializing a proxy 
     * 
     * @return Closure(InternalProxy, array):void 
     * 
     * @throws EntityNotFoundException 
     */ 
    private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure 
    { 
        return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void { 
            $original = $entityPersister->loadById($identifier); 
 
            if ($original === null) { 
                throw EntityNotFoundException::fromClassNameAndIdentifier( 
                    $classMetadata->getName(), 
                    $identifierFlattener->flattenIdentifier($classMetadata, $identifier) 
                ); 
            } 
 
            if ($proxy === $original) { 
                return; 
            } 
 
            $class = $entityPersister->getClassMetadata(); 
 
            foreach ($class->getReflectionProperties() as $property) { 
                if (isset($identifier[$property->name])) { 
                    continue; 
                } 
 
                $property->setValue($proxy, $property->getValue($original)); 
            } 
        }; 
    } 
 
    /** 
     * Creates a closure capable of finalizing state a cloned proxy 
     * 
     * @deprecated ProxyFactory::createCloner() is deprecated and will be removed in version 3.0 of doctrine/orm. 
     * 
     * @phpstan-return Closure(CommonProxy):void 
     * 
     * @throws EntityNotFoundException 
     */ 
    private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure 
    { 
        return function (CommonProxy $proxy) use ($entityPersister, $classMetadata): void { 
            if ($proxy->__isInitialized()) { 
                return; 
            } 
 
            $proxy->__setInitialized(true); 
            $proxy->__setInitializer(null); 
 
            $class      = $entityPersister->getClassMetadata(); 
            $identifier = $classMetadata->getIdentifierValues($proxy); 
            $original   = $entityPersister->loadById($identifier); 
 
            if ($original === null) { 
                throw EntityNotFoundException::fromClassNameAndIdentifier( 
                    $classMetadata->getName(), 
                    $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier) 
                ); 
            } 
 
            foreach ($class->getReflectionProperties() as $property) { 
                if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { 
                    continue; 
                } 
 
                $property->setAccessible(true); 
                $property->setValue($proxy, $property->getValue($original)); 
            } 
        }; 
    } 
 
    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'; 
    } 
 
    private function getProxyFactory(string $className): Closure 
    { 
        $skippedProperties = []; 
        $class             = $this->em->getClassMetadata($className); 
        $identifiers       = array_flip($class->getIdentifierFieldNames()); 
        $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() || ! isset($identifiers[$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->getEntityPersister($className); 
        $initializer      = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener); 
        $proxyClassName   = $this->loadProxyClass($class); 
        $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers); 
 
        $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy { 
            $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { 
                $initializer($object, $identifier); 
            }, $skippedProperties); 
 
            foreach ($identifierFields as $idField => $reflector) { 
                if (! isset($identifier[$idField])) { 
                    throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField); 
                } 
 
                $reflector->setValue($proxy, $identifier[$idField]); 
            } 
 
            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 === self::AUTOGENERATE_EVAL) { 
            $this->generateProxyClass($class, null, $proxyClassName); 
 
            return $proxyClassName; 
        } 
 
        $fileName = $this->getProxyFileName($class->getName(), $this->proxyDir); 
 
        switch ($this->autoGenerate) { 
            case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED: 
                if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) { 
                    break; 
                } 
                // no break 
            case self::AUTOGENERATE_FILE_NOT_EXISTS: 
                if (file_exists($fileName)) { 
                    break; 
                } 
                // no break 
            case self::AUTOGENERATE_ALWAYS: 
                $this->generateProxyClass($class, $fileName, $proxyClassName); 
                break; 
        } 
 
        require $fileName; 
 
        return $proxyClassName; 
    } 
 
    private function generateProxyClass(ClassMetadata $class, ?string $fileName, string $proxyClassName): void 
    { 
        $i            = strrpos($proxyClassName, '\\'); 
        $placeholders = [ 
            '<className>' => $class->getName(), 
            '<namespace>' => substr($proxyClassName, 0, $i), 
            '<proxyShortClassName>' => substr($proxyClassName, 1 + $i), 
            '<baseProxyInterface>' => 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)) { 
            throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir); 
        } 
 
        if (! is_writable($parentDirectory)) { 
            throw ORMInvalidArgumentException::proxyDirectoryNotWritable($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 
    { 
        // @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3) 
        $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 __load; 
            setLazyObjectAsInitialized as public __setInitialized; 
            isLazyObjectInitialized as private; 
            createLazyGhost as private; 
            resetLazyObject as private; 
        }'), $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, '\\'); 
    } 
}