AbstractDoctrineExtension.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bridge\Doctrine\DependencyInjection;
  11. use Symfony\Component\DependencyInjection\Alias;
  12. use Symfony\Component\DependencyInjection\ContainerBuilder;
  13. use Symfony\Component\DependencyInjection\Definition;
  14. use Symfony\Component\DependencyInjection\Reference;
  15. use Symfony\Component\HttpKernel\DependencyInjection\Extension;
  16. /**
  17. * This abstract classes groups common code that Doctrine Object Manager extensions (ORM, MongoDB, CouchDB) need.
  18. *
  19. * @author Benjamin Eberlei <kontakt@beberlei.de>
  20. */
  21. abstract class AbstractDoctrineExtension extends Extension
  22. {
  23. /**
  24. * Used inside metadata driver method to simplify aggregation of data.
  25. */
  26. protected $aliasMap = [];
  27. /**
  28. * Used inside metadata driver method to simplify aggregation of data.
  29. */
  30. protected $drivers = [];
  31. /**
  32. * @param array $objectManager A configured object manager
  33. *
  34. * @throws \InvalidArgumentException
  35. */
  36. protected function loadMappingInformation(array $objectManager, ContainerBuilder $container)
  37. {
  38. if ($objectManager['auto_mapping']) {
  39. // automatically register bundle mappings
  40. foreach (array_keys($container->getParameter('kernel.bundles')) as $bundle) {
  41. if (!isset($objectManager['mappings'][$bundle])) {
  42. $objectManager['mappings'][$bundle] = [
  43. 'mapping' => true,
  44. 'is_bundle' => true,
  45. ];
  46. }
  47. }
  48. }
  49. foreach ($objectManager['mappings'] as $mappingName => $mappingConfig) {
  50. if (null !== $mappingConfig && false === $mappingConfig['mapping']) {
  51. continue;
  52. }
  53. $mappingConfig = array_replace([
  54. 'dir' => false,
  55. 'type' => false,
  56. 'prefix' => false,
  57. ], (array) $mappingConfig);
  58. $mappingConfig['dir'] = $container->getParameterBag()->resolveValue($mappingConfig['dir']);
  59. // a bundle configuration is detected by realizing that the specified dir is not absolute and existing
  60. if (!isset($mappingConfig['is_bundle'])) {
  61. $mappingConfig['is_bundle'] = !is_dir($mappingConfig['dir']);
  62. }
  63. if ($mappingConfig['is_bundle']) {
  64. $bundle = null;
  65. foreach ($container->getParameter('kernel.bundles') as $name => $class) {
  66. if ($mappingName === $name) {
  67. $bundle = new \ReflectionClass($class);
  68. break;
  69. }
  70. }
  71. if (null === $bundle) {
  72. throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled.', $mappingName));
  73. }
  74. $mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container);
  75. if (!$mappingConfig) {
  76. continue;
  77. }
  78. }
  79. $this->assertValidMappingConfiguration($mappingConfig, $objectManager['name']);
  80. $this->setMappingDriverConfig($mappingConfig, $mappingName);
  81. $this->setMappingDriverAlias($mappingConfig, $mappingName);
  82. }
  83. }
  84. /**
  85. * Register the alias for this mapping driver.
  86. *
  87. * Aliases can be used in the Query languages of all the Doctrine object managers to simplify writing tasks.
  88. */
  89. protected function setMappingDriverAlias(array $mappingConfig, string $mappingName)
  90. {
  91. if (isset($mappingConfig['alias'])) {
  92. $this->aliasMap[$mappingConfig['alias']] = $mappingConfig['prefix'];
  93. } else {
  94. $this->aliasMap[$mappingName] = $mappingConfig['prefix'];
  95. }
  96. }
  97. /**
  98. * Register the mapping driver configuration for later use with the object managers metadata driver chain.
  99. *
  100. * @throws \InvalidArgumentException
  101. */
  102. protected function setMappingDriverConfig(array $mappingConfig, string $mappingName)
  103. {
  104. $mappingDirectory = $mappingConfig['dir'];
  105. if (!is_dir($mappingDirectory)) {
  106. throw new \InvalidArgumentException(sprintf('Invalid Doctrine mapping path given. Cannot load Doctrine mapping/bundle named "%s".', $mappingName));
  107. }
  108. $this->drivers[$mappingConfig['type']][$mappingConfig['prefix']] = realpath($mappingDirectory) ?: $mappingDirectory;
  109. }
  110. /**
  111. * If this is a bundle controlled mapping all the missing information can be autodetected by this method.
  112. *
  113. * Returns false when autodetection failed, an array of the completed information otherwise.
  114. *
  115. * @return array|false
  116. */
  117. protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \ReflectionClass $bundle, ContainerBuilder $container)
  118. {
  119. $bundleDir = \dirname($bundle->getFileName());
  120. if (!$bundleConfig['type']) {
  121. $bundleConfig['type'] = $this->detectMetadataDriver($bundleDir, $container);
  122. }
  123. if (!$bundleConfig['type']) {
  124. // skip this bundle, no mapping information was found.
  125. return false;
  126. }
  127. if (!$bundleConfig['dir']) {
  128. if (\in_array($bundleConfig['type'], ['annotation', 'staticphp'])) {
  129. $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingObjectDefaultName();
  130. } else {
  131. $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingResourceConfigDirectory();
  132. }
  133. } else {
  134. $bundleConfig['dir'] = $bundleDir.'/'.$bundleConfig['dir'];
  135. }
  136. if (!$bundleConfig['prefix']) {
  137. $bundleConfig['prefix'] = $bundle->getNamespaceName().'\\'.$this->getMappingObjectDefaultName();
  138. }
  139. return $bundleConfig;
  140. }
  141. /**
  142. * Register all the collected mapping information with the object manager by registering the appropriate mapping drivers.
  143. */
  144. protected function registerMappingDrivers(array $objectManager, ContainerBuilder $container)
  145. {
  146. // configure metadata driver for each bundle based on the type of mapping files found
  147. if ($container->hasDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'))) {
  148. $chainDriverDef = $container->getDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'));
  149. } else {
  150. $chainDriverDef = new Definition($this->getMetadataDriverClass('driver_chain'));
  151. $chainDriverDef->setPublic(false);
  152. }
  153. foreach ($this->drivers as $driverType => $driverPaths) {
  154. $mappingService = $this->getObjectManagerElementName($objectManager['name'].'_'.$driverType.'_metadata_driver');
  155. if ($container->hasDefinition($mappingService)) {
  156. $mappingDriverDef = $container->getDefinition($mappingService);
  157. $args = $mappingDriverDef->getArguments();
  158. if ('annotation' == $driverType) {
  159. $args[1] = array_merge(array_values($driverPaths), $args[1]);
  160. } else {
  161. $args[0] = array_merge(array_values($driverPaths), $args[0]);
  162. }
  163. $mappingDriverDef->setArguments($args);
  164. } elseif ('annotation' == $driverType) {
  165. $mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [
  166. new Reference($this->getObjectManagerElementName('metadata.annotation_reader')),
  167. array_values($driverPaths),
  168. ]);
  169. } else {
  170. $mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [
  171. array_values($driverPaths),
  172. ]);
  173. }
  174. $mappingDriverDef->setPublic(false);
  175. if (false !== strpos($mappingDriverDef->getClass(), 'yml') || false !== strpos($mappingDriverDef->getClass(), 'xml')) {
  176. $mappingDriverDef->setArguments([array_flip($driverPaths)]);
  177. $mappingDriverDef->addMethodCall('setGlobalBasename', ['mapping']);
  178. }
  179. $container->setDefinition($mappingService, $mappingDriverDef);
  180. foreach ($driverPaths as $prefix => $driverPath) {
  181. $chainDriverDef->addMethodCall('addDriver', [new Reference($mappingService), $prefix]);
  182. }
  183. }
  184. $container->setDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'), $chainDriverDef);
  185. }
  186. /**
  187. * Assertion if the specified mapping information is valid.
  188. *
  189. * @throws \InvalidArgumentException
  190. */
  191. protected function assertValidMappingConfiguration(array $mappingConfig, string $objectManagerName)
  192. {
  193. if (!$mappingConfig['type'] || !$mappingConfig['dir'] || !$mappingConfig['prefix']) {
  194. throw new \InvalidArgumentException(sprintf('Mapping definitions for Doctrine manager "%s" require at least the "type", "dir" and "prefix" options.', $objectManagerName));
  195. }
  196. if (!is_dir($mappingConfig['dir'])) {
  197. throw new \InvalidArgumentException(sprintf('Specified non-existing directory "%s" as Doctrine mapping source.', $mappingConfig['dir']));
  198. }
  199. if (!\in_array($mappingConfig['type'], ['xml', 'yml', 'annotation', 'php', 'staticphp'])) {
  200. throw new \InvalidArgumentException(sprintf('Can only configure "xml", "yml", "annotation", "php" or "staticphp" through the DoctrineBundle. Use your own bundle to configure other metadata drivers. You can register them by adding a new driver to the "%s" service definition.', $this->getObjectManagerElementName($objectManagerName.'_metadata_driver')));
  201. }
  202. }
  203. /**
  204. * Detects what metadata driver to use for the supplied directory.
  205. *
  206. * @return string|null A metadata driver short name, if one can be detected
  207. */
  208. protected function detectMetadataDriver(string $dir, ContainerBuilder $container)
  209. {
  210. $configPath = $this->getMappingResourceConfigDirectory();
  211. $extension = $this->getMappingResourceExtension();
  212. if (glob($dir.'/'.$configPath.'/*.'.$extension.'.xml', \GLOB_NOSORT)) {
  213. $driver = 'xml';
  214. } elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.yml', \GLOB_NOSORT)) {
  215. $driver = 'yml';
  216. } elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.php', \GLOB_NOSORT)) {
  217. $driver = 'php';
  218. } else {
  219. // add the closest existing directory as a resource
  220. $resource = $dir.'/'.$configPath;
  221. while (!is_dir($resource)) {
  222. $resource = \dirname($resource);
  223. }
  224. $container->fileExists($resource, false);
  225. return $container->fileExists($dir.'/'.$this->getMappingObjectDefaultName(), false) ? 'annotation' : null;
  226. }
  227. $container->fileExists($dir.'/'.$configPath, false);
  228. return $driver;
  229. }
  230. /**
  231. * Loads a configured object manager metadata, query or result cache driver.
  232. *
  233. * @throws \InvalidArgumentException in case of unknown driver type
  234. */
  235. protected function loadObjectManagerCacheDriver(array $objectManager, ContainerBuilder $container, string $cacheName)
  236. {
  237. $this->loadCacheDriver($cacheName, $objectManager['name'], $objectManager[$cacheName.'_driver'], $container);
  238. }
  239. /**
  240. * Loads a cache driver.
  241. *
  242. * @return string
  243. *
  244. * @throws \InvalidArgumentException
  245. */
  246. protected function loadCacheDriver(string $cacheName, string $objectManagerName, array $cacheDriver, ContainerBuilder $container)
  247. {
  248. $cacheDriverServiceId = $this->getObjectManagerElementName($objectManagerName.'_'.$cacheName);
  249. switch ($cacheDriver['type']) {
  250. case 'service':
  251. $container->setAlias($cacheDriverServiceId, new Alias($cacheDriver['id'], false));
  252. return $cacheDriverServiceId;
  253. case 'memcached':
  254. $memcachedClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.memcached.class').'%';
  255. $memcachedInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.memcached_instance.class').'%';
  256. $memcachedHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.memcached_host').'%';
  257. $memcachedPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.memcached_port').'%';
  258. $cacheDef = new Definition($memcachedClass);
  259. $memcachedInstance = new Definition($memcachedInstanceClass);
  260. $memcachedInstance->addMethodCall('addServer', [
  261. $memcachedHost, $memcachedPort,
  262. ]);
  263. $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)), $memcachedInstance);
  264. $cacheDef->addMethodCall('setMemcached', [new Reference($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)))]);
  265. break;
  266. case 'redis':
  267. $redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.redis.class').'%';
  268. $redisInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.redis_instance.class').'%';
  269. $redisHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.redis_host').'%';
  270. $redisPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.redis_port').'%';
  271. $cacheDef = new Definition($redisClass);
  272. $redisInstance = new Definition($redisInstanceClass);
  273. $redisInstance->addMethodCall('connect', [
  274. $redisHost, $redisPort,
  275. ]);
  276. $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManagerName)), $redisInstance);
  277. $cacheDef->addMethodCall('setRedis', [new Reference($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManagerName)))]);
  278. break;
  279. case 'apc':
  280. case 'apcu':
  281. case 'array':
  282. case 'xcache':
  283. case 'wincache':
  284. case 'zenddata':
  285. $cacheDef = new Definition('%'.$this->getObjectManagerElementName(sprintf('cache.%s.class', $cacheDriver['type'])).'%');
  286. break;
  287. default:
  288. throw new \InvalidArgumentException(sprintf('"%s" is an unrecognized Doctrine cache driver.', $cacheDriver['type']));
  289. }
  290. $cacheDef->setPublic(false);
  291. if (!isset($cacheDriver['namespace'])) {
  292. // generate a unique namespace for the given application
  293. if ($container->hasParameter('cache.prefix.seed')) {
  294. $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed'));
  295. } else {
  296. $seed = '_'.$container->getParameter('kernel.project_dir');
  297. $seed .= '.'.$container->getParameter('kernel.container_class');
  298. }
  299. $namespace = 'sf_'.$this->getMappingResourceExtension().'_'.$objectManagerName.'_'.ContainerBuilder::hash($seed);
  300. $cacheDriver['namespace'] = $namespace;
  301. }
  302. $cacheDef->addMethodCall('setNamespace', [$cacheDriver['namespace']]);
  303. $container->setDefinition($cacheDriverServiceId, $cacheDef);
  304. return $cacheDriverServiceId;
  305. }
  306. /**
  307. * Returns a modified version of $managerConfigs.
  308. *
  309. * The manager called $autoMappedManager will map all bundles that are not mapped by other managers.
  310. *
  311. * @return array The modified version of $managerConfigs
  312. */
  313. protected function fixManagersAutoMappings(array $managerConfigs, array $bundles)
  314. {
  315. if ($autoMappedManager = $this->validateAutoMapping($managerConfigs)) {
  316. foreach (array_keys($bundles) as $bundle) {
  317. foreach ($managerConfigs as $manager) {
  318. if (isset($manager['mappings'][$bundle])) {
  319. continue 2;
  320. }
  321. }
  322. $managerConfigs[$autoMappedManager]['mappings'][$bundle] = [
  323. 'mapping' => true,
  324. 'is_bundle' => true,
  325. ];
  326. }
  327. $managerConfigs[$autoMappedManager]['auto_mapping'] = false;
  328. }
  329. return $managerConfigs;
  330. }
  331. /**
  332. * Prefixes the relative dependency injection container path with the object manager prefix.
  333. *
  334. * @example $name is 'entity_manager' then the result would be 'doctrine.orm.entity_manager'
  335. *
  336. * @return string
  337. */
  338. abstract protected function getObjectManagerElementName(string $name);
  339. /**
  340. * Noun that describes the mapped objects such as Entity or Document.
  341. *
  342. * Will be used for autodetection of persistent objects directory.
  343. *
  344. * @return string
  345. */
  346. abstract protected function getMappingObjectDefaultName();
  347. /**
  348. * Relative path from the bundle root to the directory where mapping files reside.
  349. *
  350. * @return string
  351. */
  352. abstract protected function getMappingResourceConfigDirectory();
  353. /**
  354. * Extension used by the mapping files.
  355. *
  356. * @return string
  357. */
  358. abstract protected function getMappingResourceExtension();
  359. /**
  360. * The class name used by the various mapping drivers.
  361. */
  362. abstract protected function getMetadataDriverClass(string $driverType): string;
  363. /**
  364. * Search for a manager that is declared as 'auto_mapping' = true.
  365. *
  366. * @throws \LogicException
  367. */
  368. private function validateAutoMapping(array $managerConfigs): ?string
  369. {
  370. $autoMappedManager = null;
  371. foreach ($managerConfigs as $name => $manager) {
  372. if (!$manager['auto_mapping']) {
  373. continue;
  374. }
  375. if (null !== $autoMappedManager) {
  376. throw new \LogicException(sprintf('You cannot enable "auto_mapping" on more than one manager at the same time (found in "%s" and "%s"").', $autoMappedManager, $name));
  377. }
  378. $autoMappedManager = $name;
  379. }
  380. return $autoMappedManager;
  381. }
  382. }