vendor/doctrine/persistence/src/Persistence/Mapping/AbstractClassMetadataFactory.php line 343

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Persistence\Mapping;
  4. use Doctrine\Persistence\Mapping\Driver\MappingDriver;
  5. use Doctrine\Persistence\Proxy;
  6. use Psr\Cache\CacheItemPoolInterface;
  7. use ReflectionClass;
  8. use ReflectionException;
  9. use function array_combine;
  10. use function array_keys;
  11. use function array_map;
  12. use function array_reverse;
  13. use function array_unshift;
  14. use function assert;
  15. use function class_exists;
  16. use function ltrim;
  17. use function str_replace;
  18. use function strpos;
  19. use function strrpos;
  20. use function substr;
  21. /**
  22. * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
  23. * metadata mapping informations of a class which describes how a class should be mapped
  24. * to a relational database.
  25. *
  26. * This class was abstracted from the ORM ClassMetadataFactory.
  27. *
  28. * @template CMTemplate of ClassMetadata
  29. * @template-implements ClassMetadataFactory<CMTemplate>
  30. */
  31. abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
  32. {
  33. /**
  34. * Salt used by specific Object Manager implementation.
  35. *
  36. * @var string
  37. */
  38. protected $cacheSalt = '__CLASSMETADATA__';
  39. /** @var CacheItemPoolInterface|null */
  40. private $cache;
  41. /**
  42. * @var array<string, ClassMetadata>
  43. * @phpstan-var CMTemplate[]
  44. */
  45. private $loadedMetadata = [];
  46. /** @var bool */
  47. protected $initialized = false;
  48. /** @var ReflectionService|null */
  49. private $reflectionService = null;
  50. /** @var ProxyClassNameResolver|null */
  51. private $proxyClassNameResolver = null;
  52. public function setCache(CacheItemPoolInterface $cache): void
  53. {
  54. $this->cache = $cache;
  55. }
  56. final protected function getCache(): ?CacheItemPoolInterface
  57. {
  58. return $this->cache;
  59. }
  60. /**
  61. * Returns an array of all the loaded metadata currently in memory.
  62. *
  63. * @return ClassMetadata[]
  64. * @phpstan-return CMTemplate[]
  65. */
  66. public function getLoadedMetadata()
  67. {
  68. return $this->loadedMetadata;
  69. }
  70. /**
  71. * {@inheritDoc}
  72. */
  73. public function getAllMetadata()
  74. {
  75. if (! $this->initialized) {
  76. $this->initialize();
  77. }
  78. $driver = $this->getDriver();
  79. $metadata = [];
  80. foreach ($driver->getAllClassNames() as $className) {
  81. $metadata[] = $this->getMetadataFor($className);
  82. }
  83. return $metadata;
  84. }
  85. public function setProxyClassNameResolver(ProxyClassNameResolver $resolver): void
  86. {
  87. $this->proxyClassNameResolver = $resolver;
  88. }
  89. /**
  90. * Lazy initialization of this stuff, especially the metadata driver,
  91. * since these are not needed at all when a metadata cache is active.
  92. *
  93. * @return void
  94. */
  95. abstract protected function initialize();
  96. /**
  97. * Returns the mapping driver implementation.
  98. *
  99. * @return MappingDriver
  100. */
  101. abstract protected function getDriver();
  102. /**
  103. * Wakes up reflection after ClassMetadata gets unserialized from cache.
  104. *
  105. * @phpstan-param CMTemplate $class
  106. *
  107. * @return void
  108. */
  109. abstract protected function wakeupReflection(
  110. ClassMetadata $class,
  111. ReflectionService $reflService
  112. );
  113. /**
  114. * Initializes Reflection after ClassMetadata was constructed.
  115. *
  116. * @phpstan-param CMTemplate $class
  117. *
  118. * @return void
  119. */
  120. abstract protected function initializeReflection(
  121. ClassMetadata $class,
  122. ReflectionService $reflService
  123. );
  124. /**
  125. * Checks whether the class metadata is an entity.
  126. *
  127. * This method should return false for mapped superclasses or embedded classes.
  128. *
  129. * @phpstan-param CMTemplate $class
  130. *
  131. * @return bool
  132. */
  133. abstract protected function isEntity(ClassMetadata $class);
  134. /**
  135. * Removes the prepended backslash of a class string to conform with how php outputs class names
  136. *
  137. * @phpstan-param class-string $className
  138. *
  139. * @phpstan-return class-string
  140. */
  141. private function normalizeClassName(string $className): string
  142. {
  143. return ltrim($className, '\\');
  144. }
  145. /**
  146. * {@inheritDoc}
  147. *
  148. * @throws ReflectionException
  149. * @throws MappingException
  150. */
  151. public function getMetadataFor(string $className)
  152. {
  153. $className = $this->normalizeClassName($className);
  154. if (isset($this->loadedMetadata[$className])) {
  155. return $this->loadedMetadata[$className];
  156. }
  157. if (class_exists($className, false) && (new ReflectionClass($className))->isAnonymous()) {
  158. throw MappingException::classIsAnonymous($className);
  159. }
  160. if (! class_exists($className, false) && strpos($className, ':') !== false) {
  161. throw MappingException::nonExistingClass($className);
  162. }
  163. $realClassName = $this->getRealClass($className);
  164. if (isset($this->loadedMetadata[$realClassName])) {
  165. // We do not have the alias name in the map, include it
  166. return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
  167. }
  168. try {
  169. if ($this->cache !== null) {
  170. $cached = $this->cache->getItem($this->getCacheKey($realClassName))->get();
  171. if ($cached instanceof ClassMetadata) {
  172. /** @phpstan-var CMTemplate $cached */
  173. $this->loadedMetadata[$realClassName] = $cached;
  174. $this->wakeupReflection($cached, $this->getReflectionService());
  175. } else {
  176. $loadedMetadata = $this->loadMetadata($realClassName);
  177. $classNames = array_combine(
  178. array_map([$this, 'getCacheKey'], $loadedMetadata),
  179. $loadedMetadata
  180. );
  181. foreach ($this->cache->getItems(array_keys($classNames)) as $item) {
  182. if (! isset($classNames[$item->getKey()])) {
  183. continue;
  184. }
  185. $item->set($this->loadedMetadata[$classNames[$item->getKey()]]);
  186. $this->cache->saveDeferred($item);
  187. }
  188. $this->cache->commit();
  189. }
  190. } else {
  191. $this->loadMetadata($realClassName);
  192. }
  193. } catch (MappingException $loadingException) {
  194. $fallbackMetadataResponse = $this->onNotFoundMetadata($realClassName);
  195. if ($fallbackMetadataResponse === null) {
  196. throw $loadingException;
  197. }
  198. $this->loadedMetadata[$realClassName] = $fallbackMetadataResponse;
  199. }
  200. if ($className !== $realClassName) {
  201. // We do not have the alias name in the map, include it
  202. $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
  203. }
  204. return $this->loadedMetadata[$className];
  205. }
  206. /**
  207. * {@inheritDoc}
  208. */
  209. public function hasMetadataFor(string $className)
  210. {
  211. $className = $this->normalizeClassName($className);
  212. return isset($this->loadedMetadata[$className]);
  213. }
  214. /**
  215. * Sets the metadata descriptor for a specific class.
  216. *
  217. * NOTE: This is only useful in very special cases, like when generating proxy classes.
  218. *
  219. * @phpstan-param class-string $className
  220. * @phpstan-param CMTemplate $class
  221. *
  222. * @return void
  223. */
  224. public function setMetadataFor(string $className, ClassMetadata $class)
  225. {
  226. $this->loadedMetadata[$this->normalizeClassName($className)] = $class;
  227. }
  228. /**
  229. * Gets an array of parent classes for the given entity class.
  230. *
  231. * @phpstan-param class-string $name
  232. *
  233. * @return string[]
  234. * @phpstan-return list<class-string>
  235. */
  236. protected function getParentClasses(string $name)
  237. {
  238. // Collect parent classes, ignoring transient (not-mapped) classes.
  239. $parentClasses = [];
  240. foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
  241. if ($this->getDriver()->isTransient($parentClass)) {
  242. continue;
  243. }
  244. $parentClasses[] = $parentClass;
  245. }
  246. return $parentClasses;
  247. }
  248. /**
  249. * Loads the metadata of the class in question and all it's ancestors whose metadata
  250. * is still not loaded.
  251. *
  252. * Important: The class $name does not necessarily exist at this point here.
  253. * Scenarios in a code-generation setup might have access to XML/YAML
  254. * Mapping files without the actual PHP code existing here. That is why the
  255. * {@see \Doctrine\Persistence\Mapping\ReflectionService} interface
  256. * should be used for reflection.
  257. *
  258. * @param string $name The name of the class for which the metadata should get loaded.
  259. * @phpstan-param class-string $name
  260. *
  261. * @return array<int, string>
  262. * @phpstan-return list<string>
  263. */
  264. protected function loadMetadata(string $name)
  265. {
  266. if (! $this->initialized) {
  267. $this->initialize();
  268. }
  269. $loaded = [];
  270. $parentClasses = $this->getParentClasses($name);
  271. $parentClasses[] = $name;
  272. // Move down the hierarchy of parent classes, starting from the topmost class
  273. $parent = null;
  274. $rootEntityFound = false;
  275. $visited = [];
  276. $reflService = $this->getReflectionService();
  277. foreach ($parentClasses as $className) {
  278. if (isset($this->loadedMetadata[$className])) {
  279. $parent = $this->loadedMetadata[$className];
  280. if ($this->isEntity($parent)) {
  281. $rootEntityFound = true;
  282. array_unshift($visited, $className);
  283. }
  284. continue;
  285. }
  286. $class = $this->newClassMetadataInstance($className);
  287. $this->initializeReflection($class, $reflService);
  288. $this->doLoadMetadata($class, $parent, $rootEntityFound, $visited);
  289. $this->loadedMetadata[$className] = $class;
  290. $parent = $class;
  291. if ($this->isEntity($class)) {
  292. $rootEntityFound = true;
  293. array_unshift($visited, $className);
  294. }
  295. $this->wakeupReflection($class, $reflService);
  296. $loaded[] = $className;
  297. }
  298. return $loaded;
  299. }
  300. /**
  301. * Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions
  302. *
  303. * Override this method to implement a fallback strategy for failed metadata loading
  304. *
  305. * @return ClassMetadata|null
  306. * @phpstan-return CMTemplate|null
  307. */
  308. protected function onNotFoundMetadata(string $className)
  309. {
  310. return null;
  311. }
  312. /**
  313. * Actually loads the metadata from the underlying metadata.
  314. *
  315. * @param bool $rootEntityFound True when there is another entity (non-mapped superclass) class above the current class in the PHP class hierarchy.
  316. * @param list<class-string> $nonSuperclassParents All parent class names that are not marked as mapped superclasses, with the direct parent class being the first and the root entity class the last element.
  317. * @phpstan-param CMTemplate $class
  318. * @phpstan-param CMTemplate|null $parent
  319. *
  320. * @return void
  321. */
  322. abstract protected function doLoadMetadata(
  323. ClassMetadata $class,
  324. ?ClassMetadata $parent,
  325. bool $rootEntityFound,
  326. array $nonSuperclassParents
  327. );
  328. /**
  329. * Creates a new ClassMetadata instance for the given class name.
  330. *
  331. * @phpstan-param class-string<T> $className
  332. *
  333. * @return ClassMetadata<T>
  334. * @phpstan-return CMTemplate
  335. *
  336. * @template T of object
  337. */
  338. abstract protected function newClassMetadataInstance(string $className);
  339. /**
  340. * {@inheritDoc}
  341. */
  342. public function isTransient(string $className)
  343. {
  344. if (! $this->initialized) {
  345. $this->initialize();
  346. }
  347. if (class_exists($className, false) && (new ReflectionClass($className))->isAnonymous()) {
  348. return false;
  349. }
  350. if (! class_exists($className, false) && strpos($className, ':') !== false) {
  351. throw MappingException::nonExistingClass($className);
  352. }
  353. /** @phpstan-var class-string $className */
  354. return $this->getDriver()->isTransient($className);
  355. }
  356. /**
  357. * Sets the reflectionService.
  358. *
  359. * @return void
  360. */
  361. public function setReflectionService(ReflectionService $reflectionService)
  362. {
  363. $this->reflectionService = $reflectionService;
  364. }
  365. /**
  366. * Gets the reflection service associated with this metadata factory.
  367. *
  368. * @return ReflectionService
  369. */
  370. public function getReflectionService()
  371. {
  372. if ($this->reflectionService === null) {
  373. $this->reflectionService = new RuntimeReflectionService();
  374. }
  375. return $this->reflectionService;
  376. }
  377. protected function getCacheKey(string $realClassName): string
  378. {
  379. return str_replace('\\', '__', $realClassName) . $this->cacheSalt;
  380. }
  381. /**
  382. * Gets the real class name of a class name that could be a proxy.
  383. *
  384. * @phpstan-param class-string<Proxy<T>>|class-string<T> $class
  385. *
  386. * @phpstan-return class-string<T>
  387. *
  388. * @template T of object
  389. */
  390. private function getRealClass(string $class): string
  391. {
  392. if ($this->proxyClassNameResolver === null) {
  393. $this->createDefaultProxyClassNameResolver();
  394. }
  395. assert($this->proxyClassNameResolver !== null);
  396. return $this->proxyClassNameResolver->resolveClassName($class);
  397. }
  398. private function createDefaultProxyClassNameResolver(): void
  399. {
  400. $this->proxyClassNameResolver = new class implements ProxyClassNameResolver {
  401. /**
  402. * @phpstan-param class-string<Proxy<T>>|class-string<T> $className
  403. *
  404. * @phpstan-return class-string<T>
  405. *
  406. * @template T of object
  407. */
  408. public function resolveClassName(string $className): string
  409. {
  410. $pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
  411. if ($pos === false) {
  412. /** @phpstan-var class-string<T> */
  413. return $className;
  414. }
  415. /** @phpstan-var class-string<T> */
  416. return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
  417. }
  418. };
  419. }
  420. }