====== Using Soda to Create New Moodle Modules ====== ===== Overview ===== {{:moodle:soda-128.png? |}} Soda is a Moodle plugin to develop new modules. Soda does two things: * It constructs all your standard module functions dynamically: you don't have to set up a complete lib.php file anymore for each new module. * Soda provides you with a Model-View-Controller (MVC) framework for your module, eliminating a lot of complexity. Traditionally, all your standard module functions reside in a file called lib.php. They are prefixed with your module's name, e.g.: ''planner_get_instance''. Soda provides these functions for your new module and allows you to override their default behavior. The MVC part of Soda makes it easier to separate layout (html) from business logic and default Moodle code. It does so by providing a controller class which you can subclass for your module. Soda's API documentation can be found here: [[http://soda-api.solin.eu/|soda-api.solin.eu]] Here, we will show you how to use Soda to easily set up a new module. ===== Using Soda To Set Up a New Module ===== [[https://github.com/solin-repo/soda|Download Soda]] and install the plugin inside your Moodle's ''local'' directory, e.g.: * moodle/local/soda * public_html/local/soda Go to the administration / notifications section of your Moodle website or visit the /admin url directly. Now it's time to create your own module. Simply use Soda's shell script ''local/soda/create.php''. Here's an example which assumes your webroot is called 'moodle' and your new module 'planner': cd moodle/local/soda ./create.php planner ## Your module's name should be singular (so no 's' at the end). You can now install your module by calling the ''/admin/index.php'' script on your Moodle site. Please note that the ''create.php'' script does **not** check if a module with the same name already exists. ==== Set Up a New Module Manually ==== Instead of using Soda's ''create.php'' shell script, you can also set up a new module manually. This will give you a better idea of the anatomy of a Moodle module. Create a new directory structure for you module inside your Moodle site's mod directory. Depending on the name of your 'webroot', this might be: * public_html/mod/planner * or, moodle/mod/planner For example: cd moodle/mod mkdir planner cd planner mkdir db mkdir lang mkdir lang/en touch view.php index.php version.php lib.php mod_form.php touch lang/en/planner.php touch db/access.php Alternatively, you can download the "newmodule" template here: http://moodle.org/mod/data/view.php?d=13&rid=715 or through Git: http://docs.moodle.org/dev/NEWMODULE_Documentation. Simply replace all appearances of string "newmodule" by the name of your own module, ''planner'' in our example. **Attention**: the "newmodule" template has a value 0 for ''$module->version'' in version.php. Change this value or the module will not be installed. onno@mac-mini:~/public_html/mod/planner$ find . -name '*' -exec sed -i -e 's/newmodule/planner/g' {} \; When creating a new module manually make sure the files version.php and lang/en/[modulename].php have a few lines of mandatory code: version = 2012010400; // The current module version (Date: YYYYMMDDXX) $module->requires = 2011112900; // Requires this Moodle version $module->component = 'mod_planner'; // Full name of the plugin (used for diagnostics) $module->cron = 0; ?> Now prepare all your tables in MySQL (or whatever database you are using) and then use the xmldbeditor to extract the right xml for the newmodule/install.xml file. You'll need at least 1 table to store the module's instances. The table should have the following fields: CREATE TABLE IF NOT EXISTS `mdl_planner` ( `id` bigint(10) unsigned NOT NULL AUTO_INCREMENT, `course` bigint(10) NOT NULL, `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `intro` text COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Default comment for planner, please edit me' AUTO_INCREMENT=1 ; Your initial db/install.xml file should contain something like this:
In order to add a new instance of your module, you'll need to create the file mod_form.php. Download the file from the newmodule template from moodle.org (https://github.com/moodlehq/moodle-mod_newmodule/blob/master/mod_form.php) if needed. Replace all 'newmodule' strings with 'planner'. Inside mod_form.php, don't forget to change the class name as well: FROM: class mod_newmodule_mod_form extends moodleform_mod { TO: class mod_planner_mod_form extends moodleform_mod { Also, some strings, like 'newmodulename', should be replaced in multiple places, i.e. in both lang/en/planner.php and mod_form.php. We need to add some code to view.php, so all calls will be redirected to index.php. wwwroot}/mod/planner/index.php?id=$id"); exit; ?> From Moodle 2.3 onwards, you need to populate your ''db/access.php'' file with (at least) the following code: array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ) ); ?> After creating a basic class.planner.php, lib.php and index.php you should be able to add the module to a course. See below for creating the three files. ==== Have Your Module Use Soda ==== Create a file for your module's main class. Following our example we call this file ''class.planner.php''. dirroot}/local/soda/class.soda.php"); class planner extends soda { } // class planner ?> Use this file to override specific functions you would normally store in lib.php. For instance, to override the default function ''planner_delete_instance'', add a static method your module's main class: static function delete_instance($id) { global $DB; if (! $planner = $DB->get_record("planner", array("id" => $id))) { return false; } $result = true; # Delete any dependent records here # if (! $DB->delete_records("planner", array("id" => $planner->id))) $result = false; $tables = array( "planner_categories", "planner_completed", "planner_completed_jobs" ); foreach($tables as $table) { if (! $DB->delete_records($table, array("planner_id" => $planner->id))) $result = false; } return $result; } // function delete_instance Soda will automatically create a new function called ''planner_delete_instance'' anyway, if you instantiate your module's main class. But now it will do so with the modified behavior. Moodle expects to find your module's standard functions in lib.php, so a perfect place to do the instantiation of your module's main class, is inside planner/lib.php: ===== Organize Your Module's Code Using Soda ===== Moodle is famous for its extensibility. You can easily add new functionality by creating a new module. Unfortunately, it's also very easy to create increasingly complex code because there's no standard way to organize your code. Soda encourages you to organize your module's code using the Model-View-Controller (MVC) paradigm. Within the MVC paradigm, all code is placed in either the Controller, the View or the Model. Roughly speaking: * Model: contains business logic * Controller: holds the application logic * View: the layout for a specific page or screen Soda sets up your module with a single point of entry: index.php. Just have your module's index.php file call your main class's ''display'' method to utilize Soda as your module's MVC framework: display(); ?> This example follows our example of the previous section, where we created a ''planner'' class which extends ''class soda''. ==== Add your module to a course ==== We're one step from being able to add our module to a new course. Adjust your mod_form.php if you downloaded the newmodule files from moodle.org (or Git) or download the mod_form.php and add it to your new plugin folder (https://github.com/moodlehq/moodle-mod_newmodule ). Replace 'newmodule' with the name of your plugin. You should be able to add your module to a new course: Courses -> Add/edit courses -> Testcourse -> Add an activity ==== Convention Over Configuration ==== To use Soda as your module's MVC framework you have to follow a few conventions. Once you know these conventions, it's easier to follow them than to do a lot of upfront configuration (such as populating your lib.php file). This principle is called Convention over Configuration. A Soda-based module follows these conventions: * Each url points to index.php, e.g.: mod/planner/index.php * Soda uses parameters (derived from either a form post or the url's query string) to determine which controller and view to use. E.g.: ''mod/planner/?controller=organization&view=index'' would typically show a list (index) of organizations. * The names (class, file, and directory names) of models, views and controllers are always singular (in contrast with the Ruby on Rails web application platform). Futhermore, Soda will be looking in specific places to find your controllers and views. Your Soda-based planner module should contain this directory structure: mod/planner/controllers mod/planner/views mod/planner/models The ''models'' directory can be omitted if your data model is relatively simple (does not contain any additional business logic on top of your database tables' relations). The controllers directory contains a file for each 'topic' (or model). The ''views'' directory is further subdivided into directories for each model (or 'topic'), because you may want to have different views for each topic. For example, if our 'planner' module contains an option to provide a planned completion date for each activity, we should add an 'activity' directory inside the ''views'' directory: controllers/activity.php views/activity/index.html views/activity/edit.html Views are html snippets which may escape to php to output the value of php variables. Controllers are php files which handle specific incoming requests. ===== Soda Controllers ===== Soda expects your module's controllers to extend Soda's controller class. // filename: mod/planner/controllers/planner.php include_once("{$CFG->dirroot}/local/soda/class.controller.php"); class planner_controller extends controller { /* ... */ } // class planner_controller Each controller should be "about" a specific topic. The controller's class name and file name are derived from this topic. For instance, if a controller is about an activity, the class name is ''activity_controller'' and the file name is ''activity.php''. Usually, all possible CRUD operations for the topic are handled by its controller: * Create * Read * Update * Delete To specify what controller method must be invoked, you should use the action parameter, e.g.: ''{$CFG->wwwroot}/mod/planner/?controller=activity&action=edit&activity_id=20&id=928''. This url will invoke the method ''edit'' on an object of class ''activity_controller''. Here's an example of a controller designed to: - see if the user is logged in; - edit the activity with id $activity_id. // filename: mod/planner/controllers/activity.php include_once("{$CFG->dirroot}/local/soda/class.controller.php"); class activity_controller extends controller { function __construct($mod_name, $mod_instance_id) { parent::__construct($mod_name, $mod_instance_id); $this->require_login(); } // function __construct function edit($activity_id) { global $DB; if (! $activity = $DB->get_record('planner_activities', array('id' => $activity_id)) ) { error("Could not find planner_activities record with id = $activity_id"); } echo $this->get_view(array('activity' => $activity, 'activity_id' => $activity_id) ); } // function edit } // class activity_controller Soda's controller class provides you with a number of methods. The examples here are ''require_login'' and ''get_view''. The first is really a wrapper for the standard Moodle require_login function (but without the need to specify the course as a parameter). The second, ''get_view'', returns the html for the associated action. In this example, ''get_view'' is called from the ''edit'' action, so the view which is automatically retrieved is ''mod/planner/views/activity/edit.html''. Which would typically be a form to edit the activity. The ''get_view'' method requires you to specify the php variables you want to use in your view. ==== Actions Are Functions: Treat Them As Such ==== Actions are really function calls on a controller class. Each function should perform one task only. Here are the typical functions of a controller: class booking_controller extends controller { // Retrieves a list of bookings function index() {} // Retrieves the details of a specific booking function show() {} // Get a specific booking and display an edit form function edit() {} // Create, update or delete a booking // In Moodle, records are usually not actually erased. // Instead, a column bookings.deleted will be set to 1 (through an update) function save() { $booking = new booking($_REQUEST['booking']); if (!$booking->save()) return $this->get_view('edit'); $this->redirect_to($action = 'index'); } } If you find yourself adding many functions to the controller, consider refactoring them into the model class. This is summarized as **fat models, skinny controllers**. ===== Soda Models ===== Use a Soda model to: * Easily specify (server side) validation rules (See validation section) * Quickly save and load multiple records and their associated records ==== Conventions ==== To quickly load a bunch of records and their associated data as Soda models, you must tell Soda a few things about your data. Here's what a typical Soda model should look like: The conventions here are: * The primary key is always ''id'' * A foreign key always looks like: ''[model_name]_id'' (without the square brackets of course) * ''static $plural'' is used to tell Soda what your association field should look like if it's not simply ''[model_name]s'' ==== Finders And Loaders ==== It's usually very cumbersome to retrieve an object from an array, based on the value of an object's property. In Soda, you can use a 'finder'. Here's an example: $jackson_book = book::find_by_author('Jackson', $books); This will retrieve the first book in a collection $books with the value 'Jackson' for the property ''author''. All property names are valid finders if preceded by 'find_by_'. You can also string properties together, separating them with 'and': $book = book::find_by_author_and_year_and_publisher('Jackson', 1999, 'Penguin'); By the same token, you can use ''find_all_by_'': $jacksons_books = book::find_all_by_author_and_publisher('Jackson', 'Penguin', $books); Now here's the good part: if you omit the collection, Soda will attempt to load it on the fly from the database. $jacksons_books = book::find_all_by_author('Jackson'); Please note the Soda uses the restriction added in the first argument for the retrieval. In other words: the database query contains a clause ''WHERE author = 'Jackson'''. When used without plugging in the collection, ''find'' and ''find_all'' are functionally almost identical to ''load'' and ''load_all''. The loaders, however, allow you to specify which associations to include: $authors = author::load_all_by_publisher('Penguin', $include = array('book')); This will automatically load all associated books for each author. Please note that this takes two database calls in the background. ===== Soda Views ===== Soda views are html snippets which may use Soda's view and form helpers (based in Soda's controller). Here's an example of a view for an index of activities, which would typically reside in ''mod/planner/views/activity/index.html'':

form_open($action = false, array('action' => ($activity_id) ? 'update' : 'create')); ?> id == $activity_id) { echo ""; echo ""; echo ""; continue; } ?>
label ?>
'/>
This approach has significant advantages over a spaghetti code style lib.php file: * The graphical designer will always know where to find the html code: in the ''views'' directory. * There is very few php code in the views. And what there is, is very simple php code (mainly loops and variables). * The views can be edited in isolation, for instance with a web design tool like Dreamweaver. Furthermore, Soda provides you with some form and view helper methods. Because the view is rendered in the context of the Soda controller, you can call these helper methods using the php ''$this'' keyword, e.g.: ''$this->form_open()''. ==== A Template for Your Views ==== You can also embed all your views in a template. This is very useful if for instance you want output all content in the middle column of a three columns table. Just place a file called ''template.html'' directly inside your ''views'' directory, and have it include the view through the default variable ''$view_path'':

My First Soda based Module!

Please note that this template also conditionally outputs any validation errors on top of your view. ===== Soda Application Flow ===== In the previous sections, we have explained the nuts and bolts of Soda. Now let's see how we can put it all together. A typical course of action for the end user would be to first look up a list of items, let's say activities to be planned. Then a specific activity is edited. Finally, the activity is updated, i.e. saved to the database. Using the Soda Model-View-Controller framework, you would provide an activity controller with a method for each of the following actions: * index * edit * update Let's review each of them. ==== List of items: index ==== So, if the user visits this url: ''{$CFG->wwwroot}/mod/planner/?controller=activity'', the default method ''index'' will be invoked. The activity_controller#index method retrieves all activities from the database and hands them over to a view called ''index.html''. The index view will typically contain a list of clickable items. The user chooses an activity by clicking a link or a button, which points to: ''{$CFG->wwwroot}/mod/planner/?controller=activity&action=edit&activity_id=23''. ==== Changing a specific item: edit ==== Soda invokes the ''edit'' method on an object of class activity_controller. The edit method returns a form where the user makes the necessary changes. The changed activity is posted to ''{$CFG->wwwroot}/mod/planner/index.php'', with a few required post parameters: ''action=update'', ''activity_id=23'' and ''controller= activity''. ==== Saving the edits: update ==== This time, Soda invokes the ''update'' method, which stores the changes in the database. Please note that **whenever you call a typical database modifying action, you should redirect the browser afterwards**. A redirect prevents accidental reposts (which would otherwise happen whenever the user refreshes the browser window). If Soda detects that you have used its ''controller#redirect_to'' method, it will omit all layout. Soda will skip not just your module's layout, but also the standard Moodle header and footer. The same can be achieved by calling a Soda based module with the ''no_layout=true'' parameter (either through the querystring or as a post parameter). Here's what an update method might look like: // filename: mod/planner/controllers/activity.php function update($activity_id) { global $DB; $activity = $_POST['activity']; if (! $record = $DB->get_record('compass_activities', array('id' => $activity_id)) ) { error("Could not find compass_activities record with id = $activity_id"); } $record->label = $activity['label']; $DB->update_record('compass_activities', $record); $this->redirect_to('index'); } // function update In this example, the user is redirected back to the index (i.e. list of activities). Please note that Soda automatically invokes all methods with the ''$activity_id'' as the first parameter. If the post or get variable with the same name is not available, Soda will just insert ''false'' as the first argument for the method call. On a final note: for explanatory reasons we have skipped over security issues. For instance, you might want to check whether the user actually has the right to update the activity. === Default CRUD methods === By default, Soda provides the following CRUD methods for Soda based models (more about those later, in the section "Soda Server Side Validation"): * Save: covers both Create and Update (simply provide the id as the first argument to make updates) * Delete Save always calls a validation method first. If the model is invalid, there will be no actual insert or update and the method returns false. Delete simply sets the ''deleted'' column of your model to ''1'' and then calls ''save_without_validation''. ===== Soda Server Side Validation ===== Soda makes it very easy to perform server side validation. If you stick to a few conventions, you won't have to do a lot of work yourself. In Soda, you specify validation rules inside models. A model usually contains the information for one database record. You can populate a new model with properties and corresponding values: $organization = new organization($_POST['organization']); In this example, all fields and values of the array 'organization' are translated into properties of the object $organization. To enable this, your model should extend Soda's default model. You also need to define the actual validation rules for each of your model's properties. Here's how to: * Make sure the there's a method ''define_validation_rules'' * Inside ''define_validation_rules'', call the default Soda model method ''add_rule'' to define a new rule The method ''add_rule'' requires three arguments: - The property name - The error message - The actual validation code as an anonymous function Here's an example: add_rule('label', 'Please fill in a label.', function($label) { return ( trim($label) != '' ); }); $this->add_rule('label', 'Must be a number', function($label) { return ( is_numeric($label) ); }); $this->add_rule('email', 'Please fill in your email.', function($email) { return ( trim($email) != '' ); }); } // function define_validations } // class organization ?> Whenever the model is validated, e.g. every time it is saved by the default Soda ''save'' method, all validation rules are applied. Please note that you can also specify multiple rules for a single property. If the model is invalid, the save method will return false. In the following example: * We instantiate a new organization model with the properties derived from the form post. The original form contains fields of the type ''''. In other words: your form fields must have names that correspond exactly with the columns names of the database table where the "organization" data is stored. * We save the organization by calling the default ''save()'' method. If this call returns false, we output the form again, and populate its fields with the contents of the $organization object so the user can fix the invalid fields. function create() { $organization = new organization($_POST['organization']); if (!$organization->save()) return $this->get_view('edit'); $this->redirect_to('index'); } // function create Soda automatically keeps track of all validation errors in the static class ''soda_error''. Here's how you can output the errors, for instance inside ''views/template.html'' (so you won't have to put this code in each of your views):
===== Avoiding Repetition With Helpers ===== If you find yourself repeating a lot of layout code in your views, consider using helpers. You may also want to put generic form fields, e.g. a "countries" select box, inside a helper class. Soda supports helpers on two levels: a general helper class, named after your module, and a specific helper class, named after your controller. So, if your module is named "planner", your helper files look like this: * General: mod/planner/helpers/class.planner_helper.php * Specific: mod/planner/helpers/calendar/class.calendar_helper.php All non-static methods and properties available in your controller class are also available in your helper classes. Conversely, all methods, but not the properties, of the helper classes are available inside your controller class. Due to limitations of php, static properties cannot be made available in the context of a helper. Of course, you can still access static properties in a helper class by spelling out the name of the controller: ''planner_controller::$the_static_property''. Your helper classes must extend the Soda helper class, which is already included by Soda: model_name}_country' name='{$this->model_name}[country]'>"; echo ""; foreach(get_list_of_countries() as $key=>$country) { $selected = ($country_preset == $key) ? "selected='selected'" : ""; echo ""; } echo ""; } // function print_countries_dropdown_box() } ?> ==== Layout Helper ==== Ever since php 5.3, we can use closures ("anonymous functions") to help us out with layout issues. Here's an example: In your view: fieldset(function() use ($stories) { ?> selected == '' || $story->selected == 0) continue; ?>
. title ?>
In your helper: function fieldset($displayer) { echo "
"; } // function fieldset
That's a lot nicer than using something like ''$this->open_fieldset() ... $this->close_fieldset()'', which forces you to define a pair of functions for each layout box.