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

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. // @phpstan-ignore new.deprecatedClass, method.deprecatedClass
  149. $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);
  150. // @phpstan-ignore classConstant.deprecatedInterface, method.deprecatedClass
  151. $proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class);
  152. // @phpstan-ignore method.deprecatedClass
  153. parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);
  154. }
  155. if (! $proxyDir) {
  156. throw ORMInvalidArgumentException::proxyDirectoryRequired();
  157. }
  158. if (! $proxyNs) {
  159. throw ORMInvalidArgumentException::proxyNamespaceRequired();
  160. }
  161. if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
  162. throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
  163. }
  164. $this->em = $em;
  165. $this->uow = $em->getUnitOfWork();
  166. $this->proxyDir = $proxyDir;
  167. $this->proxyNs = $proxyNs;
  168. $this->autoGenerate = (int) $autoGenerate;
  169. $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
  170. }
  171. /**
  172. * {@inheritDoc}
  173. */
  174. public function getProxy($className, array $identifier)
  175. {
  176. if (! $this->isLazyGhostObjectEnabled) {
  177. // @phpstan-ignore method.deprecatedClass
  178. return parent::getProxy($className, $identifier);
  179. }
  180. $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
  181. return $proxyFactory($identifier);
  182. }
  183. /**
  184. * Generates proxy classes for all given classes.
  185. *
  186. * @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies.
  187. * @param string|null $proxyDir The target directory of the proxy classes. If not specified, the
  188. * directory configured on the Configuration of the EntityManager used
  189. * by this factory is used.
  190. *
  191. * @return int Number of generated proxies.
  192. */
  193. public function generateProxyClasses(array $classes, $proxyDir = null)
  194. {
  195. if (! $this->isLazyGhostObjectEnabled) {
  196. // @phpstan-ignore method.deprecatedClass
  197. return parent::generateProxyClasses($classes, $proxyDir);
  198. }
  199. $generated = 0;
  200. foreach ($classes as $class) {
  201. if ($this->skipClass($class)) {
  202. continue;
  203. }
  204. $proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
  205. $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
  206. $this->generateProxyClass($class, $proxyFileName, $proxyClassName);
  207. ++$generated;
  208. }
  209. return $generated;
  210. }
  211. /**
  212. * {@inheritDoc}
  213. *
  214. * @deprecated ProxyFactory::resetUninitializedProxy() is deprecated and will be removed in version 3.0 of doctrine/orm.
  215. */
  216. public function resetUninitializedProxy(CommonProxy $proxy)
  217. {
  218. return parent::resetUninitializedProxy($proxy);
  219. }
  220. /**
  221. * {@inheritDoc}
  222. */
  223. protected function skipClass(ClassMetadata $metadata)
  224. {
  225. return $metadata->isMappedSuperclass
  226. || $metadata->isEmbeddedClass
  227. || $metadata->getReflectionClass()->isAbstract();
  228. }
  229. /**
  230. * {@inheritDoc}
  231. *
  232. * @deprecated ProxyFactory::createProxyDefinition() is deprecated and will be removed in version 3.0 of doctrine/orm.
  233. */
  234. protected function createProxyDefinition($className)
  235. {
  236. $classMetadata = $this->em->getClassMetadata($className);
  237. $entityPersister = $this->uow->getEntityPersister($className);
  238. $initializer = $this->createInitializer($classMetadata, $entityPersister);
  239. $cloner = $this->createCloner($classMetadata, $entityPersister);
  240. return new ProxyDefinition(
  241. self::generateProxyClassName($className, $this->proxyNs),
  242. $classMetadata->getIdentifierFieldNames(),
  243. $classMetadata->getReflectionProperties(),
  244. $initializer,
  245. $cloner
  246. );
  247. }
  248. /**
  249. * Creates a closure capable of initializing a proxy
  250. *
  251. * @deprecated ProxyFactory::createInitializer() is deprecated and will be removed in version 3.0 of doctrine/orm.
  252. *
  253. * @phpstan-return Closure(CommonProxy):void
  254. *
  255. * @throws EntityNotFoundException
  256. */
  257. private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
  258. {
  259. $wakeupProxy = $classMetadata->getReflectionClass()->hasMethod('__wakeup');
  260. return function (CommonProxy $proxy) use ($entityPersister, $classMetadata, $wakeupProxy): void {
  261. $initializer = $proxy->__getInitializer();
  262. $cloner = $proxy->__getCloner();
  263. $proxy->__setInitializer(null);
  264. $proxy->__setCloner(null);
  265. if ($proxy->__isInitialized()) {
  266. return;
  267. }
  268. $properties = $proxy->__getLazyProperties();
  269. foreach ($properties as $propertyName => $property) {
  270. if (! isset($proxy->$propertyName)) {
  271. $proxy->$propertyName = $properties[$propertyName];
  272. }
  273. }
  274. $proxy->__setInitialized(true);
  275. if ($wakeupProxy) {
  276. $proxy->__wakeup();
  277. }
  278. $identifier = $classMetadata->getIdentifierValues($proxy);
  279. try {
  280. $entity = $entityPersister->loadById($identifier, $proxy);
  281. } catch (Throwable $exception) {
  282. $proxy->__setInitializer($initializer);
  283. $proxy->__setCloner($cloner);
  284. $proxy->__setInitialized(false);
  285. throw $exception;
  286. }
  287. if ($entity === null) {
  288. $proxy->__setInitializer($initializer);
  289. $proxy->__setCloner($cloner);
  290. $proxy->__setInitialized(false);
  291. throw EntityNotFoundException::fromClassNameAndIdentifier(
  292. $classMetadata->getName(),
  293. $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
  294. );
  295. }
  296. };
  297. }
  298. /**
  299. * Creates a closure capable of initializing a proxy
  300. *
  301. * @return Closure(InternalProxy, array):void
  302. *
  303. * @throws EntityNotFoundException
  304. */
  305. private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
  306. {
  307. return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
  308. $original = $entityPersister->loadById($identifier);
  309. if ($original === null) {
  310. throw EntityNotFoundException::fromClassNameAndIdentifier(
  311. $classMetadata->getName(),
  312. $identifierFlattener->flattenIdentifier($classMetadata, $identifier)
  313. );
  314. }
  315. if ($proxy === $original) {
  316. return;
  317. }
  318. $class = $entityPersister->getClassMetadata();
  319. foreach ($class->getReflectionProperties() as $property) {
  320. if (isset($identifier[$property->name])) {
  321. continue;
  322. }
  323. $property->setValue($proxy, $property->getValue($original));
  324. }
  325. };
  326. }
  327. /**
  328. * Creates a closure capable of finalizing state a cloned proxy
  329. *
  330. * @deprecated ProxyFactory::createCloner() is deprecated and will be removed in version 3.0 of doctrine/orm.
  331. *
  332. * @phpstan-return Closure(CommonProxy):void
  333. *
  334. * @throws EntityNotFoundException
  335. */
  336. private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
  337. {
  338. return function (CommonProxy $proxy) use ($entityPersister, $classMetadata): void {
  339. if ($proxy->__isInitialized()) {
  340. return;
  341. }
  342. $proxy->__setInitialized(true);
  343. $proxy->__setInitializer(null);
  344. $class = $entityPersister->getClassMetadata();
  345. $identifier = $classMetadata->getIdentifierValues($proxy);
  346. $original = $entityPersister->loadById($identifier);
  347. if ($original === null) {
  348. throw EntityNotFoundException::fromClassNameAndIdentifier(
  349. $classMetadata->getName(),
  350. $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
  351. );
  352. }
  353. foreach ($class->getReflectionProperties() as $property) {
  354. if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
  355. continue;
  356. }
  357. if (PHP_VERSION_ID < 80100) {
  358. $property->setAccessible(true);
  359. }
  360. $property->setValue($proxy, $property->getValue($original));
  361. }
  362. };
  363. }
  364. private function getProxyFileName(string $className, string $baseDirectory): string
  365. {
  366. $baseDirectory = $baseDirectory ?: $this->proxyDir;
  367. return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
  368. . str_replace('\\', '', $className) . '.php';
  369. }
  370. private function getProxyFactory(string $className): Closure
  371. {
  372. $skippedProperties = [];
  373. $class = $this->em->getClassMetadata($className);
  374. $identifiers = array_flip($class->getIdentifierFieldNames());
  375. $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
  376. $reflector = $class->getReflectionClass();
  377. while ($reflector) {
  378. foreach ($reflector->getProperties($filter) as $property) {
  379. $name = $property->name;
  380. if ($property->isStatic() || ! isset($identifiers[$name])) {
  381. continue;
  382. }
  383. $prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');
  384. $skippedProperties[$prefix . $name] = true;
  385. }
  386. $filter = ReflectionProperty::IS_PRIVATE;
  387. $reflector = $reflector->getParentClass();
  388. }
  389. $className = $class->getName(); // aliases and case sensitivity
  390. $entityPersister = $this->uow->getEntityPersister($className);
  391. $initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
  392. $proxyClassName = $this->loadProxyClass($class);
  393. $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
  394. $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
  395. $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
  396. $initializer($object, $identifier);
  397. }, $skippedProperties);
  398. foreach ($identifierFields as $idField => $reflector) {
  399. if (! isset($identifier[$idField])) {
  400. throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField);
  401. }
  402. $reflector->setValue($proxy, $identifier[$idField]);
  403. }
  404. return $proxy;
  405. }, null, $proxyClassName);
  406. return $this->proxyFactories[$className] = $proxyFactory;
  407. }
  408. private function loadProxyClass(ClassMetadata $class): string
  409. {
  410. $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
  411. if (class_exists($proxyClassName, false)) {
  412. return $proxyClassName;
  413. }
  414. if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
  415. $this->generateProxyClass($class, null, $proxyClassName);
  416. return $proxyClassName;
  417. }
  418. $fileName = $this->getProxyFileName($class->getName(), $this->proxyDir);
  419. switch ($this->autoGenerate) {
  420. case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED:
  421. if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) {
  422. break;
  423. }
  424. // no break
  425. case self::AUTOGENERATE_FILE_NOT_EXISTS:
  426. if (file_exists($fileName)) {
  427. break;
  428. }
  429. // no break
  430. case self::AUTOGENERATE_ALWAYS:
  431. $this->generateProxyClass($class, $fileName, $proxyClassName);
  432. break;
  433. }
  434. require $fileName;
  435. return $proxyClassName;
  436. }
  437. private function generateProxyClass(ClassMetadata $class, ?string $fileName, string $proxyClassName): void
  438. {
  439. $i = strrpos($proxyClassName, '\\');
  440. $placeholders = [
  441. '<className>' => $class->getName(),
  442. '<namespace>' => substr($proxyClassName, 0, $i),
  443. '<proxyShortClassName>' => substr($proxyClassName, 1 + $i),
  444. '<baseProxyInterface>' => InternalProxy::class,
  445. ];
  446. preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches);
  447. foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) {
  448. $placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class);
  449. }
  450. $proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders);
  451. if (! $fileName) {
  452. if (! class_exists($proxyClassName)) {
  453. eval(substr($proxyCode, 5));
  454. }
  455. return;
  456. }
  457. $parentDirectory = dirname($fileName);
  458. if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) {
  459. throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
  460. }
  461. if (! is_writable($parentDirectory)) {
  462. throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
  463. }
  464. $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));
  465. file_put_contents($tmpFileName, $proxyCode);
  466. @chmod($tmpFileName, 0664);
  467. rename($tmpFileName, $fileName);
  468. }
  469. private function generateUseLazyGhostTrait(ClassMetadata $class): string
  470. {
  471. // @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
  472. $code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
  473. $code = substr($code, 7 + (int) strpos($code, "\n{"));
  474. $code = substr($code, 0, (int) strpos($code, "\n}"));
  475. $code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
  476. initializeLazyObject as __load;
  477. setLazyObjectAsInitialized as public __setInitialized;
  478. isLazyObjectInitialized as private;
  479. createLazyGhost as private;
  480. resetLazyObject as private;
  481. }'), $code);
  482. return $code;
  483. }
  484. private function generateSerializeImpl(ClassMetadata $class): string
  485. {
  486. $reflector = $class->getReflectionClass();
  487. $properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';
  488. $code = '$properties = ' . $properties . ';
  489. unset($properties["\0" . self::class . "\0lazyObjectState"]);
  490. ';
  491. if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) {
  492. return $code . 'return $properties;';
  493. }
  494. return $code . '$data = [];
  495. foreach (parent::__sleep() as $name) {
  496. $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null;
  497. if (null === $k) {
  498. trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
  499. } else {
  500. $data[$k] = $value;
  501. }
  502. }
  503. return $data;';
  504. }
  505. private static function generateProxyClassName(string $className, string $proxyNamespace): string
  506. {
  507. return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
  508. }
  509. }