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

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.         /**
  144.          * @phpstan-ignore-next-line
  145.          */
  146.         return ltrim($className'\\');
  147.     }
  148.     /**
  149.      * {@inheritDoc}
  150.      *
  151.      * @throws ReflectionException
  152.      * @throws MappingException
  153.      */
  154.     public function getMetadataFor(string $className)
  155.     {
  156.         $className $this->normalizeClassName($className);
  157.         if (isset($this->loadedMetadata[$className])) {
  158.             return $this->loadedMetadata[$className];
  159.         }
  160.         if (class_exists($classNamefalse) && (new ReflectionClass($className))->isAnonymous()) {
  161.             throw MappingException::classIsAnonymous($className);
  162.         }
  163.         if (! class_exists($classNamefalse) && strpos($className':') !== false) {
  164.             throw MappingException::nonExistingClass($className);
  165.         }
  166.         $realClassName $this->getRealClass($className);
  167.         if (isset($this->loadedMetadata[$realClassName])) {
  168.             // We do not have the alias name in the map, include it
  169.             return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
  170.         }
  171.         try {
  172.             if ($this->cache !== null) {
  173.                 $cached $this->cache->getItem($this->getCacheKey($realClassName))->get();
  174.                 if ($cached instanceof ClassMetadata) {
  175.                     /** @psalm-var CMTemplate $cached */
  176.                     $this->loadedMetadata[$realClassName] = $cached;
  177.                     $this->wakeupReflection($cached$this->getReflectionService());
  178.                 } else {
  179.                     $loadedMetadata $this->loadMetadata($realClassName);
  180.                     $classNames     array_combine(
  181.                         array_map([$this'getCacheKey'], $loadedMetadata),
  182.                         $loadedMetadata
  183.                     );
  184.                     foreach ($this->cache->getItems(array_keys($classNames)) as $item) {
  185.                         if (! isset($classNames[$item->getKey()])) {
  186.                             continue;
  187.                         }
  188.                         $item->set($this->loadedMetadata[$classNames[$item->getKey()]]);
  189.                         $this->cache->saveDeferred($item);
  190.                     }
  191.                     $this->cache->commit();
  192.                 }
  193.             } else {
  194.                 $this->loadMetadata($realClassName);
  195.             }
  196.         } catch (MappingException $loadingException) {
  197.             $fallbackMetadataResponse $this->onNotFoundMetadata($realClassName);
  198.             if ($fallbackMetadataResponse === null) {
  199.                 throw $loadingException;
  200.             }
  201.             $this->loadedMetadata[$realClassName] = $fallbackMetadataResponse;
  202.         }
  203.         if ($className !== $realClassName) {
  204.             // We do not have the alias name in the map, include it
  205.             $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
  206.         }
  207.         return $this->loadedMetadata[$className];
  208.     }
  209.     /**
  210.      * {@inheritDoc}
  211.      */
  212.     public function hasMetadataFor(string $className)
  213.     {
  214.         $className $this->normalizeClassName($className);
  215.         return isset($this->loadedMetadata[$className]);
  216.     }
  217.     /**
  218.      * Sets the metadata descriptor for a specific class.
  219.      *
  220.      * NOTE: This is only useful in very special cases, like when generating proxy classes.
  221.      *
  222.      * @psalm-param class-string $className
  223.      * @psalm-param CMTemplate $class
  224.      *
  225.      * @return void
  226.      */
  227.     public function setMetadataFor(string $classNameClassMetadata $class)
  228.     {
  229.         $this->loadedMetadata[$this->normalizeClassName($className)] = $class;
  230.     }
  231.     /**
  232.      * Gets an array of parent classes for the given entity class.
  233.      *
  234.      * @psalm-param class-string $name
  235.      *
  236.      * @return string[]
  237.      * @psalm-return class-string[]
  238.      */
  239.     protected function getParentClasses(string $name)
  240.     {
  241.         // Collect parent classes, ignoring transient (not-mapped) classes.
  242.         $parentClasses = [];
  243.         foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
  244.             if ($this->getDriver()->isTransient($parentClass)) {
  245.                 continue;
  246.             }
  247.             $parentClasses[] = $parentClass;
  248.         }
  249.         return $parentClasses;
  250.     }
  251.     /**
  252.      * Loads the metadata of the class in question and all it's ancestors whose metadata
  253.      * is still not loaded.
  254.      *
  255.      * Important: The class $name does not necessarily exist at this point here.
  256.      * Scenarios in a code-generation setup might have access to XML/YAML
  257.      * Mapping files without the actual PHP code existing here. That is why the
  258.      * {@see \Doctrine\Persistence\Mapping\ReflectionService} interface
  259.      * should be used for reflection.
  260.      *
  261.      * @param string $name The name of the class for which the metadata should get loaded.
  262.      * @psalm-param class-string $name
  263.      *
  264.      * @return array<int, string>
  265.      */
  266.     protected function loadMetadata(string $name)
  267.     {
  268.         if (! $this->initialized) {
  269.             $this->initialize();
  270.         }
  271.         $loaded = [];
  272.         $parentClasses   $this->getParentClasses($name);
  273.         $parentClasses[] = $name;
  274.         // Move down the hierarchy of parent classes, starting from the topmost class
  275.         $parent          null;
  276.         $rootEntityFound false;
  277.         $visited         = [];
  278.         $reflService     $this->getReflectionService();
  279.         foreach ($parentClasses as $className) {
  280.             if (isset($this->loadedMetadata[$className])) {
  281.                 $parent $this->loadedMetadata[$className];
  282.                 if ($this->isEntity($parent)) {
  283.                     $rootEntityFound true;
  284.                     array_unshift($visited$className);
  285.                 }
  286.                 continue;
  287.             }
  288.             $class $this->newClassMetadataInstance($className);
  289.             $this->initializeReflection($class$reflService);
  290.             $this->doLoadMetadata($class$parent$rootEntityFound$visited);
  291.             $this->loadedMetadata[$className] = $class;
  292.             $parent $class;
  293.             if ($this->isEntity($class)) {
  294.                 $rootEntityFound true;
  295.                 array_unshift($visited$className);
  296.             }
  297.             $this->wakeupReflection($class$reflService);
  298.             $loaded[] = $className;
  299.         }
  300.         return $loaded;
  301.     }
  302.     /**
  303.      * Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions
  304.      *
  305.      * Override this method to implement a fallback strategy for failed metadata loading
  306.      *
  307.      * @return ClassMetadata|null
  308.      * @psalm-return CMTemplate|null
  309.      */
  310.     protected function onNotFoundMetadata(string $className)
  311.     {
  312.         return null;
  313.     }
  314.     /**
  315.      * Actually loads the metadata from the underlying metadata.
  316.      *
  317.      * @param string[] $nonSuperclassParents All parent class names that are
  318.      *                                       not marked as mapped superclasses.
  319.      * @psalm-param CMTemplate $class
  320.      * @psalm-param CMTemplate|null $parent
  321.      *
  322.      * @return void
  323.      */
  324.     abstract protected function doLoadMetadata(
  325.         ClassMetadata $class,
  326.         ?ClassMetadata $parent,
  327.         bool $rootEntityFound,
  328.         array $nonSuperclassParents
  329.     );
  330.     /**
  331.      * Creates a new ClassMetadata instance for the given class name.
  332.      *
  333.      * @psalm-param class-string<T> $className
  334.      *
  335.      * @return ClassMetadata<T>
  336.      * @psalm-return CMTemplate
  337.      *
  338.      * @template T of object
  339.      */
  340.     abstract protected function newClassMetadataInstance(string $className);
  341.     /**
  342.      * {@inheritDoc}
  343.      */
  344.     public function isTransient(string $className)
  345.     {
  346.         if (! $this->initialized) {
  347.             $this->initialize();
  348.         }
  349.         if (class_exists($classNamefalse) && (new ReflectionClass($className))->isAnonymous()) {
  350.             return false;
  351.         }
  352.         if (! class_exists($classNamefalse) && strpos($className':') !== false) {
  353.             throw MappingException::nonExistingClass($className);
  354.         }
  355.         /** @psalm-var class-string $className */
  356.         return $this->getDriver()->isTransient($className);
  357.     }
  358.     /**
  359.      * Sets the reflectionService.
  360.      *
  361.      * @return void
  362.      */
  363.     public function setReflectionService(ReflectionService $reflectionService)
  364.     {
  365.         $this->reflectionService $reflectionService;
  366.     }
  367.     /**
  368.      * Gets the reflection service associated with this metadata factory.
  369.      *
  370.      * @return ReflectionService
  371.      */
  372.     public function getReflectionService()
  373.     {
  374.         if ($this->reflectionService === null) {
  375.             $this->reflectionService = new RuntimeReflectionService();
  376.         }
  377.         return $this->reflectionService;
  378.     }
  379.     protected function getCacheKey(string $realClassName): string
  380.     {
  381.         return str_replace('\\''__'$realClassName) . $this->cacheSalt;
  382.     }
  383.     /**
  384.      * Gets the real class name of a class name that could be a proxy.
  385.      *
  386.      * @psalm-param class-string<Proxy<T>>|class-string<T> $class
  387.      *
  388.      * @psalm-return class-string<T>
  389.      *
  390.      * @template T of object
  391.      */
  392.     private function getRealClass(string $class): string
  393.     {
  394.         if ($this->proxyClassNameResolver === null) {
  395.             $this->createDefaultProxyClassNameResolver();
  396.         }
  397.         assert($this->proxyClassNameResolver !== null);
  398.         return $this->proxyClassNameResolver->resolveClassName($class);
  399.     }
  400.     private function createDefaultProxyClassNameResolver(): void
  401.     {
  402.         $this->proxyClassNameResolver = new class implements ProxyClassNameResolver {
  403.             /**
  404.              * @psalm-param class-string<Proxy<T>>|class-string<T> $className
  405.              *
  406.              * @psalm-return class-string<T>
  407.              *
  408.              * @template T of object
  409.              */
  410.             public function resolveClassName(string $className): string
  411.             {
  412.                 $pos strrpos($className'\\' Proxy::MARKER '\\');
  413.                 if ($pos === false) {
  414.                     /** @psalm-var class-string<T> */
  415.                     return $className;
  416.                 }
  417.                 /** @psalm-var class-string<T> */
  418.                 return substr($className$pos Proxy::MARKER_LENGTH 2);
  419.             }
  420.         };
  421.     }
  422. }