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

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