Working with Menus for Themeing in Drupal

In this post, first we look at how to render menu in the template. Afterwards, we look how to manipulate menu html layout and its attributes so the menu can be themed per your design. At last, we go over on having custom menu that is customized in similar way via hooks

1. Build Menu

We like to have menus in the Html scope(THEME_html_process) instead the page scope(THEME_page_process). This is because menu are shared across the page layouts in general, so it is not specific to page per say, however. Drupal comes with the menu already available in the page scope under ‘main-menu’. So, if we want the menu to be available in the html scope we have to create one:

function THEME_preprocess_html(&$vars){{
...
        // Primary nav build links.
        $vars['primary_nav'] = menu_tree(variable_get('menu_main_links_source', 'main-menu'));
}

This builds the render array of main menu and make it available in the html scope. To render the menu itself in html.tpl.php, we would:

    <?php print render($primary_nav); ?>

This take array of main menu generated in preprocess function and renders into html to display in page

2. Overwrite UL Element

Next we overwrite the default ul element and its classes with our custom via hook_menu_tree() as following:

function THEME_menu_tree(&$variables) {
    return '<ul class="nav nav-justified">' . $variables['tree'] . '</ul>';
}
3. Overwrite LI Element

At last, lets overwrite the menu elements themselves. To do so, we use hook_menu_link() as following:

function THEME_NAME_menu_link(array $variables) {
      $element = $variables['element'];
    $sub_menu = '';

    if ($element['#below']) {
        // Prevent dropdown functions from being added to management menu so it
        // does not affect the navbar module.
        if (($element['#original_link']['menu_name'] == 'management') && (module_exists('navbar'))) {
            $sub_menu = drupal_render($element['#below']);
        }
        else if ((!empty($element['#original_link']['depth'])) && ($element['#original_link']['depth'] == 1)) {
            // Add our own wrapper.
            unset($element['#below']['#theme_wrappers']);
            $sub_menu = '<ul class="dropdown-menu">' . drupal_render($element['#below']) . '</ul>';
            // Generate as standard dropdown.
//            $element['#title'] .= ' <span class="caret"></span>';
            $element['#attributes']['class'][] = 'dropdown';
            $element['#localized_options']['html'] = TRUE;

            // Set dropdown trigger element to # to prevent inadvertant page loading
            // when a submenu link is clicked.
            $element['#localized_options']['attributes']['data-target'] = '#';
            $element['#localized_options']['attributes']['class'][] = 'dropdown-toggle';
            $element['#localized_options']['attributes']['data-toggle'] = 'dropdown';
        }
    }

    $element['#localized_options']['attributes']['class'][] = 'agri-nav-item';
    $element['#localized_options']['attributes']['class'][] = 'nav-space';

    //set the parent active when child is currently selected
    if(in_array("active-trail", $element['#attributes']['class'])){
        $element['#attributes']['class'][] = 'active';
        $element['#attributes']['class'][] = 'open';
    }
    // On primary navigation menu, class 'active' is not set on active menu item.
    // @see https://drupal.org/node/1896674
    if (($element['#href'] == $_GET['q'] || ($element['#href'] == '<front>' && drupal_is_front_page())) && (empty($element['#localized_options']['language']))) {
        $element['#attributes']['class'][] = 'active';
    }
    $output = l($element['#title'], $element['#href'], $element['#localized_options']);
    return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
}

Here, we overriding everything from boostrap_menu_link() and made few changes. Added extra classes to the link element, removed caret and ensure parent of selected element is highlighted.

What If You Have Another Menu?

Say you have another menu at the bottom with different markup and class attributes. This menu id is ‘bottom-menu’

A)Build Menu

Build custom menu similar to the Main menu as following:

function THEME_preprocess_html(&$vars){
...
        $vars['bottom_nav'] = menu_tree('menu-bottom-menu');
        $vars['bottom_nav']['#theme_wrappers'] = array('menu_tree__bottom');
}

In the first line, the menu is build via menu_tree() function that takes the menu id and builds the array to render. In the second line, we specify hook for overriding UL element. There is one by default(hook_menu_tree__menu_MENU_NAME), so instead of overriding, we may have used the default one(i.e. hook_menu_tree__menu_bottom_menu)

B) Overwrite UL Element

With the override function specified, we can overwrite the UL element:

function THEME-NAME_menu_tree__bottom(&$variables) {
    return '<ul class="nav-agri nav-tabs-agri nav-justified-agri">' . $variables['tree'] . '</ul>';
} 

Here we use the hook specified, however. You may as well used the default hook(i.e. hook_menu_tree__menu_bottom_menu)

C)Overwrite LI Element

By default the override hook for LI element is hook_menu_link__MENU_ID() plus the menu id. In our case, the override function hook is hook_menu_link__menu_bottom_menu. So, we overwrite the links as following:

function THEME-NAME_menu_link__menu_bottom_menu(array $variables) {
    $element = $variables['element'];
    $output = l($element['#title'], $element['#href'], $element['#localized_options']);
    return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . "</li>\n";
}

There is no dropdown sub-menu to worry, so we create a link and wrap into the LI element before returning

A Word or Two on User Menu

Its very likely your main menu is different than the user menu, so how to handle the html layout for user menu while main menu is using the default hooks for structuring menus
1. User Tree Menu
The user menu has a custom hook – THEME_menu_tree__user_menu available to structure menu tree as following:

function THEME_menu_tree__user_menu(&$variables){
    return '<ul class="dropdown-menu">'.$variables['tree'].'</ul>';
}

2.User Menu Links
There is custom menuhook – THEME_menu_link__user_menu available to structure the menu item list

3.Menu Template file
There is also custom template file – block–system–user-menu.tpl available for you to override block html as you wish for user menu as well

Solving Menu Import Issue when Featuring Sample Data in Drupal

[UPDATE] – alias menu import module was rolled into “Sample Data” module that is of newer version. it is listed on Drupal site
Here is post about it – http://margotskapacs.com/2014/07/importexport-sample-data-and-assets-for-kickstart-all-via-features/

Currently, if you would like to export and import the sample data( like kickstart) via features, then the menus for your data breaks because it uses the hard coded path(i.e. node/id) and the new instance has different path for the nodes imported. To my understanding there is a work in place to fix this in future release of features by using the path uuid but of January, 2014 that is not available at least in stable version

The solution presented here utilizes ‘pathauto’ and its generated alias path for each node to do the mapping between the same nodes on two different Drupal instances. In another words, we export existing menu structure with alias path as reference. Once these nodes are imported(before menu import script is run) on the another Drupal instance, it automatically generates the default alias path(for custom alias, please, section ‘If Custom Path Alias…’ below). Since these alias path are the same between the drupal instances, the installation script looks up the mapping when structuring and building menu.

To maintain the existing menu structure, you will have to run a script that will generate the structure used by the menu importer script as showed in this post. This may be improved in future but author didn’t have time at this time and may not be a value since there is path uuid project in progress that will be the better solution in long run

In addition, the path for nodes has to be unique as well.

Requirement:

  1. Pathauto module has to be installed and enabled
  2. Each node have to have path alias or setting ‘Generate automatic URL alias’ selected under ‘Url path settings'(this is default behavior once ‘pathauto’ enabled, so you should need to do anything unless you have nodes present and you just installed/enabled ‘pathauto’. In that case, you have to go into each node and check the setting ‘Generate automatic URL alias’)
  3. The nodes has to be already imported before running the menu_import on the new Drupal instance
  4. (Recommended) When importing Nodes via features, ensure the StrongArm variable or configuration of ‘pathauto_node_pattern’ is also imported. This ensures that for imported nodes the generated alias path is the same as the one in current Drupal instance. This is especially important if the alias pattern for nodes is not the default one

The solution consists of two parts:

Part I: Build Exportable Menu

To build current menu structure for menu of your choice run the following script in “PHP Execute” block on Drupal instance you want to export the menu from(copy/past and then specify menus to export on line 3):

include_once DRUPAL_ROOT . '/includes/utility.inc';
//specify menues to export
$menus = array('menu-bottom-menu', 'main-menu');
$mainMenuArray = array();
//mlid to alias map
$alias_map = array();
$menu_items = array();

$output = "function _menus_altered(){ \n";
$output .= "return array (\n";

foreach ($menus as $menu) {
    $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC))
        ->fields('ml')
        ->condition('ml.menu_name', $menu)
        ->condition('ml.hidden', '0')
        ->orderBy('weight')
        ->execute()
        ->fetchAll();

    foreach ($links as $key => $link) {
        $links[$key]['options'] = unserialize($link['options']);
    }
    $mainMenuArray = array_merge($mainMenuArray, $links);

    $mlid_ins = db_select('menu_links', 'ml')
        ->fields('ml', array('mlid', 'link_path'))
        ->condition('ml.menu_name', $menu)
        ->condition('ml.hidden', '0')
        ->execute()->fetchAllAssoc('mlid', PDO::FETCH_ASSOC);

    $menu_items = $menu_items + $mlid_ins;

    $menu_ins = menu_load($menu);
    if ($menu_ins) {
        $menu_item = 'array(';
        $menu_item .= "'menu_name' => '" . $menu_ins['menu_name'] . "',";
        $menu_item .= "'title' => '" . str_replace("'", "\'", $menu_ins['title']) . "',";
        $menu_item .= "'description' => '" . str_replace("'", "\'", $menu_ins['description']) . "'";
        $menu_item .= ")";
        $output .= $menu_item . ",\n";
    } else {
        watchdog(WATCHDOG_NOTICE, "menu: $menu not found");
    }
}
$output .= "    );\n";
$output .= "}\n\n\n";

//generate the map between mlid and alias path
foreach ($menu_items as $mlid => $options) {
    $alias_map[$mlid] = drupal_get_path_alias($options['link_path']);
}
$output .= "function _menu_installed_items() {\n";
$output .= "return array (\n";
foreach ($mainMenuArray as $key => $link) {
    $alias_path = drupal_get_path_alias($link['link_path']);

//prepare link to pass in menu_link_save()
    $link_export = $link;
    $link_export['link_path'] = $alias_path;
    unset($link_export['mlid']);
    unset($link_export['router_path']);
    unset($link_export['options']['identifier']);
    $link_export['plid'] = ($link_export['plid']) ? $alias_map[$link_export['plid']] : $link_export['plid'];
    $link_export['p1'] = ($link_export['p1']) ? $alias_map[$link_export['p1']] : $link_export['p1'];
    $link_export['p2'] = ($link_export['p2']) ? $alias_map[$link_export['p2']] : $link_export['p2'];
    $link_export['p3'] = ($link_export['p3']) ? $alias_map[$link_export['p3']] : $link_export['p3'];

    $output .= "'" . $alias_path . "' => " . drupal_var_export($link_export) . ",\n";
}
$output .= ");\n";
$output .= "}\n";

drupal_set_message("<textarea rows=100 style=\"width: 100%;\">" . $output . '</textarea>');

First copy and past the above code in to “PHP Execute” block. Second, specify the menus you like to export in the line 3 variable ‘$menus’
This script generates two arrays encapsulated in functions _menu_installed_items() and _menus_altered() that you will have to supply in your custom module installation file in next step

Part – II : Build Custom Module to Import Menus

Create a custom block with the following .install script:

function MODULE_NAME_install(){
    $t = get_t();

   // clear menus and create new ones if doesn't exist
    foreach(_menus_altered() as $menu){
        //remove menu links for clean install
        menu_delete_links($menu);
        drupal_set_message($t('Links deleted from menu - ' . $menu['menu_name']));

       if(!menu_load($menu['menu_name'])){
           //doesn't exist...lets create one
           drupal_set_message($t('Missing menu '.$menu['menu_name'].'...creating'));
           menu_save($menu);
       }
    }

    //map containing the structure of the menu
    $alias_map = array();

    //shuffle so that parents are always above the children menu items
    //this is important so when building the $alias_map child is not being inserted without parent already present
    $all_items = _menu_installed_items();
    $parents_p1 = array();
    $parents_p2 = array();
    $parents_p3 = array();
    $parents_p4 = array();
    $parents_p5 = array();

    foreach($all_items as $key => $item){
        if($key == $item['p1']){
            $parents_p1[$key] = $item;
        }
        if($key == $item['p2']){
            $parents_p2[$key] = $item;
        }
        if($key == $item['p3']){
            $parents_p3[$key] = $item;
        }
        if($key == $item['p4']){
            $parents_p4[$key] = $item;
        }
        if($key == $item['p5']){
            $parents_p5[$key] = $item;
        }
    }

    $sorted_menu_list = array_merge($parents_p1,$parents_p2,$parents_p3,$parents_p4,$parents_p5);

    //add links
    foreach ($sorted_menu_list as $key => $item) {
        $alias_link_path = $item['link_path'];
        //look up node based on the alias path of existing drupal instance
        $item['link_path'] = drupal_get_normal_path($alias_link_path);

        //drupal_set_message('plid: '.$item['plid']. ' alias_map:' . $alias_map[$item['plid']] );
        $item['plid'] = ($item['plid'] !== 0 && !empty($alias_map[$item['plid']])) ? $alias_map[$item['plid']] : $item['plid'] ;
        $item['p1'] = ($item['p1'] !== 0 && !empty($alias_map[$item['p1']])) ? $alias_map[$item['p1']] : $item['p1'] ;
        $item['p2'] = ($item['p2'] !== 0 && !empty($alias_map[$item['p2']]) ) ? $alias_map[$item['p2']] : $item['p2'] ;
        $item['p3'] = ($item['p3'] !== 0 && !empty($alias_map[$item['p3']])) ? $alias_map[$item['p3']] : $item['p3'] ;

        $installed = menu_link_save($item);
        if($installed !== FALSE) {
            drupal_set_message($t('Menu Item : '. $key . ' installed'));
            //drupal_set_message('INSERT: link_path: '.$alias_link_path. ' mlip:' . $installed );
            $alias_map[$alias_link_path] = $installed;
        }else{
            drupal_set_message($t('Menu Item : '. $key . ' was not installed'));
        }
    }
    menu_link_save($item);
    menu_cache_clear_all();
    drupal_set_message($t('Installed Menu'));
}

//ADD THE TWO FUNCTIONS FROM PART I HERE

Make sure you add the two functions generated in Part I (i.e._menus_altered(),_menu_installed_items() ) in the .install file where it says ‘//ADD THE TWO FUNCTIONS FROM PART I HERE’. These are used by the script to build the new menus

Thats all….in future we hope to automize the above process by creating module if the problem is not solved soon by the features,uuid modules.

If Custom Path Alias…

The features module currently does not export/import path_alias for nodes. This is okey if you are using path alias of the node that were generated by default by pathauto module, however. If not, then your custom alias path for nodes are not exported/imported via features and the above menu export/import solution will not work. We have created module to export/import path alias published at https://github.com/kapasoft-config-scripts/designssquare_alias_path. By enabling this module, your custom path alias is going to be exported and then imported for all nodes.

Troubleshooting

All of the menu items does not import

Make sure all the menu items imported are pointing to path alias that are unique. If not unique, then you have to break into more than one menu import to work.

Add and Manipulate Pages with Menu System in Drupal

In this post, we cover different ways to add and manipulate pages with Menu System in Drupal.

Every page loaded in Drupal:

domainname.com/path/of/somekind

is transformed as following request:

domainname.com/index.php?q=path/of/somekind

This is what you will see if the ‘clean url’ setting is turned off.
In Drupal, every path is routed through single page – index.php. This is called front controller design pattern seen in many other web frameworks today. Drupal determines where to route the request by looking at the path(everything on the right of domainname.com)

Drupal takes the path and references an index it has that assigns every path to a function call. If it finds that path in the index than it calls that function and expects the function to tell Drupal what to do next. Most of the time it will provide some content to built into the final page, but it may as well redirect to go to another page or present error message like access denied

Every module determines which paths are assigned to which call back functions and all that data is stored in db table ‘menu_router’

Simple Menu Callback

In your module file MODULENAME.module, add the following:

function MODULENAME_menu() {
    $items['pages'] = array(
        'title' => 'Menu system examples',
        'description' => 'Menu system example that returns a string.',
        'page callback' => 'register_member_page',
        'access callback' => TRUE,
    );

    return $items;
}

function register_member_page() {
    $build = array(
        'header_text' => array(
            '#type' => 'markup',
            '#markup' => '<p class="lead">' . t('Membership Registration') . '</p>',
        ),
       'example_form' => drupal_get_form('collect_member_info_form'),
    );
    return $build;
}

This is implementation of hook hook_menu(). The hook_menu() expects array of call back function(i.e. register_member_page) along other info. Here the Url path defined is ‘/pages’. The ‘page callback'(i.e.register_member_page()) is the call back function routed to this url – ‘/pages’ that will tell what to do next. The ‘access callback’ specifies permissions.

We can supply content to page request in 2 ways:
1. Through string containing html and text
2. Through the build array containing content in a structred array that can be later manipulated by other modules before rendered

Next implement the page callback function ‘pages_string’ that supplies the content as simple string(Nr.1)

function pages_string() {
    $output = '
    <p>Pages can be returned as strings.</p>
    <p>Pages can be returned as <em>render arrays</em>.</p>';
    $output .= theme('item_list', array(
            'title' => 'Render arrays are better because...',
            'items' => array(
                'They allow content to be modified as an array.',
                'Arrays are a lot easier to modify than HTML.',
            ))
    );

    return $output;
}

Here we structure the output as a string and use theme(‘item_list’…) function to help construct a list order ed or unordered with heading if you wish.

Next, make sure the module is installed and go to domainname.com/pages to verity it works

How to Use Render Arrays and Tabs

When callback function returns build array instead a string, then other modules can manipulate the content by adding/removing or reorder the elements in content before its rendered

function pages_menu() {
    $items['pages'] = array(
        'title' => 'Menu system examples',
        'description' => 'Menu system example that returns a string.',
        'page callback' => 'pages_string',
        'access callback' => TRUE,
    );
    $items['pages/default'] = array(
        'title' => 'String',
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'weight' => -10,
    );
    $items['pages/render-array'] = array(
        'title' => 'Render array',
        'description' => 'Menu system example using a render array.',
        'page callback' => 'pages_render_array',
        'access arguments' => array('access content'),
        'weight' => 2,
        'type' => MENU_LOCAL_TASK,
    );

    return $items;
}

Here we taken the pages content page and added two tabs. The page ‘pages/default’ is the default tab by specifying the type to default. Important to note, that the default tab doesn’t have the page callback specified, so it falls back to the parent callback function – ‘pages_string’

Another tab(‘pages/render-array’) is added with new call back function ‘pages_render_array’. This tab has access arguments. Unless boolean is specified, it expects function. If no specified as it is here, then it uses default callback ‘user_access’ and it expect permission to be passed which we do here with ‘content access’

The build render array looks as following:

function pages_render_array() {
    $build = array(
        'string_paragraph' => array(
            '#type' => 'markup',
            '#markup' => '<p>Pages can be returned as strings.</p>',
        ),
        'render_array_paragraph' => array(
            '#type' => 'markup',
            '#markup' => '<p>Pages can be returned as <em>render arrays</em>.</p>',
        ),
        'why_render_arrays' => array(
            '#items' => array('They allow content to be modified as an array.', 'Arrays are a lot easier to modify than HTML.'),
            '#title' => 'Render arrays are better because...',
            '#theme' => 'item_list',
        ),
    );
    return $build;
}

Here we specify to element to be html strings and then the third is unorder list. This is being passed to theme() function ‘item_list’ as specified

Drupal caches menu registry, so you have to tell Drupal to rebuild the registry and you can do by clearing the cache resulting to rebuild

Sub-Tabs

function pages_menu() {
...
    $items['pages/render-array'] = array(
        'title' => 'Render array',
        'description' => 'Menu system example using a render array.',
        'page callback' => 'pages_render_array',
        'access arguments' => array('access content'),
        'weight' => 2,
        'type' => MENU_LOCAL_TASK,
    );
    $items['pages/render-array/tab1'] = array(
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'title' => 'Tab 1',
    );
    $items['pages/render-array/tab2'] = array(
        'title' => 'Tab 2',
        'description' => 'Demonstrating secondary tabs.',
        'page callback' => 'pages_render_array',
        'access callback' => TRUE,
        'type' => MENU_LOCAL_TASK,
    );

    return $items;
}

Here we define two new items – ‘pages/render-array/tab1’ and ‘pages/render-array/tab2’ both of which are subpages of tab ‘pages/render-array’, so its important that the sub-tab path corresponds to the pages path its sub-tab from

How to Add Page Without Menu Item

function pages_menu() {
...
        $items['pages/callback'] = array(
        'title' => 'Example of a callback type',
        'page callback' => 'pages_render_array',
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
...

Here we define separate page by specifying the type to be ‘MENU_CALLBACK’ instead ‘MENU_LOCAL_CALLBACK’. After clearing cash, the new page is displayed at domain.com/pages/callback end point

How to Pass Variables Through the Path

function pages_menu() {
...
 $items['pages/argument'] = array(
        'title' => 'Argument',
        'description' => 'Menu system example using an argument.',
        'page callback' => 'pages_argument',
        'access arguments' => array('access content'),
    );
}

function pages_argument($arg1) {
    $build['argument_paragraph'] = array(
        '#type' => 'markup',
        '#markup' => '<p>' . t('The argument passed was @arg1.', array('@arg1' => $arg1)) . '</p>',
    );

    return $build;
}

The only difference is to change the callback function to accept arguments. Here the callback function ‘pages_argument’ takes an arguments. So, with path domain.com/pages/argument/hello’, the argument ‘hello’ is going to be passed to the callback function ‘pages_argument’. For more arguments you just add them to the callback function parameter. Also can use the Drupals args() function

How To Use Placeholders to Pass Arguments in Middle of the Path

When you know the beginning and the end of the path but not the middle (i.e. node/*/edit)

function pages_menu() {
...
 $items['pages/%/placeholder'] = array(
        'title' => 'Placeholder',
        'description' => 'Menu system example using a placeholder.',
        'page callback' => 'pages_argument',
        'page arguments' => array(1),
        'access arguments' => array('access content'),
    );
}

There are two places to register the placeholder in hook_menu(). In the path, by specifying ‘%’ and ‘page arguments’ attributed as highlighted above. The number 1 specifies the location(starting from 0) of the path variable of placeholder. Afterwards, this variable is passed to the callback function

How To Create Dynamic Title With Title Callback

function pages_menu() {
...
  $items['pages/title/%'] = array(
        'description' => 'Example of a dynamic title.',
        'page callback' => 'pages_render_array',
        'access callback' => TRUE,
        'title callback' => 'pages_title_callback',
        'title arguments' => array(2),
    );
...
}

function pages_title_callback($arg) {
    return 'There is an argument in this title: ' . $arg;
}

Here, we specify the title callback and pass an argument. Afterward, we declare the callback that returns a string and is rendered as title

How To Modify Page Output with hook_page_alter()

How do adjust menu items that have been defined by other modules
How do we change the value returned by callback function defined by other modules

The idea of hook_page_alter() is that before content is rendered, modules have the last chance to manipulate the rendered page content.

function THEME_page_alter(&$page) {
    $page['content']['system_main']['why_render_arrays']['#weight'] = -10;
}

Here we reorder the array so that element item list named ‘why_render_arrays’ is moved up before other elements

How To Modify Menu Items With hook_menu_alter()

Instead modifying the render array, we modify the callback function all together. We will replace one page already exist with another we defined
Anything that is defined in hook_menu will be passed through the hook_menu_alter where it can be altered one more time

function pages_menu_alter(&$items) {
    $items['admin/content']['title'] = 'The Goods';
    $items['admin/content']['page callback'] = 'pages_render_array';
}

Here we override the admin page with our own defined page as declared in callback ‘pages_render_array’

How Define Path To Be Admin

Perhaps, you would like your new custom path to be part of Admin path whether its because you like to have the page in overlay or take advantage of the admin permissions, then:

function MODULE-NAME_admin_paths() {
    $paths = array(
        'path/*/edit' => TRUE,
    );
    return $paths;
}

here all the path of pattern – “‘path/*/edit” is going to be admin path with all the admin access rights, overlay functionality,etc.

How Use Include File To Improve Performance

On every page load, every .module file got loaded for every module currently installed. If we can make the code shorter in those .module files then we can improve the performance. One way doing it is by separating out the .module code into separate include files and then use special parameter in the hook_menu() item declaration goes in fetches that include file when needed. The idea, all of the code is not needed in the .module file at the same time.

function pages_menu() {
...
$items['pages/external'] = array(
        'title' => 'External file',
        'description' => 'Example of using an include for a page callback.',
        'page callback' => 'pages_external',
        'access callback' => TRUE,
        'file' => 'pages.external.inc',
        'file path' => drupal_get_path('module', 'NAMEOFMODULE'),
    );
...
}

Here, we specify the callback function ‘pages_external’ but the function is not in the .modules file. it is in the file ‘pages.external.inc’ that we specify to load in the highlighted lines. It will only include this file, when requesting ‘pages/external’