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

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