Containers
What are containers?
In Totara 13 we introduced a new internal tool called Container. This is built based on reusing the course's table and all of its associated components, which are module, course section and context.
How Totara containers work
The Container works based on Totara hooks that behave like middleware. For every page that either requires or optionally requires the course's ID, there is a hook in place in order to help the container implementation to redirect away from that page, as these pages are mainly built for courses. The hooks themselves are not smart enough to redirect away from the course-related page if the course record is not an actual course. The redirection will have to be a part of the container's implementation which the watcher(s) should check for the container type and decide what to do with it.
Changes within data layer:
- A new field within table ttr_course called containertype - a flag to define the container implementation that associate with this record.
- A new field within table ttr_course_categories called issystem - a flag to tell which categories are able to be maintained by the user and which are maintained by the system itself. Since a container cannot exist without a category, this field in the course's category table is introduced to help us hide all non-course-related categories from the end-users - even the Site Administrator. Note that categories with this flag will not be able to be viewed/edited/deleted by the end users.
For every new record within the ttr_course table the field containertype will default to 'container_course' as this is backward compatible with any customisable tables in the course made by third parties. However, if the course record is inserted via container API, it will be sure that the containertype is set to the correct value as the container implementation.
Main API classes and methods
The main abstract classes for containers are:
Class name | Purpose |
---|---|
\core_container\container | An abstract class that contains all the metadata related to a container, which can be overridden by the implementation. It is also used to instantiate the container instance. |
\core_container\section | Represents the course's section, which does not contain any business logics but a purely read/write access to data layer. |
\core_container\module | Represents the course's module record, which it does not contain any business logics, and only read/write to data layer. |
 List of hooks that are used within container's abstract layer:
Hook class name | Purpose |
---|---|
report_log\hook\index_view | To help redirect away from a course-related page. |
report_loglive\hook\index_view | To help redirect away from a course-related page. |
report_outline\hook\index_view | To help redirect away from a course-related page. |
report_participation\hook\index_view | To help redirect away from a course-related page. |
gradereport_grader\hook\index_view | To help redirect away from a course-related page. |
gradereport_history\hook\index_view | To help redirect away from a course-related page. |
gradereport_outcomes\hook\index_view | To help redirect away from a course-related page. |
gradereport_overview\hook\index_view | To help redirect away from a course-related page. |
gradereport_singleview\hook\index_view | To help redirect away from a course-related page. |
gradereport_user\hook\index_view | To help redirect away from a course-related page. |
totara_core\hook\backup_import_view | To help redirect away from a course-related page. |
totara_core\hook\backup_restore_file_view | To help redirect away from a course-related page. |
totara_core\hook\backup_view | To help redirect away from a course-related page. |
totara_core\hook\configure_enrol_instances | To help redirect away from a course-related page. |
totara_core\hook\edit_enrol_instances | To help redirect away from a course-related page. |
totara_core\hook\enrol_index_page | To help redirect away from a course-related page. |
totara_core\hook\group_index | To help redirect away from a course-related page. |
totara_core\hook\mod_add | To help redirect away from a course-related page. |
totara_core\hook\mod_update | To help redirect away from a course-related page. |
core_badges\hook\index_view | To help redirect away from a course-related page. |
core_badges\hook\new_badge_view | To help redirect away from a course-related page. |
core_badges\hook\view | To help redirect away from a course-related page. |
core_completion\hook\completion_editor | To help redirect away from a course-related page. |
core_completion\hook\course_archive_completion | To help redirect away from a course-related page. |
core_completion\hook\course_completion | To help redirect away from a course-related page. |
core_course\hook\competency_view | To help redirect away from a course-related page. |
core_course\hook\course_edit_view | To help redirect away from a course-related page. |
core_course\hook\course_view | To help redirect away from a course-related page. |
core_course\hook\reminders_view | To help redirect away from a course-related page. |
core_course\hook\reset_view | To help redirect away from a course-related page. |
core_course\hook\switchrole_view | To help redirect away from a course-related page. |
core_grades\hook\edit_tree_view | To help redirect away from a course-related page. |
core_grades\hook\letter_view | To help redirect away from a course-related page. |
core_grades\hook\outcome_view | To help redirect away from a course-related page. |
core_grades\hook\scale_view | To help redirect away from a course-related page. |
core_grades\hook\settings_view | To help redirect away from a course-related page. |
core_question\hook\category_view | To help redirect away from a course-related page. |
core_question\hook\edit_view | To help redirect away from a course-related page. |
core_question\hook\export_view | To help redirect away from a course-related page. |
core_question\hook\import_view | To help redirect away from a course-related page. |
core_container\hook\module_supported_in_container | To help with filtering all the course modules that the container implementation does not need. |
Helper scripts
There are helper scripts that will help the developer to provide/check the hooks for the newly introduced container.
The scripts are located in:
/container/cli/create_hook_file.php => Help to create a hooks.php file which is located within '/container/type/{new-container}/db'. /container/cli/check_hook.php => Help to check if all the redirect hooks are captured.
How to add new container
To introduce a new container type to the system, create a new directory under /server/container/type/{your-container-name}. For example, if the developer wants to add a new container 'micro_course' then there should be an expected directory created with a path as below:
/container/type/micro_course
Each of the containers will have these expected classes and files within the same path as above:
// Example of new container micro_course /container/type/micro_course . +-- classes | +-- micro_course.php // This file has to have the same name as the container's type directory. | | +-- module | | +-- module.php | | +-- section | | + section.php | +-- version.php
The file /container/type/micro_course/classes/micro_courses is the main container's class, which it will have to extend the class 'core_container\container'.
<?php // File /container/type/micro_course/classes/micro_course class micro_course extends \core_container\container { // Extending any abstract function needed. Mostly none. }
Note
The class that introduces a container must have the same exact name as container's type, as this is for the container's factory to auto-pickup and auto-resolve whenever a part of a system is trying to retrieve and instantiate a container.Â
Section
The section is a part of the container, and it represents a row of a table {course_sections}. It has pre-defined all the CRUD functionalities within itself, however these functionalities are only about writing to the data layer. There are no business logics running within the abstract layer, it is up to the implementation to add the business logics around it at the implementation layer.Â
When the developer introduces a new container the section class will have to be included as a part of the container as well.Â
Note
<?php // File /container/type/micro_course/classes/section/{section}.php namespace container_micro_course\section; class section extends \core_container\section\section { }
It does not have to be implemented with functionalities if the newly introduced container does not want to integrate or use the old course section. However, it has to be included in order to let core container code pick it up.
Module
Module is a part of the section, and as with the section, when the developer introduces the new container, the module class will have to be included as a part of the container as well.Â
Note
<?php // File /container/type/micro_course/classes/module/{module}.php namespace container_micro_course\module; class module extends \core_container\module\module { }
It does not have to be implemented with functionalities if the newly introduced container does not want to integrate or use the old course's module. However, it has to be included in order to let the core container code pick it up.
Category
By default a container can only exist within the system with an associated category. If the category is not provided to the container record, the process will try to look up an available category with the ID number that contains the container type, and assumes that this category will be used. However, if the related category is not found then it will create a new category, which is a sub category of the main course category. If the container implementation wants to define what the ID number value can be and what the name of category can be, then all it needs to do is to implement these two interfaces:
- core_container\facade\category_id_number_provider  => To give the container category's ID number.
- core_container\facade\category_name_provider.      => To give the container category's name.
<?php // File /container/type/micro_course/classes/micro_course class micro_course extends \core_container\container implements \core_container\facade\category_id_number_provider, \core_container\facade\category_name_provider { public static function get_container_category_name(): string { return get_string('category_name', 'micro_course'); } public static function get_container_category_id_number(): string { return 'something-unique-id'; } }
Best practices
There are may ways to fetch the container record and instantiate it within the system, which it can become a nightmare to maintain or deprecate them in many places later on. Furthermore, fetching container/course record many times in process can consume resources and leads to a performance issue, hence the abstract layer of container has provided a factory functionality to look up for the record and automatically detect the container implementation that can instantiate a correct container type instance. This factory also caches the record(s) within a request and can invalidate them by hand(s) within a process (if needs).
<?php // Example of a record: // [ // 'id' => 42, // 'containertype' => 'container_micro_course', // 'fullname' => 'This is micro course container' // ] // The first time requesting container record - the record will be looked up and cached for next request. $micro_course = \core_factory\factory::from_id(42); var_dump($micro_course->id); // 42 var_dump($micro_course->containertype); // 'container_micro_course'