vendor/gedmo/doctrine-extensions/src/Mapping/MappedEventSubscriber.php line 242

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Doctrine Behavioral Extensions package.
  4. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. namespace Gedmo\Mapping;
  9. use Doctrine\Common\Annotations\AnnotationReader;
  10. use Doctrine\Common\Annotations\PsrCachedReader;
  11. use Doctrine\Common\Annotations\Reader;
  12. use Doctrine\Common\EventArgs;
  13. use Doctrine\Common\EventSubscriber;
  14. use Doctrine\Deprecations\Deprecation;
  15. use Doctrine\ODM\MongoDB\DocumentManager;
  16. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as DocumentClassMetadata;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use Doctrine\ORM\Mapping\ClassMetadata as EntityClassMetadata;
  19. use Doctrine\ORM\Mapping\ClassMetadataInfo as LegacyEntityClassMetadata;
  20. use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
  21. use Doctrine\Persistence\Mapping\ClassMetadata;
  22. use Doctrine\Persistence\ObjectManager;
  23. use Gedmo\Exception\InvalidArgumentException;
  24. use Gedmo\Mapping\Driver\AttributeReader;
  25. use Gedmo\Mapping\Event\AdapterInterface;
  26. use Gedmo\Mapping\Event\ClockAwareAdapterInterface;
  27. use Psr\Cache\CacheItemPoolInterface;
  28. use Psr\Clock\ClockInterface;
  29. use Symfony\Component\Cache\Adapter\ArrayAdapter;
  30. /**
  31. * This is extension of event subscriber class and is
  32. * used specifically for handling the extension metadata
  33. * mapping for extensions.
  34. *
  35. * It dries up some reusable code which is common for
  36. * all extensions who maps additional metadata through
  37. * extended drivers
  38. *
  39. * @phpstan-template TConfig of array
  40. * @phpstan-template TEventAdapter of AdapterInterface
  41. *
  42. * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  43. */
  44. abstract class MappedEventSubscriber implements EventSubscriber
  45. {
  46. /**
  47. * Static List of cached object configurations
  48. * leaving it static for reasons to look into
  49. * other listener configuration
  50. *
  51. * @var array<string, array<string, array<string, mixed>>>
  52. *
  53. * @phpstan-var array<string, array<class-string, array<string, mixed>>>
  54. */
  55. protected static $configurations = [];
  56. /**
  57. * Listener name, etc: sluggable
  58. *
  59. * @var string
  60. */
  61. protected $name;
  62. /**
  63. * ExtensionMetadataFactory used to read the extension
  64. * metadata through the extension drivers
  65. *
  66. * @var array<int, ExtensionMetadataFactory>
  67. */
  68. private array $extensionMetadataFactory = [];
  69. /**
  70. * List of event adapters used for this listener
  71. *
  72. * @var array<string, AdapterInterface>
  73. */
  74. private array $adapters = [];
  75. /**
  76. * Custom annotation reader
  77. *
  78. * @var Reader|AttributeReader|object|false|null
  79. */
  80. private $annotationReader = false;
  81. /**
  82. * @var Reader|AttributeReader|false|null
  83. */
  84. private static $defaultAnnotationReader = false;
  85. /**
  86. * @var CacheItemPoolInterface|null
  87. */
  88. private $cacheItemPool;
  89. private ?ClockInterface $clock = null;
  90. public function __construct()
  91. {
  92. $parts = explode('\\', $this->getNamespace());
  93. $this->name = end($parts);
  94. }
  95. /**
  96. * Get the configuration for specific object class
  97. * if cache driver is present it scans it also
  98. *
  99. * @param string $class
  100. *
  101. * @phpstan-param class-string $class
  102. *
  103. * @return array<string, mixed>
  104. *
  105. * @phpstan-return TConfig
  106. */
  107. public function getConfiguration(ObjectManager $objectManager, $class)
  108. {
  109. if (isset(self::$configurations[$this->name][$class])) {
  110. return self::$configurations[$this->name][$class];
  111. }
  112. $config = [];
  113. $cacheItemPool = $this->getCacheItemPool($objectManager);
  114. $cacheId = ExtensionMetadataFactory::getCacheId($class, $this->getNamespace());
  115. $cacheItem = $cacheItemPool->getItem($cacheId);
  116. if ($cacheItem->isHit()) {
  117. $config = $cacheItem->get();
  118. self::$configurations[$this->name][$class] = $config;
  119. } else {
  120. // re-generate metadata on cache miss
  121. $this->loadMetadataForObjectClass($objectManager, $objectManager->getClassMetadata($class));
  122. if (isset(self::$configurations[$this->name][$class])) {
  123. $config = self::$configurations[$this->name][$class];
  124. }
  125. }
  126. $objectClass = $config['useObjectClass'] ?? $class;
  127. if ($objectClass !== $class) {
  128. $this->getConfiguration($objectManager, $objectClass);
  129. }
  130. return $config;
  131. }
  132. /**
  133. * Get extended metadata mapping reader
  134. *
  135. * @return ExtensionMetadataFactory
  136. */
  137. public function getExtensionMetadataFactory(ObjectManager $objectManager)
  138. {
  139. $oid = spl_object_id($objectManager);
  140. if (!isset($this->extensionMetadataFactory[$oid])) {
  141. if (false === $this->annotationReader) {
  142. // create default annotation/attribute reader for extensions
  143. $this->annotationReader = $this->getDefaultAnnotationReader();
  144. }
  145. $this->extensionMetadataFactory[$oid] = new ExtensionMetadataFactory(
  146. $objectManager,
  147. $this->getNamespace(),
  148. $this->annotationReader,
  149. $this->getCacheItemPool($objectManager)
  150. );
  151. }
  152. return $this->extensionMetadataFactory[$oid];
  153. }
  154. /**
  155. * Set the annotation reader instance
  156. *
  157. * When originally implemented, `Doctrine\Common\Annotations\Reader` was not available,
  158. * therefore this method may accept any object implementing these methods from the interface:
  159. *
  160. * getClassAnnotations([reflectionClass])
  161. * getClassAnnotation([reflectionClass], [name])
  162. * getPropertyAnnotations([reflectionProperty])
  163. * getPropertyAnnotation([reflectionProperty], [name])
  164. *
  165. * @param Reader|AttributeReader|object $reader
  166. *
  167. * @return void
  168. *
  169. * @note Providing any object is deprecated, as of 4.0 an {@see AttributeReader} will be required
  170. */
  171. public function setAnnotationReader($reader)
  172. {
  173. if ($reader instanceof Reader) {
  174. Deprecation::trigger(
  175. 'gedmo/doctrine-extensions',
  176. 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2772',
  177. 'Annotations support is deprecated, migrate your application to use attributes and pass an instance of %s to the %s() method instead.',
  178. AttributeReader::class,
  179. __METHOD__
  180. );
  181. } elseif (!$reader instanceof AttributeReader) {
  182. Deprecation::trigger(
  183. 'gedmo/doctrine-extensions',
  184. 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2558',
  185. 'Providing an annotation reader which does not implement %s or is not an instance of %s to %s() is deprecated.',
  186. Reader::class,
  187. AttributeReader::class,
  188. __METHOD__
  189. );
  190. }
  191. $this->annotationReader = $reader;
  192. }
  193. final public function setCacheItemPool(CacheItemPoolInterface $cacheItemPool): void
  194. {
  195. $this->cacheItemPool = $cacheItemPool;
  196. }
  197. final public function setClock(ClockInterface $clock): void
  198. {
  199. $this->clock = $clock;
  200. }
  201. /**
  202. * Scans the objects for extended annotations
  203. * event subscribers must subscribe to loadClassMetadata event
  204. *
  205. * @param ClassMetadata<object> $metadata
  206. *
  207. * @return void
  208. */
  209. public function loadMetadataForObjectClass(ObjectManager $objectManager, $metadata)
  210. {
  211. assert($metadata instanceof DocumentClassMetadata || $metadata instanceof EntityClassMetadata || $metadata instanceof LegacyEntityClassMetadata);
  212. $factory = $this->getExtensionMetadataFactory($objectManager);
  213. try {
  214. $config = $factory->getExtensionMetadata($metadata);
  215. } catch (\ReflectionException $e) {
  216. // entity\document generator is running
  217. $config = []; // will not store a cached version, to remap later
  218. }
  219. if ([] !== $config) {
  220. self::$configurations[$this->name][$metadata->getName()] = $config;
  221. }
  222. }
  223. /**
  224. * Get an event adapter to handle event specific
  225. * methods
  226. *
  227. * @throws InvalidArgumentException if event is not recognized
  228. *
  229. * @return AdapterInterface
  230. *
  231. * @phpstan-return TEventAdapter
  232. */
  233. protected function getEventAdapter(EventArgs $args)
  234. {
  235. $class = get_class($args);
  236. if (preg_match('@Doctrine\\\([^\\\]+)@', $class, $m) && in_array($m[1], ['ODM', 'ORM'], true)) {
  237. if (!isset($this->adapters[$m[1]])) {
  238. $adapterClass = $this->getNamespace().'\\Mapping\\Event\\Adapter\\'.$m[1];
  239. if (!\class_exists($adapterClass)) {
  240. $adapterClass = 'Gedmo\\Mapping\\Event\\Adapter\\'.$m[1];
  241. }
  242. $this->adapters[$m[1]] = new $adapterClass();
  243. if ($this->adapters[$m[1]] instanceof ClockAwareAdapterInterface && $this->clock instanceof ClockInterface) {
  244. $this->adapters[$m[1]]->setClock($this->clock);
  245. }
  246. }
  247. $this->adapters[$m[1]]->setEventArgs($args);
  248. return $this->adapters[$m[1]];
  249. }
  250. throw new InvalidArgumentException('Event mapper does not support event arg class: '.$class);
  251. }
  252. /**
  253. * Get the namespace of extension event subscriber.
  254. * used for cache id of extensions also to know where
  255. * to find Mapping drivers and event adapters
  256. *
  257. * @return string
  258. */
  259. abstract protected function getNamespace();
  260. /**
  261. * Sets the value for a mapped field
  262. *
  263. * @param object $object
  264. * @param string $field
  265. * @param mixed $oldValue
  266. * @param mixed $newValue
  267. *
  268. * @return void
  269. */
  270. protected function setFieldValue(AdapterInterface $adapter, $object, $field, $oldValue, $newValue)
  271. {
  272. $manager = $adapter->getObjectManager();
  273. $meta = $manager->getClassMetadata(get_class($object));
  274. $uow = $manager->getUnitOfWork();
  275. $meta->getReflectionProperty($field)->setValue($object, $newValue);
  276. $uow->propertyChanged($object, $field, $oldValue, $newValue);
  277. $adapter->recomputeSingleObjectChangeSet($uow, $meta, $object);
  278. }
  279. /**
  280. * Get the default annotation or attribute reader for extensions, creating it if necessary.
  281. *
  282. * If a reader cannot be created due to missing requirements, no default will be set as the reader is only required for annotation or attribute metadata,
  283. * and the {@see ExtensionMetadataFactory} can handle raising an error if it tries to create a mapping driver that requires this reader.
  284. *
  285. * @return Reader|AttributeReader|null
  286. */
  287. private function getDefaultAnnotationReader()
  288. {
  289. if (false === self::$defaultAnnotationReader) {
  290. if (class_exists(PsrCachedReader::class)) {
  291. self::$defaultAnnotationReader = new PsrCachedReader(new AnnotationReader(), new ArrayAdapter());
  292. } elseif (\PHP_VERSION_ID >= 80000) {
  293. self::$defaultAnnotationReader = new AttributeReader();
  294. } else {
  295. self::$defaultAnnotationReader = null;
  296. }
  297. }
  298. return self::$defaultAnnotationReader;
  299. }
  300. private function getCacheItemPool(ObjectManager $objectManager): CacheItemPoolInterface
  301. {
  302. if (null !== $this->cacheItemPool) {
  303. return $this->cacheItemPool;
  304. }
  305. // TODO: The user should configure its own cache, we are using the one from Doctrine for BC. We should deprecate using
  306. // the one from Doctrine when the bundle offers an easy way to configure this cache, otherwise users using the bundle
  307. // will see lots of deprecations without an easy way to avoid them.
  308. if ($objectManager instanceof EntityManagerInterface || $objectManager instanceof DocumentManager) {
  309. $metadataFactory = $objectManager->getMetadataFactory();
  310. $getCache = \Closure::bind(static fn (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface => $metadataFactory->getCache(), null, \get_class($metadataFactory));
  311. $metadataCache = $getCache($metadataFactory);
  312. if (null !== $metadataCache) {
  313. $this->cacheItemPool = $metadataCache;
  314. return $this->cacheItemPool;
  315. }
  316. }
  317. $this->cacheItemPool = new ArrayAdapter();
  318. return $this->cacheItemPool;
  319. }
  320. }