themes: MDL-19077 implement the renderer_factory instrastructure.
[moodle.git] / lib / outputlib.php
CommitLineData
571fa828 1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18
19/**
20 * Functions for generating the HTML that Moodle should output.
21 *
22 * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
23 * for an overview.
24 *
25 * @package moodlecore
26 * @copyright 2009 Tim Hunt
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later (5)
28 */
29
30
31/**
32 * A renderer factory is just responsible for creating an appropriate renderer
33 * for any given part of Moodle.
34 *
35 * Which renderer factory to use is chose by the current theme, and an instance
36 * if created automatically when the theme is set up.
37 *
38 * A renderer factory must also have a constructor that takes a theme object and
39 * a moodle_page object. (See {@link renderer_factory_base::__construct} for an example.)
40 *
41 * @copyright 2009 Tim Hunt
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 * @since Moodle 2.0
44 */
45interface renderer_factory {
46 /**
47 * Return the renderer for a particular part of Moodle.
48 *
49 * The renderer interfaces are defined by classes called moodle_..._renderer
50 * where ... is the name of the module, which, will be defined in this file
51 * for core parts of Moodle, and in a file called renderer.php for plugins.
52 *
53 * There is no separate interface definintion for renderers. Instead we
54 * take advantage of PHP being a dynamic languages. The renderer returned
55 * does not need to be a subclass of the moodle_..._renderer base class, it
56 * just needs to impmenent the same interface. This is sometimes called
57 * 'Duck typing'. For a tricky example, see {@link template_renderer} below.
58 * renderer ob
59 *
60 * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
61 * @return object an object implementing the requested renderer interface.
62 */
63 public function get_renderer($module);
64}
65
66
67/**
68 * This is a base class to help you implement the renderer_factory interface.
69 *
70 * It keeps a cache of renderers that have been constructed, so you only need
71 * to construct each one once in you subclass.
72 *
73 * It also has a method to get the name of, and include the renderer.php with
74 * the definition of, the standard renderer class for a given module.
75 *
76 * @copyright 2009 Tim Hunt
77 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
78 * @since Moodle 2.0
79 */
80abstract class renderer_factory_base implements renderer_factory {
81 /** The theme we are rendering for. */
82 protected $theme;
83
84 /** The page we are doing output for. */
85 protected $page;
86
87 /** Used to cache renderers as they are created. */
88 protected $renderers = array();
89
90 /**
91 * Constructor.
92 * @param object $theme the theme we are rendering for.
93 * @param moodle_page $page the page we are doing output for.
94 */
95 public function __construct($theme, $page) {
96 $this->theme = $theme;
97 $this->page = $page;
98 }
99
100 /* Implement the interface method. */
101 public function get_renderer($module) {
102 // Cache the renderers by module name, and delegate the actual
103 // construction to the create_renderer method.
104 if (!array_key_exists($module, $this->renderers)) {
105 $this->renderers[$module] = $this->create_renderer($module);
106 }
107
108 return $this->renderers[$module];
109 }
110
111 /**
112 * Subclasses should override this method to actually create an instance of
113 * the appropriate renderer class, based on the module name. That is,
114 * this method should implement the same contract as
115 * {@link renderer_factory::get_renderer}.
116 *
117 * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
118 * @return object an object implementing the requested renderer interface.
119 */
120 abstract public function create_renderer($module);
121
122 /**
123 * For a given module name, return the name of the standard renderer class
124 * that defines the renderer interface for that module.
125 *
126 * Also, if it exists, include the renderer.php file for that module, so
127 * the class definition of the default renderer has been loaded.
128 *
129 * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
130 * @return string the name of the standard renderer class for that module.
131 */
132 protected function standard_renderer_class_for_module($module) {
133 $pluginrenderer = get_plugin_dir($module) . '/renderer.php';
134 if (file_exists($pluginrenderer)) {
135 include_once($pluginrenderer);
136 }
137 $class = 'moodle_' . $module . '_renderer';
138 if (!class_exists($class)) {
139 throw new coding_exception('Request for an unknown renderer class ' . $class);
140 }
141 return $class;
142 }
143}
144
145
146/**
147 * This is the default renderer factory for Moodle. It simply returns an instance
148 * of the appropriate standard renderer class.
149 *
150 * @copyright 2009 Tim Hunt
151 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
152 * @since Moodle 2.0
153 */
154class standard_renderer_factory extends renderer_factory_base {
155 /**
156 * Constructor.
157 * @param object $theme the theme we are rendering for.
158 * @param moodle_page $page the page we are doing output for.
159 */
160 public function __construct($theme, $page) {
161 parent::__construct($theme, $page);
162 }
163
164 /* Implement the subclass method. */
165 public function create_renderer($module) {
166 if ($module == 'core') {
167 return new moodle_core_renderer($this->page->opencontainers);
168 } else {
169 $class = $this->standard_renderer_class_for_module($module);
170 return new $class($this->page->opencontainers, $this->get_renderer('core'));
171 }
172 }
173}
174
175
176/**
177 * This is a slight variatoin on the standard_renderer_factory that uses
178 * custom_corners_core_renderer instead of moodle_core_renderer.
179 *
180 * This generates the slightly different HTML that the custom_corners theme expects.
181 *
182 * @copyright 2009 Tim Hunt
183 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
184 * @since Moodle 2.0
185 */
186class custom_corners_renderer_factory extends standard_renderer_factory {
187 /**
188 * Constructor.
189 * @param object $theme the theme we are rendering for.
190 * @param moodle_page $page the page we are doing output for.
191 */
192 public function __construct($theme, $page) {
193 parent::__construct($theme, $page);
194 $this->renderers = array('core' => new custom_corners_core_renderer($this->page->opencontainers));
195 }
196}
197
198
199/**
200 * This is renderer factory allows themes to override the standard renderers using
201 * php code.
202 *
203 * It will load any code from theme/mytheme/renderers.php and
204 * theme/parenttheme/renderers.php, if then exist. Then whenever you ask for
205 * a renderer for 'component', it will create a mytheme_component_renderer or a
206 * parenttheme_component_renderer, instead of a moodle_component_renderer,
207 * if either of those classes exist.
208 *
209 * This generates the slightly different HTML that the custom_corners theme expects.
210 *
211 * @copyright 2009 Tim Hunt
212 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
213 * @since Moodle 2.0
214 */
215class theme_overridden_renderer_factory extends standard_renderer_factory {
216 protected $prefixes = array();
217
218 /**
219 * Constructor.
220 * @param object $theme the theme we are rendering for.
221 * @param moodle_page $page the page we are doing output for.
222 */
223 public function __construct($theme, $page) {
224 global $CFG;
225 parent::__construct($theme, $page);
226
227 // Initialise $this->prefixes.
228 $renderersfile = $theme->dir . '/renderers.php';
229 if (is_readable($renderersfile)) {
230 include_once($renderersfile);
231 $this->prefixes[] = $theme->name . '_';
232 }
233 if (!empty($theme->parent)) {
234 $renderersfile = $CFG->themedir .'/'. $theme->parent . '/renderers.php';
235 if (is_readable($renderersfile)) {
236 include_once($renderersfile);
237 $this->prefixes[] = $theme->parent . '_';
238 }
239 }
240 }
241
242 /* Implement the subclass method. */
243 public function create_renderer($module) {
244 foreach ($this->prefixes as $prefix) {
245 $classname = $prefix . $module . '_renderer';
246 if (class_exists($classname)) {
247 if ($module == 'core') {
248 return new $classname($this->page->opencontainers);
249 } else {
250 return new $classname($this->page->opencontainers, $this->get_renderer('core'));
251 }
252 }
253 }
254 return parent::create_renderer($module);
255 }
256}
257
258
259/**
260 * This is renderer factory that allows you to create templated themes.
261 *
262 * This should be considered an experimental proof of concept. In particular,
263 * the performance is probably not very good. Do not try to use in on a busy site
264 * without doing careful load testing first!
265 *
266 * This renderer factory returns instances of {@link template_renderer} class
267 * which which implement the corresponding renderer interface in terms of
268 * templates. To use this your theme must have a templates folder inside it.
269 * Then suppose the method moodle_core_renderer::greeting($name = 'world');
270 * exists. Then, a call to $OUTPUT->greeting() will cause the template
271 * /theme/yourtheme/templates/core/greeting.php to be rendered, with the variable
272 * $name available. The greeting.php template might contain
273 *
274 * <pre>
275 * <h1>Hello <?php echo $name ?>!</h1>
276 * </pre>
277 *
278 * @copyright 2009 Tim Hunt
279 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
280 * @since Moodle 2.0
281 */
282class template_renderer_factory extends renderer_factory_base {
283 /**
284 * An array of paths of where to search for templates. Normally this theme,
285 * the parent theme then the standardtemplate theme. (If some of these do
286 * not exist, or are the same as each other, then the list will be shorter.
287 */
288 protected $searchpaths = array();
289
290 /**
291 * Constructor.
292 * @param object $theme the theme we are rendering for.
293 * @param moodle_page $page the page we are doing output for.
294 */
295 public function __construct($theme, $page) {
296 global $CFG;
297 parent::__construct($theme, $page);
298
299 // Initialise $this->searchpaths.
300 if ($theme->name != 'standardtemplate') {
301 $templatesdir = $theme->dir . '/templates';
302 if (is_dir($templatesdir)) {
303 $this->searchpaths[] = $templatesdir;
304 }
305 }
306 if (!empty($theme->parent)) {
307 $templatesdir = $CFG->themedir .'/'. $theme->parent . '/templates';
308 if (is_dir($templatesdir)) {
309 $this->searchpaths[] = $templatesdir;
310 }
311 }
312 $this->searchpaths[] = $CFG->themedir .'/standardtemplate/templates';
313 }
314
315 /* Implement the subclass method. */
316 public function create_renderer($module) {
317 // Refine the list of search paths for this module.
318 $searchpaths = array();
319 foreach ($this->searchpaths as $rootpath) {
320 $path = $rootpath . '/' . $module;
321 if (is_dir($path)) {
322 $searchpaths[] = $path;
323 }
324 }
325
326 // Create a template_renderer that copies the API of the standard renderer.
327 $copiedclass = $this->standard_renderer_class_for_module($module);
328 return new template_renderer($copiedclass, $searchpaths, $this->page->opencontainers);
329 }
330}
331
332
333/**
334 * Simple base class for Moodle renderers.
335 *
336 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
337 *
338 * @copyright 2009 Tim Hunt
339 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
340 * @since Moodle 2.0
341 */
342class moodle_renderer_base {
343 /** @var xhtml_container_stack the xhtml_container_stack to use. */
344 protected $containerstack;
345
346 /**
347 * Constructor
348 * @param $containerstack the xhtml_container_stack to use.
349 */
350 public function __construct($containerstack) {
351 $this->containerstack = $containerstack;
352 }
353}
354
355
356/**
357 * This is the templated renderer which copies the API of another class, replacing
358 * all methods calls with instantiation of a template.
359 *
360 * When the method method_name is called, this class will search for a template
361 * called method_name.php in the folders in $searchpaths, taking the first one
362 * that it finds. Then it will set up variables for each of the arguments of that
363 * method, and render the template. This is implemented in the {@link __call()}
364 * PHP magic method.
365 *
366 * Methods like print_box_start and print_box_end are handles specially, and
367 * implemented in terms of the print_box.php method.
368 *
369 * @copyright 2009 Tim Hunt
370 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
371 * @since Moodle 2.0
372 */
373class template_renderer extends moodle_renderer_base {
374 /** @var ReflectionClass information about the class whose API we are copying. */
375 protected $copiedclass;
376 /** @var array of places to search for templates. */
377 protected $searchpaths;
378
379 /**
380 * Magic word used when breaking apart container templates to implement
381 * _start and _end methods.
382 */
383 const contentstoken = '-@#-Contents-go-here-#@-';
384
385 /**
386 * Constructor
387 * @param string $copiedclass the name of a class whose API we should be copying.
388 * @param $searchpaths a list of folders to search for templates in.
389 * @param $containerstack the xhtml_container_stack to use.
390 */
391 public function __construct($copiedclass, $searchpaths, $containerstack) {
392 parent::__construct($containerstack);
393 $this->copiedclass = new ReflectionClass($copiedclass);
394 $this->searchpaths = $searchpaths;
395 }
396
397 /* PHP magic method implementation. */
398 public function __call($method, $arguments) {
399 if (substr($method, -6) == '_start') {
400 return $this->process_start(substr($method, 0, -6), $arguments);
401 } else if (substr($method, -4) == '_end') {
402 return $this->process_end(substr($method, 0, -4), $arguments);
403 } else {
404 return $this->process_template($method, $arguments);
405 }
406 }
407
408 /**
409 * Render the template for a given method of the renderer class we are copying,
410 * using the arguments passed.
411 * @param string $method the method that was called.
412 * @param array $arguments the arguments that were passed to it.
413 * @return string the HTML to be output.
414 */
415 protected function process_template($method, $arguments) {
416 if (!$this->copiedclass->hasMethod($method) ||
417 !$this->copiedclass->getMethod($method)->isPublic()) {
418 throw new coding_exception('Unknown method ' . $method);
419 }
420
421 // Find the template file for this method.
422 $template = $this->find_template($method);
423
424 // Use the reflection API to find out what variable names the arguments
425 // should be stored in, and fill in any missing ones with the defaults.
426 $namedarguments = array();
427 $expectedparams = $this->copiedclass->getMethod($method)->getParameters();
428 foreach ($expectedparams as $param) {
429 $paramname = $param->getName();
430 if (!empty($arguments)) {
431 $namedarguments[$paramname] = array_shift($arguments);
432 } else if ($param->isDefaultValueAvailable()) {
433 $namedarguments[$paramname] = $param->getDefaultValue();
434 } else {
435 throw new coding_exception('Missing required argument ' . $paramname);
436 }
437 }
438
439 // Actually render the template.
440 return $this->render_template($template, $namedarguments);
441 }
442
443 /**
444 * Actually do the work of rendering the template.
445 * @param $_template the full path to the template file.
446 * @param $_namedarguments an array variable name => value, the variables
447 * that should be available to the template.
448 * @return string the HTML to be output.
449 */
450 protected function render_template($_template, $_namedarguments) {
451 // Note, we intentionally break the coding guidelines with regards to
452 // local variable names used in this function, so that they do not clash
453 // with the names of any variables being passed to the template.
454
455 // Set up the global variables that the template may wish to access.
456 global $CFG, $PAGE, $THEME;
457
458 // And the parameters from the function call.
459 extract($_namedarguments);
460
461 // Include the template, capturing the output.
462 ob_start();
463 include($_template);
464 $_result = ob_get_contents();
465 ob_end_clean();
466
467 return $_result;
468 }
469
470 /**
471 * Searches the folders in {@link $searchpaths} to try to find a template for
472 * this method name. Throws an exception if one cannot be found.
473 * @param string $method the method name.
474 * @return string the full path of the template to use.
475 */
476 protected function find_template($method) {
477 foreach ($this->searchpaths as $path) {
478 $filename = $path . '/' . $method . '.php';
479 if (file_exists($filename)) {
480 return $filename;
481 }
482 }
483 throw new coding_exception('Cannot find template for ' . $this->copiedclass->getName() . '::' . $method);
484 }
485
486 /**
487 * Handle methods like print_box_start by using the print_box template,
488 * splitting the result, pusing the end onto the stack, then returning the start.
489 * @param string $method the method that was called, with _start stripped off.
490 * @param array $arguments the arguments that were passed to it.
491 * @return string the HTML to be output.
492 */
493 protected function process_start($template, $arguments) {
494 array_unshift($arguments, self::contentstoken);
495 $html = $this->process_template($template, $arguments);
496 list($start, $end) = explode(self::contentstoken, $html, 2);
497 $this->containerstack->push($template, $end);
498 return $start;
499 }
500
501 /**
502 * Handle methods like print_box_end, we just need to pop the end HTML from
503 * the stack.
504 * @param string $method the method that was called, with _end stripped off.
505 * @param array $arguments not used. Assumed to be irrelevant.
506 * @return string the HTML to be output.
507 */
508 protected function process_end($template, $arguments) {
509 return $this->containerstack->pop($template);
510 }
511
512 /**
513 * @return array the list of paths where this class searches for templates.
514 */
515 public function get_search_paths() {
516 return $this->searchpaths;
517 }
518
519 /**
520 * @return string the name of the class whose API we are copying.
521 */
522 public function get_copied_class() {
523 return $this->copiedclass->getName();
524 }
525}
526
527
528/**
529 * This class keeps track of which HTML tags are currently open.
530 *
531 * This makes it much easier to always generate well formed XHTML output, even
532 * if execution terminates abruptly. Any time you output some opening HTML
533 * without the matching closing HTML, you should push the neccessary close tags
534 * onto the stack.
535 *
536 * @copyright 2009 Tim Hunt
537 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
538 * @since Moodle 2.0
539 */
540class xhtml_container_stack {
541 /** @var array stores the list of open containers. */
542 protected $opencontainsers = array();
543
544 /**
545 * Push the close HTML for a recently opened container onto the stack.
546 * @param string $type The type of container. This is checked when {@link pop()}
547 * is called and must match, otherwise a developer debug warning is output.
548 * @param string $closehtml The HTML required to close the container.
549 */
550 public function push($type, $closehtml) {
551 $container = new stdClass;
552 $container->type = $type;
553 $container->closehtml = $closehtml;
554 array_push($this->opencontainsers, $container);
555 }
556
557 /**
558 * Pop the HTML for the next closing container from the stack. The $type
559 * must match the type passed when the container was opened, otherwise a
560 * warning will be output.
561 * @param string $type The type of container.
562 * @return string the HTML requried to close the container.
563 */
564 public function pop($type) {
565 if (empty($this->opencontainsers)) {
566 debugging('There are no more open containers. This suggests there is a nesting problem.', DEBUG_DEVELOPER);
567 return;
568 }
569
570 $container = array_pop($this->opencontainsers);
571 if ($container->type != $type) {
572 debugging('The type of container to be closed (' . $container->type .
573 ') does not match the type of the next open container (' . $type .
574 '). This suggests there is a nesting problem.', DEBUG_DEVELOPER);
575 }
576 return $container->closehtml;
577 }
578
579 /**
580 * Close all but the last open container. This is useful in places like error
581 * handling, where you want to close all the open containers (apart from <body>)
582 * before outputting the error message.
583 * @return string the HTML requried to close any open containers inside <body>.
584 */
585 public function pop_all_but_last() {
586 $output = '';
587 while (count($this->opencontainsers) > 1) {
588 $container = array_pop($this->opencontainsers);
589 $output .= $container->closehtml;
590 }
591 return $output;
592 }
593
594 /**
595 * You can call this function if you want to throw away an instance of this
596 * class without properly emptying the stack (for example, in a unit test).
597 * Calling this method stops the destruct method from outputting a developer
598 * debug warning. After calling this method, the instance can no longer be used.
599 */
600 public function discard() {
601 $this->opencontainsers = null;
602 }
603
604 /**
605 * Emergency fallback. If we get to the end of processing and not all
606 * containers have been closed, output the rest with a developer debug warning.
607 */
608 public function __destruct() {
609 if (empty($this->opencontainsers)) {
610 return;
611 }
612
613 debugging('Some containers were left open. This suggests there is a nesting problem.', DEBUG_DEVELOPER);
614 echo $this->pop_all_but_last();
615 $container = array_pop($this->opencontainsers);
616 echo $container->closehtml;
617 }
618}
619
620
621/**
622 * The standard implementation of the moodle_core_renderer interface.
623 *
624 * @copyright 2009 Tim Hunt
625 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
626 * @since Moodle 2.0
627 */
628class moodle_core_renderer extends moodle_renderer_base {
629
630 // TODO
631}
632
633
634/**
635 * A renderer for the custom corner theme, and other themes based on it.
636 *
637 * Generates the slightly different HTML that the custom corners theme wants.
638 *
639 * @copyright 2009 Tim Hunt
640 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
641 * @since Moodle 2.0
642 */
643class custom_corners_core_renderer extends moodle_core_renderer {
644
645 // TODO
646}
647