MenuTreeStorageTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. namespace Drupal\KernelTests\Core\Menu;
  3. use Drupal\Component\Plugin\Exception\PluginException;
  4. use Drupal\Core\Menu\MenuTreeParameters;
  5. use Drupal\Core\Menu\MenuTreeStorage;
  6. use Drupal\KernelTests\KernelTestBase;
  7. /**
  8. * Tests the menu tree storage.
  9. *
  10. * @group Menu
  11. *
  12. * @see \Drupal\Core\Menu\MenuTreeStorage
  13. */
  14. class MenuTreeStorageTest extends KernelTestBase {
  15. /**
  16. * The tested tree storage.
  17. *
  18. * @var \Drupal\Core\Menu\MenuTreeStorage
  19. */
  20. protected $treeStorage;
  21. /**
  22. * The database connection.
  23. *
  24. * @var \Drupal\Core\Database\Connection
  25. */
  26. protected $connection;
  27. /**
  28. * {@inheritdoc}
  29. */
  30. protected function setUp() {
  31. parent::setUp();
  32. $this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), $this->container->get('cache_tags.invalidator'), 'menu_tree');
  33. $this->connection = $this->container->get('database');
  34. }
  35. /**
  36. * Tests the tree storage when no tree was built yet.
  37. */
  38. public function testBasicMethods() {
  39. $this->doTestEmptyStorage();
  40. $this->doTestTable();
  41. }
  42. /**
  43. * Ensures that there are no menu links by default.
  44. */
  45. protected function doTestEmptyStorage() {
  46. $this->assertEqual(0, $this->treeStorage->countMenuLinks());
  47. }
  48. /**
  49. * Ensures that table gets created on the fly.
  50. */
  51. protected function doTestTable() {
  52. // Test that we can create a tree storage with an arbitrary table name and
  53. // that selecting from the storage creates the table.
  54. $tree_storage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), $this->container->get('cache_tags.invalidator'), 'test_menu_tree');
  55. $this->assertFalse($this->connection->schema()->tableExists('test_menu_tree'), 'Test table is not yet created');
  56. $tree_storage->countMenuLinks();
  57. $this->assertTrue($this->connection->schema()->tableExists('test_menu_tree'), 'Test table was created');
  58. }
  59. /**
  60. * Tests with a simple linear hierarchy.
  61. */
  62. public function testSimpleHierarchy() {
  63. // Add some links with parent on the previous one and test some values.
  64. // <tools>
  65. // - test1
  66. // -- test2
  67. // --- test3
  68. $this->addMenuLink('test1', '');
  69. $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
  70. $this->addMenuLink('test2', 'test1');
  71. $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test2']);
  72. $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 2], ['test1']);
  73. $this->addMenuLink('test3', 'test2');
  74. $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test2', 'test3']);
  75. $this->assertMenuLink('test2', ['has_children' => 1, 'depth' => 2], ['test1'], ['test3']);
  76. $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 3], ['test2', 'test1']);
  77. }
  78. /**
  79. * Tests the tree with moving links inside the hierarchy.
  80. */
  81. public function testMenuLinkMoving() {
  82. // Before the move.
  83. // <tools>
  84. // - test1
  85. // -- test2
  86. // --- test3
  87. // - test4
  88. // -- test5
  89. // --- test6
  90. $this->addMenuLink('test1', '');
  91. $this->addMenuLink('test2', 'test1');
  92. $this->addMenuLink('test3', 'test2');
  93. $this->addMenuLink('test4', '');
  94. $this->addMenuLink('test5', 'test4');
  95. $this->addMenuLink('test6', 'test5');
  96. $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test2', 'test3']);
  97. $this->assertMenuLink('test2', ['has_children' => 1, 'depth' => 2], ['test1'], ['test3']);
  98. $this->assertMenuLink('test4', ['has_children' => 1, 'depth' => 1], [], ['test5', 'test6']);
  99. $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 2], ['test4'], ['test6']);
  100. $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 3], ['test5', 'test4']);
  101. $this->moveMenuLink('test2', 'test5');
  102. // After the 1st move.
  103. // <tools>
  104. // - test1
  105. // - test4
  106. // -- test5
  107. // --- test2
  108. // ---- test3
  109. // --- test6
  110. $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
  111. $this->assertMenuLink('test2', ['has_children' => 1, 'depth' => 3], ['test5', 'test4'], ['test3']);
  112. $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 4], ['test2', 'test5', 'test4']);
  113. $this->assertMenuLink('test4', ['has_children' => 1, 'depth' => 1], [], ['test5', 'test2', 'test3', 'test6']);
  114. $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 2], ['test4'], ['test2', 'test3', 'test6']);
  115. $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 3], ['test5', 'test4']);
  116. $this->moveMenuLink('test4', 'test1');
  117. $this->moveMenuLink('test3', 'test1');
  118. // After the next 2 moves.
  119. // <tools>
  120. // - test1
  121. // -- test3
  122. // -- test4
  123. // --- test5
  124. // ---- test2
  125. // ---- test6
  126. $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test4', 'test5', 'test2', 'test3', 'test6']);
  127. $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 4], ['test5', 'test4', 'test1']);
  128. $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 2], ['test1']);
  129. $this->assertMenuLink('test4', ['has_children' => 1, 'depth' => 2], ['test1'], ['test2', 'test5', 'test6']);
  130. $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 3], ['test4', 'test1'], ['test2', 'test6']);
  131. $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 4], ['test5', 'test4', 'test1']);
  132. // Deleting a link in the middle should re-attach child links to the parent.
  133. $this->treeStorage->delete('test4');
  134. // After the delete.
  135. // <tools>
  136. // - test1
  137. // -- test3
  138. // -- test5
  139. // --- test2
  140. // --- test6
  141. $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test5', 'test2', 'test3', 'test6']);
  142. $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 3], ['test5', 'test1']);
  143. $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 2], ['test1']);
  144. $this->assertFalse($this->treeStorage->load('test4'));
  145. $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 2], ['test1'], ['test2', 'test6']);
  146. $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 3], ['test5', 'test1']);
  147. }
  148. /**
  149. * Tests with disabled child links.
  150. */
  151. public function testMenuDisabledChildLinks() {
  152. // Add some links with parent on the previous one and test some values.
  153. // <tools>
  154. // - test1
  155. // -- test2 (disabled)
  156. $this->addMenuLink('test1', '');
  157. $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
  158. $this->addMenuLink('test2', 'test1', '<front>', [], 'tools', ['enabled' => 0]);
  159. // The 1st link does not have any visible children, so has_children is 0.
  160. $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
  161. $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 2, 'enabled' => 0], ['test1']);
  162. // Add more links with parent on the previous one.
  163. // <footer>
  164. // - footerA
  165. // ===============
  166. // <tools>
  167. // - test1
  168. // -- test2 (disabled)
  169. // --- test3
  170. // ---- test4
  171. // ----- test5
  172. // ------ test6
  173. // ------- test7
  174. // -------- test8
  175. // --------- test9
  176. $this->addMenuLink('footerA', '', '<front>', [], 'footer');
  177. $visible_children = [];
  178. for ($i = 3; $i <= $this->treeStorage->maxDepth(); $i++) {
  179. $parent = $i - 1;
  180. $this->addMenuLink("test$i", "test$parent");
  181. $visible_children[] = "test$i";
  182. }
  183. // The 1st link does not have any visible children, so has_children is still
  184. // 0. However, it has visible links below it that will be found.
  185. $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1], [], $visible_children);
  186. // This should fail since test9 would end up at greater than max depth.
  187. try {
  188. $this->moveMenuLink('test1', 'footerA');
  189. $this->fail('Exception was not thrown');
  190. }
  191. catch (PluginException $e) {
  192. // Expected exception; just continue testing.
  193. }
  194. // The opposite move should work, and change the has_children flag.
  195. $this->moveMenuLink('footerA', 'test1');
  196. $visible_children[] = 'footerA';
  197. $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], $visible_children);
  198. }
  199. /**
  200. * Tests the loadTreeData method.
  201. */
  202. public function testLoadTree() {
  203. $this->addMenuLink('test1', '');
  204. $this->addMenuLink('test2', 'test1');
  205. $this->addMenuLink('test3', 'test2');
  206. $this->addMenuLink('test4');
  207. $this->addMenuLink('test5', 'test4');
  208. $data = $this->treeStorage->loadTreeData('tools', new MenuTreeParameters());
  209. $tree = $data['tree'];
  210. $this->assertCount(1, $tree['test1']['subtree']);
  211. $this->assertCount(1, $tree['test1']['subtree']['test2']['subtree']);
  212. $this->assertCount(0, $tree['test1']['subtree']['test2']['subtree']['test3']['subtree']);
  213. $this->assertCount(1, $tree['test4']['subtree']);
  214. $this->assertCount(0, $tree['test4']['subtree']['test5']['subtree']);
  215. $parameters = new MenuTreeParameters();
  216. $parameters->setActiveTrail(['test4', 'test5']);
  217. $data = $this->treeStorage->loadTreeData('tools', $parameters);
  218. $tree = $data['tree'];
  219. $this->assertCount(1, $tree['test1']['subtree']);
  220. $this->assertFalse($tree['test1']['in_active_trail']);
  221. $this->assertCount(1, $tree['test1']['subtree']['test2']['subtree']);
  222. $this->assertFalse($tree['test1']['subtree']['test2']['in_active_trail']);
  223. $this->assertCount(0, $tree['test1']['subtree']['test2']['subtree']['test3']['subtree']);
  224. $this->assertFalse($tree['test1']['subtree']['test2']['subtree']['test3']['in_active_trail']);
  225. $this->assertCount(1, $tree['test4']['subtree']);
  226. $this->assertTrue($tree['test4']['in_active_trail']);
  227. $this->assertCount(0, $tree['test4']['subtree']['test5']['subtree']);
  228. $this->assertTrue($tree['test4']['subtree']['test5']['in_active_trail']);
  229. // Add some conditions to ensure that conditions work as expected.
  230. $parameters = new MenuTreeParameters();
  231. $parameters->addCondition('parent', 'test1');
  232. $data = $this->treeStorage->loadTreeData('tools', $parameters);
  233. $this->assertCount(1, $data['tree']);
  234. $this->assertEqual($data['tree']['test2']['definition']['id'], 'test2');
  235. $this->assertEqual($data['tree']['test2']['subtree'], []);
  236. // Test for only enabled links.
  237. $link = $this->treeStorage->load('test3');
  238. $link['enabled'] = FALSE;
  239. $this->treeStorage->save($link);
  240. $link = $this->treeStorage->load('test4');
  241. $link['enabled'] = FALSE;
  242. $this->treeStorage->save($link);
  243. $link = $this->treeStorage->load('test5');
  244. $link['enabled'] = FALSE;
  245. $this->treeStorage->save($link);
  246. $parameters = new MenuTreeParameters();
  247. $parameters->onlyEnabledLinks();
  248. $data = $this->treeStorage->loadTreeData('tools', $parameters);
  249. $this->assertCount(1, $data['tree']);
  250. $this->assertEqual($data['tree']['test1']['definition']['id'], 'test1');
  251. $this->assertCount(1, $data['tree']['test1']['subtree']);
  252. $this->assertEqual($data['tree']['test1']['subtree']['test2']['definition']['id'], 'test2');
  253. $this->assertEqual($data['tree']['test1']['subtree']['test2']['subtree'], []);
  254. }
  255. /**
  256. * Tests finding the subtree height with content menu links.
  257. */
  258. public function testSubtreeHeight() {
  259. // root
  260. // - child1
  261. // -- child2
  262. // --- child3
  263. // ---- child4
  264. $this->addMenuLink('root');
  265. $this->addMenuLink('child1', 'root');
  266. $this->addMenuLink('child2', 'child1');
  267. $this->addMenuLink('child3', 'child2');
  268. $this->addMenuLink('child4', 'child3');
  269. $this->assertEqual($this->treeStorage->getSubtreeHeight('root'), 5);
  270. $this->assertEqual($this->treeStorage->getSubtreeHeight('child1'), 4);
  271. $this->assertEqual($this->treeStorage->getSubtreeHeight('child2'), 3);
  272. $this->assertEqual($this->treeStorage->getSubtreeHeight('child3'), 2);
  273. $this->assertEqual($this->treeStorage->getSubtreeHeight('child4'), 1);
  274. }
  275. /**
  276. * Ensure hierarchy persists after a menu rebuild.
  277. */
  278. public function testMenuRebuild() {
  279. // root
  280. // - child1
  281. // -- child2
  282. // --- child3
  283. // ---- child4
  284. $this->addMenuLink('root');
  285. $this->addMenuLink('child1', 'root');
  286. $this->addMenuLink('child2', 'child1');
  287. $this->addMenuLink('child3', 'child2');
  288. $this->addMenuLink('child4', 'child3');
  289. $this->assertEqual($this->treeStorage->getSubtreeHeight('root'), 5);
  290. $this->assertEqual($this->treeStorage->getSubtreeHeight('child1'), 4);
  291. $this->assertEqual($this->treeStorage->getSubtreeHeight('child2'), 3);
  292. $this->assertEqual($this->treeStorage->getSubtreeHeight('child3'), 2);
  293. $this->assertEqual($this->treeStorage->getSubtreeHeight('child4'), 1);
  294. // Intentionally leave child3 out to mimic static or external links.
  295. $definitions = $this->treeStorage->loadMultiple(['root', 'child1', 'child2', 'child4']);
  296. $this->treeStorage->rebuild($definitions);
  297. $this->assertEqual($this->treeStorage->getSubtreeHeight('root'), 5);
  298. $this->assertEqual($this->treeStorage->getSubtreeHeight('child1'), 4);
  299. $this->assertEqual($this->treeStorage->getSubtreeHeight('child2'), 3);
  300. $this->assertEqual($this->treeStorage->getSubtreeHeight('child3'), 2);
  301. $this->assertEqual($this->treeStorage->getSubtreeHeight('child4'), 1);
  302. }
  303. /**
  304. * Tests MenuTreeStorage::loadByProperties().
  305. */
  306. public function testLoadByProperties() {
  307. $tests = [
  308. ['foo' => 'bar'],
  309. [0 => 'wrong'],
  310. ];
  311. $message = 'An invalid property name throws an exception.';
  312. foreach ($tests as $properties) {
  313. try {
  314. $this->treeStorage->loadByProperties($properties);
  315. $this->fail($message);
  316. }
  317. catch (\InvalidArgumentException $e) {
  318. $this->assertRegExp('/^An invalid property name, .+ was specified. Allowed property names are:/', $e->getMessage(), 'Found expected exception message.');
  319. }
  320. }
  321. $this->addMenuLink('test_link.1', '', 'test', [], 'menu1');
  322. $properties = ['menu_name' => 'menu1'];
  323. $links = $this->treeStorage->loadByProperties($properties);
  324. $this->assertEqual('menu1', $links['test_link.1']['menu_name']);
  325. $this->assertEqual('test', $links['test_link.1']['route_name']);
  326. }
  327. /**
  328. * Adds a link with the given ID and supply defaults.
  329. */
  330. protected function addMenuLink($id, $parent = '', $route_name = 'test', $route_parameters = [], $menu_name = 'tools', $extra = []) {
  331. $link = [
  332. 'id' => $id,
  333. 'menu_name' => $menu_name,
  334. 'route_name' => $route_name,
  335. 'route_parameters' => $route_parameters,
  336. 'title' => 'test',
  337. 'parent' => $parent,
  338. 'options' => [],
  339. 'metadata' => [],
  340. ] + $extra;
  341. $this->treeStorage->save($link);
  342. }
  343. /**
  344. * Moves the link with the given ID so it's under a new parent.
  345. *
  346. * @param string $id
  347. * The ID of the menu link to move.
  348. * @param string $new_parent
  349. * The ID of the new parent link.
  350. */
  351. protected function moveMenuLink($id, $new_parent) {
  352. $menu_link = $this->treeStorage->load($id);
  353. $menu_link['parent'] = $new_parent;
  354. $this->treeStorage->save($menu_link);
  355. }
  356. /**
  357. * Tests that a link's stored representation matches the expected values.
  358. *
  359. * @param string $id
  360. * The ID of the menu link to test
  361. * @param array $expected_properties
  362. * A keyed array of column names and values like has_children and depth.
  363. * @param array $parents
  364. * An ordered array of the IDs of the menu links that are the parents.
  365. * @param array $children
  366. * Array of child IDs that are visible (enabled == 1).
  367. */
  368. protected function assertMenuLink($id, array $expected_properties, array $parents = [], array $children = []) {
  369. $query = $this->connection->select('menu_tree');
  370. $query->fields('menu_tree');
  371. $query->condition('id', $id);
  372. foreach ($expected_properties as $field => $value) {
  373. $query->condition($field, $value);
  374. }
  375. $all = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
  376. $this->assertCount(1, $all, "Found link $id matching all the expected properties");
  377. $raw = reset($all);
  378. // Put the current link onto the front.
  379. array_unshift($parents, $raw['id']);
  380. $query = $this->connection->select('menu_tree');
  381. $query->fields('menu_tree', ['id', 'mlid']);
  382. $query->condition('id', $parents, 'IN');
  383. $found_parents = $query->execute()->fetchAllKeyed(0, 1);
  384. $this->assertEqual(count($parents), count($found_parents), 'Found expected number of parents');
  385. $this->assertEqual($raw['depth'], count($found_parents), 'Number of parents is the same as the depth');
  386. $materialized_path = $this->treeStorage->getRootPathIds($id);
  387. $this->assertEqual(array_values($materialized_path), array_values($parents), 'Parents match the materialized path');
  388. // Check that the selected mlid values of the parents are in the correct
  389. // column, including the link's own.
  390. for ($i = $raw['depth']; $i >= 1; $i--) {
  391. $parent_id = array_shift($parents);
  392. $this->assertEqual($raw["p$i"], $found_parents[$parent_id], "mlid of parent matches at column p$i");
  393. }
  394. for ($i = $raw['depth'] + 1; $i <= $this->treeStorage->maxDepth(); $i++) {
  395. $this->assertEqual($raw["p$i"], 0, "parent is 0 at column p$i greater than depth");
  396. }
  397. if ($parents) {
  398. $this->assertEqual($raw['parent'], end($parents), 'Ensure that the parent field is set properly');
  399. }
  400. $found_children = array_keys($this->treeStorage->loadAllChildren($id));
  401. // We need both these checks since the 2nd will pass if there are extra
  402. // IDs loaded in $found_children.
  403. $this->assertEqual(count($children), count($found_children), "Found expected number of children for $id");
  404. $this->assertEqual(array_intersect($children, $found_children), $children, 'Child IDs match');
  405. }
  406. }