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

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