MenuTreeStorage.php 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498
  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\Query\SelectInterface;
  11. use Drupal\Core\Database\SchemaObjectExistsException;
  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. // Build the cache ID; sort 'expanded' and 'conditions' to prevent duplicate
  784. // cache items.
  785. sort($parameters->expandedParents);
  786. asort($parameters->conditions);
  787. $tree_cid = "tree-data:$menu_name:" . serialize($parameters);
  788. $cache = $this->menuCacheBackend->get($tree_cid);
  789. if ($cache && isset($cache->data)) {
  790. $data = $cache->data;
  791. // Cache the definitions in memory so they don't need to be loaded again.
  792. $this->definitions += $data['definitions'];
  793. unset($data['definitions']);
  794. }
  795. else {
  796. $links = $this->loadLinks($menu_name, $parameters);
  797. $data['tree'] = $this->doBuildTreeData($links, $parameters->activeTrail, $parameters->minDepth);
  798. $data['definitions'] = [];
  799. $data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']);
  800. $this->menuCacheBackend->set($tree_cid, $data, Cache::PERMANENT, ['config:system.menu.' . $menu_name]);
  801. // The definitions were already added to $this->definitions in
  802. // $this->doBuildTreeData()
  803. unset($data['definitions']);
  804. }
  805. return $data;
  806. }
  807. /**
  808. * Loads links in the given menu, according to the given tree parameters.
  809. *
  810. * @param string $menu_name
  811. * A menu name.
  812. * @param \Drupal\Core\Menu\MenuTreeParameters $parameters
  813. * The parameters to determine which menu links to be loaded into a tree.
  814. * This method will set the absolute minimum depth, which is used in
  815. * MenuTreeStorage::doBuildTreeData().
  816. *
  817. * @return array
  818. * A flat array of menu links that are part of the menu. Each array element
  819. * is an associative array of information about the menu link, containing
  820. * the fields from the {menu_tree} table. This array must be ordered
  821. * depth-first.
  822. */
  823. protected function loadLinks($menu_name, MenuTreeParameters $parameters) {
  824. $query = $this->connection->select($this->table, $this->options);
  825. $query->fields($this->table);
  826. // Allow a custom root to be specified for loading a menu link tree. If
  827. // omitted, the default root (i.e. the actual root, '') is used.
  828. if ($parameters->root !== '') {
  829. $root = $this->loadFull($parameters->root);
  830. // If the custom root does not exist, we cannot load the links below it.
  831. if (!$root) {
  832. return [];
  833. }
  834. // When specifying a custom root, we only want to find links whose
  835. // parent IDs match that of the root; that's how we ignore the rest of the
  836. // tree. In other words: we exclude everything unreachable from the
  837. // custom root.
  838. for ($i = 1; $i <= $root['depth']; $i++) {
  839. $query->condition("p$i", $root["p$i"]);
  840. }
  841. // When specifying a custom root, the menu is determined by that root.
  842. $menu_name = $root['menu_name'];
  843. // If the custom root exists, then we must rewrite some of our
  844. // parameters; parameters are relative to the root (default or custom),
  845. // but the queries require absolute numbers, so adjust correspondingly.
  846. if (isset($parameters->minDepth)) {
  847. $parameters->minDepth += $root['depth'];
  848. }
  849. else {
  850. $parameters->minDepth = $root['depth'];
  851. }
  852. if (isset($parameters->maxDepth)) {
  853. $parameters->maxDepth += $root['depth'];
  854. }
  855. }
  856. // If no minimum depth is specified, then set the actual minimum depth,
  857. // depending on the root.
  858. if (!isset($parameters->minDepth)) {
  859. if ($parameters->root !== '' && $root) {
  860. $parameters->minDepth = $root['depth'];
  861. }
  862. else {
  863. $parameters->minDepth = 1;
  864. }
  865. }
  866. for ($i = 1; $i <= $this->maxDepth(); $i++) {
  867. $query->orderBy('p' . $i, 'ASC');
  868. }
  869. $query->condition('menu_name', $menu_name);
  870. if (!empty($parameters->expandedParents)) {
  871. $query->condition('parent', $parameters->expandedParents, 'IN');
  872. }
  873. if (isset($parameters->minDepth) && $parameters->minDepth > 1) {
  874. $query->condition('depth', $parameters->minDepth, '>=');
  875. }
  876. if (isset($parameters->maxDepth)) {
  877. $query->condition('depth', $parameters->maxDepth, '<=');
  878. }
  879. // Add custom query conditions, if any were passed.
  880. if (!empty($parameters->conditions)) {
  881. // Only allow conditions that are testing definition fields.
  882. $parameters->conditions = array_intersect_key($parameters->conditions, array_flip($this->definitionFields()));
  883. $serialized_fields = $this->serializedFields();
  884. foreach ($parameters->conditions as $column => $value) {
  885. if (is_array($value)) {
  886. $operator = $value[1];
  887. $value = $value[0];
  888. }
  889. else {
  890. $operator = '=';
  891. }
  892. if (in_array($column, $serialized_fields)) {
  893. $value = serialize($value);
  894. }
  895. $query->condition($column, $value, $operator);
  896. }
  897. }
  898. $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
  899. return $links;
  900. }
  901. /**
  902. * Traverses the menu tree and collects all the route names and definitions.
  903. *
  904. * @param array $tree
  905. * The menu tree you wish to operate on.
  906. * @param array $definitions
  907. * An array to accumulate definitions by reference.
  908. *
  909. * @return array
  910. * Array of route names, with all values being unique.
  911. */
  912. protected function collectRoutesAndDefinitions(array $tree, array &$definitions) {
  913. return array_values($this->doCollectRoutesAndDefinitions($tree, $definitions));
  914. }
  915. /**
  916. * Collects all the route names and definitions.
  917. *
  918. * @param array $tree
  919. * A menu link tree from MenuTreeStorage::doBuildTreeData()
  920. * @param array $definitions
  921. * The collected definitions which are populated by reference.
  922. *
  923. * @return array
  924. * The collected route names.
  925. */
  926. protected function doCollectRoutesAndDefinitions(array $tree, array &$definitions) {
  927. $route_names = [];
  928. foreach (array_keys($tree) as $id) {
  929. $definitions[$id] = $this->definitions[$id];
  930. if (!empty($definition['route_name'])) {
  931. $route_names[$definition['route_name']] = $definition['route_name'];
  932. }
  933. if ($tree[$id]['subtree']) {
  934. $route_names += $this->doCollectRoutesAndDefinitions($tree[$id]['subtree'], $definitions);
  935. }
  936. }
  937. return $route_names;
  938. }
  939. /**
  940. * {@inheritdoc}
  941. */
  942. public function loadSubtreeData($id, $max_relative_depth = NULL) {
  943. $tree = [];
  944. $root = $this->loadFull($id);
  945. if (!$root) {
  946. return $tree;
  947. }
  948. $parameters = new MenuTreeParameters();
  949. $parameters->setRoot($id)->onlyEnabledLinks();
  950. return $this->loadTreeData($root['menu_name'], $parameters);
  951. }
  952. /**
  953. * {@inheritdoc}
  954. */
  955. public function menuNameInUse($menu_name) {
  956. $query = $this->connection->select($this->table, $this->options);
  957. $query->addField($this->table, 'mlid');
  958. $query->condition('menu_name', $menu_name);
  959. $query->range(0, 1);
  960. return (bool) $this->safeExecuteSelect($query);
  961. }
  962. /**
  963. * {@inheritdoc}
  964. */
  965. public function getMenuNames() {
  966. $query = $this->connection->select($this->table, $this->options);
  967. $query->addField($this->table, 'menu_name');
  968. $query->distinct();
  969. return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
  970. }
  971. /**
  972. * {@inheritdoc}
  973. */
  974. public function countMenuLinks($menu_name = NULL) {
  975. $query = $this->connection->select($this->table, $this->options);
  976. if ($menu_name) {
  977. $query->condition('menu_name', $menu_name);
  978. }
  979. return $this->safeExecuteSelect($query->countQuery())->fetchField();
  980. }
  981. /**
  982. * {@inheritdoc}
  983. */
  984. public function getAllChildIds($id) {
  985. $root = $this->loadFull($id);
  986. if (!$root) {
  987. return [];
  988. }
  989. $query = $this->connection->select($this->table, $this->options);
  990. $query->fields($this->table, ['id']);
  991. $query->condition('menu_name', $root['menu_name']);
  992. for ($i = 1; $i <= $root['depth']; $i++) {
  993. $query->condition("p$i", $root["p$i"]);
  994. }
  995. // The next p column should not be empty. This excludes the root link.
  996. $query->condition("p$i", 0, '>');
  997. return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
  998. }
  999. /**
  1000. * {@inheritdoc}
  1001. */
  1002. public function loadAllChildren($id, $max_relative_depth = NULL) {
  1003. $parameters = new MenuTreeParameters();
  1004. $parameters->setRoot($id)->excludeRoot()->setMaxDepth($max_relative_depth)->onlyEnabledLinks();
  1005. $links = $this->loadLinks(NULL, $parameters);
  1006. foreach ($links as $id => $link) {
  1007. $links[$id] = $this->prepareLink($link);
  1008. }
  1009. return $links;
  1010. }
  1011. /**
  1012. * Prepares the data for calling $this->treeDataRecursive().
  1013. */
  1014. protected function doBuildTreeData(array $links, array $parents = [], $depth = 1) {
  1015. // Reverse the array so we can use the more efficient array_pop() function.
  1016. $links = array_reverse($links);
  1017. return $this->treeDataRecursive($links, $parents, $depth);
  1018. }
  1019. /**
  1020. * Builds the data representing a menu tree.
  1021. *
  1022. * The function is a bit complex because the rendering of a link depends on
  1023. * the next menu link.
  1024. *
  1025. * @param array $links
  1026. * A flat array of menu links that are part of the menu. Each array element
  1027. * is an associative array of information about the menu link, containing
  1028. * the fields from the $this->table. This array must be ordered
  1029. * depth-first. MenuTreeStorage::loadTreeData() includes a sample query.
  1030. * @param array $parents
  1031. * An array of the menu link ID values that are in the path from the current
  1032. * page to the root of the menu tree.
  1033. * @param int $depth
  1034. * The minimum depth to include in the returned menu tree.
  1035. *
  1036. * @return array
  1037. * The fully built tree.
  1038. *
  1039. * @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData()
  1040. */
  1041. protected function treeDataRecursive(array &$links, array $parents, $depth) {
  1042. $tree = [];
  1043. while ($tree_link_definition = array_pop($links)) {
  1044. $tree[$tree_link_definition['id']] = [
  1045. 'definition' => $this->prepareLink($tree_link_definition, TRUE),
  1046. 'has_children' => $tree_link_definition['has_children'],
  1047. // We need to determine if we're on the path to root so we can later
  1048. // build the correct active trail.
  1049. 'in_active_trail' => in_array($tree_link_definition['id'], $parents),
  1050. 'subtree' => [],
  1051. 'depth' => $tree_link_definition['depth'],
  1052. ];
  1053. // Look ahead to the next link, but leave it on the array so it's
  1054. // available to other recursive function calls if we return or build a
  1055. // sub-tree.
  1056. $next = end($links);
  1057. // Check whether the next link is the first in a new sub-tree.
  1058. if ($next && $next['depth'] > $depth) {
  1059. // Recursively call doBuildTreeData to build the sub-tree.
  1060. $tree[$tree_link_definition['id']]['subtree'] = $this->treeDataRecursive($links, $parents, $next['depth']);
  1061. // Fetch next link after filling the sub-tree.
  1062. $next = end($links);
  1063. }
  1064. // Determine if we should exit the loop and return.
  1065. if (!$next || $next['depth'] < $depth) {
  1066. break;
  1067. }
  1068. }
  1069. return $tree;
  1070. }
  1071. /**
  1072. * Checks if the tree table exists and create it if not.
  1073. *
  1074. * @return bool
  1075. * TRUE if the table was created, FALSE otherwise.
  1076. *
  1077. * @throws \Drupal\Component\Plugin\Exception\PluginException
  1078. * If a database error occurs.
  1079. */
  1080. protected function ensureTableExists() {
  1081. try {
  1082. if (!$this->connection->schema()->tableExists($this->table)) {
  1083. $this->connection->schema()->createTable($this->table, static::schemaDefinition());
  1084. return TRUE;
  1085. }
  1086. }
  1087. catch (SchemaObjectExistsException $e) {
  1088. // If another process has already created the config table, attempting to
  1089. // recreate it will throw an exception. In this case just catch the
  1090. // exception and do nothing.
  1091. return TRUE;
  1092. }
  1093. catch (\Exception $e) {
  1094. throw new PluginException($e->getMessage(), NULL, $e);
  1095. }
  1096. return FALSE;
  1097. }
  1098. /**
  1099. * Determines serialized fields in the storage.
  1100. *
  1101. * @return array
  1102. * A list of fields that are serialized in the database.
  1103. */
  1104. protected function serializedFields() {
  1105. if (empty($this->serializedFields)) {
  1106. $schema = static::schemaDefinition();
  1107. foreach ($schema['fields'] as $name => $field) {
  1108. if (!empty($field['serialize'])) {
  1109. $this->serializedFields[] = $name;
  1110. }
  1111. }
  1112. }
  1113. return $this->serializedFields;
  1114. }
  1115. /**
  1116. * Determines fields that are part of the plugin definition.
  1117. *
  1118. * @return array
  1119. * The list of the subset of fields that are part of the plugin definition.
  1120. */
  1121. protected function definitionFields() {
  1122. return $this->definitionFields;
  1123. }
  1124. /**
  1125. * Defines the schema for the tree table.
  1126. *
  1127. * @return array
  1128. * The schema API definition for the SQL storage table.
  1129. *
  1130. * @internal
  1131. */
  1132. protected static function schemaDefinition() {
  1133. $schema = [
  1134. 'description' => 'Contains the menu tree hierarchy.',
  1135. 'fields' => [
  1136. 'menu_name' => [
  1137. 'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.",
  1138. 'type' => 'varchar_ascii',
  1139. 'length' => 32,
  1140. 'not null' => TRUE,
  1141. 'default' => '',
  1142. ],
  1143. 'mlid' => [
  1144. 'description' => 'The menu link ID (mlid) is the integer primary key.',
  1145. 'type' => 'serial',
  1146. 'unsigned' => TRUE,
  1147. 'not null' => TRUE,
  1148. ],
  1149. 'id' => [
  1150. 'description' => 'Unique machine name: the plugin ID.',
  1151. 'type' => 'varchar_ascii',
  1152. 'length' => 255,
  1153. 'not null' => TRUE,
  1154. ],
  1155. 'parent' => [
  1156. 'description' => 'The plugin ID for the parent of this link.',
  1157. 'type' => 'varchar_ascii',
  1158. 'length' => 255,
  1159. 'not null' => TRUE,
  1160. 'default' => '',
  1161. ],
  1162. 'route_name' => [
  1163. 'description' => 'The machine name of a defined Symfony Route this menu item represents.',
  1164. 'type' => 'varchar_ascii',
  1165. 'length' => 255,
  1166. ],
  1167. 'route_param_key' => [
  1168. 'description' => 'An encoded string of route parameters for loading by route.',
  1169. 'type' => 'varchar',
  1170. 'length' => 255,
  1171. ],
  1172. 'route_parameters' => [
  1173. 'description' => 'Serialized array of route parameters of this menu link.',
  1174. 'type' => 'blob',
  1175. 'size' => 'big',
  1176. 'not null' => FALSE,
  1177. 'serialize' => TRUE,
  1178. ],
  1179. 'url' => [
  1180. 'description' => 'The external path this link points to (when not using a route).',
  1181. 'type' => 'varchar',
  1182. 'length' => 255,
  1183. 'not null' => TRUE,
  1184. 'default' => '',
  1185. ],
  1186. 'title' => [
  1187. 'description' => 'The serialized title for the link. May be a TranslatableMarkup.',
  1188. 'type' => 'blob',
  1189. 'size' => 'big',
  1190. 'not null' => FALSE,
  1191. 'serialize' => TRUE,
  1192. ],
  1193. 'description' => [
  1194. 'description' => 'The serialized description of this link - used for admin pages and title attribute. May be a TranslatableMarkup.',
  1195. 'type' => 'blob',
  1196. 'size' => 'big',
  1197. 'not null' => FALSE,
  1198. 'serialize' => TRUE,
  1199. ],
  1200. 'class' => [
  1201. 'description' => 'The class for this link plugin.',
  1202. 'type' => 'text',
  1203. 'not null' => FALSE,
  1204. ],
  1205. 'options' => [
  1206. 'description' => 'A serialized array of URL options, such as a query string or HTML attributes.',
  1207. 'type' => 'blob',
  1208. 'size' => 'big',
  1209. 'not null' => FALSE,
  1210. 'serialize' => TRUE,
  1211. ],
  1212. 'provider' => [
  1213. 'description' => 'The name of the module that generated this link.',
  1214. 'type' => 'varchar_ascii',
  1215. 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
  1216. 'not null' => TRUE,
  1217. 'default' => 'system',
  1218. ],
  1219. 'enabled' => [
  1220. '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)',
  1221. 'type' => 'int',
  1222. 'not null' => TRUE,
  1223. 'default' => 1,
  1224. 'size' => 'small',
  1225. ],
  1226. 'discovered' => [
  1227. 'description' => 'A flag for whether the link was discovered, so can be purged on rebuild',
  1228. 'type' => 'int',
  1229. 'not null' => TRUE,
  1230. 'default' => 0,
  1231. 'size' => 'small',
  1232. ],
  1233. 'expanded' => [
  1234. '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)',
  1235. 'type' => 'int',
  1236. 'not null' => TRUE,
  1237. 'default' => 0,
  1238. 'size' => 'small',
  1239. ],
  1240. 'weight' => [
  1241. 'description' => 'Link weight among links in the same menu at the same depth.',
  1242. 'type' => 'int',
  1243. 'not null' => TRUE,
  1244. 'default' => 0,
  1245. ],
  1246. 'metadata' => [
  1247. 'description' => 'A serialized array of data that may be used by the plugin instance.',
  1248. 'type' => 'blob',
  1249. 'size' => 'big',
  1250. 'not null' => FALSE,
  1251. 'serialize' => TRUE,
  1252. ],
  1253. 'has_children' => [
  1254. 'description' => 'Flag indicating whether any enabled links have this link as a parent (1 = enabled children exist, 0 = no enabled children).',
  1255. 'type' => 'int',
  1256. 'not null' => TRUE,
  1257. 'default' => 0,
  1258. 'size' => 'small',
  1259. ],
  1260. 'depth' => [
  1261. 'description' => 'The depth relative to the top level. A link with empty parent will have depth == 1.',
  1262. 'type' => 'int',
  1263. 'not null' => TRUE,
  1264. 'default' => 0,
  1265. 'size' => 'small',
  1266. ],
  1267. 'p1' => [
  1268. '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.',
  1269. 'type' => 'int',
  1270. 'unsigned' => TRUE,
  1271. 'not null' => TRUE,
  1272. 'default' => 0,
  1273. ],
  1274. 'p2' => [
  1275. 'description' => 'The second mlid in the materialized path. See p1.',
  1276. 'type' => 'int',
  1277. 'unsigned' => TRUE,
  1278. 'not null' => TRUE,
  1279. 'default' => 0,
  1280. ],
  1281. 'p3' => [
  1282. 'description' => 'The third mlid in the materialized path. See p1.',
  1283. 'type' => 'int',
  1284. 'unsigned' => TRUE,
  1285. 'not null' => TRUE,
  1286. 'default' => 0,
  1287. ],
  1288. 'p4' => [
  1289. 'description' => 'The fourth mlid in the materialized path. See p1.',
  1290. 'type' => 'int',
  1291. 'unsigned' => TRUE,
  1292. 'not null' => TRUE,
  1293. 'default' => 0,
  1294. ],
  1295. 'p5' => [
  1296. 'description' => 'The fifth mlid in the materialized path. See p1.',
  1297. 'type' => 'int',
  1298. 'unsigned' => TRUE,
  1299. 'not null' => TRUE,
  1300. 'default' => 0,
  1301. ],
  1302. 'p6' => [
  1303. 'description' => 'The sixth mlid in the materialized path. See p1.',
  1304. 'type' => 'int',
  1305. 'unsigned' => TRUE,
  1306. 'not null' => TRUE,
  1307. 'default' => 0,
  1308. ],
  1309. 'p7' => [
  1310. 'description' => 'The seventh mlid in the materialized path. See p1.',
  1311. 'type' => 'int',
  1312. 'unsigned' => TRUE,
  1313. 'not null' => TRUE,
  1314. 'default' => 0,
  1315. ],
  1316. 'p8' => [
  1317. 'description' => 'The eighth mlid in the materialized path. See p1.',
  1318. 'type' => 'int',
  1319. 'unsigned' => TRUE,
  1320. 'not null' => TRUE,
  1321. 'default' => 0,
  1322. ],
  1323. 'p9' => [
  1324. 'description' => 'The ninth mlid in the materialized path. See p1.',
  1325. 'type' => 'int',
  1326. 'unsigned' => TRUE,
  1327. 'not null' => TRUE,
  1328. 'default' => 0,
  1329. ],
  1330. 'form_class' => [
  1331. 'description' => 'meh',
  1332. 'type' => 'varchar',
  1333. 'length' => 255,
  1334. ],
  1335. ],
  1336. 'indexes' => [
  1337. 'menu_parents' => [
  1338. 'menu_name',
  1339. 'p1',
  1340. 'p2',
  1341. 'p3',
  1342. 'p4',
  1343. 'p5',
  1344. 'p6',
  1345. 'p7',
  1346. 'p8',
  1347. 'p9',
  1348. ],
  1349. // @todo Test this index for effectiveness.
  1350. // https://www.drupal.org/node/2302197
  1351. 'menu_parent_expand_child' => [
  1352. 'menu_name', 'expanded',
  1353. 'has_children',
  1354. ['parent', 16],
  1355. ],
  1356. 'route_values' => [
  1357. ['route_name', 32],
  1358. ['route_param_key', 16],
  1359. ],
  1360. ],
  1361. 'primary key' => ['mlid'],
  1362. 'unique keys' => [
  1363. 'id' => ['id'],
  1364. ],
  1365. ];
  1366. return $schema;
  1367. }
  1368. /**
  1369. * Find any previously discovered menu links that no longer exist.
  1370. *
  1371. * @param array $definitions
  1372. * The new menu link definitions.
  1373. * @return array
  1374. * A list of menu link IDs that no longer exist.
  1375. */
  1376. protected function findNoLongerExistingLinks(array $definitions) {
  1377. if ($definitions) {
  1378. $query = $this->connection->select($this->table, NULL, $this->options);
  1379. $query->addField($this->table, 'id');
  1380. $query->condition('discovered', 1);
  1381. $query->condition('id', array_keys($definitions), 'NOT IN');
  1382. // Starting from links with the greatest depth will minimize the amount
  1383. // of re-parenting done by the menu storage.
  1384. $query->orderBy('depth', 'DESC');
  1385. $result = $query->execute()->fetchCol();
  1386. }
  1387. else {
  1388. $result = [];
  1389. }
  1390. return $result;
  1391. }
  1392. /**
  1393. * Purge menu links from the database.
  1394. *
  1395. * @param array $ids
  1396. * A list of menu link IDs to be purged.
  1397. */
  1398. protected function doDeleteMultiple(array $ids) {
  1399. $this->connection->delete($this->table, $this->options)
  1400. ->condition('id', $ids, 'IN')
  1401. ->execute();
  1402. }
  1403. }