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:
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:
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
By default the controller calls require_login()
with the default arguments automatically.
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:
$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:
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.
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); }
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'); }