Development:Theme engines for Moodle?

From MoodleDocs

Jump to: navigation, search

Moodle 2.0


This proposal is an attempt to achieve all the things people have been asking for in relation to themes in Moodle 2.0. Well, not everything. Some of the things that are being asked for are covered by the blocks proposal.--Tim Hunt 06:25, 20 February 2009 (CST) (Oh, and please ignore the typo in the diagrams.)

Note: This article is a work in progress. Please use the page comments or an appropriate moodle.org forum for any recommendations/suggestions for improvement.


Contents

Goals

  • Existing Moodle core, activity modules, and other plugins should continue to work unchanged.
  • Existing themes should continue to work unchanged.
  • Themes should be able to control the HTML that Moodle generates.
    • They should be able to do so using templates if they want.
    • We should be able to support any templating language, although initially we will concentrate on pure PHP templates.
    • Themes should be able to change HTML without the overhead of templates too.
  • The HTML throughout Moodle should be as consistent as possible, and have good classes and ids, so themes that just modify the CSS should have as much flexibility as possible.
  • A new plugin should look good in a theme that generates non-standard HTML, even if the theme designer was not previously aware of that particular plugin while they made their theme.
  • In order to take full advantage of the new themeability, existing code will necessarily have to change. However, it should be possible to make those changes progressively, whenever is convenient.


Solution

There is a lot to take in here, please bear with me. I am going to try to explain it a bit at a time.


1. Turn weblib into a class

This is an internal refactoring to enable what follows, but does not really change anything.

All the print_whatever functions in lib/weblib.php are replaced by methods on a moodle_core_renderer class. We take this opportunity to make their name, parameter lists, etc. more consistent.

To ensure that legacy code continues to work, we will move all the old weblib functions into lib/deprecatedlib.php. The body of these functions just becomes a call to the moodle_core_renderer class.

Image:Standard_theme_engine.png

2. Let the theme choose the renderers

Why I say renderers, plural, will become clear in a minute. There are about to be a lot more of them in the picture.

The theme will be able to choose a renderer factory. That is an object that decides which sort of render should be created. When you call renderer_factory::get_renderer(), you ask for a particular type of renderer, for example a 'core_renderer', and you either get a standard 'moodle_core_renderer', or a subclass of it.

As part of initialising the theme, we will create a global $OUT, which is an instance of the moodle_core_renderer that the theme wants. The core renderer will be used in a lot of places, so we want it to be easily accessible.

Image:Theme_chooses_renderer.png

3. Every plugin should have its own renderer

This is where have to start changing existing code, if we want to start taking advantage of the new flexibility. Every plugin in Moodle should define its own renderer class, to output things that are specific to that module.

In the diagram, I have used the forum module as an example. Naturally, there will be a print_post method, and others (you need many more than I have shown).

As well as just defining the renderer interface, you have to provide a default implementation. That default implementation should mostly be written in terms of the lower-level output methods in moodle_core_renderer. So, for example, print_listing, which has to print the forum introduction at the top, should use $OUT->print_box($forum->intro, ...), just like at present, so that the standard HTML is as consistent as possible.

The aim is to reduce each Moodle script, for example view.php, to:

<?php
// All the necessary PHP Logic, as at present.
$forum_renderer = $renderer_factory->get_renderer('mod/forum');
print_header();
$forum_renderer->print_listing(...);
print_footer();
?>

Image:Forum_renderer.png


4. But, we don't have to make every renderer immediately

This is really just to re-enforce what I already said. Because of the legacy functions in deprecatedlib.php, existing code will continue to work.

At least, because everything goes through the moodle_core_renderer, themes have some control over the HTML that is output.

There is a simple change that you can do, where you replace each deprecated print_box call with $OUT->print_box. That would be the first part of converting legacy code to the new system.

And when it comes to creating the renderer class for your plugin, you do not have to do it all at once. You can do it a bit at a time. This means that the bits that people are most desperate to change in their themes, for example moodle_forum_renderer::print_post, can be created quickly, without having to wait for the more obscure and difficult parts of the code like mod/forum/subscribers.php to be converted.

This would also apply to places where it is difficult to separate the presentation code from the logic code. There might be some bits where it is never possible to give the theme much control.

Image:Legacy_keeps_working.png

5. We can add a template renderer

Well, so far we have just refactored a lot of code (or at least changed the coding guidelines in a way that tells everyone to refactor their code at some time in the future) without getting any real benefit. This section is where we start to reap the benefits.

I have been talking vaguely about the theme choosing 'a subclass of moodle_core_renderer', but what everyone wants is a template_core_renderer, that uses templates like box_template.php and so on, and a template_forum_renderer, that uses forum_post_template.php and so on.

Of course, we will need a standard set of templates to use by default. They will be provided by the standardtemplate theme, or whatever we want to call it.

6. What happens when someone makes a new plugin?

That is all very well, but what happens when someone makes a new plugin? The standardtemplate theme will not have any templates for it.

Well, at least the moodle_newmod_renderer that comes with the new mod/newmod will be implemented in terms of of moodle_core_renderer, so the HTML output will consistent with the rest of the theme, even if the phptemplate engine does not let you have complete control over that HTML, nor the standardtemplate theme provide a default template.

Extra templates can be added to the standardtemplate theme later.

(Ooh! I have just had a really evil thought about how to implement template_renderer_factory to avoid this limitation. I love dynamically typed interpreted lanuages. We could also let plugins include their own default templates, in the same way that they can include their own language strings.)

7. Conclusion

So what does this all mean? It means that people who want to create a theme have the following options:

  1. Just like at present, you can make a theme that inherits from the standard theme, and just change the CSS and header.html and footer.html. (This is theme/mytheme in the diagram).
  2. If you think standardtemplate has nicer HTML, you could use that as your parent theme, but still choose only to change the CSS and header.html and footer.html. (theme/mytheme2).
  3. You could use the flexibility that standardtemplate gives, and derive your theme from that, and change some of the templates. Of course, you would only need to provide the templates that you have changed. For the rest, the default ones will be used. (theme/mytemplatedtheme)
  4. You could write a whole new templating engine, for example themeengine/smarty, and make a theme out of Smarty templates. Since the default renderers will render everything in Moodle, you actually only need to make Smarty templates for the things you want to customise. (Not shown in diagram)
  5. Or, you could create a custom theme by writing subclasses of the moodle_xxx_renderers you want to change that output the HTML you want using PHP echo statements. This might give better performance than a theme that used separate template files. (Not shown in diagram)

How much of this picture should we implement in Moodle 2.0

That is, specifically, how much should Moodle.com commit to implementing, given that we want to release Moodle 2.0 this century.

I think we should:

  • Do the core code that makes this picture possible:
    • create moodle_core_renderer;
    • implement renderer_factory, and creating $OUT based on the theme;
    • move old methods to deprecatedlib, and make them call the new $OUT global.
  • Create moodle_custom_corners_renderer, to get the custom corners mess out of moodle_core_renderer, and to prove that bit of the concept.
  • Replace all the deprecated print_something calls with $OUT->print_something throughout core code.
  • Implement a very simple version of themeengine/phptemplates, as a proof of concept.
  • Convert a couple of other parts of moodle to start using a renderer class, to prove the concept. Perhaps just forum post, and a block. We should pick things that are not too difficult, and things that themers are likely to want to customise.

We do not have the resource to:

  • Refactor all the core plugins to use a renderer class. (At least this can be done a bit at a time, and it is the kind of thing where volunteers can step forwards to help with different bits).
  • Create theme/standardtemplate. One is lead to believe there are volunteers in the community who can't wait to do this.
  • Develop a highly optimised themeengine/phptemplates. Will be easier once we have more templates to test with anyway.

We should encourage people in the community to tackle as many of the bits that we are unable to do ourselves. (Would any of them make GSoC projects?)

Credits

  • Drupal's themes system gave me some useful idea.
  • The people who engaged with me constructively in the Themes and General developer forums.
  • Design patterns people.

Implementation issues discussions

How will outside code reference the rendererfactory?

Options include

  1. $OUT->get_renderer('forum'); ('qtype_multichoice', etc.)
  2. $PAGE->get_renderer('forum');
  3. $PAGE->theme->get_renderer('forum');

Of these, I think the 3rd makes the most sense.

$OUT is created by the renderer factory (it is ...->get_renderer('core');, so it seems silly to insist that that class contains a back-reference to the factory that created it just so that we can have this API.

I aslo think this is too specific a responsibility to give $PAGE (although it would almost certainly delegate.) $PAGE is about recording information about the current page, rather than implementing functionality.

So that is why I like $PAGE->theme->get_renderer('forum'); It is $PAGE's responsibility to know the current theme, and it may as well do that by holding a reference to a theme object, that implements the renderer factory interface, plus anything else needed.

See also