vendor/doctrine/common/src/Proxy/ProxyGenerator.php line 1197

Open in your IDE?
  1. <?php
  2. namespace Doctrine\Common\Proxy;
  3. use BackedEnum;
  4. use Doctrine\Common\Proxy\Exception\InvalidArgumentException;
  5. use Doctrine\Common\Proxy\Exception\UnexpectedValueException;
  6. use Doctrine\Common\Util\ClassUtils;
  7. use Doctrine\Persistence\Mapping\ClassMetadata;
  8. use ReflectionIntersectionType;
  9. use ReflectionMethod;
  10. use ReflectionNamedType;
  11. use ReflectionParameter;
  12. use ReflectionProperty;
  13. use ReflectionType;
  14. use ReflectionUnionType;
  15. use function array_combine;
  16. use function array_diff;
  17. use function array_key_exists;
  18. use function array_map;
  19. use function array_slice;
  20. use function array_unique;
  21. use function assert;
  22. use function bin2hex;
  23. use function call_user_func;
  24. use function chmod;
  25. use function class_exists;
  26. use function dirname;
  27. use function explode;
  28. use function file;
  29. use function file_put_contents;
  30. use function get_class;
  31. use function implode;
  32. use function in_array;
  33. use function interface_exists;
  34. use function is_callable;
  35. use function is_dir;
  36. use function is_scalar;
  37. use function is_string;
  38. use function is_writable;
  39. use function lcfirst;
  40. use function ltrim;
  41. use function method_exists;
  42. use function mkdir;
  43. use function preg_match;
  44. use function preg_match_all;
  45. use function preg_replace;
  46. use function preg_split;
  47. use function random_bytes;
  48. use function rename;
  49. use function rtrim;
  50. use function sprintf;
  51. use function str_replace;
  52. use function strpos;
  53. use function strrev;
  54. use function strtolower;
  55. use function strtr;
  56. use function substr;
  57. use function trim;
  58. use function var_export;
  59. use const DIRECTORY_SEPARATOR;
  60. use const PHP_VERSION_ID;
  61. use const PREG_SPLIT_DELIM_CAPTURE;
  62. /**
  63. * This factory is used to generate proxy classes.
  64. * It builds proxies from given parameters, a template and class metadata.
  65. *
  66. * @deprecated The ProxyGenerator class is deprecated since doctrine/common 3.5.
  67. */
  68. class ProxyGenerator
  69. {
  70. /**
  71. * Used to match very simple id methods that don't need
  72. * to be decorated since the identifier is known.
  73. */
  74. public const PATTERN_MATCH_ID_METHOD = <<<'EOT'
  75. ((?(DEFINE)
  76. (?<type>\\?[a-z_\x7f-\xff][\w\x7f-\xff]*(?:\\[a-z_\x7f-\xff][\w\x7f-\xff]*)*)
  77. (?<intersection_type>(?&type)\s*&\s*(?&type))
  78. (?<union_type>(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type))(?:\s*\|\s*(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type)))+)
  79. )(?:public\s+)?(?:function\s+%s\s*\(\)\s*)\s*(?::\s*(?:(?&union_type)|(?&intersection_type)|(?:\??(?&type)))\s*)?{\s*return\s*\$this->%s;\s*})i
  80. EOT;
  81. /**
  82. * The namespace that contains all proxy classes.
  83. *
  84. * @var string
  85. */
  86. private $proxyNamespace;
  87. /**
  88. * The directory that contains all proxy classes.
  89. *
  90. * @var string
  91. */
  92. private $proxyDirectory;
  93. /**
  94. * Map of callables used to fill in placeholders set in the template.
  95. *
  96. * @var string[]|callable[]
  97. */
  98. protected $placeholders = [
  99. 'baseProxyInterface' => Proxy::class,
  100. 'additionalProperties' => '',
  101. ];
  102. /**
  103. * Template used as a blueprint to generate proxies.
  104. *
  105. * @var string
  106. */
  107. protected $proxyClassTemplate = '<?php
  108. namespace <namespace>;
  109. <enumUseStatements>
  110. /**
  111. * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE\'S PROXY GENERATOR
  112. */
  113. class <proxyShortClassName> extends \<className> implements \<baseProxyInterface>
  114. {
  115. /**
  116. * @var \Closure the callback responsible for loading properties in the proxy object. This callback is called with
  117. * three parameters, being respectively the proxy object to be initialized, the method that triggered the
  118. * initialization process and an array of ordered parameters that were passed to that method.
  119. *
  120. * @see \Doctrine\Common\Proxy\Proxy::__setInitializer
  121. */
  122. public $__initializer__;
  123. /**
  124. * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object
  125. *
  126. * @see \Doctrine\Common\Proxy\Proxy::__setCloner
  127. */
  128. public $__cloner__;
  129. /**
  130. * @var boolean flag indicating if this object was already initialized
  131. *
  132. * @see \Doctrine\Persistence\Proxy::__isInitialized
  133. */
  134. public $__isInitialized__ = false;
  135. /**
  136. * @var array<string, null> properties to be lazy loaded, indexed by property name
  137. */
  138. public static $lazyPropertiesNames = <lazyPropertiesNames>;
  139. /**
  140. * @var array<string, mixed> default values of properties to be lazy loaded, with keys being the property names
  141. *
  142. * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties
  143. */
  144. public static $lazyPropertiesDefaults = <lazyPropertiesDefaults>;
  145. <additionalProperties>
  146. <constructorImpl>
  147. <magicGet>
  148. <magicSet>
  149. <magicIsset>
  150. <sleepImpl>
  151. <wakeupImpl>
  152. <cloneImpl>
  153. /**
  154. * Forces initialization of the proxy
  155. */
  156. public function __load(): void
  157. {
  158. $this->__initializer__ && $this->__initializer__->__invoke($this, \'__load\', []);
  159. }
  160. /**
  161. * {@inheritDoc}
  162. * @internal generated method: use only when explicitly handling proxy specific loading logic
  163. */
  164. public function __isInitialized(): bool
  165. {
  166. return $this->__isInitialized__;
  167. }
  168. /**
  169. * {@inheritDoc}
  170. * @internal generated method: use only when explicitly handling proxy specific loading logic
  171. */
  172. public function __setInitialized($initialized): void
  173. {
  174. $this->__isInitialized__ = $initialized;
  175. }
  176. /**
  177. * {@inheritDoc}
  178. * @internal generated method: use only when explicitly handling proxy specific loading logic
  179. */
  180. public function __setInitializer(?\Closure $initializer = null): void
  181. {
  182. $this->__initializer__ = $initializer;
  183. }
  184. /**
  185. * {@inheritDoc}
  186. * @internal generated method: use only when explicitly handling proxy specific loading logic
  187. */
  188. public function __getInitializer(): ?\Closure
  189. {
  190. return $this->__initializer__;
  191. }
  192. /**
  193. * {@inheritDoc}
  194. * @internal generated method: use only when explicitly handling proxy specific loading logic
  195. */
  196. public function __setCloner(?\Closure $cloner = null): void
  197. {
  198. $this->__cloner__ = $cloner;
  199. }
  200. /**
  201. * {@inheritDoc}
  202. * @internal generated method: use only when explicitly handling proxy specific cloning logic
  203. */
  204. public function __getCloner(): ?\Closure
  205. {
  206. return $this->__cloner__;
  207. }
  208. /**
  209. * {@inheritDoc}
  210. * @internal generated method: use only when explicitly handling proxy specific loading logic
  211. * @deprecated no longer in use - generated code now relies on internal components rather than generated public API
  212. * @static
  213. */
  214. public function __getLazyProperties(): array
  215. {
  216. return self::$lazyPropertiesDefaults;
  217. }
  218. <methods>
  219. }
  220. ';
  221. /**
  222. * Initializes a new instance of the <tt>ProxyFactory</tt> class that is
  223. * connected to the given <tt>EntityManager</tt>.
  224. *
  225. * @param string $proxyDirectory The directory to use for the proxy classes. It must exist.
  226. * @param string $proxyNamespace The namespace to use for the proxy classes.
  227. *
  228. * @throws InvalidArgumentException
  229. */
  230. public function __construct($proxyDirectory, $proxyNamespace)
  231. {
  232. if (! $proxyDirectory) {
  233. throw InvalidArgumentException::proxyDirectoryRequired();
  234. }
  235. if (! $proxyNamespace) {
  236. throw InvalidArgumentException::proxyNamespaceRequired();
  237. }
  238. $this->proxyDirectory = $proxyDirectory;
  239. $this->proxyNamespace = $proxyNamespace;
  240. }
  241. /**
  242. * Sets a placeholder to be replaced in the template.
  243. *
  244. * @param string $name
  245. * @param string|callable $placeholder
  246. *
  247. * @throws InvalidArgumentException
  248. */
  249. public function setPlaceholder($name, $placeholder)
  250. {
  251. if (! is_string($placeholder) && ! is_callable($placeholder)) {
  252. throw InvalidArgumentException::invalidPlaceholder($name);
  253. }
  254. $this->placeholders[$name] = $placeholder;
  255. }
  256. /**
  257. * Sets the base template used to create proxy classes.
  258. *
  259. * @param string $proxyClassTemplate
  260. */
  261. public function setProxyClassTemplate($proxyClassTemplate)
  262. {
  263. $this->proxyClassTemplate = (string) $proxyClassTemplate;
  264. }
  265. /**
  266. * Generates a proxy class file.
  267. *
  268. * @param ClassMetadata $class Metadata for the original class.
  269. * @param string|bool $fileName Filename (full path) for the generated class. If none is given, eval() is used.
  270. *
  271. * @throws InvalidArgumentException
  272. * @throws UnexpectedValueException
  273. */
  274. public function generateProxyClass(ClassMetadata $class, $fileName = false)
  275. {
  276. $this->verifyClassCanBeProxied($class);
  277. preg_match_all('(<([a-zA-Z]+)>)', $this->proxyClassTemplate, $placeholderMatches);
  278. $placeholderMatches = array_combine($placeholderMatches[0], $placeholderMatches[1]);
  279. $placeholders = [];
  280. foreach ($placeholderMatches as $placeholder => $name) {
  281. $placeholders[$placeholder] = $this->placeholders[$name] ?? [$this, 'generate' . $name];
  282. }
  283. foreach ($placeholders as & $placeholder) {
  284. if (! is_callable($placeholder)) {
  285. continue;
  286. }
  287. $placeholder = call_user_func($placeholder, $class);
  288. }
  289. $proxyCode = strtr($this->proxyClassTemplate, $placeholders);
  290. if (! $fileName) {
  291. $proxyClassName = $this->generateNamespace($class) . '\\' . $this->generateProxyShortClassName($class);
  292. if (! class_exists($proxyClassName)) {
  293. eval(substr($proxyCode, 5));
  294. }
  295. return;
  296. }
  297. $parentDirectory = dirname($fileName);
  298. if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
  299. throw UnexpectedValueException::proxyDirectoryNotWritable($this->proxyDirectory);
  300. }
  301. if (! is_writable($parentDirectory)) {
  302. throw UnexpectedValueException::proxyDirectoryNotWritable($this->proxyDirectory);
  303. }
  304. $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));
  305. file_put_contents($tmpFileName, $proxyCode);
  306. @chmod($tmpFileName, 0664);
  307. rename($tmpFileName, $fileName);
  308. }
  309. /** @throws InvalidArgumentException */
  310. private function verifyClassCanBeProxied(ClassMetadata $class)
  311. {
  312. if ($class->getReflectionClass()->isFinal()) {
  313. throw InvalidArgumentException::classMustNotBeFinal($class->getName());
  314. }
  315. if ($class->getReflectionClass()->isAbstract()) {
  316. throw InvalidArgumentException::classMustNotBeAbstract($class->getName());
  317. }
  318. if (PHP_VERSION_ID >= 80200 && $class->getReflectionClass()->isReadOnly()) {
  319. throw InvalidArgumentException::classMustNotBeReadOnly($class->getName());
  320. }
  321. }
  322. /**
  323. * Generates the proxy short class name to be used in the template.
  324. *
  325. * @return string
  326. */
  327. private function generateProxyShortClassName(ClassMetadata $class)
  328. {
  329. $proxyClassName = ClassUtils::generateProxyClassName($class->getName(), $this->proxyNamespace);
  330. $parts = explode('\\', strrev($proxyClassName), 2);
  331. return strrev($parts[0]);
  332. }
  333. /**
  334. * Generates the proxy namespace.
  335. *
  336. * @return string
  337. */
  338. private function generateNamespace(ClassMetadata $class)
  339. {
  340. $proxyClassName = ClassUtils::generateProxyClassName($class->getName(), $this->proxyNamespace);
  341. $parts = explode('\\', strrev($proxyClassName), 2);
  342. return strrev($parts[1]);
  343. }
  344. /**
  345. * Enums must have a use statement when used as public property defaults.
  346. */
  347. public function generateEnumUseStatements(ClassMetadata $class): string
  348. {
  349. if (PHP_VERSION_ID < 80100) {
  350. return "\n";
  351. }
  352. $defaultProperties = $class->getReflectionClass()->getDefaultProperties();
  353. $lazyLoadedPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  354. $enumClasses = [];
  355. foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
  356. $name = $property->getName();
  357. if (! in_array($name, $lazyLoadedPublicProperties, true)) {
  358. continue;
  359. }
  360. if (array_key_exists($name, $defaultProperties) && $defaultProperties[$name] instanceof BackedEnum) {
  361. $enumClassNameParts = explode('\\', get_class($defaultProperties[$name]));
  362. $enumClasses[] = $enumClassNameParts[0];
  363. }
  364. }
  365. return implode(
  366. "\n",
  367. array_map(
  368. static function ($className) {
  369. return 'use ' . $className . ';';
  370. },
  371. array_unique($enumClasses)
  372. )
  373. ) . "\n";
  374. }
  375. /**
  376. * Generates the original class name.
  377. *
  378. * @return string
  379. */
  380. private function generateClassName(ClassMetadata $class)
  381. {
  382. return ltrim($class->getName(), '\\');
  383. }
  384. /**
  385. * Generates the array representation of lazy loaded public properties and their default values.
  386. *
  387. * @return string
  388. */
  389. private function generateLazyPropertiesNames(ClassMetadata $class)
  390. {
  391. $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  392. $values = [];
  393. foreach ($lazyPublicProperties as $name) {
  394. $values[$name] = null;
  395. }
  396. return var_export($values, true);
  397. }
  398. /**
  399. * Generates the array representation of lazy loaded public properties names.
  400. *
  401. * @return string
  402. */
  403. private function generateLazyPropertiesDefaults(ClassMetadata $class)
  404. {
  405. return var_export($this->getLazyLoadedPublicProperties($class), true);
  406. }
  407. /**
  408. * Generates the constructor code (un-setting public lazy loaded properties, setting identifier field values).
  409. *
  410. * @return string
  411. */
  412. private function generateConstructorImpl(ClassMetadata $class)
  413. {
  414. $constructorImpl = <<<'EOT'
  415. public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
  416. {
  417. EOT;
  418. $toUnset = array_map(static function (string $name): string {
  419. return '$this->' . $name;
  420. }, $this->getLazyLoadedPublicPropertiesNames($class));
  421. return $constructorImpl . ($toUnset === [] ? '' : ' unset(' . implode(', ', $toUnset) . ");\n")
  422. . <<<'EOT'
  423. $this->__initializer__ = $initializer;
  424. $this->__cloner__ = $cloner;
  425. }
  426. EOT;
  427. }
  428. /**
  429. * Generates the magic getter invoked when lazy loaded public properties are requested.
  430. *
  431. * @return string
  432. */
  433. private function generateMagicGet(ClassMetadata $class)
  434. {
  435. $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  436. $reflectionClass = $class->getReflectionClass();
  437. $hasParentGet = false;
  438. $returnReference = '';
  439. $inheritDoc = '';
  440. $name = '$name';
  441. $parametersString = '$name';
  442. $returnTypeHint = null;
  443. if ($reflectionClass->hasMethod('__get')) {
  444. $hasParentGet = true;
  445. $inheritDoc = '{@inheritDoc}';
  446. $methodReflection = $reflectionClass->getMethod('__get');
  447. if ($methodReflection->returnsReference()) {
  448. $returnReference = '& ';
  449. }
  450. $methodParameters = $methodReflection->getParameters();
  451. $name = '$' . $methodParameters[0]->getName();
  452. $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name']);
  453. $returnTypeHint = $this->getMethodReturnType($methodReflection);
  454. }
  455. if (empty($lazyPublicProperties) && ! $hasParentGet) {
  456. return '';
  457. }
  458. $magicGet = <<<EOT
  459. /**
  460. * $inheritDoc
  461. * @param string \$name
  462. */
  463. public function {$returnReference}__get($parametersString)$returnTypeHint
  464. {
  465. EOT;
  466. if (! empty($lazyPublicProperties)) {
  467. $magicGet .= <<<'EOT'
  468. if (\array_key_exists($name, self::$lazyPropertiesNames)) {
  469. $this->__initializer__ && $this->__initializer__->__invoke($this, '__get', [$name]);
  470. EOT;
  471. if ($returnTypeHint === ': void') {
  472. $magicGet .= "\n return;";
  473. } else {
  474. $magicGet .= "\n return \$this->\$name;";
  475. }
  476. $magicGet .= <<<'EOT'
  477. }
  478. EOT;
  479. }
  480. if ($hasParentGet) {
  481. $magicGet .= <<<'EOT'
  482. $this->__initializer__ && $this->__initializer__->__invoke($this, '__get', [$name]);
  483. EOT;
  484. if ($returnTypeHint === ': void') {
  485. $magicGet .= <<<'EOT'
  486. parent::__get($name);
  487. return;
  488. EOT;
  489. } elseif ($returnTypeHint === ': never') {
  490. $magicGet .= <<<'EOT'
  491. parent::__get($name);
  492. EOT;
  493. } else {
  494. $magicGet .= <<<'EOT'
  495. return parent::__get($name);
  496. EOT;
  497. }
  498. } else {
  499. $magicGet .= sprintf(<<<EOT
  500. trigger_error(sprintf('Undefined property: %%s::$%%s', __CLASS__, %s), E_USER_NOTICE);
  501. EOT
  502. , $name);
  503. }
  504. return $magicGet . "\n }";
  505. }
  506. /**
  507. * Generates the magic setter (currently unused).
  508. *
  509. * @return string
  510. */
  511. private function generateMagicSet(ClassMetadata $class)
  512. {
  513. $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  514. $reflectionClass = $class->getReflectionClass();
  515. $hasParentSet = false;
  516. $inheritDoc = '';
  517. $parametersString = '$name, $value';
  518. $returnTypeHint = null;
  519. if ($reflectionClass->hasMethod('__set')) {
  520. $hasParentSet = true;
  521. $inheritDoc = '{@inheritDoc}';
  522. $methodReflection = $reflectionClass->getMethod('__set');
  523. $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name', 'value']);
  524. $returnTypeHint = $this->getMethodReturnType($methodReflection);
  525. }
  526. if (empty($lazyPublicProperties) && ! $hasParentSet) {
  527. return '';
  528. }
  529. $magicSet = <<<EOT
  530. /**
  531. * $inheritDoc
  532. * @param string \$name
  533. * @param mixed \$value
  534. */
  535. public function __set($parametersString)$returnTypeHint
  536. {
  537. EOT;
  538. if (! empty($lazyPublicProperties)) {
  539. $magicSet .= <<<'EOT'
  540. if (\array_key_exists($name, self::$lazyPropertiesNames)) {
  541. $this->__initializer__ && $this->__initializer__->__invoke($this, '__set', [$name, $value]);
  542. $this->$name = $value;
  543. return;
  544. }
  545. EOT;
  546. }
  547. if ($hasParentSet) {
  548. $magicSet .= <<<'EOT'
  549. $this->__initializer__ && $this->__initializer__->__invoke($this, '__set', [$name, $value]);
  550. EOT;
  551. if ($returnTypeHint === ': void') {
  552. $magicSet .= <<<'EOT'
  553. parent::__set($name, $value);
  554. return;
  555. EOT;
  556. } elseif ($returnTypeHint === ': never') {
  557. $magicSet .= <<<'EOT'
  558. parent::__set($name, $value);
  559. EOT;
  560. } else {
  561. $magicSet .= <<<'EOT'
  562. return parent::__set($name, $value);
  563. EOT;
  564. }
  565. } else {
  566. $magicSet .= ' $this->$name = $value;';
  567. }
  568. return $magicSet . "\n }";
  569. }
  570. /**
  571. * Generates the magic issetter invoked when lazy loaded public properties are checked against isset().
  572. *
  573. * @return string
  574. */
  575. private function generateMagicIsset(ClassMetadata $class)
  576. {
  577. $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  578. $hasParentIsset = $class->getReflectionClass()->hasMethod('__isset');
  579. $parametersString = '$name';
  580. $returnTypeHint = null;
  581. if ($hasParentIsset) {
  582. $methodReflection = $class->getReflectionClass()->getMethod('__isset');
  583. $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name']);
  584. $returnTypeHint = $this->getMethodReturnType($methodReflection);
  585. }
  586. if (empty($lazyPublicProperties) && ! $hasParentIsset) {
  587. return '';
  588. }
  589. $inheritDoc = $hasParentIsset ? '{@inheritDoc}' : '';
  590. $magicIsset = <<<EOT
  591. /**
  592. * $inheritDoc
  593. * @param string \$name
  594. * @return boolean
  595. */
  596. public function __isset($parametersString)$returnTypeHint
  597. {
  598. EOT;
  599. if (! empty($lazyPublicProperties)) {
  600. $magicIsset .= <<<'EOT'
  601. if (\array_key_exists($name, self::$lazyPropertiesNames)) {
  602. $this->__initializer__ && $this->__initializer__->__invoke($this, '__isset', [$name]);
  603. return isset($this->$name);
  604. }
  605. EOT;
  606. }
  607. if ($hasParentIsset) {
  608. $magicIsset .= <<<'EOT'
  609. $this->__initializer__ && $this->__initializer__->__invoke($this, '__isset', [$name]);
  610. return parent::__isset($name);
  611. EOT;
  612. } else {
  613. $magicIsset .= ' return false;';
  614. }
  615. return $magicIsset . "\n }";
  616. }
  617. /**
  618. * Generates implementation for the `__sleep` method of proxies.
  619. *
  620. * @return string
  621. */
  622. private function generateSleepImpl(ClassMetadata $class)
  623. {
  624. $reflectionClass = $class->getReflectionClass();
  625. $hasParentSleep = $reflectionClass->hasMethod('__sleep');
  626. $inheritDoc = $hasParentSleep ? '{@inheritDoc}' : '';
  627. $returnTypeHint = $hasParentSleep ? $this->getMethodReturnType($reflectionClass->getMethod('__sleep')) : '';
  628. $sleepImpl = <<<EOT
  629. /**
  630. * $inheritDoc
  631. * @return array
  632. */
  633. public function __sleep()$returnTypeHint
  634. {
  635. EOT;
  636. if ($hasParentSleep) {
  637. return $sleepImpl . <<<'EOT'
  638. $properties = array_merge(['__isInitialized__'], parent::__sleep());
  639. if ($this->__isInitialized__) {
  640. $properties = array_diff($properties, array_keys(self::$lazyPropertiesNames));
  641. }
  642. return $properties;
  643. }
  644. EOT;
  645. }
  646. $allProperties = ['__isInitialized__'];
  647. foreach ($class->getReflectionClass()->getProperties() as $prop) {
  648. assert($prop instanceof ReflectionProperty);
  649. if ($prop->isStatic()) {
  650. continue;
  651. }
  652. $allProperties[] = $prop->isPrivate()
  653. ? "\0" . $prop->getDeclaringClass()->getName() . "\0" . $prop->getName()
  654. : $prop->getName();
  655. }
  656. $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  657. $protectedProperties = array_diff($allProperties, $lazyPublicProperties);
  658. foreach ($allProperties as &$property) {
  659. $property = var_export($property, true);
  660. }
  661. foreach ($protectedProperties as &$property) {
  662. $property = var_export($property, true);
  663. }
  664. $allProperties = implode(', ', $allProperties);
  665. $protectedProperties = implode(', ', $protectedProperties);
  666. return $sleepImpl . <<<EOT
  667. if (\$this->__isInitialized__) {
  668. return [$allProperties];
  669. }
  670. return [$protectedProperties];
  671. }
  672. EOT;
  673. }
  674. /**
  675. * Generates implementation for the `__wakeup` method of proxies.
  676. *
  677. * @return string
  678. */
  679. private function generateWakeupImpl(ClassMetadata $class)
  680. {
  681. $reflectionClass = $class->getReflectionClass();
  682. $hasParentWakeup = $reflectionClass->hasMethod('__wakeup');
  683. $unsetPublicProperties = [];
  684. foreach ($this->getLazyLoadedPublicPropertiesNames($class) as $lazyPublicProperty) {
  685. $unsetPublicProperties[] = '$this->' . $lazyPublicProperty;
  686. }
  687. $shortName = $this->generateProxyShortClassName($class);
  688. $inheritDoc = $hasParentWakeup ? '{@inheritDoc}' : '';
  689. $returnTypeHint = $hasParentWakeup ? $this->getMethodReturnType($reflectionClass->getMethod('__wakeup')) : '';
  690. $wakeupImpl = <<<EOT
  691. /**
  692. * $inheritDoc
  693. */
  694. public function __wakeup()$returnTypeHint
  695. {
  696. if ( ! \$this->__isInitialized__) {
  697. \$this->__initializer__ = function ($shortName \$proxy) {
  698. \$proxy->__setInitializer(null);
  699. \$proxy->__setCloner(null);
  700. \$existingProperties = get_object_vars(\$proxy);
  701. foreach (\$proxy::\$lazyPropertiesDefaults as \$property => \$defaultValue) {
  702. if ( ! array_key_exists(\$property, \$existingProperties)) {
  703. \$proxy->\$property = \$defaultValue;
  704. }
  705. }
  706. };
  707. EOT;
  708. if (! empty($unsetPublicProperties)) {
  709. $wakeupImpl .= "\n unset(" . implode(', ', $unsetPublicProperties) . ');';
  710. }
  711. $wakeupImpl .= "\n }";
  712. if ($hasParentWakeup) {
  713. $wakeupImpl .= "\n parent::__wakeup();";
  714. }
  715. $wakeupImpl .= "\n }";
  716. return $wakeupImpl;
  717. }
  718. /**
  719. * Generates implementation for the `__clone` method of proxies.
  720. *
  721. * @return string
  722. */
  723. private function generateCloneImpl(ClassMetadata $class)
  724. {
  725. $reflectionClass = $class->getReflectionClass();
  726. $hasParentClone = $reflectionClass->hasMethod('__clone');
  727. $returnTypeHint = $hasParentClone ? $this->getMethodReturnType($reflectionClass->getMethod('__clone')) : '';
  728. $inheritDoc = $hasParentClone ? '{@inheritDoc}' : '';
  729. $callParentClone = $hasParentClone ? "\n parent::__clone();\n" : '';
  730. return <<<EOT
  731. /**
  732. * $inheritDoc
  733. */
  734. public function __clone()$returnTypeHint
  735. {
  736. \$this->__cloner__ && \$this->__cloner__->__invoke(\$this, '__clone', []);
  737. $callParentClone }
  738. EOT;
  739. }
  740. /**
  741. * Generates decorated methods by picking those available in the parent class.
  742. *
  743. * @return string
  744. */
  745. private function generateMethods(ClassMetadata $class)
  746. {
  747. $methods = '';
  748. $methodNames = [];
  749. $reflectionMethods = $class->getReflectionClass()->getMethods(ReflectionMethod::IS_PUBLIC);
  750. $skippedMethods = [
  751. '__sleep' => true,
  752. '__clone' => true,
  753. '__wakeup' => true,
  754. '__get' => true,
  755. '__set' => true,
  756. '__isset' => true,
  757. ];
  758. foreach ($reflectionMethods as $method) {
  759. $name = $method->getName();
  760. if (
  761. $method->isConstructor() ||
  762. isset($skippedMethods[strtolower($name)]) ||
  763. isset($methodNames[$name]) ||
  764. $method->isFinal() ||
  765. $method->isStatic() ||
  766. ( ! $method->isPublic())
  767. ) {
  768. continue;
  769. }
  770. $methodNames[$name] = true;
  771. $methods .= "\n /**\n"
  772. . " * {@inheritDoc}\n"
  773. . " */\n"
  774. . ' public function ';
  775. if ($method->returnsReference()) {
  776. $methods .= '&';
  777. }
  778. $methods .= $name . '(' . $this->buildParametersString($method->getParameters()) . ')';
  779. $methods .= $this->getMethodReturnType($method);
  780. $methods .= "\n" . ' {' . "\n";
  781. if ($this->isShortIdentifierGetter($method, $class)) {
  782. $identifier = lcfirst(substr($name, 3));
  783. $fieldType = $class->getTypeOfField($identifier);
  784. $cast = in_array($fieldType, ['integer', 'smallint'], true) ? '(int) ' : '';
  785. $methods .= ' if ($this->__isInitialized__ === false) {' . "\n";
  786. $methods .= ' ';
  787. $methods .= $this->shouldProxiedMethodReturn($method) ? 'return ' : '';
  788. $methods .= $cast . ' parent::' . $method->getName() . "();\n";
  789. $methods .= ' }' . "\n\n";
  790. }
  791. $invokeParamsString = implode(', ', $this->getParameterNamesForInvoke($method->getParameters()));
  792. $callParamsString = implode(', ', $this->getParameterNamesForParentCall($method->getParameters()));
  793. $methods .= "\n \$this->__initializer__ "
  794. . '&& $this->__initializer__->__invoke($this, ' . var_export($name, true)
  795. . ', [' . $invokeParamsString . ']);'
  796. . "\n\n "
  797. . ($this->shouldProxiedMethodReturn($method) ? 'return ' : '')
  798. . 'parent::' . $name . '(' . $callParamsString . ');'
  799. . "\n" . ' }' . "\n";
  800. }
  801. return $methods;
  802. }
  803. /**
  804. * Generates the Proxy file name.
  805. *
  806. * @param string $className
  807. * @param string $baseDirectory Optional base directory for proxy file name generation.
  808. * If not specified, the directory configured on the Configuration of the
  809. * EntityManager will be used by this factory.
  810. * @psalm-param class-string $className
  811. *
  812. * @return string
  813. */
  814. public function getProxyFileName($className, $baseDirectory = null)
  815. {
  816. $baseDirectory = $baseDirectory ?: $this->proxyDirectory;
  817. return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . Proxy::MARKER
  818. . str_replace('\\', '', $className) . '.php';
  819. }
  820. /**
  821. * Checks if the method is a short identifier getter.
  822. *
  823. * What does this mean? For proxy objects the identifier is already known,
  824. * however accessing the getter for this identifier usually triggers the
  825. * lazy loading, leading to a query that may not be necessary if only the
  826. * ID is interesting for the userland code (for example in views that
  827. * generate links to the entity, but do not display anything else).
  828. *
  829. * @param ReflectionMethod $method
  830. *
  831. * @return bool
  832. */
  833. private function isShortIdentifierGetter($method, ClassMetadata $class)
  834. {
  835. $identifier = lcfirst(substr($method->getName(), 3));
  836. $startLine = $method->getStartLine();
  837. $endLine = $method->getEndLine();
  838. $cheapCheck = $method->getNumberOfParameters() === 0
  839. && substr($method->getName(), 0, 3) === 'get'
  840. && in_array($identifier, $class->getIdentifier(), true)
  841. && $class->hasField($identifier)
  842. && ($endLine - $startLine <= 4);
  843. if ($cheapCheck) {
  844. $code = file($method->getFileName());
  845. $code = trim(implode(' ', array_slice($code, $startLine - 1, $endLine - $startLine + 1)));
  846. $pattern = sprintf(self::PATTERN_MATCH_ID_METHOD, $method->getName(), $identifier);
  847. if (preg_match($pattern, $code)) {
  848. return true;
  849. }
  850. }
  851. return false;
  852. }
  853. /**
  854. * Generates the list of public properties to be lazy loaded.
  855. *
  856. * @return array<int, string>
  857. */
  858. private function getLazyLoadedPublicPropertiesNames(ClassMetadata $class): array
  859. {
  860. $properties = [];
  861. foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
  862. $name = $property->getName();
  863. if ((! $class->hasField($name) && ! $class->hasAssociation($name)) || $class->isIdentifier($name)) {
  864. continue;
  865. }
  866. $properties[] = $name;
  867. }
  868. return $properties;
  869. }
  870. /**
  871. * Generates the list of default values of public properties.
  872. *
  873. * @return mixed[]
  874. */
  875. private function getLazyLoadedPublicProperties(ClassMetadata $class)
  876. {
  877. $defaultProperties = $class->getReflectionClass()->getDefaultProperties();
  878. $lazyLoadedPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
  879. $defaultValues = [];
  880. foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
  881. $name = $property->getName();
  882. if (! in_array($name, $lazyLoadedPublicProperties, true)) {
  883. continue;
  884. }
  885. if (array_key_exists($name, $defaultProperties)) {
  886. $defaultValues[$name] = $defaultProperties[$name];
  887. } elseif (method_exists($property, 'getType')) {
  888. $propertyType = $property->getType();
  889. if ($propertyType !== null && $propertyType->allowsNull()) {
  890. $defaultValues[$name] = null;
  891. }
  892. }
  893. }
  894. return $defaultValues;
  895. }
  896. /**
  897. * @param ReflectionParameter[] $parameters
  898. * @param string[] $renameParameters
  899. *
  900. * @return string
  901. */
  902. private function buildParametersString(array $parameters, array $renameParameters = [])
  903. {
  904. $parameterDefinitions = [];
  905. $i = -1;
  906. foreach ($parameters as $param) {
  907. assert($param instanceof ReflectionParameter);
  908. $i++;
  909. $parameterDefinition = '';
  910. $parameterType = $this->getParameterType($param);
  911. if ($parameterType !== null) {
  912. $parameterDefinition .= $parameterType . ' ';
  913. }
  914. if ($param->isPassedByReference()) {
  915. $parameterDefinition .= '&';
  916. }
  917. if ($param->isVariadic()) {
  918. $parameterDefinition .= '...';
  919. }
  920. $parameterDefinition .= '$' . ($renameParameters ? $renameParameters[$i] : $param->getName());
  921. $parameterDefinition .= $this->getParameterDefaultValue($param);
  922. $parameterDefinitions[] = $parameterDefinition;
  923. }
  924. return implode(', ', $parameterDefinitions);
  925. }
  926. /** @return string|null */
  927. private function getParameterType(ReflectionParameter $parameter)
  928. {
  929. if (! $parameter->hasType()) {
  930. return null;
  931. }
  932. $declaringFunction = $parameter->getDeclaringFunction();
  933. assert($declaringFunction instanceof ReflectionMethod);
  934. return $this->formatType($parameter->getType(), $declaringFunction, $parameter);
  935. }
  936. /** @return string */
  937. private function getParameterDefaultValue(ReflectionParameter $parameter)
  938. {
  939. if (! $parameter->isDefaultValueAvailable()) {
  940. return '';
  941. }
  942. if (PHP_VERSION_ID < 80100 || is_scalar($parameter->getDefaultValue())) {
  943. return ' = ' . var_export($parameter->getDefaultValue(), true);
  944. }
  945. $value = rtrim(substr(explode('$' . $parameter->getName() . ' = ', (string) $parameter, 2)[1], 0, -2));
  946. if (strpos($value, '\\') !== false || strpos($value, '::') !== false) {
  947. $value = preg_split("/('(?:[^'\\\\]*+(?:\\\\.)*+)*+')/", $value, -1, PREG_SPLIT_DELIM_CAPTURE);
  948. foreach ($value as $i => $part) {
  949. if ($i % 2 === 0) {
  950. $value[$i] = preg_replace('/(?<![a-zA-Z0-9_\x7f-\xff\\\\])[a-zA-Z0-9_\x7f-\xff]++(?:\\\\[a-zA-Z0-9_\x7f-\xff]++|::)++/', '\\\\\0', $part);
  951. }
  952. }
  953. $value = implode('', $value);
  954. }
  955. return ' = ' . $value;
  956. }
  957. /**
  958. * @param ReflectionParameter[] $parameters
  959. *
  960. * @return string[]
  961. */
  962. private function getParameterNamesForInvoke(array $parameters)
  963. {
  964. return array_map(
  965. static function (ReflectionParameter $parameter) {
  966. return '$' . $parameter->getName();
  967. },
  968. $parameters
  969. );
  970. }
  971. /**
  972. * @param ReflectionParameter[] $parameters
  973. *
  974. * @return string[]
  975. */
  976. private function getParameterNamesForParentCall(array $parameters)
  977. {
  978. return array_map(
  979. static function (ReflectionParameter $parameter) {
  980. $name = '';
  981. if ($parameter->isVariadic()) {
  982. $name .= '...';
  983. }
  984. $name .= '$' . $parameter->getName();
  985. return $name;
  986. },
  987. $parameters
  988. );
  989. }
  990. /** @return string */
  991. private function getMethodReturnType(ReflectionMethod $method)
  992. {
  993. if (! $method->hasReturnType()) {
  994. return '';
  995. }
  996. return ': ' . $this->formatType($method->getReturnType(), $method);
  997. }
  998. /** @return bool */
  999. private function shouldProxiedMethodReturn(ReflectionMethod $method)
  1000. {
  1001. if (! $method->hasReturnType()) {
  1002. return true;
  1003. }
  1004. return ! in_array(
  1005. strtolower($this->formatType($method->getReturnType(), $method)),
  1006. ['void', 'never'],
  1007. true
  1008. );
  1009. }
  1010. /** @return string */
  1011. private function formatType(
  1012. ReflectionType $type,
  1013. ReflectionMethod $method,
  1014. ?ReflectionParameter $parameter = null
  1015. ) {
  1016. if ($type instanceof ReflectionUnionType) {
  1017. return implode('|', array_map(
  1018. function (ReflectionType $unionedType) use ($method, $parameter) {
  1019. if ($unionedType instanceof ReflectionIntersectionType) {
  1020. return '(' . $this->formatType($unionedType, $method, $parameter) . ')';
  1021. }
  1022. return $this->formatType($unionedType, $method, $parameter);
  1023. },
  1024. $type->getTypes()
  1025. ));
  1026. }
  1027. if ($type instanceof ReflectionIntersectionType) {
  1028. return implode('&', array_map(
  1029. function (ReflectionType $intersectedType) use ($method, $parameter) {
  1030. return $this->formatType($intersectedType, $method, $parameter);
  1031. },
  1032. $type->getTypes()
  1033. ));
  1034. }
  1035. assert($type instanceof ReflectionNamedType);
  1036. $name = $type->getName();
  1037. $nameLower = strtolower($name);
  1038. if ($nameLower === 'static') {
  1039. $name = 'static';
  1040. }
  1041. if ($nameLower === 'self') {
  1042. $name = $method->getDeclaringClass()->getName();
  1043. }
  1044. if ($nameLower === 'parent') {
  1045. $name = $method->getDeclaringClass()->getParentClass()->getName();
  1046. }
  1047. if (! $type->isBuiltin() && ! class_exists($name) && ! interface_exists($name) && $name !== 'static') {
  1048. if ($parameter !== null) {
  1049. throw UnexpectedValueException::invalidParameterTypeHint(
  1050. $method->getDeclaringClass()->getName(),
  1051. $method->getName(),
  1052. $parameter->getName()
  1053. );
  1054. }
  1055. throw UnexpectedValueException::invalidReturnTypeHint(
  1056. $method->getDeclaringClass()->getName(),
  1057. $method->getName()
  1058. );
  1059. }
  1060. if (! $type->isBuiltin() && $name !== 'static') {
  1061. $name = '\\' . $name;
  1062. }
  1063. if (
  1064. $type->allowsNull()
  1065. && ! in_array($name, ['mixed', 'null'], true)
  1066. ) {
  1067. $name = '?' . $name;
  1068. }
  1069. return $name;
  1070. }
  1071. }