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

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.      * @psalm-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.      * @psalm-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.      * @psalm-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.      * @psalm-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.      * @psalm-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.      * @psalm-param class-string $className
  138.      *
  139.      * @psalm-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($classNamefalse) && (new ReflectionClass($className))->isAnonymous()) {
  158.             throw MappingException::classIsAnonymous($className);
  159.         }
  160.         if (! class_exists($classNamefalse) && 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.                     /** @psalm-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.      * @psalm-param class-string $className
  220.      * @psalm-param CMTemplate $class
  221.      *
  222.      * @return void
  223.      */
  224.     public function setMetadataFor(string $classNameClassMetadata $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.      * @psalm-param class-string $name
  232.      *
  233.      * @return string[]
  234.      * @psalm-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.      * @psalm-param class-string $name
  260.      *
  261.      * @return array<int, string>
  262.      * @psalm-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.      * @psalm-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 string[] $nonSuperclassParents All parent class names that are
  316.      *                                       not marked as mapped superclasses.
  317.      * @psalm-param CMTemplate $class
  318.      * @psalm-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.      * @psalm-param class-string<T> $className
  332.      *
  333.      * @return ClassMetadata<T>
  334.      * @psalm-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($classNamefalse) && (new ReflectionClass($className))->isAnonymous()) {
  348.             return false;
  349.         }
  350.         if (! class_exists($classNamefalse) && strpos($className':') !== false) {
  351.             throw MappingException::nonExistingClass($className);
  352.         }
  353.         /** @psalm-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.      * @psalm-param class-string<Proxy<T>>|class-string<T> $class
  385.      *
  386.      * @psalm-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.              * @psalm-param class-string<Proxy<T>>|class-string<T> $className
  403.              *
  404.              * @psalm-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.                     /** @psalm-var class-string<T> */
  413.                     return $className;
  414.                 }
  415.                 /** @psalm-var class-string<T> */
  416.                 return substr($className$pos Proxy::MARKER_LENGTH 2);
  417.             }
  418.         };
  419.     }
  420. }