Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Extending the theme custom menu

From MoodleDocs

This is a quick tutorial that will run you through extending the custom menu by overriding your theme's renderer.

This tutorial is primarily PHP coding, and ranges from moderate to advanced difficulty.

Before we begin

First things first, you need to know a little something about the custom menu. The custom menu is populated from the manually entered input in "Theme Settings". Once the administrator has entered the custom menu items and saved them, the following steps are what they go through in order to be displayed:

  1. The theme calls $OUTPUT->custom_menu() which is the core_renderer's method responsible for producing the custom menu.
  2. core_renderer::custom_menu() does the following:
    1. Checks to make sure the admin has added custom menu items.
    2. Creates a new custom_menu object. When the custom menu object is created, it turns whatever the admin entered, into a structured array of information.
    3. Calls core_renderer::render_custom_menu and gives it the custom_menu object.
  3. core_renderer::render_custom_menu is where the magic happens. It does the following:
    1. First, it checks to make sure custom_menu contains items.
    2. Initializes the JavaScript for the custom menu. This is the YUI3 menunode component.
    3. Creates the HTML base of the custom menu.
    4. Then iterates through all of the items and calls the custom menu and passes them to another function that will turn them into HTML correctly.

I'm going to stop there, as there is no need at this point to go into any more detail. Don't worry if you are a little lost, it will get clearer as we start working through code.

In this tutorial will we look at how to extend the theme's renderer in two different way.

  1. Add a dynamic My Courses branch to the custom menu that will display all of the courses a user is enrolled in.
  2. Get the custom menu to fetch strings from the language files rather than just static strings you type, allowing for a translatable custom menu.

Before you start into this tutorial you should have read the following tutorial, or at least have a good knowledge of the the Moodle theme engine and Moodle development.

As this is a quick tutorial I am going to assume you have created a theme, tested it and got it all working, and are already well on your way to becoming a theme guru.

Getting started

Alright, as you have already have your theme ready to go preparation is pretty simple, we are going to go through and create (if you haven't already) the following files:

  • theme/themename/lib.php
  • theme/themename/renderers.php
  • theme/themename/lang/en/themename.php

Next step open up your themes config.php file and add the following configuration option to it (at the bottom):

$THEME->rendererfactory = 'theme_overridden_renderer_factory';

If you've already got a custom renderer you will already have this line.

And thats it! now we move on to extending the custom menu.

Adding the My Courses branch

So the point of this extension is to add a My Courses branch to the end of the custom menu.

The plan is to have the my courses branch and then within that branch have an entry for every course the user is enrolled in, much the same as the my courses branch of the navigation.

In order to achieve this we need to add some items to the custom menu, the my courses branch and all of the courses within it. We can do this overriding the core_renderers render_custom_menu method, of more accuratly create our own render_custom_menu method that adds our items and then calls the original. Remember we only need to add items, we don't need to change what is being produced.

So within our renderers.php file add the following code:

class theme_themename_core_renderer extends core_renderer {

    protected function render_custom_menu(custom_menu $menu) {
        // Our code will go here shortly
    }

}

So that is pretty simple right?!

Here we are defining our core_renderer which will contain our overridden method render_custom_menu which we have also defined there. Note the definition of the render_custom_menu must be the same as the original, this means it must be protected and it must take one argument custom_menu $menu.

Next step is to write the code for our render_custom_menu method. As stated above we want to add a branch and courses to the end of it which we can do with the following bit of code:

$mycourses = $this->page->navigation->get('mycourses');

if (isloggedin() && $mycourses && $mycourses->has_children()) {
    $branchlabel = get_string('mycourses');
    $branchurl   = new moodle_url('/course/index.php');
    $branchtitle = $branchlabel;
    $branchsort  = 10000;

    $branch = $menu->add($branchlabel, $branchurl, $branchtitle, $branchsort);

    foreach ($mycourses->children as $coursenode) {
        $branch->add($coursenode->get_content(), $coursenode->action, $coursenode->get_title());
    }
}

return parent::render_custom_menu($menu);

So how this all works....

$mycourses = $this->page->navigation->get('mycourses');

This line is a little bit of smarts, what we are doing is getting the mycourses branch from the navigation. We could make all of the database calls and work it all out ourselves but this will be much better for performance and is easier!

The next line of code is an if statement that checks three things

  1. The user is logged in, you must be logged in to see your courses.
  2. That we have a mycourses object, if the user isn't enrolled in anything they won't have a mycourses object.
  3. The the mycourses object has children, if it exists it should but it is better to be safe than get errors.

Within the if statement we are doing two main things happening, the first is to add the branch to the menu:

$branchlabel = get_string('mycourses');
$branchurl   = new moodle_url('/course/index.php');
$branchtitle = $branchlabel;
$branchsort  = 10000;

$branch = $menu->add($branchlabel, $branchurl, $branchtitle, $branchsort);

When adding a new branch we need to give it three things:

  1. A label $branchlabel.
  2. A URL $branchurl.
  3. A title $branchtitle in this case I have just set it to the same as $branchlabel because I am lazy, you can make it anything you want.
  4. A sort order $branchsort this just needs to be high enough that it ends up on last place where we want it. Make it small to put it at the front.

The final line of code from above simply adds it to the menu and collects a reference to it so that we can add courses to it.

The second thing being done in the if statement is iterate through all of the courses in the mycourses object and add them to the branch. That is done with the following bit of code:

foreach ($mycourses->children as $coursenode) {
    $branch->add($coursenode->get_content(), $coursenode->action, $coursenode->get_title());
}

So here we go through all of the mycourses objects children (which we know will be courses) and for each one we add an item to the branch. When adding items here, like above, we need to give it the following:

  1. A label $coursenode->get_content() returns us the text in the navigation node.
  2. A url $coursenode->action which is the URL for the node.
  3. A title $coursenode->get_title().

In this case we don't need to set a sort order because the navigation has already ordered them correctly!

So now we are through the if statement and there is only one line of code left:

return parent::render_custom_menu($menu);

This line of code simply calls the original core_renderer::render_custom_menu method to do all of the remaining work. Because we don't need to change the display at all we don't need to redo what it does, we can just use it!.


How easy way that?! it's all done, if you open you site in your browser and log in the custom menu should now contain a my courses branch (providing you are enrolled in courses).

Loading labels from language files

If you run a site that is available in more than one language and you want to make use of the custom menu then you will probably be very interested in this.

The idea behind this extension is to load the labels and titles that get shown in the custom menu from the theme's language files rather than having just the fixed strings you type into the admin setting.

In order to achieve this we need to do somehow override the custom_menu_item class so that it loads the strings from the database if they match a special pattern. We can then update the admin setting to use our patter and wallah! things should load from the language files.

What makes this tricky is that we can't just override the custom_menu_item class, we also need to override the thing that makes create custom_menu_item instances which in this case is the core_renderer::render_custom_menu_item method.

Overriding the custom_menu_item class

Within your themes lib.php file add the following lines of code:

class theme_themename_transmuted_custom_menu_item extends custom_menu_item {
    public function __construct(custom_menu_item $menunode) {
        // Our code will go here
    }
}

What we have done here is create an overridden custom_menu_item class called transmuted_custom_menu_item. Within that class we are overriding the constructor, the constructor will be given the original menu item, and we will need to instantiate this object with the properties of the original. Once we have instantiated it, with the properties of the original, we can alter then as we like.

Next we need to write the contents of the __construct method:

parent::__construct($menunode->get_text(), $menunode->get_url(), $menunode->get_title(), $menunode->get_sort_order(), $menunode->get_parent());
$this->children = $menunode->get_children();

$matches = array();
if (preg_match('/^\[\[([a-zA-Z0-9\-\_\:]+)\]\]$/', $this->text, $matches)) {
    try {
        $this->text = get_string($matches[1], 'theme_themename');
    } catch (Exception $e) {
        $this->text = $matches[1];
    }
}

$matches = array();
if (preg_match('/^\[\[([a-zA-Z0-9\-\_\:]+)\]\]$/', $this->title, $matches)) {
    try {
        $this->title = get_string($matches[1], 'theme_themename');
    } catch (Exception $e) {
        $this->title = $matches[1];
    }
}

So there are essentially four things going on here.

First we need to populate this object with the properties of the original item. We do this by calling the original constructor with $menunode properties as shown below.

parent::__construct($menunode->get_text(), $menunode->get_url(), $menunode->get_title(), $menunode->get_sort_order(), $menunode->get_parent());

Next we need to copy the properties that are not included in the original constructor, in this case there is only one $this->children. This should be an array of all of this item's children (sub items).

$this->children = $menunode->get_children();

Now that we have all the properties set up we can modify them. In our case we want to check if the items label and title match a special pattern and if they do we want to fetch them from the language file instead. The special pattern is [[stringname]] where stringname is the name of the string that we want to get from the language file.

$matches = array();
if (preg_match('/^\[\[([a-zA-Z0-9\-\_\:]+)\]\]$/', $this->text, $matches)) {
    try {
        $this->text = get_string($matches[1], 'theme_themename');
    } catch (Exception $e) {
        $this->text = $matches[1];
    }
}

In the code above we do the following:

  1. Create an array $matches which will hold the stringname if the label matches the pattern.
  2. We attempt to match the label to the pattern. If it does the $matches array now contains the stringname.
  3. If it did match we call get string as shown.

The third and final thing is to do the above again but this time for the title.

With that done our transmutable_custom_menu_item class is complete. Next step is to make use of it.

Overriding render_custom_menu_item

The next step is make use of the transmutable_custom_menu_item class we created previously.

We can do this by overriding the core_renderer::render_custom_menu_item method within our renderers.php file as shown below.

protected function render_custom_menu_item(custom_menu_item $menunode) {
    $transmutedmenunode = new theme_themename_transmuted_custom_menu_item($menunode);
    return parent::render_custom_menu_item($transmutedmenunode);
}

This is pretty simply, essentially we are using our transmuted_custom_menu_item class like a façade to the original custom_menu_item class by creating a new transmuted instance using the original. Once we have the transmuted object we can call the original render_custom_menu_item method with it.

And that is it! Congratulations if you got it this far.

The only thing left to do is add strings to your themes language file as required!

Complete source

// theme/themename/renderers.php

class theme_themename_core_renderer extends core_renderer {

    protected function render_custom_menu(custom_menu $menu) {
        
        $mycourses = $this->page->navigation->get('mycourses');

        if (isloggedin() && $mycourses && $mycourses->has_children()) {
            $branchlabel = get_string('mycourses');
            $branchurl   = new moodle_url('/course/index.php');
            $branchtitle = $branchlabel;
            $branchsort  = 10000;

            $branch = $menu->add($branchlabel, $branchurl, $branchtitle, $branchsort);

            foreach ($mycourses->children as $coursenode) {
                $branch->add($coursenode->get_content(), $coursenode->action, $coursenode->get_title());
            }
        }

        return parent::render_custom_menu($menu);
    }

    protected function render_custom_menu_item(custom_menu_item $menunode) {
        $transmutedmenunode = new theme_themename_transmuted_custom_menu_item($menunode);
        return parent::render_custom_menu_item($transmutedmenunode);
    }

}
// theme/themename/lib.php

class theme_themename_transmuted_custom_menu_item extends custom_menu_item {
    public function __construct(custom_menu_item $menunode) {
        parent::__construct($menunode->get_text(), $menunode->get_url(), $menunode->get_title(), $menunode->get_sort_order(), $menunode->get_parent());
        $this->children = $menunode->get_children();

        $matches = array();
        if (preg_match('/^\[\[([a-zA-Z0-9\-\_\:]+)\]\]$/', $this->text, $matches)) {
            try {
                $this->text = get_string($matches[1], 'theme_themename');
            } catch (Exception $e) {
                $this->text = $matches[1];
            }
        }

        $matches = array();
        if (preg_match('/^\[\[([a-zA-Z0-9\-\_\:]+)\]\]$/', $this->title, $matches)) {
            try {
                $this->title = get_string($matches[1], 'theme_themename');
            } catch (Exception $e) {
                $this->title = $matches[1];
            }
        }
    }
}

See also