vendor/doctrine/orm/src/Proxy/ProxyFactory.php line 333

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Proxy;
  4. use Closure;
  5. use Doctrine\Common\Proxy\AbstractProxyFactory;
  6. use Doctrine\Common\Proxy\Proxy as CommonProxy;
  7. use Doctrine\Common\Proxy\ProxyDefinition;
  8. use Doctrine\Common\Proxy\ProxyGenerator;
  9. use Doctrine\Deprecations\Deprecation;
  10. use Doctrine\ORM\EntityManagerInterface;
  11. use Doctrine\ORM\EntityNotFoundException;
  12. use Doctrine\ORM\ORMInvalidArgumentException;
  13. use Doctrine\ORM\Persisters\Entity\EntityPersister;
  14. use Doctrine\ORM\Proxy\Proxy as LegacyProxy;
  15. use Doctrine\ORM\UnitOfWork;
  16. use Doctrine\ORM\Utility\IdentifierFlattener;
  17. use Doctrine\Persistence\Mapping\ClassMetadata;
  18. use Doctrine\Persistence\Proxy;
  19. use ReflectionProperty;
  20. use Symfony\Component\VarExporter\ProxyHelper;
  21. use Throwable;
  22. use function array_combine;
  23. use function array_flip;
  24. use function array_intersect_key;
  25. use function bin2hex;
  26. use function chmod;
  27. use function class_exists;
  28. use function dirname;
  29. use function file_exists;
  30. use function file_put_contents;
  31. use function filemtime;
  32. use function is_bool;
  33. use function is_dir;
  34. use function is_int;
  35. use function is_writable;
  36. use function ltrim;
  37. use function mkdir;
  38. use function preg_match_all;
  39. use function random_bytes;
  40. use function rename;
  41. use function rtrim;
  42. use function str_replace;
  43. use function strpos;
  44. use function strrpos;
  45. use function strtr;
  46. use function substr;
  47. use function ucfirst;
  48. use const DIRECTORY_SEPARATOR;
  49. use const PHP_VERSION_ID;
  50. /**
  51.  * This factory is used to create proxy objects for entities at runtime.
  52.  */
  53. class ProxyFactory extends AbstractProxyFactory
  54. {
  55.     /**
  56.      * Never autogenerate a proxy and rely that it was generated by some
  57.      * process before deployment.
  58.      */
  59.     public const AUTOGENERATE_NEVER 0;
  60.     /**
  61.      * Always generates a new proxy in every request.
  62.      *
  63.      * This is only sane during development.
  64.      */
  65.     public const AUTOGENERATE_ALWAYS 1;
  66.     /**
  67.      * Autogenerate the proxy class when the proxy file does not exist.
  68.      *
  69.      * This strategy causes a file_exists() call whenever any proxy is used the
  70.      * first time in a request.
  71.      */
  72.     public const AUTOGENERATE_FILE_NOT_EXISTS 2;
  73.     /**
  74.      * Generate the proxy classes using eval().
  75.      *
  76.      * This strategy is only sane for development, and even then it gives me
  77.      * the creeps a little.
  78.      */
  79.     public const AUTOGENERATE_EVAL 3;
  80.     /**
  81.      * Autogenerate the proxy class when the proxy file does not exist or
  82.      * when the proxied file changed.
  83.      *
  84.      * This strategy causes a file_exists() call whenever any proxy is used the
  85.      * first time in a request. When the proxied file is changed, the proxy will
  86.      * be updated.
  87.      */
  88.     public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED 4;
  89.     private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
  90. <?php
  91. namespace <namespace>;
  92. /**
  93.  * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR
  94.  */
  95. class <proxyShortClassName> extends \<className> implements \<baseProxyInterface>
  96. {
  97.     <useLazyGhostTrait>
  98.     public function __isInitialized(): bool
  99.     {
  100.         return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
  101.     }
  102.     public function __serialize(): array
  103.     {
  104.         <serializeImpl>
  105.     }
  106. }
  107. EOPHP;
  108.     /** @var EntityManagerInterface The EntityManager this factory is bound to. */
  109.     private $em;
  110.     /** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */
  111.     private $uow;
  112.     /** @var string */
  113.     private $proxyDir;
  114.     /** @var string */
  115.     private $proxyNs;
  116.     /** @var self::AUTOGENERATE_* */
  117.     private $autoGenerate;
  118.     /**
  119.      * The IdentifierFlattener used for manipulating identifiers
  120.      *
  121.      * @var IdentifierFlattener
  122.      */
  123.     private $identifierFlattener;
  124.     /** @var array<class-string, Closure> */
  125.     private $proxyFactories = [];
  126.     /** @var bool */
  127.     private $isLazyGhostObjectEnabled true;
  128.     /**
  129.      * Initializes a new instance of the <tt>ProxyFactory</tt> class that is
  130.      * connected to the given <tt>EntityManager</tt>.
  131.      *
  132.      * @param EntityManagerInterface    $em           The EntityManager the new factory works for.
  133.      * @param string                    $proxyDir     The directory to use for the proxy classes. It must exist.
  134.      * @param string                    $proxyNs      The namespace to use for the proxy classes.
  135.      * @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes.
  136.      */
  137.     public function __construct(EntityManagerInterface $em$proxyDir$proxyNs$autoGenerate self::AUTOGENERATE_NEVER)
  138.     {
  139.         if (! $em->getConfiguration()->isLazyGhostObjectEnabled()) {
  140.             if (PHP_VERSION_ID >= 80100) {
  141.                 Deprecation::trigger(
  142.                     'doctrine/orm',
  143.                     'https://github.com/doctrine/orm/pull/10837/',
  144.                     '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.'
  145.                 );
  146.             }
  147.             $this->isLazyGhostObjectEnabled false;
  148.             $proxyGenerator = new ProxyGenerator($proxyDir$proxyNs);
  149.             // @phpstan-ignore classConstant.deprecatedInterface
  150.             $proxyGenerator->setPlaceholder('baseProxyInterface'LegacyProxy::class);
  151.             parent::__construct($proxyGenerator$em->getMetadataFactory(), $autoGenerate);
  152.         }
  153.         if (! $proxyDir) {
  154.             throw ORMInvalidArgumentException::proxyDirectoryRequired();
  155.         }
  156.         if (! $proxyNs) {
  157.             throw ORMInvalidArgumentException::proxyNamespaceRequired();
  158.         }
  159.         if (is_int($autoGenerate) ? $autoGenerate || $autoGenerate : ! is_bool($autoGenerate)) {
  160.             throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
  161.         }
  162.         $this->em                  $em;
  163.         $this->uow                 $em->getUnitOfWork();
  164.         $this->proxyDir            $proxyDir;
  165.         $this->proxyNs             $proxyNs;
  166.         $this->autoGenerate        = (int) $autoGenerate;
  167.         $this->identifierFlattener = new IdentifierFlattener($this->uow$em->getMetadataFactory());
  168.     }
  169.     /**
  170.      * {@inheritDoc}
  171.      */
  172.     public function getProxy($className, array $identifier)
  173.     {
  174.         if (! $this->isLazyGhostObjectEnabled) {
  175.             return parent::getProxy($className$identifier);
  176.         }
  177.         $proxyFactory $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
  178.         return $proxyFactory($identifier);
  179.     }
  180.     /**
  181.      * Generates proxy classes for all given classes.
  182.      *
  183.      * @param ClassMetadata[] $classes  The classes (ClassMetadata instances) for which to generate proxies.
  184.      * @param string|null     $proxyDir The target directory of the proxy classes. If not specified, the
  185.      *                                  directory configured on the Configuration of the EntityManager used
  186.      *                                  by this factory is used.
  187.      *
  188.      * @return int Number of generated proxies.
  189.      */
  190.     public function generateProxyClasses(array $classes$proxyDir null)
  191.     {
  192.         if (! $this->isLazyGhostObjectEnabled) {
  193.             return parent::generateProxyClasses($classes$proxyDir);
  194.         }
  195.         $generated 0;
  196.         foreach ($classes as $class) {
  197.             if ($this->skipClass($class)) {
  198.                 continue;
  199.             }
  200.             $proxyFileName  $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
  201.             $proxyClassName self::generateProxyClassName($class->getName(), $this->proxyNs);
  202.             $this->generateProxyClass($class$proxyFileName$proxyClassName);
  203.             ++$generated;
  204.         }
  205.         return $generated;
  206.     }
  207.     /**
  208.      * {@inheritDoc}
  209.      *
  210.      * @deprecated ProxyFactory::resetUninitializedProxy() is deprecated and will be removed in version 3.0 of doctrine/orm.
  211.      */
  212.     public function resetUninitializedProxy(CommonProxy $proxy)
  213.     {
  214.         return parent::resetUninitializedProxy($proxy);
  215.     }
  216.     /**
  217.      * {@inheritDoc}
  218.      */
  219.     protected function skipClass(ClassMetadata $metadata)
  220.     {
  221.         return $metadata->isMappedSuperclass
  222.             || $metadata->isEmbeddedClass
  223.             || $metadata->getReflectionClass()->isAbstract();
  224.     }
  225.     /**
  226.      * {@inheritDoc}
  227.      *
  228.      * @deprecated ProxyFactory::createProxyDefinition() is deprecated and will be removed in version 3.0 of doctrine/orm.
  229.      */
  230.     protected function createProxyDefinition($className)
  231.     {
  232.         $classMetadata   $this->em->getClassMetadata($className);
  233.         $entityPersister $this->uow->getEntityPersister($className);
  234.         $initializer $this->createInitializer($classMetadata$entityPersister);
  235.         $cloner      $this->createCloner($classMetadata$entityPersister);
  236.         return new ProxyDefinition(
  237.             self::generateProxyClassName($className$this->proxyNs),
  238.             $classMetadata->getIdentifierFieldNames(),
  239.             $classMetadata->getReflectionProperties(),
  240.             $initializer,
  241.             $cloner
  242.         );
  243.     }
  244.     /**
  245.      * Creates a closure capable of initializing a proxy
  246.      *
  247.      * @deprecated ProxyFactory::createInitializer() is deprecated and will be removed in version 3.0 of doctrine/orm.
  248.      *
  249.      * @psalm-return Closure(CommonProxy):void
  250.      *
  251.      * @throws EntityNotFoundException
  252.      */
  253.     private function createInitializer(ClassMetadata $classMetadataEntityPersister $entityPersister): Closure
  254.     {
  255.         $wakeupProxy $classMetadata->getReflectionClass()->hasMethod('__wakeup');
  256.         return function (CommonProxy $proxy) use ($entityPersister$classMetadata$wakeupProxy): void {
  257.             $initializer $proxy->__getInitializer();
  258.             $cloner      $proxy->__getCloner();
  259.             $proxy->__setInitializer(null);
  260.             $proxy->__setCloner(null);
  261.             if ($proxy->__isInitialized()) {
  262.                 return;
  263.             }
  264.             $properties $proxy->__getLazyProperties();
  265.             foreach ($properties as $propertyName => $property) {
  266.                 if (! isset($proxy->$propertyName)) {
  267.                     $proxy->$propertyName $properties[$propertyName];
  268.                 }
  269.             }
  270.             $proxy->__setInitialized(true);
  271.             if ($wakeupProxy) {
  272.                 $proxy->__wakeup();
  273.             }
  274.             $identifier $classMetadata->getIdentifierValues($proxy);
  275.             try {
  276.                 $entity $entityPersister->loadById($identifier$proxy);
  277.             } catch (Throwable $exception) {
  278.                 $proxy->__setInitializer($initializer);
  279.                 $proxy->__setCloner($cloner);
  280.                 $proxy->__setInitialized(false);
  281.                 throw $exception;
  282.             }
  283.             if ($entity === null) {
  284.                 $proxy->__setInitializer($initializer);
  285.                 $proxy->__setCloner($cloner);
  286.                 $proxy->__setInitialized(false);
  287.                 throw EntityNotFoundException::fromClassNameAndIdentifier(
  288.                     $classMetadata->getName(),
  289.                     $this->identifierFlattener->flattenIdentifier($classMetadata$identifier)
  290.                 );
  291.             }
  292.         };
  293.     }
  294.     /**
  295.      * Creates a closure capable of initializing a proxy
  296.      *
  297.      * @return Closure(InternalProxy, array):void
  298.      *
  299.      * @throws EntityNotFoundException
  300.      */
  301.     private function createLazyInitializer(ClassMetadata $classMetadataEntityPersister $entityPersisterIdentifierFlattener $identifierFlattener): Closure
  302.     {
  303.         return static function (InternalProxy $proxy, array $identifier) use ($entityPersister$classMetadata$identifierFlattener): void {
  304.             $original $entityPersister->loadById($identifier);
  305.             if ($original === null) {
  306.                 throw EntityNotFoundException::fromClassNameAndIdentifier(
  307.                     $classMetadata->getName(),
  308.                     $identifierFlattener->flattenIdentifier($classMetadata$identifier)
  309.                 );
  310.             }
  311.             if ($proxy === $original) {
  312.                 return;
  313.             }
  314.             $class $entityPersister->getClassMetadata();
  315.             foreach ($class->getReflectionProperties() as $property) {
  316.                 if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
  317.                     continue;
  318.                 }
  319.                 $property->setValue($proxy$property->getValue($original));
  320.             }
  321.         };
  322.     }
  323.     /**
  324.      * Creates a closure capable of finalizing state a cloned proxy
  325.      *
  326.      * @deprecated ProxyFactory::createCloner() is deprecated and will be removed in version 3.0 of doctrine/orm.
  327.      *
  328.      * @psalm-return Closure(CommonProxy):void
  329.      *
  330.      * @throws EntityNotFoundException
  331.      */
  332.     private function createCloner(ClassMetadata $classMetadataEntityPersister $entityPersister): Closure
  333.     {
  334.         return function (CommonProxy $proxy) use ($entityPersister$classMetadata): void {
  335.             if ($proxy->__isInitialized()) {
  336.                 return;
  337.             }
  338.             $proxy->__setInitialized(true);
  339.             $proxy->__setInitializer(null);
  340.             $class      $entityPersister->getClassMetadata();
  341.             $identifier $classMetadata->getIdentifierValues($proxy);
  342.             $original   $entityPersister->loadById($identifier);
  343.             if ($original === null) {
  344.                 throw EntityNotFoundException::fromClassNameAndIdentifier(
  345.                     $classMetadata->getName(),
  346.                     $this->identifierFlattener->flattenIdentifier($classMetadata$identifier)
  347.                 );
  348.             }
  349.             foreach ($class->getReflectionProperties() as $property) {
  350.                 if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
  351.                     continue;
  352.                 }
  353.                 $property->setAccessible(true);
  354.                 $property->setValue($proxy$property->getValue($original));
  355.             }
  356.         };
  357.     }
  358.     private function getProxyFileName(string $classNamestring $baseDirectory): string
  359.     {
  360.         $baseDirectory $baseDirectory ?: $this->proxyDir;
  361.         return rtrim($baseDirectoryDIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR InternalProxy::MARKER
  362.             str_replace('\\'''$className) . '.php';
  363.     }
  364.     private function getProxyFactory(string $className): Closure
  365.     {
  366.         $skippedProperties = [];
  367.         $class             $this->em->getClassMetadata($className);
  368.         $identifiers       array_flip($class->getIdentifierFieldNames());
  369.         $filter            ReflectionProperty::IS_PUBLIC ReflectionProperty::IS_PROTECTED ReflectionProperty::IS_PRIVATE;
  370.         $reflector         $class->getReflectionClass();
  371.         while ($reflector) {
  372.             foreach ($reflector->getProperties($filter) as $property) {
  373.                 $name $property->name;
  374.                 if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
  375.                     continue;
  376.                 }
  377.                 $prefix $property->isPrivate() ? "\0" $property->class "\0" : ($property->isProtected() ? "\0*\0" '');
  378.                 $skippedProperties[$prefix $name] = true;
  379.             }
  380.             $filter    ReflectionProperty::IS_PRIVATE;
  381.             $reflector $reflector->getParentClass();
  382.         }
  383.         $className        $class->getName(); // aliases and case sensitivity
  384.         $entityPersister  $this->uow->getEntityPersister($className);
  385.         $initializer      $this->createLazyInitializer($class$entityPersister$this->identifierFlattener);
  386.         $proxyClassName   $this->loadProxyClass($class);
  387.         $identifierFields array_intersect_key($class->getReflectionProperties(), $identifiers);
  388.         $proxyFactory Closure::bind(static function (array $identifier) use ($initializer$skippedProperties$identifierFields$className): InternalProxy {
  389.             $proxy self::createLazyGhost(static function (InternalProxy $object) use ($initializer$identifier): void {
  390.                 $initializer($object$identifier);
  391.             }, $skippedProperties);
  392.             foreach ($identifierFields as $idField => $reflector) {
  393.                 if (! isset($identifier[$idField])) {
  394.                     throw ORMInvalidArgumentException::missingPrimaryKeyValue($className$idField);
  395.                 }
  396.                 $reflector->setValue($proxy$identifier[$idField]);
  397.             }
  398.             return $proxy;
  399.         }, null$proxyClassName);
  400.         return $this->proxyFactories[$className] = $proxyFactory;
  401.     }
  402.     private function loadProxyClass(ClassMetadata $class): string
  403.     {
  404.         $proxyClassName self::generateProxyClassName($class->getName(), $this->proxyNs);
  405.         if (class_exists($proxyClassNamefalse)) {
  406.             return $proxyClassName;
  407.         }
  408.         if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
  409.             $this->generateProxyClass($classnull$proxyClassName);
  410.             return $proxyClassName;
  411.         }
  412.         $fileName $this->getProxyFileName($class->getName(), $this->proxyDir);
  413.         switch ($this->autoGenerate) {
  414.             case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED:
  415.                 if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) {
  416.                     break;
  417.                 }
  418.                 // no break
  419.             case self::AUTOGENERATE_FILE_NOT_EXISTS:
  420.                 if (file_exists($fileName)) {
  421.                     break;
  422.                 }
  423.                 // no break
  424.             case self::AUTOGENERATE_ALWAYS:
  425.                 $this->generateProxyClass($class$fileName$proxyClassName);
  426.                 break;
  427.         }
  428.         require $fileName;
  429.         return $proxyClassName;
  430.     }
  431.     private function generateProxyClass(ClassMetadata $class, ?string $fileNamestring $proxyClassName): void
  432.     {
  433.         $i            strrpos($proxyClassName'\\');
  434.         $placeholders = [
  435.             '<className>' => $class->getName(),
  436.             '<namespace>' => substr($proxyClassName0$i),
  437.             '<proxyShortClassName>' => substr($proxyClassName$i),
  438.             '<baseProxyInterface>' => InternalProxy::class,
  439.         ];
  440.         preg_match_all('(<([a-zA-Z]+)>)'self::PROXY_CLASS_TEMPLATE$placeholderMatches);
  441.         foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) {
  442.             $placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' ucfirst($name)}($class);
  443.         }
  444.         $proxyCode strtr(self::PROXY_CLASS_TEMPLATE$placeholders);
  445.         if (! $fileName) {
  446.             if (! class_exists($proxyClassName)) {
  447.                 eval(substr($proxyCode5));
  448.             }
  449.             return;
  450.         }
  451.         $parentDirectory dirname($fileName);
  452.         if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory0775true)) {
  453.             throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
  454.         }
  455.         if (! is_writable($parentDirectory)) {
  456.             throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
  457.         }
  458.         $tmpFileName $fileName '.' bin2hex(random_bytes(12));
  459.         file_put_contents($tmpFileName$proxyCode);
  460.         @chmod($tmpFileName0664);
  461.         rename($tmpFileName$fileName);
  462.     }
  463.     private function generateUseLazyGhostTrait(ClassMetadata $class): string
  464.     {
  465.         $code ProxyHelper::generateLazyGhost($class->getReflectionClass());
  466.         $code substr($code+ (int) strpos($code"\n{"));
  467.         $code substr($code0, (int) strpos($code"\n}"));
  468.         $code str_replace('LazyGhostTrait;'str_replace("\n    ""\n"'LazyGhostTrait {
  469.             initializeLazyObject as __load;
  470.             setLazyObjectAsInitialized as public __setInitialized;
  471.             isLazyObjectInitialized as private;
  472.             createLazyGhost as private;
  473.             resetLazyObject as private;
  474.         }'), $code);
  475.         return $code;
  476.     }
  477.     private function generateSerializeImpl(ClassMetadata $class): string
  478.     {
  479.         $reflector  $class->getReflectionClass();
  480.         $properties $reflector->hasMethod('__serialize') ? 'parent::__serialize()' '(array) $this';
  481.         $code '$properties = ' $properties ';
  482.         unset($properties["\0" . self::class . "\0lazyObjectState"]);
  483.         ';
  484.         if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) {
  485.             return $code 'return $properties;';
  486.         }
  487.         return $code '$data = [];
  488.         foreach (parent::__sleep() as $name) {
  489.             $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' $reflector->name '\0$name"] ?? $k = null;
  490.             if (null === $k) {
  491.                 trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
  492.             } else {
  493.                 $data[$k] = $value;
  494.             }
  495.         }
  496.         return $data;';
  497.     }
  498.     private static function generateProxyClassName(string $classNamestring $proxyNamespace): string
  499.     {
  500.         return rtrim($proxyNamespace'\\') . '\\' Proxy::MARKER '\\' ltrim($className'\\');
  501.     }
  502. }