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