diff --git a/examples.module b/examples.module index 96cb44f..1fbcd6b 100644 --- a/examples.module +++ b/examples.module @@ -51,6 +51,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..bcfc9f1 --- /dev/null +++ b/menu_example/menu_example.links.menu.yml @@ -0,0 +1,92 @@ +# 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 + +example.menu_example.path_override: + title: Path Override + description: '' + parent: 'examples.menu_example' + route_name: example.menu_example.path_override + weight: 70 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..789bb48 --- /dev/null +++ b/menu_example/src/Controller/MenuExampleController.php @@ -0,0 +1,268 @@ +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::fromRoute('examples.menu_example.permissioned_controlled'); + return [ + '#markup' => $this->t('A menu item that requires the "access protected menu example" permission is at @link', [ + '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(), + ]), + ]; + } + + /** + * 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::fromRoute('examples.menu_example.custom_access_page'); + return [ + '#markup' => $this->t('A menu item that requires the user to posess a role of "authenticated" is at @link', [ + '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(), + ]), + ]; + } + + /** + * 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.'), + ]; + } + + /** + * Give the user a link to the route-only page. + * + * @throws \InvalidArgumentException + */ + public function routeOnly() { + $url = Url::fromRoute('examples.menu_example.route_only.callback'); + return [ + '#markup' => $this->t('A menu entry with no menu link is at @link', [ + '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(), + ]), + ]; + } + + /** + * 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.'), + ]; + } + + /** + * Uses the path and title to determine the page content. + * + * This controller is mapped dynamically based on the 'route_callbacks:' key + * in the routing YAML file. + * + * @param string $path + * Path/URL of menu item. + * @param string $title + * Title of menu item. + * + * @return array + * Controller response. + * + * @see Drupal\menu_example\Routing\MenuExampleDynamicRoutes + */ + public function tabsPage($path, $title) { + $secondary = substr_count($path, '/') > 2 ? 'secondary ' : ''; + return [ + '#markup' => $this->t('This is the @secondary tab "@tabname" in the "basic tabs" example.', ['@secondary' => $secondary, '@tabname' => $title]), + ]; + } + + /** + * Demonstrates use of optional 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 URL for single arguments. + $url_single = Url::fromRoute('examples.menu_example.use_url_arguments', ['arg1' => 'one']); + + // Prepare URL for multiple arguments. + $url_double = Url::fromRoute('examples.menu_example.use_url_arguments', ['arg1' => 'one', 'arg2' => 'two']); + + // Add these argument links to the page content. + $markup = $this->t('This page demonstrates using arguments in the url. For example, access it with @link_single for single argument or @link_double for two arguments in URL', [ + '@link_single' => Link::createFromRoute($url_single->getInternalPath(), $url_single->getRouteName(), $url_single->getRouteParameters())->toString(), + '@link_double' => Link::createFromRoute($url_double->getInternalPath(), $url_double->getRouteName(), $url_double->getRouteParameters())->toString(), + ]); + + // Process the arguments if they're provided. + if (!empty($arg1)) { + $markup .= '
' . $this->t('Argument 1 = @arg', ['@arg' => $arg1]) . '
'; + } + if (!empty($arg2)) { + $markup .= '
' . $this->t('Argument 2 = @arg', ['@arg' => $arg2]) . '
'; + } + + // Finally return the markup. + return [ + '#markup' => $markup, + ]; + } + + /** + * Demonstrate generation of dynamic creation of page title. + * + * @see \Drupal\menu_example\Controller\MenuExampleController::backTitle() + */ + public function titleCallbackContent() { + 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 titleCallback() { + return [ + '#markup' => $this->t('The new title is your username: @name', [ + '@name' => $this->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::fromRoute('examples.menu_example.placeholder_argument.display', ['arg' => 3343]); + return [ + '#markup' => $this->t('Demonstrate placeholders by visiting @link', [ + '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName(), $url->getRouteParameters())->toString(), + ]), + ]; + } + + /** + * 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..6ee88a3 --- /dev/null +++ b/menu_example/src/Routing/MenuExampleDynamicRoutes.php @@ -0,0 +1,57 @@ + '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..953e17f --- /dev/null +++ b/menu_example/templates/description.html.twig @@ -0,0 +1,17 @@ +{# + +Description text for the Menu 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..206accc --- /dev/null +++ b/menu_example/tests/src/Functional/MenuExampleTest.php @@ -0,0 +1,116 @@ +placeBlock('system_menu_block:main'); + } + + /** + * Test all the routes. + */ + public function testMenuExampleRoutes() { + $assert = $this->assertSession(); + // Key is route, value is page contents. + $routes = [ + 'examples.menu_example' => 'This page is displayed by the simplest (and base) menu example.', + 'examples.menu_example.permissioned' => 'A menu item that requires the "access protected menu example" permission', + 'examples.menu_example.custom_access' => 'A menu item that requires the user to posess', + 'examples.menu_example.custom_access_page' => 'This menu entry will not be visible and access will result in a 403', + 'examples.menu_example.route_only' => 'A menu entry with no menu link is', + 'examples.menu_example.use_url_arguments' => 'This page demonstrates using arguments in the url', + 'examples.menu_example.title_callbacks' => 'The title of this page is dynamically changed by the title callback', + 'examples.menu_example.placeholder_argument' => 'Demonstrate placeholders by visiting', + 'example.menu_example.path_override' => 'This menu item was created strictly to allow the RouteSubscriber', + 'examples.menu_example.alternate_menu' => 'This will be in the Main menu instead of the default Tools menu', + ]; + $this->drupalLogin($this->createUser()); + $this->drupalGet(Url::fromRoute('')); + // Check that all the links appear in the tools menu. + foreach (array_keys($routes) as $route) { + $assert->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); + } + + // Add routes that are not in the tools menu. + $routes['examples.menu_example.route_only.callback'] = 'The route entry has no corresponding menu links entry'; + // Check that all the routes are reachable and contain content. + foreach ($routes as $route => $content) { + $this->drupalGet(Url::fromRoute($route)); + $assert->statusCodeEquals(200); + $assert->pageTextContains($content); + } + + // Check some special-case routes. First is the required argument path. + $arg = 2377; + $this->drupalGet(Url::fromRoute('examples.menu_example.placeholder_argument.display', ['arg' => $arg])); + $assert->statusCodeEquals(200); + $assert->pageTextContains($arg); + + // Check the generated route_callbacks tabs. + $dynamic_routes = [ + 'examples.menu_example.tabs_second', + 'examples.menu_example.tabs_third', + 'examples.menu_example.tabs_fourth', + 'examples.menu_example.tabs_default_second', + 'examples.menu_example.tabs_default_third', + ]; + $this->drupalGet(Url::fromRoute('examples.menu_example.tabs')); + foreach ($dynamic_routes as $route) { + $assert->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); + } + foreach ($dynamic_routes as $route) { + $this->drupalGet(Url::fromRoute($route)); + $assert->statusCodeEquals(200); + } + + // Check the special permission route. + $this->drupalGet(Url::fromRoute('examples.menu_example.permissioned_controlled')); + $assert->statusCodeEquals(403); + $this->drupalLogin($this->createUser(['access protected menu example'])); + $this->drupalGet(Url::fromRoute('examples.menu_example.permissioned_controlled')); + $assert->statusCodeEquals(200); + $assert->pageTextContains('This menu entry will not show and the page will not be accessible'); + + // We've already determined that the custom access route is reachable so now + // we log out and make sure it tells us 403 because we're not authenticated. + $this->drupalLogout(); + $this->drupalGet(Url::fromRoute('examples.menu_example.custom_access_page')); + $assert->statusCodeEquals(403); + } + +}