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

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 < 0 || $autoGenerate > 4 : ! 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. * @phpstan-return Closure(CommonProxy):void
  250. *
  251. * @throws EntityNotFoundException
  252. */
  253. private function createInitializer(ClassMetadata $classMetadata, EntityPersister $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 $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $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. * @phpstan-return Closure(CommonProxy):void
  329. *
  330. * @throws EntityNotFoundException
  331. */
  332. private function createCloner(ClassMetadata $classMetadata, EntityPersister $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 $className, string $baseDirectory): string
  359. {
  360. $baseDirectory = $baseDirectory ?: $this->proxyDir;
  361. return rtrim($baseDirectory, DIRECTORY_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($proxyClassName, false)) {
  406. return $proxyClassName;
  407. }
  408. if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
  409. $this->generateProxyClass($class, null, $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 $fileName, string $proxyClassName): void
  432. {
  433. $i = strrpos($proxyClassName, '\\');
  434. $placeholders = [
  435. '<className>' => $class->getName(),
  436. '<namespace>' => substr($proxyClassName, 0, $i),
  437. '<proxyShortClassName>' => substr($proxyClassName, 1 + $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($proxyCode, 5));
  448. }
  449. return;
  450. }
  451. $parentDirectory = dirname($fileName);
  452. if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) {
  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($tmpFileName, 0664);
  461. rename($tmpFileName, $fileName);
  462. }
  463. private function generateUseLazyGhostTrait(ClassMetadata $class): string
  464. {
  465. $code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
  466. $code = substr($code, 7 + (int) strpos($code, "\n{"));
  467. $code = substr($code, 0, (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 $className, string $proxyNamespace): string
  499. {
  500. return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
  501. }
  502. }