PageRenderTime 61ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/core/lib/Drupal/Core/Menu/MenuTreeStorage.php

http://github.com/drupal/drupal
PHP | 1494 lines | 919 code | 95 blank | 480 comment | 101 complexity | e7236e9a2ae02acafb8c30e38a3c1aba MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Core\Menu;
  3. use Drupal\Component\Plugin\Exception\PluginException;
  4. use Drupal\Component\Utility\UrlHelper;
  5. use Drupal\Core\Cache\Cache;
  6. use Drupal\Core\Cache\CacheBackendInterface;
  7. use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
  8. use Drupal\Core\Database\Connection;
  9. use Drupal\Core\Database\Database;
  10. use Drupal\Core\Database\DatabaseException;
  11. use Drupal\Core\Database\Query\SelectInterface;
  12. /**
  13. * Provides a menu tree storage using the database.
  14. */
  15. class MenuTreeStorage implements MenuTreeStorageInterface {
  16. /**
  17. * The maximum depth of a menu links tree.
  18. */
  19. const MAX_DEPTH = 9;
  20. /**
  21. * The database connection.
  22. *
  23. * @var \Drupal\Core\Database\Connection
  24. */
  25. protected $connection;
  26. /**
  27. * Cache backend instance for the extracted tree data.
  28. *
  29. * @var \Drupal\Core\Cache\CacheBackendInterface
  30. */
  31. protected $menuCacheBackend;
  32. /**
  33. * The cache tags invalidator.
  34. *
  35. * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
  36. */
  37. protected $cacheTagsInvalidator;
  38. /**
  39. * The database table name.
  40. *
  41. * @var string
  42. */
  43. protected $table;
  44. /**
  45. * Additional database connection options to use in queries.
  46. *
  47. * @var array
  48. */
  49. protected $options = [];
  50. /**
  51. * Stores definitions that have already been loaded for better performance.
  52. *
  53. * An array of plugin definition arrays, keyed by plugin ID.
  54. *
  55. * @var array
  56. */
  57. protected $definitions = [];
  58. /**
  59. * List of serialized fields.
  60. *
  61. * @var array
  62. */
  63. protected $serializedFields;
  64. /**
  65. * List of plugin definition fields.
  66. *
  67. * @todo Decide how to keep these field definitions in sync.
  68. * https://www.drupal.org/node/2302085
  69. *
  70. * @see \Drupal\Core\Menu\MenuLinkManager::$defaults
  71. *
  72. * @var array
  73. */
  74. protected $definitionFields = [
  75. 'menu_name',
  76. 'route_name',
  77. 'route_parameters',
  78. 'url',
  79. 'title',
  80. 'description',
  81. 'parent',
  82. 'weight',
  83. 'options',
  84. 'expanded',
  85. 'enabled',
  86. 'provider',
  87. 'metadata',
  88. 'class',
  89. 'form_class',
  90. 'id',
  91. ];
  92. /**
  93. * Constructs a new \Drupal\Core\Menu\MenuTreeStorage.
  94. *
  95. * @param \Drupal\Core\Database\Connection $connection
  96. * A Database connection to use for reading and writing configuration data.
  97. * @param \Drupal\Core\Cache\CacheBackendInterface $menu_cache_backend
  98. * Cache backend instance for the extracted tree data.
  99. * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
  100. * The cache tags invalidator.
  101. * @param string $table
  102. * A database table name to store configuration data in.
  103. * @param array $options
  104. * (optional) Any additional database connection options to use in queries.
  105. */
  106. public function __construct(Connection $connection, CacheBackendInterface $menu_cache_backend, CacheTagsInvalidatorInterface $cache_tags_invalidator, $table, array $options = []) {
  107. $this->connection = $connection;
  108. $this->menuCacheBackend = $menu_cache_backend;
  109. $this->cacheTagsInvalidator = $cache_tags_invalidator;
  110. $this->table = $table;
  111. $this->options = $options;
  112. }
  113. /**
  114. * {@inheritdoc}
  115. */
  116. public function maxDepth() {
  117. return static::MAX_DEPTH;
  118. }
  119. /**
  120. * {@inheritdoc}
  121. */
  122. public function resetDefinitions() {
  123. $this->definitions = [];
  124. }
  125. /**
  126. * {@inheritdoc}
  127. */
  128. public function rebuild(array $definitions) {
  129. $links = [];
  130. $children = [];
  131. $top_links = [];
  132. // Fetch the list of existing menus, in case some are not longer populated
  133. // after the rebuild.
  134. $before_menus = $this->getMenuNames();
  135. if ($definitions) {
  136. foreach ($definitions as $id => $link) {
  137. // Flag this link as discovered, i.e. saved via rebuild().
  138. $link['discovered'] = 1;
  139. // Note: The parent we set here might be just stored in the {menu_tree}
  140. // table, so it will not end up in $top_links. Therefore the later loop
  141. // on the orphan links, will handle those cases.
  142. if (!empty($link['parent'])) {
  143. $children[$link['parent']][$id] = $id;
  144. }
  145. else {
  146. // A top level link - we need them to root our tree.
  147. $top_links[$id] = $id;
  148. $link['parent'] = '';
  149. }
  150. $links[$id] = $link;
  151. }
  152. }
  153. foreach ($top_links as $id) {
  154. $this->saveRecursive($id, $children, $links);
  155. }
  156. // Handle any children we didn't find starting from top-level links.
  157. foreach ($children as $orphan_links) {
  158. foreach ($orphan_links as $id) {
  159. // Check for a parent that is not loaded above since only internal links
  160. // are loaded above.
  161. $parent = $this->loadFull($links[$id]['parent']);
  162. // If there is a parent add it to the links to be used in
  163. // ::saveRecursive().
  164. if ($parent) {
  165. $links[$links[$id]['parent']] = $parent;
  166. }
  167. else {
  168. // Force it to the top level.
  169. $links[$id]['parent'] = '';
  170. }
  171. $this->saveRecursive($id, $children, $links);
  172. }
  173. }
  174. $result = $this->findNoLongerExistingLinks($definitions);
  175. // Remove all such items.
  176. if ($result) {
  177. $this->purgeMultiple($result);
  178. }
  179. $this->resetDefinitions();
  180. $affected_menus = $this->getMenuNames() + $before_menus;
  181. // Invalidate any cache tagged with any menu name.
  182. $cache_tags = Cache::buildTags('config:system.menu', $affected_menus, '.');
  183. $this->cacheTagsInvalidator->invalidateTags($cache_tags);
  184. $this->resetDefinitions();
  185. // Every item in the cache bin should have one of the menu cache tags but it
  186. // is not guaranteed, so invalidate everything in the bin.
  187. $this->menuCacheBackend->invalidateAll();
  188. }
  189. /**
  190. * Purges multiple menu links that no longer exist.
  191. *
  192. * @param array $ids
  193. * An array of menu link IDs.
  194. */
  195. protected function purgeMultiple(array $ids) {
  196. $loaded = $this->loadFullMultiple($ids);
  197. foreach ($loaded as $id => $link) {
  198. if ($link['has_children']) {
  199. $children = $this->loadByProperties(['parent' => $id]);
  200. foreach ($children as $child) {
  201. $child['parent'] = $link['parent'];
  202. $this->save($child);
  203. }
  204. }
  205. }
  206. $this->doDeleteMultiple($ids);
  207. }
  208. /**
  209. * Executes a select query while making sure the database table exists.
  210. *
  211. * @param \Drupal\Core\Database\Query\SelectInterface $query
  212. * The select object to be executed.
  213. *
  214. * @return \Drupal\Core\Database\StatementInterface|null
  215. * A prepared statement, or NULL if the query is not valid.
  216. *
  217. * @throws \Exception
  218. * Thrown if the table could not be created or the database connection
  219. * failed.
  220. */
  221. protected function safeExecuteSelect(SelectInterface $query) {
  222. try {
  223. return $query->execute();
  224. }
  225. catch (\Exception $e) {
  226. // If there was an exception, try to create the table.
  227. if ($this->ensureTableExists()) {
  228. return $query->execute();
  229. }
  230. // Some other failure that we can not recover from.
  231. throw $e;
  232. }
  233. }
  234. /**
  235. * {@inheritdoc}
  236. */
  237. public function save(array $link) {
  238. $affected_menus = $this->doSave($link);
  239. $this->resetDefinitions();
  240. $cache_tags = Cache::buildTags('config:system.menu', $affected_menus, '.');
  241. $this->cacheTagsInvalidator->invalidateTags($cache_tags);
  242. return $affected_menus;
  243. }
  244. /**
  245. * Saves a link without clearing caches.
  246. *
  247. * @param array $link
  248. * A definition, according to $definitionFields, for a
  249. * \Drupal\Core\Menu\MenuLinkInterface plugin.
  250. *
  251. * @return array
  252. * The menu names affected by the save operation. This will be one menu
  253. * name if the link is saved to the sane menu, or two if it is saved to a
  254. * new menu.
  255. *
  256. * @throws \Exception
  257. * Thrown if the storage back-end does not exist and could not be created.
  258. * @throws \Drupal\Component\Plugin\Exception\PluginException
  259. * Thrown if the definition is invalid, for example, if the specified parent
  260. * would cause the links children to be moved to greater than the maximum
  261. * depth.
  262. */
  263. protected function doSave(array $link) {
  264. $affected_menus = [];
  265. // Get the existing definition if it exists. This does not use
  266. // self::loadFull() to avoid the unserialization of fields with 'serialize'
  267. // equal to TRUE as defined in self::schemaDefinition(). The makes $original
  268. // easier to compare with the return value of self::preSave().
  269. $query = $this->connection->select($this->table, $this->options);
  270. $query->fields($this->table);
  271. $query->condition('id', $link['id']);
  272. $original = $this->safeExecuteSelect($query)->fetchAssoc();
  273. if ($original) {
  274. $link['mlid'] = $original['mlid'];
  275. $link['has_children'] = $original['has_children'];
  276. $affected_menus[$original['menu_name']] = $original['menu_name'];
  277. $fields = $this->preSave($link, $original);
  278. // If $link matches the $original data then exit early as there are no
  279. // changes to make. Use array_diff_assoc() to check if they match because:
  280. // - Some of the data types of the values are not the same. The values
  281. // in $original are all strings because they have come from database but
  282. // $fields contains typed values.
  283. // - MenuTreeStorage::preSave() removes the 'mlid' from $fields.
  284. // - The order of the keys in $original and $fields is different.
  285. if (array_diff_assoc($fields, $original) == [] && array_diff_assoc($original, $fields) == ['mlid' => $link['mlid']]) {
  286. return $affected_menus;
  287. }
  288. }
  289. $transaction = $this->connection->startTransaction();
  290. try {
  291. if (!$original) {
  292. // Generate a new mlid.
  293. $options = ['return' => Database::RETURN_INSERT_ID] + $this->options;
  294. $link['mlid'] = $this->connection->insert($this->table, $options)
  295. ->fields(['id' => $link['id'], 'menu_name' => $link['menu_name']])
  296. ->execute();
  297. $fields = $this->preSave($link, []);
  298. }
  299. // We may be moving the link to a new menu.
  300. $affected_menus[$fields['menu_name']] = $fields['menu_name'];
  301. $query = $this->connection->update($this->table, $this->options);
  302. $query->condition('mlid', $link['mlid']);
  303. $query->fields($fields)
  304. ->execute();
  305. if ($original) {
  306. $this->updateParentalStatus($original);
  307. }
  308. $this->updateParentalStatus($link);
  309. }
  310. catch (\Exception $e) {
  311. $transaction->rollBack();
  312. throw $e;
  313. }
  314. return $affected_menus;
  315. }
  316. /**
  317. * Fills in all the fields the database save needs, using the link definition.
  318. *
  319. * @param array $link
  320. * The link definition to be updated.
  321. * @param array $original
  322. * The link definition before the changes. May be empty if not found.
  323. *
  324. * @return array
  325. * The values which will be stored.
  326. *
  327. * @throws \Drupal\Component\Plugin\Exception\PluginException
  328. * Thrown when the specific depth exceeds the maximum.
  329. */
  330. protected function preSave(array &$link, array $original) {
  331. static $schema_fields, $schema_defaults;
  332. if (empty($schema_fields)) {
  333. $schema = static::schemaDefinition();
  334. $schema_fields = $schema['fields'];
  335. foreach ($schema_fields as $name => $spec) {
  336. if (isset($spec['default'])) {
  337. $schema_defaults[$name] = $spec['default'];
  338. }
  339. }
  340. }
  341. // Try to find a parent link. If found, assign it and derive its menu.
  342. $parent = $this->findParent($link, $original);
  343. if ($parent) {
  344. $link['parent'] = $parent['id'];
  345. $link['menu_name'] = $parent['menu_name'];
  346. }
  347. else {
  348. $link['parent'] = '';
  349. }
  350. // If no corresponding parent link was found, move the link to the
  351. // top-level.
  352. foreach ($schema_defaults as $name => $default) {
  353. if (!isset($link[$name])) {
  354. $link[$name] = $default;
  355. }
  356. }
  357. $fields = array_intersect_key($link, $schema_fields);
  358. // Sort the route parameters so that the query string will be the same.
  359. asort($fields['route_parameters']);
  360. // Since this will be urlencoded, it's safe to store and match against a
  361. // text field.
  362. $fields['route_param_key'] = $fields['route_parameters'] ? UrlHelper::buildQuery($fields['route_parameters']) : '';
  363. foreach ($this->serializedFields() as $name) {
  364. if (isset($fields[$name])) {
  365. $fields[$name] = serialize($fields[$name]);
  366. }
  367. }
  368. $this->setParents($fields, $parent, $original);
  369. // Need to check both parent and menu_name, since parent can be empty in any
  370. // menu.
  371. if ($original && ($link['parent'] != $original['parent'] || $link['menu_name'] != $original['menu_name'])) {
  372. $this->moveChildren($fields, $original);
  373. }
  374. // We needed the mlid above, but not in the update query.
  375. unset($fields['mlid']);
  376. // Cast Booleans to int, if needed.
  377. $fields['enabled'] = (int) $fields['enabled'];
  378. $fields['expanded'] = (int) $fields['expanded'];
  379. return $fields;
  380. }
  381. /**
  382. * {@inheritdoc}
  383. */
  384. public function delete($id) {
  385. // Children get re-attached to the menu link's parent.
  386. $item = $this->loadFull($id);
  387. // It's possible the link is already deleted.
  388. if ($item) {
  389. $parent = $item['parent'];
  390. $children = $this->loadByProperties(['parent' => $id]);
  391. foreach ($children as $child) {
  392. $child['parent'] = $parent;
  393. $this->save($child);
  394. }
  395. $this->doDeleteMultiple([$id]);
  396. $this->updateParentalStatus($item);
  397. // Many children may have moved.
  398. $this->resetDefinitions();
  399. $this->cacheTagsInvalidator->invalidateTags(['config:system.menu.' . $item['menu_name']]);
  400. }
  401. }
  402. /**
  403. * {@inheritdoc}
  404. */
  405. public function getSubtreeHeight($id) {
  406. $original = $this->loadFull($id);
  407. return $original ? $this->doFindChildrenRelativeDepth($original) + 1 : 0;
  408. }
  409. /**
  410. * Finds the relative depth of this link's deepest child.
  411. *
  412. * @param array $original
  413. * The parent definition used to find the depth.
  414. *
  415. * @return int
  416. * Returns the relative depth.
  417. */
  418. protected function doFindChildrenRelativeDepth(array $original) {
  419. $query = $this->connection->select($this->table, $this->options);
  420. $query->addField($this->table, 'depth');
  421. $query->condition('menu_name', $original['menu_name']);
  422. $query->orderBy('depth', 'DESC');
  423. $query->range(0, 1);
  424. for ($i = 1; $i <= static::MAX_DEPTH && $original["p$i"]; $i++) {
  425. $query->condition("p$i", $original["p$i"]);
  426. }
  427. $max_depth = $this->safeExecuteSelect($query)->fetchField();
  428. return ($max_depth > $original['depth']) ? $max_depth - $original['depth'] : 0;
  429. }
  430. /**
  431. * Sets the materialized path field values based on the parent.
  432. *
  433. * @param array $fields
  434. * The menu link.
  435. * @param array|false $parent
  436. * The parent menu link.
  437. * @param array $original
  438. * The original menu link.
  439. */
  440. protected function setParents(array &$fields, $parent, array $original) {
  441. // Directly fill parents for top-level links.
  442. if (empty($fields['parent'])) {
  443. $fields['p1'] = $fields['mlid'];
  444. for ($i = 2; $i <= $this->maxDepth(); $i++) {
  445. $fields["p$i"] = 0;
  446. }
  447. $fields['depth'] = 1;
  448. }
  449. // Otherwise, ensure that this link's depth is not beyond the maximum depth
  450. // and fill parents based on the parent link.
  451. else {
  452. // @todo We want to also check $original['has_children'] here, but that
  453. // will be 0 even if there are children if those are not enabled.
  454. // has_children is really just the rendering hint. So, we either need
  455. // to define another column (has_any_children), or do the extra query.
  456. // https://www.drupal.org/node/2302149
  457. if ($original) {
  458. $limit = $this->maxDepth() - $this->doFindChildrenRelativeDepth($original) - 1;
  459. }
  460. else {
  461. $limit = $this->maxDepth() - 1;
  462. }
  463. if ($parent['depth'] > $limit) {
  464. throw new PluginException("The link with ID {$fields['id']} or its children exceeded the maximum depth of {$this->maxDepth()}");
  465. }
  466. $fields['depth'] = $parent['depth'] + 1;
  467. $i = 1;
  468. while ($i < $fields['depth']) {
  469. $p = 'p' . $i++;
  470. $fields[$p] = $parent[$p];
  471. }
  472. $p = 'p' . $i++;
  473. // The parent (p1 - p9) corresponding to the depth always equals the mlid.
  474. $fields[$p] = $fields['mlid'];
  475. while ($i <= static::MAX_DEPTH) {
  476. $p = 'p' . $i++;
  477. $fields[$p] = 0;
  478. }
  479. }
  480. }
  481. /**
  482. * Re-parents a link's children when the link itself is moved.
  483. *
  484. * @param array $fields
  485. * The changed menu link.
  486. * @param array $original
  487. * The original menu link.
  488. */
  489. protected function moveChildren($fields, $original) {
  490. $query = $this->connection->update($this->table, $this->options);
  491. $query->fields(['menu_name' => $fields['menu_name']]);
  492. $expressions = [];
  493. for ($i = 1; $i <= $fields['depth']; $i++) {
  494. $expressions[] = ["p$i", ":p_$i", [":p_$i" => $fields["p$i"]]];
  495. }
  496. $j = $original['depth'] + 1;
  497. while ($i <= $this->maxDepth() && $j <= $this->maxDepth()) {
  498. $expressions[] = ['p' . $i++, 'p' . $j++, []];
  499. }
  500. while ($i <= $this->maxDepth()) {
  501. $expressions[] = ['p' . $i++, 0, []];
  502. }
  503. $shift = $fields['depth'] - $original['depth'];
  504. if ($shift > 0) {
  505. // The order of expressions must be reversed so the new values don't
  506. // overwrite the old ones before they can be used because "Single-table
  507. // UPDATE assignments are generally evaluated from left to right".
  508. // @see http://dev.mysql.com/doc/refman/5.0/en/update.html
  509. $expressions = array_reverse($expressions);
  510. }
  511. foreach ($expressions as $expression) {
  512. $query->expression($expression[0], $expression[1], $expression[2]);
  513. }
  514. $query->expression('depth', 'depth + :depth', [':depth' => $shift]);
  515. $query->condition('menu_name', $original['menu_name']);
  516. for ($i = 1; $i <= $this->maxDepth() && $original["p$i"]; $i++) {
  517. $query->condition("p$i", $original["p$i"]);
  518. }
  519. $query->execute();
  520. }
  521. /**
  522. * Loads the parent definition if it exists.
  523. *
  524. * @param array $link
  525. * The link definition to find the parent of.
  526. * @param array|false $original
  527. * The original link that might be used to find the parent if the parent
  528. * is not set on the $link, or FALSE if the original could not be loaded.
  529. *
  530. * @return array|false
  531. * Returns a definition array, or FALSE if no parent was found.
  532. */
  533. protected function findParent($link, $original) {
  534. $parent = FALSE;
  535. // This item is explicitly top-level, skip the rest of the parenting.
  536. if (isset($link['parent']) && empty($link['parent'])) {
  537. return $parent;
  538. }
  539. // If we have a parent link ID, try to use that.
  540. $candidates = [];
  541. if (isset($link['parent'])) {
  542. $candidates[] = $link['parent'];
  543. }
  544. elseif (!empty($original['parent']) && $link['menu_name'] == $original['menu_name']) {
  545. // Otherwise, fall back to the original parent.
  546. $candidates[] = $original['parent'];
  547. }
  548. foreach ($candidates as $id) {
  549. $parent = $this->loadFull($id);
  550. if ($parent) {
  551. break;
  552. }
  553. }
  554. return $parent;
  555. }
  556. /**
  557. * Sets has_children for the link's parent if it has visible children.
  558. *
  559. * @param array $link
  560. * The link to get a parent ID from.
  561. */
  562. protected function updateParentalStatus(array $link) {
  563. // If parent is empty, there is nothing to update.
  564. if (!empty($link['parent'])) {
  565. // Check if at least one visible child exists in the table.
  566. $query = $this->connection->select($this->table, $this->options);
  567. $query->addExpression('1');
  568. $query->range(0, 1);
  569. $query
  570. ->condition('menu_name', $link['menu_name'])
  571. ->condition('parent', $link['parent'])
  572. ->condition('enabled', 1);
  573. $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
  574. $this->connection->update($this->table, $this->options)
  575. ->fields(['has_children' => $parent_has_children])
  576. ->condition('id', $link['parent'])
  577. ->execute();
  578. }
  579. }
  580. /**
  581. * Prepares a link by unserializing values and saving the definition.
  582. *
  583. * @param array $link
  584. * The data loaded in the query.
  585. * @param bool $intersect
  586. * If TRUE, filter out values that are not part of the actual definition.
  587. *
  588. * @return array
  589. * The prepared link data.
  590. */
  591. protected function prepareLink(array $link, $intersect = FALSE) {
  592. foreach ($this->serializedFields() as $name) {
  593. if (isset($link[$name])) {
  594. $link[$name] = unserialize($link[$name]);
  595. }
  596. }
  597. if ($intersect) {
  598. $link = array_intersect_key($link, array_flip($this->definitionFields()));
  599. }
  600. $this->definitions[$link['id']] = $link;
  601. return $link;
  602. }
  603. /**
  604. * {@inheritdoc}
  605. */
  606. public function loadByProperties(array $properties) {
  607. $query = $this->connection->select($this->table, $this->options);
  608. $query->fields($this->table, $this->definitionFields());
  609. foreach ($properties as $name => $value) {
  610. if (!in_array($name, $this->definitionFields(), TRUE)) {
  611. $fields = implode(', ', $this->definitionFields());
  612. throw new \InvalidArgumentException("An invalid property name, $name was specified. Allowed property names are: $fields.");
  613. }
  614. $query->condition($name, $value);
  615. }
  616. $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
  617. foreach ($loaded as $id => $link) {
  618. $loaded[$id] = $this->prepareLink($link);
  619. }
  620. return $loaded;
  621. }
  622. /**
  623. * {@inheritdoc}
  624. */
  625. public function loadByRoute($route_name, array $route_parameters = [], $menu_name = NULL) {
  626. // Sort the route parameters so that the query string will be the same.
  627. asort($route_parameters);
  628. // Since this will be urlencoded, it's safe to store and match against a
  629. // text field.
  630. // @todo Standardize an efficient way to load by route name and parameters
  631. // in place of system path. https://www.drupal.org/node/2302139
  632. $param_key = $route_parameters ? UrlHelper::buildQuery($route_parameters) : '';
  633. $query = $this->connection->select($this->table, $this->options);
  634. $query->fields($this->table, $this->definitionFields());
  635. $query->condition('route_name', $route_name);
  636. $query->condition('route_param_key', $param_key);
  637. if ($menu_name) {
  638. $query->condition('menu_name', $menu_name);
  639. }
  640. // Make the ordering deterministic.
  641. $query->orderBy('depth');
  642. $query->orderBy('weight');
  643. $query->orderBy('id');
  644. $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
  645. foreach ($loaded as $id => $link) {
  646. $loaded[$id] = $this->prepareLink($link);
  647. }
  648. return $loaded;
  649. }
  650. /**
  651. * {@inheritdoc}
  652. */
  653. public function loadMultiple(array $ids) {
  654. $missing_ids = array_diff($ids, array_keys($this->definitions));
  655. if ($missing_ids) {
  656. $query = $this->connection->select($this->table, $this->options);
  657. $query->fields($this->table, $this->definitionFields());
  658. $query->condition('id', $missing_ids, 'IN');
  659. $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
  660. foreach ($loaded as $id => $link) {
  661. $this->definitions[$id] = $this->prepareLink($link);
  662. }
  663. }
  664. return array_intersect_key($this->definitions, array_flip($ids));
  665. }
  666. /**
  667. * {@inheritdoc}
  668. */
  669. public function load($id) {
  670. if (isset($this->definitions[$id])) {
  671. return $this->definitions[$id];
  672. }
  673. $loaded = $this->loadMultiple([$id]);
  674. return isset($loaded[$id]) ? $loaded[$id] : FALSE;
  675. }
  676. /**
  677. * Loads all table fields, not just those that are in the plugin definition.
  678. *
  679. * @param string $id
  680. * The menu link ID.
  681. *
  682. * @return array
  683. * The loaded menu link definition or an empty array if not be found.
  684. */
  685. protected function loadFull($id) {
  686. $loaded = $this->loadFullMultiple([$id]);
  687. return isset($loaded[$id]) ? $loaded[$id] : [];
  688. }
  689. /**
  690. * Loads all table fields for multiple menu link definitions by ID.
  691. *
  692. * @param array $ids
  693. * The IDs to load.
  694. *
  695. * @return array
  696. * The loaded menu link definitions.
  697. */
  698. protected function loadFullMultiple(array $ids) {
  699. $query = $this->connection->select($this->table, $this->options);
  700. $query->fields($this->table);
  701. $query->condition('id', $ids, 'IN');
  702. $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
  703. foreach ($loaded as &$link) {
  704. foreach ($this->serializedFields() as $name) {
  705. if (isset($link[$name])) {
  706. $link[$name] = unserialize($link[$name]);
  707. }
  708. }
  709. }
  710. return $loaded;
  711. }
  712. /**
  713. * {@inheritdoc}
  714. */
  715. public function getRootPathIds($id) {
  716. $subquery = $this->connection->select($this->table, $this->options);
  717. // @todo Consider making this dynamic based on static::MAX_DEPTH or from the
  718. // schema if that is generated using static::MAX_DEPTH.
  719. // https://www.drupal.org/node/2302043
  720. $subquery->fields($this->table, ['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9']);
  721. $subquery->condition('id', $id);
  722. $result = current($subquery->execute()->fetchAll(\PDO::FETCH_ASSOC));
  723. $ids = array_filter($result);
  724. if ($ids) {
  725. $query = $this->connection->select($this->table, $this->options);
  726. $query->fields($this->table, ['id']);
  727. $query->orderBy('depth', 'DESC');
  728. $query->condition('mlid', $ids, 'IN');
  729. // @todo Cache this result in memory if we find it is being used more
  730. // than once per page load. https://www.drupal.org/node/2302185
  731. return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
  732. }
  733. return [];
  734. }
  735. /**
  736. * {@inheritdoc}
  737. */
  738. public function getExpanded($menu_name, array $parents) {
  739. // @todo Go back to tracking in state or some other way which menus have
  740. // expanded links? https://www.drupal.org/node/2302187
  741. do {
  742. $query = $this->connection->select($this->table, $this->options);
  743. $query->fields($this->table, ['id']);
  744. $query->condition('menu_name', $menu_name);
  745. $query->condition('expanded', 1);
  746. $query->condition('has_children', 1);
  747. $query->condition('enabled', 1);
  748. $query->condition('parent', $parents, 'IN');
  749. $query->condition('id', $parents, 'NOT IN');
  750. $result = $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
  751. $parents += $result;
  752. } while (!empty($result));
  753. return $parents;
  754. }
  755. /**
  756. * Saves menu links recursively.
  757. *
  758. * @param string $id
  759. * The definition ID.
  760. * @param array $children
  761. * An array of IDs of child links collected by parent ID.
  762. * @param array $links
  763. * An array of all definitions keyed by ID.
  764. */
  765. protected function saveRecursive($id, &$children, &$links) {
  766. if (!empty($links[$id]['parent']) && empty($links[$links[$id]['parent']])) {
  767. // Invalid parent ID, so remove it.
  768. $links[$id]['parent'] = '';
  769. }
  770. $this->doSave($links[$id]);
  771. if (!empty($children[$id])) {
  772. foreach ($children[$id] as $next_id) {
  773. $this->saveRecursive($next_id, $children, $links);
  774. }
  775. }
  776. // Remove processed link names so we can find stragglers.
  777. unset($children[$id]);
  778. }
  779. /**
  780. * {@inheritdoc}
  781. */
  782. public function loadTreeData($menu_name, MenuTreeParameters $parameters) {
  783. $tree_cid = "tree-data:$menu_name:" . serialize($parameters);
  784. $cache = $this->menuCacheBackend->get($tree_cid);
  785. if ($cache && isset($cache->data)) {
  786. $data = $cache->data;
  787. // Cache the definitions in memory so they don't need to be loaded again.
  788. $this->definitions += $data['definitions'];
  789. unset($data['definitions']);
  790. }
  791. else {
  792. $links = $this->loadLinks($menu_name, $parameters);
  793. $data['tree'] = $this->doBuildTreeData($links, $parameters->activeTrail, $parameters->minDepth);
  794. $data['definitions'] = [];
  795. $data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']);
  796. $this->menuCacheBackend->set($tree_cid, $data, Cache::PERMANENT, ['config:system.menu.' . $menu_name]);
  797. // The definitions were already added to $this->definitions in
  798. // $this->doBuildTreeData()
  799. unset($data['definitions']);
  800. }
  801. return $data;
  802. }
  803. /**
  804. * Loads links in the given menu, according to the given tree parameters.
  805. *
  806. * @param string $menu_name
  807. * A menu name.
  808. * @param \Drupal\Core\Menu\MenuTreeParameters $parameters
  809. * The parameters to determine which menu links to be loaded into a tree.
  810. * This method will set the absolute minimum depth, which is used in
  811. * MenuTreeStorage::doBuildTreeData().
  812. *
  813. * @return array
  814. * A flat array of menu links that are part of the menu. Each array element
  815. * is an associative array of information about the menu link, containing
  816. * the fields from the {menu_tree} table. This array must be ordered
  817. * depth-first.
  818. */
  819. protected function loadLinks($menu_name, MenuTreeParameters $parameters) {
  820. $query = $this->connection->select($this->table, $this->options);
  821. $query->fields($this->table);
  822. // Allow a custom root to be specified for loading a menu link tree. If
  823. // omitted, the default root (i.e. the actual root, '') is used.
  824. if ($parameters->root !== '') {
  825. $root = $this->loadFull($parameters->root);
  826. // If the custom root does not exist, we cannot load the links below it.
  827. if (!$root) {
  828. return [];
  829. }
  830. // When specifying a custom root, we only want to find links whose
  831. // parent IDs match that of the root; that's how we ignore the rest of the
  832. // tree. In other words: we exclude everything unreachable from the
  833. // custom root.
  834. for ($i = 1; $i <= $root['depth']; $i++) {
  835. $query->condition("p$i", $root["p$i"]);
  836. }
  837. // When specifying a custom root, the menu is determined by that root.
  838. $menu_name = $root['menu_name'];
  839. // If the custom root exists, then we must rewrite some of our
  840. // parameters; parameters are relative to the root (default or custom),
  841. // but the queries require absolute numbers, so adjust correspondingly.
  842. if (isset($parameters->minDepth)) {
  843. $parameters->minDepth += $root['depth'];
  844. }
  845. else {
  846. $parameters->minDepth = $root['depth'];
  847. }
  848. if (isset($parameters->maxDepth)) {
  849. $parameters->maxDepth += $root['depth'];
  850. }
  851. }
  852. // If no minimum depth is specified, then set the actual minimum depth,
  853. // depending on the root.
  854. if (!isset($parameters->minDepth)) {
  855. if ($parameters->root !== '' && $root) {
  856. $parameters->minDepth = $root['depth'];
  857. }
  858. else {
  859. $parameters->minDepth = 1;
  860. }
  861. }
  862. for ($i = 1; $i <= $this->maxDepth(); $i++) {
  863. $query->orderBy('p' . $i, 'ASC');
  864. }
  865. $query->condition('menu_name', $menu_name);
  866. if (!empty($parameters->expandedParents)) {
  867. $query->condition('parent', $parameters->expandedParents, 'IN');
  868. }
  869. if (isset($parameters->minDepth) && $parameters->minDepth > 1) {
  870. $query->condition('depth', $parameters->minDepth, '>=');
  871. }
  872. if (isset($parameters->maxDepth)) {
  873. $query->condition('depth', $parameters->maxDepth, '<=');
  874. }
  875. // Add custom query conditions, if any were passed.
  876. if (!empty($parameters->conditions)) {
  877. // Only allow conditions that are testing definition fields.
  878. $parameters->conditions = array_intersect_key($parameters->conditions, array_flip($this->definitionFields()));
  879. $serialized_fields = $this->serializedFields();
  880. foreach ($parameters->conditions as $column => $value) {
  881. if (is_array($value)) {
  882. $operator = $value[1];
  883. $value = $value[0];
  884. }
  885. else {
  886. $operator = '=';
  887. }
  888. if (in_array($column, $serialized_fields)) {
  889. $value = serialize($value);
  890. }
  891. $query->condition($column, $value, $operator);
  892. }
  893. }
  894. $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
  895. return $links;
  896. }
  897. /**
  898. * Traverses the menu tree and collects all the route names and definitions.
  899. *
  900. * @param array $tree
  901. * The menu tree you wish to operate on.
  902. * @param array $definitions
  903. * An array to accumulate definitions by reference.
  904. *
  905. * @return array
  906. * Array of route names, with all values being unique.
  907. */
  908. protected function collectRoutesAndDefinitions(array $tree, array &$definitions) {
  909. return array_values($this->doCollectRoutesAndDefinitions($tree, $definitions));
  910. }
  911. /**
  912. * Collects all the route names and definitions.
  913. *
  914. * @param array $tree
  915. * A menu link tree from MenuTreeStorage::doBuildTreeData()
  916. * @param array $definitions
  917. * The collected definitions which are populated by reference.
  918. *
  919. * @return array
  920. * The collected route names.
  921. */
  922. protected function doCollectRoutesAndDefinitions(array $tree, array &$definitions) {
  923. $route_names = [];
  924. foreach (array_keys($tree) as $id) {
  925. $definitions[$id] = $this->definitions[$id];
  926. if (!empty($definition['route_name'])) {
  927. $route_names[$definition['route_name']] = $definition['route_name'];
  928. }
  929. if ($tree[$id]['subtree']) {
  930. $route_names += $this->doCollectRoutesAndDefinitions($tree[$id]['subtree'], $definitions);
  931. }
  932. }
  933. return $route_names;
  934. }
  935. /**
  936. * {@inheritdoc}
  937. */
  938. public function loadSubtreeData($id, $max_relative_depth = NULL) {
  939. $tree = [];
  940. $root = $this->loadFull($id);
  941. if (!$root) {
  942. return $tree;
  943. }
  944. $parameters = new MenuTreeParameters();
  945. $parameters->setRoot($id)->onlyEnabledLinks();
  946. return $this->loadTreeData($root['menu_name'], $parameters);
  947. }
  948. /**
  949. * {@inheritdoc}
  950. */
  951. public function menuNameInUse($menu_name) {
  952. $query = $this->connection->select($this->table, $this->options);
  953. $query->addField($this->table, 'mlid');
  954. $query->condition('menu_name', $menu_name);
  955. $query->range(0, 1);
  956. return (bool) $this->safeExecuteSelect($query);
  957. }
  958. /**
  959. * {@inheritdoc}
  960. */
  961. public function getMenuNames() {
  962. $query = $this->connection->select($this->table, $this->options);
  963. $query->addField($this->table, 'menu_name');
  964. $query->distinct();
  965. return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
  966. }
  967. /**
  968. * {@inheritdoc}
  969. */
  970. public function countMenuLinks($menu_name = NULL) {
  971. $query = $this->connection->select($this->table, $this->options);
  972. if ($menu_name) {
  973. $query->condition('menu_name', $menu_name);
  974. }
  975. return $this->safeExecuteSelect($query->countQuery())->fetchField();
  976. }
  977. /**
  978. * {@inheritdoc}
  979. */
  980. public function getAllChildIds($id) {
  981. $root = $this->loadFull($id);
  982. if (!$root) {
  983. return [];
  984. }
  985. $query = $this->connection->select($this->table, $this->options);
  986. $query->fields($this->table, ['id']);
  987. $query->condition('menu_name', $root['menu_name']);
  988. for ($i = 1; $i <= $root['depth']; $i++) {
  989. $query->condition("p$i", $root["p$i"]);
  990. }
  991. // The next p column should not be empty. This excludes the root link.
  992. $query->condition("p$i", 0, '>');
  993. return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
  994. }
  995. /**
  996. * {@inheritdoc}
  997. */
  998. public function loadAllChildren($id, $max_relative_depth = NULL) {
  999. $parameters = new MenuTreeParameters();
  1000. $parameters->setRoot($id)->excludeRoot()->setMaxDepth($max_relative_depth)->onlyEnabledLinks();
  1001. $links = $this->loadLinks(NULL, $parameters);
  1002. foreach ($links as $id => $link) {
  1003. $links[$id] = $this->prepareLink($link);
  1004. }
  1005. return $links;
  1006. }
  1007. /**
  1008. * Prepares the data for calling $this->treeDataRecursive().
  1009. */
  1010. protected function doBuildTreeData(array $links, array $parents = [], $depth = 1) {
  1011. // Reverse the array so we can use the more efficient array_pop() function.
  1012. $links = array_reverse($links);
  1013. return $this->treeDataRecursive($links, $parents, $depth);
  1014. }
  1015. /**
  1016. * Builds the data representing a menu tree.
  1017. *
  1018. * The function is a bit complex because the rendering of a link depends on
  1019. * the next menu link.
  1020. *
  1021. * @param array $links
  1022. * A flat array of menu links that are part of the menu. Each array element
  1023. * is an associative array of information about the menu link, containing
  1024. * the fields from the $this->table. This array must be ordered
  1025. * depth-first. MenuTreeStorage::loadTreeData() includes a sample query.
  1026. * @param array $parents
  1027. * An array of the menu link ID values that are in the path from the current
  1028. * page to the root of the menu tree.
  1029. * @param int $depth
  1030. * The minimum depth to include in the returned menu tree.
  1031. *
  1032. * @return array
  1033. * The fully built tree.
  1034. *
  1035. * @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData()
  1036. */
  1037. protected function treeDataRecursive(array &$links, array $parents, $depth) {
  1038. $tree = [];
  1039. while ($tree_link_definition = array_pop($links)) {
  1040. $tree[$tree_link_definition['id']] = [
  1041. 'definition' => $this->prepareLink($tree_link_definition, TRUE),
  1042. 'has_children' => $tree_link_definition['has_children'],
  1043. // We need to determine if we're on the path to root so we can later
  1044. // build the correct active trail.
  1045. 'in_active_trail' => in_array($tree_link_definition['id'], $parents),
  1046. 'subtree' => [],
  1047. 'depth' => $tree_link_definition['depth'],
  1048. ];
  1049. // Look ahead to the next link, but leave it on the array so it's
  1050. // available to other recursive function calls if we return or build a
  1051. // sub-tree.
  1052. $next = end($links);
  1053. // Check whether the next link is the first in a new sub-tree.
  1054. if ($next && $next['depth'] > $depth) {
  1055. // Recursively call doBuildTreeData to build the sub-tree.
  1056. $tree[$tree_link_definition['id']]['subtree'] = $this->treeDataRecursive($links, $parents, $next['depth']);
  1057. // Fetch next link after filling the sub-tree.
  1058. $next = end($links);
  1059. }
  1060. // Determine if we should exit the loop and return.
  1061. if (!$next || $next['depth'] < $depth) {
  1062. break;
  1063. }
  1064. }
  1065. return $tree;
  1066. }
  1067. /**
  1068. * Checks if the tree table exists and create it if not.
  1069. *
  1070. * @return bool
  1071. * TRUE if the table was created, FALSE otherwise.
  1072. *
  1073. * @throws \Drupal\Component\Plugin\Exception\PluginException
  1074. * If a database error occurs.
  1075. */
  1076. protected function ensureTableExists() {
  1077. try {
  1078. if (!$this->connection->schema()->tableExists($this->table)) {
  1079. $this->connection->schema()->createTable($this->table, static::schemaDefinition());
  1080. return TRUE;
  1081. }
  1082. }
  1083. catch (DatabaseException $e) {
  1084. // If another process has already created the config table, attempting to
  1085. // recreate it will throw an exception. In this case just catch the
  1086. // exception and do nothing.
  1087. return TRUE;
  1088. }
  1089. catch (\Exception $e) {
  1090. throw new PluginException($e->getMessage(), NULL, $e);
  1091. }
  1092. return FALSE;
  1093. }
  1094. /**
  1095. * Determines serialized fields in the storage.
  1096. *
  1097. * @return array
  1098. * A list of fields that are serialized in the database.
  1099. */
  1100. protected function serializedFields() {
  1101. if (empty($this->serializedFields)) {
  1102. $schema = static::schemaDefinition();
  1103. foreach ($schema['fields'] as $name => $field) {
  1104. if (!empty($field['serialize'])) {
  1105. $this->serializedFields[] = $name;
  1106. }
  1107. }
  1108. }
  1109. return $this->serializedFields;
  1110. }
  1111. /**
  1112. * Determines fields that are part of the plugin definition.
  1113. *
  1114. * @return array
  1115. * The list of the subset of fields that are part of the plugin definition.
  1116. */
  1117. protected function definitionFields() {
  1118. return $this->definitionFields;
  1119. }
  1120. /**
  1121. * Defines the schema for the tree table.
  1122. *
  1123. * @return array
  1124. * The schema API definition for the SQL storage table.
  1125. *
  1126. * @internal
  1127. */
  1128. protected static function schemaDefinition() {
  1129. $schema = [
  1130. 'description' => 'Contains the menu tree hierarchy.',
  1131. 'fields' => [
  1132. 'menu_name' => [
  1133. 'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.",
  1134. 'type' => 'varchar_ascii',
  1135. 'length' => 32,
  1136. 'not null' => TRUE,
  1137. 'default' => '',
  1138. ],
  1139. 'mlid' => [
  1140. 'description' => 'The menu link ID (mlid) is the integer primary key.',
  1141. 'type' => 'serial',
  1142. 'unsigned' => TRUE,
  1143. 'not null' => TRUE,
  1144. ],
  1145. 'id' => [
  1146. 'description' => 'Unique machine name: the plugin ID.',
  1147. 'type' => 'varchar_ascii',
  1148. 'length' => 255,
  1149. 'not null' => TRUE,
  1150. ],
  1151. 'parent' => [
  1152. 'description' => 'The plugin ID for the parent of this link.',
  1153. 'type' => 'varchar_ascii',
  1154. 'length' => 255,
  1155. 'not null' => TRUE,
  1156. 'default' => '',
  1157. ],
  1158. 'route_name' => [
  1159. 'description' => 'The machine name of a defined Symfony Route this menu item represents.',
  1160. 'type' => 'varchar_ascii',
  1161. 'length' => 255,
  1162. ],
  1163. 'route_param_key' => [
  1164. 'description' => 'An encoded string of route parameters for loading by route.',
  1165. 'type' => 'varchar',
  1166. 'length' => 255,
  1167. ],
  1168. 'route_parameters' => [
  1169. 'description' => 'Serialized array of route parameters of this menu link.',
  1170. 'type' => 'blob',
  1171. 'size' => 'big',
  1172. 'not null' => FALSE,
  1173. 'serialize' => TRUE,
  1174. ],
  1175. 'url' => [
  1176. 'description' => 'The external path this link points to (when not using a route).',
  1177. 'type' => 'varchar',
  1178. 'length' => 255,
  1179. 'not null' => TRUE,
  1180. 'default' => '',
  1181. ],
  1182. 'title' => [
  1183. 'description' => 'The serialized title for the link. May be a TranslatableMarkup.',
  1184. 'type' => 'blob',
  1185. 'size' => 'big',
  1186. 'not null' => FALSE,
  1187. 'serialize' => TRUE,
  1188. ],
  1189. 'description' => [
  1190. 'description' => 'The serialized description of this link - used for admin pages and title attribute. May be a TranslatableMarkup.',
  1191. 'type' => 'blob',
  1192. 'size' => 'big',
  1193. 'not null' => FALSE,
  1194. 'serialize' => TRUE,
  1195. ],
  1196. 'class' => [
  1197. 'description' => 'The class for this link plugin.',
  1198. 'type' => 'text',
  1199. 'not null' => FALSE,
  1200. ],
  1201. 'options' => [
  1202. 'description' => 'A serialized array of URL options, such as a query string or HTML attributes.',
  1203. 'type' => 'blob',
  1204. 'size' => 'big',
  1205. 'not null' => FALSE,
  1206. 'serialize' => TRUE,
  1207. ],
  1208. 'provider' => [
  1209. 'description' => 'The name of the module that generated this link.',
  1210. 'type' => 'varchar_ascii',
  1211. 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
  1212. 'not null' => TRUE,
  1213. 'default' => 'system',
  1214. ],
  1215. 'enabled' => [
  1216. 'description' => 'A flag for whether the link should be rendered in menus. (0 = a disabled menu item that may be shown on admin screens, 1 = a normal, visible link)',
  1217. 'type' => 'int',
  1218. 'not null' => TRUE,
  1219. 'default' => 1,
  1220. 'size' => 'small',
  1221. ],
  1222. 'discovered' => [
  1223. 'description' => 'A flag for whether the link was discovered, so can be purged on rebuild',
  1224. 'type' => 'int',
  1225. 'not null' => TRUE,
  1226. 'default' => 0,
  1227. 'size' => 'small',
  1228. ],
  1229. 'expanded' => [
  1230. 'description' => 'Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)',
  1231. 'type' => 'int',
  1232. 'not null' => TRUE,
  1233. 'default' => 0,
  1234. 'size' => 'small',
  1235. ],
  1236. 'weight' => [
  1237. 'description' => 'Link weight among links in the same menu at the same depth.',
  1238. 'type' => 'int',
  1239. 'not null' => TRUE,
  1240. 'default' => 0,
  1241. ],
  1242. 'metadata' => [
  1243. 'description' => 'A serialized array of data that may be used by the plugin instance.',
  1244. 'type' => 'blob',
  1245. 'size' => 'big',
  1246. 'not null' => FALSE,
  1247. 'serialize' => TRUE,
  1248. ],
  1249. 'has_children' => [
  1250. 'description' => 'Flag indicating whether any enabled links have this link as a parent (1 = enabled children exist, 0 = no enabled children).',
  1251. 'type' => 'int',
  1252. 'not null' => TRUE,
  1253. 'default' => 0,
  1254. 'size' => 'small',
  1255. ],
  1256. 'depth' => [
  1257. 'description' => 'The depth relative to the top level. A link with empty parent will have depth == 1.',
  1258. 'type' => 'int',
  1259. 'not null' => TRUE,
  1260. 'default' => 0,
  1261. 'size' => 'small',
  1262. ],
  1263. 'p1' => [
  1264. 'description' => 'The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the parent link mlid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.',
  1265. 'type' => 'int',
  1266. 'unsigned' => TRUE,
  1267. 'not null' => TRUE,
  1268. 'default' => 0,
  1269. ],
  1270. 'p2' => [
  1271. 'description' => 'The second mlid in the materialized path. See p1.',
  1272. 'type' => 'int',
  1273. 'unsigned' => TRUE,
  1274. 'not null' => TRUE,
  1275. 'default' => 0,
  1276. ],
  1277. 'p3' => [
  1278. 'description' => 'The third mlid in the materialized path. See p1.',
  1279. 'type' => 'int',
  1280. 'unsigned' => TRUE,
  1281. 'not null' => TRUE,
  1282. 'default' => 0,
  1283. ],
  1284. 'p4' => [
  1285. 'description' => 'The fourth mlid in the materialized path. See p1.',
  1286. 'type' => 'int',
  1287. 'unsigned' => TRUE,
  1288. 'not null' => TRUE,
  1289. 'default' => 0,
  1290. ],
  1291. 'p5' => [
  1292. 'description' => 'The fifth mlid in the materialized path. See p1.',
  1293. 'type' => 'int',
  1294. 'unsigned' => TRUE,
  1295. 'not null' => TRUE,
  1296. 'default' => 0,
  1297. ],
  1298. 'p6' => [
  1299. 'description' => 'The sixth mlid in the materialized path. See p1.',
  1300. 'type' => 'int',
  1301. 'unsigned' => TRUE,
  1302. 'not null' => TRUE,
  1303. 'default' => 0,
  1304. ],
  1305. 'p7' => [
  1306. 'description' => 'The seventh mlid in the materialized path. See p1.',
  1307. 'type' => 'int',
  1308. 'unsigned' => TRUE,
  1309. 'not null' => TRUE,
  1310. 'default' => 0,
  1311. ],
  1312. 'p8' => [
  1313. 'description' => 'The eighth mlid in the materialized path. See p1.',
  1314. 'type' => 'int',
  1315. 'unsigned' => TRUE,
  1316. 'not null' => TRUE,
  1317. 'default' => 0,
  1318. ],
  1319. 'p9' => [
  1320. 'description' => 'The ninth mlid in the materialized path. See p1.',
  1321. 'type' => 'int',
  1322. 'unsigned' => TRUE,
  1323. 'not null' => TRUE,
  1324. 'default' => 0,
  1325. ],
  1326. 'form_class' => [
  1327. 'description' => 'meh',
  1328. 'type' => 'varchar',
  1329. 'length' => 255,
  1330. ],
  1331. ],
  1332. 'indexes' => [
  1333. 'menu_parents' => [
  1334. 'menu_name',
  1335. 'p1',
  1336. 'p2',
  1337. 'p3',
  1338. 'p4',
  1339. 'p5',
  1340. 'p6',
  1341. 'p7',
  1342. 'p8',
  1343. 'p9',
  1344. ],
  1345. // @todo Test this index for effectiveness.
  1346. // https://www.drupal.org/node/2302197
  1347. 'menu_parent_expand_child' => [
  1348. 'menu_name', 'expanded',
  1349. 'has_children',
  1350. ['parent', 16],
  1351. ],
  1352. 'route_values' => [
  1353. ['route_name', 32],
  1354. ['route_param_key', 16],
  1355. ],
  1356. ],
  1357. 'primary key' => ['mlid'],
  1358. 'unique keys' => [
  1359. 'id' => ['id'],
  1360. ],
  1361. ];
  1362. return $schema;
  1363. }
  1364. /**
  1365. * Find any previously discovered menu links that no longer exist.
  1366. *
  1367. * @param array $definitions
  1368. * The new menu link definitions.
  1369. * @return array
  1370. * A list of menu link IDs that no longer exist.
  1371. */
  1372. protected function findNoLongerExistingLinks(array $definitions) {
  1373. if ($definitions) {
  1374. $query = $this->connection->select($this->table, NULL, $this->options);
  1375. $query->addField($this->table, 'id');
  1376. $query->condition('discovered', 1);
  1377. $query->condition('id', array_keys($definitions), 'NOT IN');
  1378. // Starting from links with the greatest depth will minimize the amount
  1379. // of re-parenting done by the menu storage.
  1380. $query->orderBy('depth', 'DESC');
  1381. $result = $query->execute()->fetchCol();
  1382. }
  1383. else {
  1384. $result = [];
  1385. }
  1386. return $result;
  1387. }
  1388. /**
  1389. * Purge menu links from the database.
  1390. *
  1391. * @param array $ids
  1392. * A list of menu link IDs to be purged.
  1393. */
  1394. protected function doDeleteMultiple(array $ids) {
  1395. $this->connection->delete($this->table, $this->options)
  1396. ->condition('id', $ids, 'IN')
  1397. ->execute();
  1398. }
  1399. }