diff --git a/examples.module b/examples.module index 1333b6e..2c9c08f 100644 --- a/examples.module +++ b/examples.module @@ -49,6 +49,7 @@ function examples_toolbar() { 'file_example' => 'file_example.fileapi', 'hooks_example' => 'hooks_example.description', 'js_example' => 'js_example.info', + 'menu_example' => 'examples.menu_example', 'node_type_example' => 'config_node_type_example.description', 'page_example' => 'page_example_description', 'pager_example' => 'pager_example.page', diff --git a/menu_example/menu_example.info.yml b/menu_example/menu_example.info.yml new file mode 100644 index 0000000..e9f6e4e --- /dev/null +++ b/menu_example/menu_example.info.yml @@ -0,0 +1,7 @@ +name: Menu Example +type: module +description: 'An example module showing the main steps to define and handling menu links in Drupal 8 ' +package: 'Example modules' +core: 8.x +dependencies: + - drupal:examples diff --git a/menu_example/menu_example.links.menu.yml b/menu_example/menu_example.links.menu.yml new file mode 100644 index 0000000..785af62 --- /dev/null +++ b/menu_example/menu_example.links.menu.yml @@ -0,0 +1,85 @@ +# This file links a menu link with route_name. +# +# 'title' key is only key required. +# +# 'description' is is shown either as a tooltip on the item or in the admin UI +# as the description of the option on the page/ +# +# 'weight' is used to order the items (higher weights get placed towards the +# end of the menu among items on the same level). +# +# 'route_name' is used to link menu link to corresponding route. +# +# 'parent' is used to put item into the menu hierarchy by referring to the +# parent menu link name. +# +examples.menu_example: + title: 'Menu Example' + description: 'Simplest possible menu type, and the parent menu entry for others' + expanded: 1 + route_name: examples.menu_example + +examples.menu_example.alternate_menu: + title: 'Menu Example: Menu in alternate menu' + #If menu_name is omitted, the "Tools" menu will be used. + menu_name: 'main' + route_name: examples.menu_example.alternate_menu + +examples.menu_example.permissioned: + title: 'Permissioned Example' + parent: examples.menu_example + expanded: 1 + route_name: examples.menu_example.permissioned + weight: 10 + +examples.menu_example.permissioned_controlled: + title: 'Permissioned Menu Item' + parent: examples.menu_example.permissioned + route_name: examples.menu_example.permissioned_controlled + weight: 10 + +examples.menu_example.custom_access: + title: 'Custom Access Example' + parent: examples.menu_example + expanded: 1 + route_name: examples.menu_example.custom_access + weight: -5 + +examples.menu_example.custom_access_page: + title: 'Custom Access Menu Item' + parent: examples.menu_example.custom_access + route_name: examples.menu_example.custom_access_page + +examples.menu_example.route_only: + title: 'Route only example' + parent: examples.menu_example + route_name: examples.menu_example.route_only + weight: 20 + +examples.menu_example.tabs: + title: 'Tabs' + description: 'Shows how to create primary and secondary tabs' + parent: examples.menu_example + route_name: examples.menu_example.tabs + weight: 30 + +examples.menu_example.use_url_arguments: + title: 'URL Arguments' + description: 'The page callback can use the arguments provided after the path used as key' + parent: examples.menu_example + route_name: examples.menu_example.use_url_arguments + weight: 40 + +examples.menu_example.title_callbacks: + title: 'Dynamic title' + description: 'The title of this menu item is dynamically generated' + parent: examples.menu_example + route_name: examples.menu_example.title_callbacks + weight: 50 + +examples.menu_example.placeholder_argument: + title: Placeholder Arguments + description: '' + parent: 'examples.menu_example' + route_name: examples.menu_example.placeholder_argument + weight: 60 diff --git a/menu_example/menu_example.links.task.yml b/menu_example/menu_example.links.task.yml new file mode 100644 index 0000000..b50df55 --- /dev/null +++ b/menu_example/menu_example.links.task.yml @@ -0,0 +1,53 @@ +# This file creates static local tasks (Tabs). +# This file will be needed to place in module root. +# +# 'title' of the tab will show up on the user interface and tab. +# +# 'base_route' is the same as the name of the route where the "default" tab +# appears. The base_route is used to group together related tabs. +# +# 'weight' is used to provide weights for the tabs if needed. +# The tab whose route is the same as the base_route will by default +# get a negative weight and appear on the left. +# +# 'parent_id' is used to create multi level of tabs. +# To relate a tab to its parent use same name as parent_id as shown below in +# examples.menu_example.tabs.secondary. +# +examples.menu_example.tabs: + route_name: examples.menu_example.tabs + title: Default primary tab + base_route: examples.menu_example.tabs + +examples.menu_example.tabs_second: + route_name: examples.menu_example.tabs_second + title: Second + base_route: examples.menu_example.tabs + weight: 2 + +examples.menu_example.tabs_third: + route_name: examples.menu_example.tabs_third + title: Third + base_route: examples.menu_example.tabs + weight: 3 + +examples.menu_example.tabs_fourth: + route_name: examples.menu_example.tabs_fourth + title: Fourth + base_route: examples.menu_example.tabs + weight: 4 + +examples.menu_example.tabs.secondary: + route_name: examples.menu_example.tabs + title: Default secondary tab + parent_id: examples.menu_example.tabs + +examples.menu_example.tabs_default_second: + route_name: examples.menu_example.tabs_default_second + title: Second + parent_id: examples.menu_example.tabs + +examples.menu_example.tabs_default_third: + route_name: examples.menu_example.tabs_default_third + title: Third + parent_id: examples.menu_example.tabs diff --git a/menu_example/menu_example.module b/menu_example/menu_example.module new file mode 100644 index 0000000..0521971 --- /dev/null +++ b/menu_example/menu_example.module @@ -0,0 +1,43 @@ +isAuthenticated()); + } + +} diff --git a/menu_example/src/Controller/MenuExampleController.php b/menu_example/src/Controller/MenuExampleController.php new file mode 100644 index 0000000..e35c62c --- /dev/null +++ b/menu_example/src/Controller/MenuExampleController.php @@ -0,0 +1,250 @@ +description(), + ]; + } + + /** + * Show a menu link in a menu other than the default "Navigation" menu. + */ + public function alternateMenu() { + return [ + '#markup' => $this->t('This will be in the Main menu instead of the default Tools menu'), + ]; + + } + + /** + * A menu entry with simple permissions using 'access protected menu example'. + * + * @throws \InvalidArgumentException + */ + public function permissioned() { + $url = Url::fromUri('internal:/examples/menu-example/permissioned/controlled'); + $link = Link::fromTextAndUrl($this->t('examples/menu-example/permissioned/controlled'), $url)->toString(); + return [ + '#markup' => $this->t('A menu item that requires the "access protected menu example" permission is at @link', ['@link' => $link]), + ]; + } + + /** + * Only accessible when the user will be granted with required permission. + * + * The permission is defined in file menu_examples.permissions.yml. + */ + public function permissionedControlled() { + return [ + '#markup' => $this->t('This menu entry will not show and the page will not be accessible without the "access protected menu example" permission to current user.'), + ]; + } + + /** + * Demonstrates the use of custom access check in routes. + * + * @throws \InvalidArgumentException + + * @see \Drupal\menu_example\Controller\MenuExampleController::customAccessPage() + */ + public function customAccess() { + $url = Url::fromUri('internal:/examples/menu-example/custom-access/page'); + $link = Link::fromTextAndUrl($this->t('examples/menu-example/custom-access/page'), $url)->toString(); + return [ + '#markup' => $this->t('A menu item that requires the user to posess a role of "authenticated" is at @link', ['@link' => $link]), + ]; + } + + /** + * Content will be displayed only if access check is satisfied. + * + * @see \Drupal\menu_example\Controller\MenuExampleController::customAccess() + */ + public function customAccessPage() { + return [ + '#markup' => $this->t('This menu entry will not be visible and access will result + in a 403 error unless the user has the "authenticated" role. This is + accomplished with a custom access check plugin.'), + ]; + } + + /** + * Only callback without a menu link. + * + * @throws \InvalidArgumentException + */ + public function routeOnly() { + $url = Url::fromUri('internal:/examples/menu-example/route-only/callback'); + $link = Link::fromTextAndUrl($this->t('examples/menu-example/route-only/callback'), $url)->toString(); + return [ + '#markup' => $this->t('A menu entry with no menu link is at @link', ['@link' => $link]), + ]; + } + + /** + * Such callbacks can be user for creating web services in Drupal 8. + */ + public function routeOnlyCallback() { + return [ + '#markup' => $this->t('The route entry has no corresponding menu links entry, so it provides a route without a menu link, but it is the same in every other way to the simplest example.'), + ]; + } + + /** + * @param string $path + * Path/URL of menu item. + * + * @param string $title + * Title of menu item. + * + * @return array + * Controller response. + */ + public function tabsPage($path, $title) { + $secondary = substr_count($path, '/') > 2 ? 'secondary ' : ''; + return array( + '#markup' => $this->t('This is the @secondary tab "@tabname" in the "basic tabs" example.', ['@secondary' => $secondary, '@tabname' => $title]), + ); + } + + /** + * Demonstrates use of url arguments in for menu item. + * + * @param string $arg1 + * First argument of url. + * @param string $arg2 + * Second argument of url. + * + * @return array + * Controller response. + * + * @see https://www.drupal.org/docs/8/api/routing-system/parameters-in-routes + */ + public function urlArgument($arg1, $arg2) { + // Perpare link for single arguments. + $url = Url::fromUri('internal:/examples/menu-example/use-url-arguments/one'); + $link = Link::fromTextAndUrl($this->t('examples/menu-example/use-url-arguments/one'), $url)->toString(); + + // Prepare link for multiple arguments. + $urltwo = Url::fromUri('internal:/examples/menu-example/use-url-arguments/one/two'); + $linktwo = Link::fromTextAndUrl($this->t('examples/menu-example/use-url-arguments/one/two'), $urltwo)->toString(); + + $markup = $this->t('This page demonstrates using arguments in the url. For example, access it with @link for single argument or @linktwo for two arguments in URL', ['@link' => $link, '@linktwo' => $linktwo]); + + if (!empty($arg1)) { + $markup .= '
' . $this->t('Argument 1 = @arg', array('@arg' => $arg1)) . '
'; + } + if (!empty($arg2)) { + $markup .= '
' . $this->t('Argument 2 = @arg', array('@arg' => $arg2)) . '
'; + } + + return [ + '#markup' => $markup, + + ]; + } + + /** + * Demonstrate generation of dynamic creation of page title. + * + * @see \Drupal\menu_example\Controller\MenuExampleController::backTitle() + */ + public function titleCallback() { + return [ + '#markup' => $this->t('The title of this page is dynamically changed by the title callback for this route defined in menu_example.routing.yml.'), + ]; + } + + /** + * Generates title dynamically. + * + * @see \Drupal\menu_example\Controller\MenuExampleController::titleCallback() + */ + public function backTitle() { + return \Drupal::currentUser()->getDisplayName(); + } + + /** + * Demonstrates how you can provide a placeholder url arguments. + * + * @throws \InvalidArgumentException + * + * @see \Drupal\menu_example\Controller\MenuExampleController::placeholderArgsDisplay() + * @see https://www.drupal.org/docs/8/api/routing-system/using-parameters-in-routes + */ + public function placeholderArgs() { + $url = Url::fromUri('internal:/examples/menu-example/placeholder-argument/3343/display'); + $link = Link::fromTextAndUrl($this->t('examples/menu-example/placeholder-argument/3343/display'), $url)->toString(); + return [ + '#markup' => $this->t('Demonstrate placeholders by visiting @link', ['@link' => $link]), + ]; + } + + /** + * Displays placeholder argument supplied in URL. + * + * @param int $arg + * URL argument. + * + * @return array + * URL argument. + * + * @see \Drupal\menu_example\Controller\MenuExampleController::placeholderArgs() + */ + public function placeholderArgsDisplay($arg) { + return [ + '#markup' => $arg, + ]; + + } + + /** + * Demonstrate how one can alter the existing routes. + */ + public function pathOverride() { + return [ + '#markup' => $this->t('This menu item was created strictly to allow the RouteSubscriber class to have something to operate on. menu_example.routing.yml defined the path as examples/menu_example/menu_original_path. The alterRoutes() changes it to examples/menu_example/menu_altered_path. You can try navigating to both paths and see what happens!'), + ]; + } + +} diff --git a/menu_example/src/Routing/MenuExampleDynamicRoutes.php b/menu_example/src/Routing/MenuExampleDynamicRoutes.php new file mode 100644 index 0000000..ca75524 --- /dev/null +++ b/menu_example/src/Routing/MenuExampleDynamicRoutes.php @@ -0,0 +1,54 @@ + 'Default primary tab', + 'tabs/second' => 'Second', + 'tabs/third' => 'Third', + 'tabs/fourth' => 'Fourth', + 'tabs/default/second' => 'Second', + 'tabs/default/third' => 'Third', + ]; + + foreach ($tabs as $path => $title) { + $machine_name = 'examples.menu_example.'. str_replace('/', '_', $path); + $routes[$machine_name] = new Route( + // Path to attach this route to: + '/examples/menu-example/' . $path, + // Route defaults: + [ + '_controller' => '\Drupal\menu_example\Controller\MenuExampleController::tabsPage', + '_title' => $title, + 'path' => $path, + 'title' => $title, + ], + // Route requirements: + [ + '_access' => 'TRUE', + ] + ); + } + + return $routes; + } + +} diff --git a/menu_example/src/Routing/RouteSubscriber.php b/menu_example/src/Routing/RouteSubscriber.php new file mode 100644 index 0000000..6f78dc6 --- /dev/null +++ b/menu_example/src/Routing/RouteSubscriber.php @@ -0,0 +1,32 @@ +get('example.menu_example.path_override'); + // Set the new path. + $route->setPath('/examples/menu-example/menu-altered-path'); + // Change title to indicate changes. + $route->setDefault('_title', 'Menu item altered by RouteSubscriber::alterRoutes'); + } + +} diff --git a/menu_example/templates/description.html.twig b/menu_example/templates/description.html.twig new file mode 100644 index 0000000..fc46589 --- /dev/null +++ b/menu_example/templates/description.html.twig @@ -0,0 +1,17 @@ +{# + +Description text for the Block Example. + +#} + +{% set link = path('examples.menu_example.route_only') %} + +{% trans %} + +

This page is displayed by the simplest (and base) menu example. Note that +the title of the page is the same as the link title. +You can also visit a similar page with no menu link. +There are a number of examples here, from the most basic (like this one) to +extravagant mappings of loaded placeholder arguments. Enjoy!

+ +{% endtrans %} diff --git a/menu_example/tests/src/Functional/MenuExampleTest.php b/menu_example/tests/src/Functional/MenuExampleTest.php new file mode 100644 index 0000000..ce070e3 --- /dev/null +++ b/menu_example/tests/src/Functional/MenuExampleTest.php @@ -0,0 +1,91 @@ +assertText('Menu Example'); + $this->clickLink(t('Menu Example')); + $this->assertText(t('This page is displayed by the simplest (and base) menu example')); + + $this->drupalGet('examples/menu-example/permissioned'); + $this->assertText(t('Permissioned Example')); + + $this->clickLink('examples/menu-example/permissioned/controlled'); + $this->assertResponse(403); + + // Create a user with permissions to access protected menu entry. + $web_user = $this->drupalCreateUser(); + + // Use custom overridden drupalLogin function to verify the user is logged + // in. + $this->drupalLogin($web_user); + + // Check that our title callback changing /user dynamically is working. + $this->assertText(t("@name", ['@name' => $web_user->getUsername()])); + + // Now start testing other menu entries. + $this->drupalGet('examples/menu-example'); + + $this->clickLink(t('Custom Access Example')); + $this->assertText(t('Custom Access Example')); + $this->drupalGet('examples/menu-example/custom-access/page'); + $this->assertResponse(200); + + $this->drupalGet('examples/menu-example'); + + $this->clickLink(t('Route only example')); + + $this->clickLink(t('Tabs')); + $this->assertText(t('This is the "tabs" menu entry')); + + $this->drupalGet('examples/menu-example/tabs/second'); + $this->assertText(t('This is the tab "Second" in the "basic tabs" example')); + + $this->clickLink(t('three')); + $this->assertText(t('This is the tab "Thrid" in the "basic tabs" example')); + + $this->clickLink(t('URL Arguments')); + + $this->drupalGet('examples/menu-example/use-url-arguments/one/two'); + $this->assertText(t('Argument 1 = one')); + $this->assertText(t('Argument 2 = two')); + + $this->clickLink(t('Placeholder Arguments')); + + $this->clickLink(t('examples/menu-example/placeholder-argument/3343/display')); + $this->assertText('3343'); + + $this->drupalGet('examples/menu-example/menu-altered-path'); + $this->assertText('Menu item altered by RouteSubscriber::alterRoutes'); + + } + +}