Model-view-controller (MVC) pattern

Following the popular model-view-controller (MVC) pattern controllers should take care of handling the access control, handling the inputs and coordinating the flow between model and view. The controller receives an input from the users, then processes the user's data with the help of a model and passes the results back via a view.

This documentation deals only with controllers and views. It does not cover the principles of the MVC pattern in detail. There are plenty of resources regarding the MVC pattern, a very basic one can be found on Wikipedia.

Basic controllers

Defining controllers

Below you can find an example of a basic controller class.

The controller always extends the base controller class included in the totara_mvc plugin. Each action you want to implement needs to be prefixed by 'action_' or you can implement a single action controller.

namespace my_plugin\controllers;

class my_controller extends \totara_mvc\controller {

    protected function setup_context(): context {
        // This method is mandatory to ensure the correct context is set at the very beginning.
        return \context_system::instance();
    }

    public function action_show() {
        // Setting the url is mandatory
		$this->set_url('/my_plugin/show.php');
        // do something

        // render a template
        return new \totara_mvc\view('my_plugin/show');
    }

    public function action_edit() {
        $this->set_url('/my_plugin/show.php');
        // do something

        // render a template
        return new \totara_mvc\view('my_plugin/edit');
    }

}

You can define as many actions as you want.

As we don't have a routing component you need to create a publicly accessible page, initiate your controller and call the process function:

/my_plugin/my_page.php
require_once(__DIR__ . '/../../config.php');

// to trigger and render the show action
// you can use a one liner
(new \my_plugin\controllers\my_controller())->process('show');

Please note that this is a very simple example. The following sections will give you some more details on how to achieve certain things.

Single action controllers 

If you just want a controller to handle one action only then you can implement the action method:

namespace my_plugin\controllers;

class my_controller extends \totara_mvc\controller {

    protected $url = '/my_plugin/index.php';

    protected function setup_context(): context {
        return \context_system::instance();
    }

    public function action() {
        // do something

        // render a template
        return new \totara_mvc\view('my_plugin/edit');
    }

}

In your page file just call process() without any parameters:

/server/my_plugin/index.php
require_once(__DIR__ . '/../../config.php');

(new \my_plugin\controllers\my_controller())->process();

URLs

It is mandatory to set a URL for a controller. This can be done either for the whole controller by overriding the $url property or per action using the $this→set_url() method.

If the URL is not set there will be an exception thrown when processing the controller.

Please make sure you set the correct URL and the parameters.

Features

Authorisation / access control

If for some reason you don't want your controller to call it set the $require_login property to false:

class my_controller extends \totara_mvc\controller {

    protected $require_login = false;

    // ...

}

You can also set $auto_login_guest to false if you don't want the default behaviour.

class my_controller extends \totara_mvc\controller {

    protected $auto_login_guest = false;

    // ...

}

There is also a shortcut method to check capabilities:

    public function action() {
        // this can be chained as well
        $this->require_capability('my_plugin/capability1', context_system::instance())
            ->require_capability('my_plugin/capability2', context_system::instance());

        // ...
    }

Context

As setting the correct context is one of most important things for a page the controller forces you to implement the setup_context() method.

Think carefully which context you need. If you are in a course set you will need the course context, if you are in a module set then use the module context, etc. The context also is critical for the require_login() call.

class my_controller extends \totara_mvc\controller {

    protected function setup_context(): context {
        $course_id = $this->get_required_param('course_id', PARAM_INT);
        return \context_course::instance($course_id);
    }

    public function action() {
        // Context is stored in context class property
        $context = $this->get_context();

        // Once the context is set it should not be changed anymore
        // ...
    }

}

Parameters

There are shortcuts to get the parameters passed via GET or POST to the page:

    public function action() {
        $optional_param = $this->get_optional_param('param1', 0, PARAM_INT);
        $required_param = $this->get_required_param('param2', PARAM_INT);

        $optional_param_array = $this->get_optional_param_array('param_array1', [], PARAM_INT);
        $required_param_array = $this->get_required_param_array('param_array2', PARAM_INT);

        // ...
    }

Layout

Usually the layout would be part of the view layer but due to the quirks of the rendering system in Totara the layout has to be set by the controller at the earliest point in time to avoid problems later. The output and rendering system of Totara is highly lazy-loading and on the very first rendering call (be it a button, block or anything) the layout is baked in and a later change might not have the desired effect.

To set the layout in a controller set the respective property.

Please note that as the layout is set on the controller it is shared by all actions defined in it. 

class my_controller extends \totara_mvc\controller {

    protected $layout = 'noblocks';

    // ...

}

Admin controller

Admin pages should use \totara_mvc\admin_controller as a base to make sure all admin-related checks are done.

The most important part for admin pages is that admin_external_page_setup() is called at the very beginning. The admin_controller abstracts this away, handles authorisation and also automatically sets title, URL, layout, etc. for you.

You still need to define your admin page in the settings.php file:

settings.php
$ADMIN->add( 'category', new admin_externalpage('myexternalpage', "page title", "/url/to/page.php", ['my/capability']));

Then by using the identifier of the external page you can easily create your admin controller action:

/**
 * Optional parameters to admin_external_page_setup() can also be defined as class properties.
 */
class my_controller extends \totara_mvc\admin_controller {

    // This property is the minimum you have to define
    protected $admin_external_page_name = 'myexternalpage';

    // By default admin pages are using the 'admin' layout
    // but here you can override the default layout if you want
    protected $layout = 'noblocks';

    public function action() {
        // do something
        
        // title and url by default is taken from the external page defined in settings.php
	    // but can still be overridden for you view
		return new \totara_mvc\view('my_example/example', ['template' => 'data']);
    }

}

Views

Tui view

To render a tui page component use the built-in tui view:

Default template
    public function action() {
        // do something

        $additional_props = [
            // You can pass additional props to the tui component
        ];

        return \totara_mvc\tui_view::create('my_plugin/pages/MyPageComponent', $additional_props);
    }

Generic view

A controller actions always has to return an instance of a class which implements the \totara_mvc\viewable interface. You can return your custom view/viewable but the easiest way is to use the generic view class. Its main purpose is to render a mustache template.

    public function action() {
        // handle input, access control
        // call your model
        // Prepare your template data
        $template_data = [];
        $template_data['key1'] = 'value1';
        $template_data['key2'] = 'value2';
        $template_data['key3'] = 'value3';

        // renders your my_first_template.mustache template
        return new \totara_mvc\view('my_plugin/my_first_template', $template_data);
    }

Configuration options

There are several methods to influence your view:

    public function action() {
        // do things

        return (new \totara_mvc\view(null))
            ->set_title('my custom page title')
            ->set_template('my_plugin/template') // alternative to using the constructor
            ->set_data(['key' => 'value']); // alternative to using the constructor
    }

Extending the generic view

Sometimes you want to encapsulate code which belongs to your view and goes beyond a simple approach. Then you can extend the view and override the prepare_output() method:

namespace my_plugin\views;

class my_custom_view extends \totara_mvc\view {

    // You can specify what title to use
    // in your view class by defining either a string
    // or an array with [nameofstring, component]
    protected $title = ['titlestring', 'my_plugin'];

    public function __construct() {
        parent::__construct('my_plugin/my_template');
    }

    protected function prepare_output($output) {
        $template_data = [
            'has_crumbtrail' => true,
            'has_pagination' => true,
            // If a template var contains a \renderable instance it
            // is automatically rendered when the view
            // gets rendered, no need to render it now
            'search' => $this->create_search()
        ];

        return array_merge($output, $template_data);
    }

    private function create_search() {
        return \totara_core\output\select_search_text::create(
            'text',
            get_string('my_search_box', 'my_plugin'),
            true
        );
    }

}

Then in your controller action you can simply use your view like:

    public function action() {
        // do things

        return new \my_plugin\views\my_custom_view();
    }

Report view

The report view can be used to render an embedded report. To load the report use the has_report trait in your controller and call its load_embedded_report function. Then pass the report instance to the report view.

It uses a report template defined in the totara_mvc plugin by default but you can also use your own custom template.

Default template
    use \totara_mvc\has_report;    

    public function action() {
        $report = $this->load_embedded_report('my_report_shortname');

		$debug = $this->get_optional_param('debug', false, PARAM_BOOL);

        return \totara_mvc\report_view::create_from_report($report, $debug);
    }
Custom template
    use \totara_mvc\has_report;    

    public function action() {
        $report = $this->load_embedded_report('my_report_shortname');

		$debug = $this->get_optional_param('debug', false, PARAM_BOOL);

        return \totara_mvc\report_view::create_from_report($report, $debug, 'my_plugin/custom_report_template');
    }