ViewsData.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <?php
  2. namespace Drupal\views;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Cache\Cache;
  5. use Drupal\Core\Cache\CacheBackendInterface;
  6. use Drupal\Core\Config\ConfigFactoryInterface;
  7. use Drupal\Core\Extension\ModuleHandlerInterface;
  8. use Drupal\Core\Language\LanguageManagerInterface;
  9. /**
  10. * Class to manage and lazy load cached views data.
  11. *
  12. * If a table is requested and cannot be loaded from cache, all data is then
  13. * requested from cache. A table-specific cache entry will then be created for
  14. * the requested table based on this cached data. Table data is only rebuilt
  15. * when no cache entry for all table data can be retrieved.
  16. */
  17. class ViewsData {
  18. /**
  19. * The base cache ID to use.
  20. *
  21. * @var string
  22. */
  23. protected $baseCid = 'views_data';
  24. /**
  25. * The cache backend to use.
  26. *
  27. * @var \Drupal\Core\Cache\CacheBackendInterface
  28. */
  29. protected $cacheBackend;
  30. /**
  31. * Table data storage.
  32. *
  33. * This is used for explicitly requested tables.
  34. *
  35. * @var array
  36. */
  37. protected $storage = [];
  38. /**
  39. * All table storage data loaded from cache.
  40. *
  41. * This is used when all data has been loaded from the cache to prevent
  42. * further cache get calls when rebuilding all data or for single tables.
  43. *
  44. * @var array
  45. */
  46. protected $allStorage = [];
  47. /**
  48. * Whether the data has been fully loaded in this request.
  49. *
  50. * @var bool
  51. */
  52. protected $fullyLoaded = FALSE;
  53. /**
  54. * Whether or not to skip data caching and rebuild data each time.
  55. *
  56. * @var bool
  57. */
  58. protected $skipCache = FALSE;
  59. /**
  60. * The current language code.
  61. *
  62. * @var string
  63. */
  64. protected $langcode;
  65. /**
  66. * Stores a module manager to invoke hooks.
  67. *
  68. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  69. */
  70. protected $moduleHandler;
  71. /**
  72. * The language manager.
  73. *
  74. * @var \Drupal\Core\Language\LanguageManagerInterface
  75. */
  76. protected $languageManager;
  77. /**
  78. * Constructs this ViewsData object.
  79. *
  80. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
  81. * The cache backend to use.
  82. * @param \Drupal\Core\Config\ConfigFactoryInterface $config
  83. * The configuration factory object to use.
  84. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  85. * The module handler class to use for invoking hooks.
  86. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
  87. * The language manager.
  88. */
  89. public function __construct(CacheBackendInterface $cache_backend, ConfigFactoryInterface $config, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) {
  90. $this->cacheBackend = $cache_backend;
  91. $this->moduleHandler = $module_handler;
  92. $this->languageManager = $language_manager;
  93. $this->langcode = $this->languageManager->getCurrentLanguage()->getId();
  94. $this->skipCache = $config->get('views.settings')->get('skip_cache');
  95. }
  96. /**
  97. * Gets all table data.
  98. *
  99. * @see https://www.drupal.org/node/2723553
  100. *
  101. * @return array
  102. * An array of table data.
  103. */
  104. public function getAll() {
  105. if (!$this->fullyLoaded) {
  106. $this->allStorage = $this->getData();
  107. }
  108. // Set storage from allStorage outside of the fullyLoaded check to prevent
  109. // cache calls on requests that have requested all data to get a single
  110. // tables data. Make sure $this->storage is populated in this case.
  111. $this->storage = $this->allStorage;
  112. return $this->allStorage;
  113. }
  114. /**
  115. * Gets data for a particular table, or all tables.
  116. *
  117. * @param string|null $key
  118. * The key of the cache entry to retrieve. Defaults to NULL, this will
  119. * return all table data.
  120. *
  121. * @deprecated NULL $key deprecated in Drupal 8.2.x and will be removed in
  122. * 9.0.0. Use getAll() instead.
  123. *
  124. * @see https://www.drupal.org/node/2723553
  125. *
  126. * @return array
  127. * An array of table data.
  128. */
  129. public function get($key = NULL) {
  130. if (!$key) {
  131. return $this->getAll();
  132. }
  133. if (!isset($this->storage[$key])) {
  134. // Prepare a cache ID for get and set.
  135. $cid = $this->baseCid . ':' . $key;
  136. $from_cache = FALSE;
  137. if ($data = $this->cacheGet($cid)) {
  138. $this->storage[$key] = $data->data;
  139. $from_cache = TRUE;
  140. }
  141. // If there is no cached entry and data is not already fully loaded,
  142. // rebuild. This will stop requests for invalid tables calling getData.
  143. elseif (!$this->fullyLoaded) {
  144. $this->allStorage = $this->getData();
  145. }
  146. if (!$from_cache) {
  147. if (!isset($this->allStorage[$key])) {
  148. // Write an empty cache entry if no information for that table
  149. // exists to avoid repeated cache get calls for this table and
  150. // prevent loading all tables unnecessarily.
  151. $this->storage[$key] = [];
  152. $this->allStorage[$key] = [];
  153. }
  154. else {
  155. $this->storage[$key] = $this->allStorage[$key];
  156. }
  157. // Create a cache entry for the requested table.
  158. $this->cacheSet($cid, $this->allStorage[$key]);
  159. }
  160. }
  161. return $this->storage[$key];
  162. }
  163. /**
  164. * Gets data from the cache backend.
  165. *
  166. * @param string $cid
  167. * The cache ID to return.
  168. *
  169. * @return mixed
  170. * The cached data, if any. This will immediately return FALSE if the
  171. * $skipCache property is TRUE.
  172. */
  173. protected function cacheGet($cid) {
  174. if ($this->skipCache) {
  175. return FALSE;
  176. }
  177. return $this->cacheBackend->get($this->prepareCid($cid));
  178. }
  179. /**
  180. * Sets data to the cache backend.
  181. *
  182. * @param string $cid
  183. * The cache ID to set.
  184. * @param mixed $data
  185. * The data that will be cached.
  186. */
  187. protected function cacheSet($cid, $data) {
  188. return $this->cacheBackend->set($this->prepareCid($cid), $data, Cache::PERMANENT, ['views_data', 'config:core.extension']);
  189. }
  190. /**
  191. * Prepares the cache ID by appending a language code.
  192. *
  193. * @param string $cid
  194. * The cache ID to prepare.
  195. *
  196. * @return string
  197. * The prepared cache ID.
  198. */
  199. protected function prepareCid($cid) {
  200. return $cid . ':' . $this->langcode;
  201. }
  202. /**
  203. * Gets all data invoked by hook_views_data().
  204. *
  205. * This is requested from the cache before being rebuilt.
  206. *
  207. * @return array
  208. * An array of all data.
  209. */
  210. protected function getData() {
  211. $this->fullyLoaded = TRUE;
  212. if ($data = $this->cacheGet($this->baseCid)) {
  213. return $data->data;
  214. }
  215. else {
  216. $modules = $this->moduleHandler->getImplementations('views_data');
  217. $data = [];
  218. foreach ($modules as $module) {
  219. $views_data = $this->moduleHandler->invoke($module, 'views_data');
  220. // Set the provider key for each base table.
  221. foreach ($views_data as &$table) {
  222. if (isset($table['table']) && !isset($table['table']['provider'])) {
  223. $table['table']['provider'] = $module;
  224. }
  225. }
  226. $data = NestedArray::mergeDeep($data, $views_data);
  227. }
  228. $this->moduleHandler->alter('views_data', $data);
  229. $this->processEntityTypes($data);
  230. // Keep a record with all data.
  231. $this->cacheSet($this->baseCid, $data);
  232. return $data;
  233. }
  234. }
  235. /**
  236. * Links tables with 'entity type' to respective generic entity-type tables.
  237. *
  238. * @param array $data
  239. * The array of data to alter entity data for, passed by reference.
  240. */
  241. protected function processEntityTypes(array &$data) {
  242. foreach ($data as $table_name => $table_info) {
  243. // Add in a join from the entity-table if an entity-type is given.
  244. if (!empty($table_info['table']['entity type'])) {
  245. $entity_table = 'views_entity_' . $table_info['table']['entity type'];
  246. $data[$entity_table]['table']['join'][$table_name] = [
  247. 'left_table' => $table_name,
  248. ];
  249. $data[$entity_table]['table']['entity type'] = $table_info['table']['entity type'];
  250. // Copy over the default table group if we have none yet.
  251. if (!empty($table_info['table']['group']) && empty($data[$entity_table]['table']['group'])) {
  252. $data[$entity_table]['table']['group'] = $table_info['table']['group'];
  253. }
  254. }
  255. }
  256. }
  257. /**
  258. * Fetches a list of all base tables available.
  259. *
  260. * @return array
  261. * An array of base table data keyed by table name. Each item contains the
  262. * following keys:
  263. * - title: The title label for the base table.
  264. * - help: The help text for the base table.
  265. * - weight: The weight of the base table.
  266. */
  267. public function fetchBaseTables() {
  268. $tables = [];
  269. foreach ($this->get() as $table => $info) {
  270. if (!empty($info['table']['base'])) {
  271. $tables[$table] = [
  272. 'title' => $info['table']['base']['title'],
  273. 'help' => !empty($info['table']['base']['help']) ? $info['table']['base']['help'] : '',
  274. 'weight' => !empty($info['table']['base']['weight']) ? $info['table']['base']['weight'] : 0,
  275. ];
  276. }
  277. }
  278. // Sorts by the 'weight' and then by 'title' element.
  279. uasort($tables, function ($a, $b) {
  280. if ($a['weight'] != $b['weight']) {
  281. return $a['weight'] < $b['weight'] ? -1 : 1;
  282. }
  283. if ($a['title'] != $b['title']) {
  284. return $a['title'] < $b['title'] ? -1 : 1;
  285. }
  286. return 0;
  287. });
  288. return $tables;
  289. }
  290. /**
  291. * Clears the class storage and cache.
  292. */
  293. public function clear() {
  294. $this->storage = [];
  295. $this->allStorage = [];
  296. $this->fullyLoaded = FALSE;
  297. Cache::invalidateTags(['views_data']);
  298. }
  299. }