Difference between revisions of "User:Frédéric Massart"

Jump to: navigation, search
Line 1: Line 1:
{{Moodle 3.3}}Moodle exporters are classes which receive data and serialise it to a simple pre-defined structure. They ensure that the format of the data exported is uniform and easily maintainable. They are also used to generate the signature (parameters and return values) of [[External functions API|external functions]].
+
{{Moodle 3.3}}If you are creating [[Persistent|persistent]] entries from forms, we've got something for you. You can use the class ''core\form\persistent'' as a base for your form instead of ''moodleform''. Our persistent form class comes handy tools, such as automatic validation.
  
== Introduction ==
+
_Note: for more information about forms themselves, [[Category:Formslib|head this way]]._
  
When dealing with external functions (Ajax, web services, ...) and rendering methods we often hit a situation where the same object is used, and exported, from multiple places. When that situation arises, the code that serialises our objects gets duplicated, or becomes inconsistent.
+
== Defining your form ==
  
We made the exporters to remedy to this situation. An exporter clearly defines what data it exports, and contains the logic which transform the incoming data into the exported data. As it knows everything, it can generate the structure required by external functions automatically.
 
  
This means that not only developers have less code to maintain, but they also have a more robust structure which can easily evolve with their needs. If a new property needs to be exported, it is simply added to the exporter class, and automatically all usage of the exporter inherits this added property.
+
First of all, let
  
== Defining properties ==
+
First, here is some code to create a form for the persistent we worked in [[Persistent|the persistent documentation]]:
  
The method ''define_properties()'' returns a list of the properties expected for the incoming data, and for the exported data. This also means that to create (or update) you will require those properties.
 
  
<code php>
 
/**
 
* Return the list of properties.
 
*
 
* @return array
 
*/
 
protected static function define_properties() {
 
    return array(
 
        'id' => array(
 
            'type' => PARAM_INT
 
        ),
 
        'username' => array(
 
            'type' => PARAM_ALPHANUMEXT
 
        ),
 
    );
 
}
 
</code>
 
  
=== Property attributes ===
+
== Linking to a persistent ==
  
Each property is configured using the following attributes:
+
In order for the form class to know what persistent we'll be dealing with, we must declare the ''protected static $persistentclass'' variable. The latter contains the fully qualified name of the persistent class.
  
;type
+
<code php>
: The only mandatory attribute. It must either be one of the many PARAM_* constants, or an array of properties.
+
/** @var string Persistent class name. */
;default
+
protected static $persistentclass = 'example\\status';
: The default value when the value was not provided. When not specified, a value is required.
+
</code>
;null
 
: Either of the constants NULL_ALLOWED or NULL_NOT_ALLOWED telling if the null value is accepted. This defaults to NULL_NOT_ALLOWED.
 
;optional
 
: Whether the property can be omitted completely. Defaults to false.
 
;multiple
 
: Whether there will be more one or more entries under this property. Defaults to false.
 
  
Although this is not a ''rule'', it is recommended that the ''standard properties'' (by opposition to [[#Additional_properties|additional properties]]) only use the ''type'' [[#Property_attributes|attribute]], and only with ''PARAM_*'' constants.
+
== Defining the form fields ==
  
== Using an exporter ==
+
Unfortunately this is not automatically done for us, so let's add our fields to the ''definition()'' method like you would do for any form.
  
Once we've got a [[#Minimalist|minimalist exporter]] set-up, here is how to use it.
+
<code php>
 +
/**
 +
* Define the form.
 +
*/
 +
public function definition() {
 +
    $mform = $this->_form;
  
=== Exporting the data ===
+
    // User ID.
 +
    $mform->addElement('hidden', 'userid');
 +
    $mform->setType('userid', PARAM_INT);
 +
    $mform->setConstant('userid', $this->_customdata['userid']);
  
<code php>
+
    // Message.
$data = (object) ['id' => 123, 'username' => 'batman'];
+
    $mform->addElement('editor', 'message', 'Message');
 +
    $mform->setType('message', PARAM_RAW);
  
// The only time we can give data to our exporter is when instantiating it.
+
    // Location.
$ue = new user_exporter($data);
+
    $mform->addElement('text', 'location', 'Location');
 +
    $mform->setType('location', PARAM_ALPHANUMEXT);
  
// To export, we must pass the reference to a renderer.
+
    $this->add_action_buttons();
$data = $ue->export($OUTPUT);
+
}
 
</code>
 
</code>
  
If we print the content of ''$data'', we will obtain this:
+
All of this is pretty standard, except for the ''userid''. When creating a new 'status', we do not want our users to be in control of this value. Therefore we define it as a hidden value which we lock (using ''setConstant'') to the value we created our form with. All mandatory fields of the persistent need to be defined, but if they are not customised by the user, then they must be hidden and combined with ''setConstant''.
  
<code>
+
Also note that the ''id'' property is not included. It is not required, nor recommended, to add it to your fields it will be handled automatically.
stdClass Object
+
 
(
+
== Using the form ==
    [id] => 123
 
    [username] => batman
 
)
 
</code>
 
  
Now, I agree that this is not quite impressive. But wait until you read about [[#Abiding_to_text_formatting_rules|automatically formatting text]], and [[#In_external_functions|usage in external functions]].
+
When instantiating the form, there are two little things that you need to pay attention to.  
  
=== In external functions ===
+
Firstly you should always pass the URL of the current page, including the different query parameters it included. We need this to be able to display the form with its validation errors without affecting anything else.
  
Let's imagine that we have an external function ''get_users'' which returns a list of users. For now we only want to export the user ID and their user name, so we'll use our exporter. Let's ask our exporter to create the external structure for us:
+
Secondly, the persistent instance must be provided to the form through the custom data. That instance will be used to populate the form with initial data, typically when you are editing an object. When you don't have a persistent instance yet, probably because your user will be creating a new one, then simply pass null.
  
 
<code php>
 
<code php>
public static function get_users_returns() {
+
$customdata = [
     return external_multiple_structure(
+
     'persistent' => $persistent,  // An instance, or null.
         user_exporter::get_read_structure()
+
    'userid' => $USER->id         // For the hidden userid field.
    );
+
];
}
+
$form = new status_form($PAGE->url->out(false), $customdata);
 
</code>
 
</code>
  
Now that this is done, we must use our exporter to export our users' data.
+
Just like any other form, we will be using ''get_data()'' to validate the form. The only difference is that to determine whether we are editing an object, or creating a new one, we will check if the ''id'' value was returned to us. The peristent form will return the ID value from the persistent we gave it. Then it's up to you to decide how to apply the data, most likely you will defer the logic to another part of your code, one that ensures that all capability checks are fulfilled.  
  
 
<code php>
 
<code php>
public static function get_users() {
+
// Get the data. This ensures that the form was validated.
    global $DB, $PAGE;
+
if (($data = $form->get_data())) {
     $output = $PAGE->get_renderer('core');
+
 
    $users = $DB->get_records('user', null, '', 'id, username', 0, 10); // Get 10 users.
+
     if (empty($data->id)) {
    $result = [];
+
        // If we don't have an ID, we know that we must create a new record.
    foreach ($users as $userdata) {
+
        // Call your API to create a new persistent from this data.
         $exporter = new user_exporter($userdata);
+
        // Or, do the following if you don't want capability checks (discouraged).
         $result[] = $exporter->export($output);
+
        $persistent = new status(null, $data);
 +
        $persistent->create();
 +
    } else {
 +
        // We had an ID, this means that we are going to update a record.
 +
        // Call your API to update the persistent from the data.
 +
        // Or, do the following if you don't want capability checks (discouraged).
 +
         $persistent->from_record($data);
 +
         $persistent->update();
 
     }
 
     }
     return $result;
+
 
 +
     // We are done, so let's redirect somewhere.
 +
    redirect(new moodle_url('/'));
 
}
 
}
 
</code>
 
</code>
  
Lastly, if you had another external function to create users, you could use the exporter to get the structure of the incoming data. That helps if you want your external functions to require more information to create your ''users'' as your needs grow. The following indicates that the external function requires the fields 'id' and 'username' to be passed in the key 'user'.
+
== Additional validation ==
 +
 
 +
There are times where the built-in validation of the persistent is not enough. Usually you would use the method ''validation()'', but as the form persistent class does some extra stuff to make it easier for you, you must use the ''extra_validation()'' method. The latter works almost just like the ''validation()'' one.
  
 
<code php>
 
<code php>
public static function create_user_parameters() {
+
/**
     return new external_function_parameters([
+
* Extra validation.
        'user' => user_exporter::get_create_structure()
+
*
    ]);
+
* @param  stdClass $data Data to validate.
}
+
* @param  array $files Array of files.
 +
* @param  array $errors Currently reported errors.
 +
* @return array of additional errors, or overridden errors.
 +
*/
 +
protected function extra_validation($data, $files, array &$errors) {
 +
     $newerrors = array();
 +
 
 +
    if ($data->location === 'SFO') {
 +
        $newerrors['location'] = 'San-Francisco Airport is not accepted from the form.';
 +
    }
  
public static function create_user($user) {
+
     return $newerrors;
    // Mandatory parameters validation.
 
    $params = self::validate_parameters(self::create_user_parameters(), ['user' => $user]);
 
     $user = $params['user'];
 
    ...
 
 
}
 
}
 
</code>
 
</code>
  
Important note: when used in the ''parameters'', the exporter's structure must always be included under another key, above we used ''user''. Else this would not flexible, and may not generate a valid structure for some protocols. You should also checkout ''get_update_structure()'' which is essentially the same except that it does requires an ''ID''.
+
The typical additional validation will return an array of errors, those will override any previous error. Rarely you will need to remove previously reported errors, hence the reference to ''$errors'' given, which you can modify directly. Do not abuse it though, this should only be used when you have no other choice.
 +
 
 +
== Foreign fields ==
  
== Abiding to text formatting rules ==
+
By default, the form class tries to be smart at detecting foreign fields such as the submit button. Failure to do so will cause trouble during validation or other else. So when your form becomes more complex, if it includes more submit buttons, or when it deals with other fields for example files, we must specify it.
  
If we had to pass the ''$OUTPUT'' during export as seen previously, that is because we are handling the text formatting automatically for you. Remember the functions ''format_text()'' and ''format_string()''? They are used to apply [[Filters|filters]] on the content typically submitted by users, but also to convert it from a few given formats to HTML.
+
=== Fields to ignore completely ===
  
Upon export, the exporter looks at the ''type'' of all your properties. When it finds a property of type ''PARAM_TEXT'', it will use ''format_string()''. However, if it finds a property using ''PARAM_RAW'' and there is another property of the same name but ending with ''format'' it will use ''format_text()''.
+
The fields to remove are never validated and they are not returned when calling ''get_data()''. By default the submit button is added to this list so that when we call ''get_data()'' we only get the persistent-related fields. To remove more fields, re-declare the ''protected static $fieldstoremove'' class variable.
  
 
<code php>
 
<code php>
'description' => array(
+
/** @var array Fields to remove when getting the final data. */
    'type' => PARAM_RAW,
+
protected static $fieldstoremove = array('submitbutton', 'areyouhappy');
),
 
'descriptionformat' => array(
 
    'type' => PARAM_INT,
 
),
 
 
</code>
 
</code>
  
With the two above properties (not [[#Additional_properties|other properties]]) added, let's see what happens when the user's description is in the Markdown format and we export it.
+
Do not forget to add the ''submitbutton'' back in there.
  
<code php>
+
=== Fields to validate ===
$data = (object) [
 
    'id' => 123,
 
    'username' => 'batman',
 
    'description' => 'Hello __world__!',
 
    'descriptionformat' => FORMAT_MARKDOWN
 
];
 
$ue = new user_exporter($data);
 
$data = $ue->export($OUTPUT);
 
</code>
 
  
Unsurprisingly, this is what comes out of it:
+
What about when we have a ''legit'' but that does not belong to the persistent? We still want to validate it ourselves, but we don't want it to be validated by the persistent as it will cause an error. In that case we define it in the ''protected static $foreignfields'' class variable.
  
<code>
+
<code php>
stdClass Object
+
/** @var array Fields to remove from the persistent validation. */
(
+
protected static $foreignfields = array('updatedelay');
    [id] => 123
 
    [username] => batman
 
    [description] => <p>Hello <strong>world</strong>!</p>
 
    [descriptionformat] => 1  // Corresponds to FORMAT_HTML.
 
)
 
 
</code>
 
</code>
  
Psst... If you're wondering where we get the ''context'' from, look at [[#Related_objects|related objects]].
+
Now the persistent will not validate this field, and we will get the ''updatedelay'' value when we call ''get_data()''. Just don't forget to remove it before you feed the data to your persistent.
a
 
== Additional properties ==
 
 
 
The list of ''other'' properties are to be seen as ''additional'' properties which do not need to be given to the exporter for it to export them. They are dynamically generated from the data provided (and the [[#Related objects|related objects]], more on that later). For example, if we wanted our exporter to provide the URL to a user's profile, we wouldn't need the developer to pass it beforehand, we could generate it based on the ID which was already provided.
 
 
 
''Other'' properties are only included in the ''read'' structure of an object (''get_read_structure'' and ''read_properties_definition'') as they are dynamically generated. They are not required, nor needed, to ''create'' or ''update'' an object.
 
  
 
<code php>
 
<code php>
/**
+
if (($data = $form->get_data())) {
* Return the list of additional properties.
+
    $updatedelay = $data->updatedelay;
 
+
    unset($data->updatedelay);
* @return array
+
    $newpersistent = new status(0, $data);
*/
 
protected static function define_other_properties() {
 
    return array(
 
        'profileurl' => array(
 
            'type' => PARAM_URL
 
        ),
 
        'statuses' => array(
 
            'type' => status_exporter::read_properties_definition(),
 
            'multiple' => true,
 
            'optional' => true
 
        ),
 
    );
 
 
}
 
}
 
</code>
 
</code>
  
The snippet above defines that we will always export a URL under the property ''profileurl'', and that we will either export a list of ''status_exporters'', or not. As you can see, the ''type'' can use the ''read'' properties of another exporter which allows exporters to be nested.
+
This method is particularily useful when dealing with file managers.
 +
 
 +
== Examples ==
  
If you have defined ''other'' properties, then you must also add the logic to export them. This is done by adding the method ''get_other_values(renderer_base $output)'' to your exporter. Here is an example in which we ignored the ''statuses'' as they are optional.
+
=== Minimalist ===
  
 
<code php>
 
<code php>
/**
+
class status_form extends \core\form\persistent {
* Get the additional values to inject while exporting.
+
   
*
+
    /** @var string Persistent class name. */
* @param renderer_base $output The renderer.
+
    protected static $persistentclass = 'example\\status';
* @return array Keys are the property names, values are their values.
 
*/
 
protected function get_other_values(renderer_base $output) {
 
    $profileurl = new moodle_url('/user/profile.php', ['id' => $this->data->id]);
 
    return [
 
        'profileurl' => $profileurl->out(false)
 
    ];
 
}
 
</code>
 
  
Important note: ''additional properties'' cannot override ''standard'' properties, so make sure you the names do not conflict.
+
    /**
 +
    * Define the form.
 +
    */
 +
    public function definition() {
 +
        $mform = $this->_form;
  
== Related objects ==
+
        // User ID.
 +
        $mform->addElement('hidden', 'userid');
 +
        $mform->setType('userid', PARAM_INT);
 +
        $mform->setConstant('userid', $this->_customdata['userid']);
  
There are times we need more information inside the exporter in order to export it. That may be as simple as the ''context'', but it can also be other objects to be used when exporting ''other'' properties.
+
        // Message.
 +
        $mform->addElement('editor', 'message', 'Message');
 +
        $mform->setType('message', PARAM_RAW);
  
Related objects need to be defined within the exporter, that is to ensure that they are always provided and are of the right type. In some occasions they can be marked as optional, generally when the related object does not exist. For instance, if we had an issue exporter, an optional related object could be the peer reviewer, as we don't always have a peer reviewer.
+
        // Location.
 +
        $mform->addElement('text', 'location', 'Location');
 +
        $mform->setType('location', PARAM_ALPHANUMEXT);
  
Use the method ''protected static define_related()'' as follows. The keys are an arbitrary name for the related object, and the values are the fully qualified name of the class. The name of the class can be followed by ''[]'' and/or ''?'' to respectively indicate a list of these objects, and an optional related.
+
        $this->add_action_buttons();
 +
    }
  
<code php>
+
}
    /**
 
    * Returns a list of objects that are related.
 
    *
 
    * @return array
 
    */
 
    protected static function define_related() {
 
        return array(
 
            'context' => 'context',
 
            'statuses' => 'some\\namespace\\status[]',
 
            'mother' => 'family\\mother?',
 
            'brothers' => 'family\\brother[]?',
 
        );
 
    }
 
 
</code>
 
</code>
  
We give the related objects to the ''exporter'' when we instantiate it, like this:
+
=== More advanced ===
  
 
<code php>
 
<code php>
$data = (object) ['id' => 123, 'username' => 'batman'];
+
class status_form extends \core\form\persistent {
$relateds = [
 
    'context' => context_system::instance(),
 
    'statuses' => [
 
        new some\namespace\status('Hello'),
 
        new some\namespace\status('World!'),
 
    ],
 
    'mother' => null,
 
    'brothers' => null
 
];
 
$ue = new user_exporter($data, $relateds);
 
</code>
 
  
Note that optional related must still be provided but as ''null''.
+
    /** @var string Persistent class name. */
 +
    protected static $persistentclass = 'example\\status';
  
== Exporters and persistent ==
+
    /** @var array Fields to remove when getting the final data. */
 +
    protected static $fieldstoremove = array('submitbutton', 'areyouhappy');
  
Great news, if you have a [[Persistent|persistent]] and you want to export them, all the work is pretty much done for you. All of the persistent's properties will automatically be added to your exporter if you extend the class ''core\external\persistent_exporter'', and add the method ''define_class()''.
+
    /** @var array Fields to remove from the persistent validation. */
 
+
    protected static $foreignfields = array('updatedelay');
<code php>
 
class status_exporter extends core\external\persistent {
 
  
 
     /**
 
     /**
     * Returns the specific class the persistent should be an instance of.
+
     * Define the form.
    *
 
    * @return string
 
 
     */
 
     */
     protected static function define_class() {
+
     public function definition() {
         return 'some\\namespace\\status';
+
         $mform = $this->_form;
    }
 
  
}
+
        // User ID.
</code>
+
        $mform->addElement('hidden', 'userid');
+
        $mform->setType('userid', PARAM_INT);
And if you wanted to add more to it, there is always the [[#Additional_properties|other properties]].
+
        $mform->setConstant('userid', $this->_customdata['userid']);
  
== Common pitfalls ==
+
        // Message.
 +
        $mform->addElement('editor', 'message', 'Message');
 +
        $mform->setType('message', PARAM_RAW);
  
# Exporters must not extend other exporters. They would become too unpredictable.
+
        // Location.
# Exporters do not validate your data. They use the properties' attributes to generate the structure required by the [[External functions API]], the latter is responsible for using the structure to validate the data.
+
        $mform->addElement('text', 'location', 'Location');
# When exporters are nested, the ''type'' to use should be ''other_exporter::read_properties_definition()'' and not ''other_exporter::get_read_structure()''.
+
        $mform->setType('location', PARAM_ALPHANUMEXT);
  
== Examples ==
+
        // Status update delay.
 +
        $mform->addElement('duration', 'updatedelay', 'Status update delay');
  
=== Minimalist ===
+
        // Are you happy?
 +
        $mform->addElement('selectyesno', 'areyouhappy', 'Are you happy?');
  
<code php>
+
        $this->add_action_buttons();
class user_exporter extends core\external\exporter {
+
    }
  
 
     /**
 
     /**
     * Return the list of properties.
+
     * Extra validation.
 
     *
 
     *
     * @return array
+
    * @param  stdClass $data Data to validate.
 +
    * @param  array $files Array of files.
 +
    * @param  array $errors Currently reported errors.
 +
     * @return array of additional errors, or overridden errors.
 
     */
 
     */
     protected static function define_properties() {
+
     protected function extra_validation($data, $files, array &$errors) {
         return array(
+
         $newerrors = array();
            'id' => array(
+
 
                'type' => PARAM_INT
+
        if ($data->location === 'SFO') {
            ),
+
             $newerrors['location'] = 'San-Francisco Airport is not accepted from the form.';
             'username' => array(
+
        }
                'type' => PARAM_ALPHANUMEXT
+
 
            ),
+
         return $newerrors;
         );
 
 
     }
 
     }
 
 
}
 
}
 
</code>
 
</code>
  
=== More advanced ===
+
=== Using the form ===
 +
 
 +
Consider the following code to be a page you users will access at '/example.php'.
  
 
<code php>
 
<code php>
namespace example\external;
+
require 'config.php';
  
use core\external\exporter;
+
// Check if we go an ID.
use renderer_base;
+
$id = optional_param('id', null, PARAM_INT);
use moodle_url;
 
  
class user_exporter extends exporter {
+
// Set the PAGE URL (and mandatory context). Note the ID being recorded, this is important.
 +
$PAGE->set_context(context_system::instance());
 +
$PAGE->set_url(new moodle_url('/example.php', ['id' => $id]));
  
    /**
+
// Instantiate a persistent object if we received an ID. Typically receiving an ID
    * Return the list of properties.
+
// means that we are going to be updating an object rather than creating a new one.
    *
+
$persistent = null;
    * @return array
+
if (!empty($id)) {
    */
+
    $persistent = new status($id);
    protected static function define_properties() {
+
}
        return array(
 
            'id' => array(
 
                'type' => PARAM_INT
 
            ),
 
            'username' => array(
 
                'type' => PARAM_ALPHANUMEXT
 
            ),
 
            'description' => array(
 
                'type' => PARAM_RAW,
 
            ),
 
            'descriptionformat' => array(
 
                'type' => PARAM_INT,
 
            ),
 
        );
 
    }
 
  
     /**
+
// Create the form instance. We need to use the current URL and the custom data.
    * Return the list of additional properties.
+
$customdata = [
 +
    'persistent' => $persistent,
 +
     'userid' => $USER->id        // For the hidden userid field.
 +
];
 +
$form = new status_form($PAGE->url->out(false), $customdata);
  
    * @return array
+
// Get the data. This ensures that the form was validated.
    */
+
if (($data = $form->get_data())) {
    protected static function define_other_properties() {
 
        return array(
 
            'profileurl' => array(
 
                'type' => PARAM_URL
 
            ),
 
            'statuses' => array(
 
                'type' => status_exporter::read_properties_definition(),
 
                'multiple' => true,
 
                'optional' => true
 
            ),
 
        );
 
    }
 
  
     /**
+
     if (empty($data->id)) {
    * Returns a list of objects that are related.
+
        // If we don't have an ID, we know that we must create a new record.
    *
+
        // Call your API to create a new persistent from this data.
    * @return array
+
        // Or, do the following if you don't want capability checks (discouraged).
    */
+
        $persistent = new status(null, $data);
    protected static function define_related() {
+
         $persistent->create();
         return array(
+
    } else {
            'context' => 'context',
+
        // We had an ID, this means that we are going to update a record.
            'statuses' => 'some\\namespace\\status[]',
+
        // Call your API to update the persistent from the data.
         );
+
        // Or, do the following if you don't want capability checks (discouraged).
 +
        $persistent->from_record($data);
 +
         $persistent->update();
 
     }
 
     }
  
     /**
+
     // We are done, so let's redirect somewhere.
    * Get the additional values to inject while exporting.
+
     redirect(new moodle_url('/'));
    *
+
}
    * @param renderer_base $output The renderer.
 
    * @return array Keys are the property names, values are their values.
 
    */
 
     protected function get_other_values(renderer_base $output) {
 
        $statuses = [];
 
        foreach ($this->related['statuses'] as $status) {
 
            $exporter = new status_exporter($status);
 
            $statuses[] = $exporter->export($output);
 
        }
 
  
        $profileurl = new moodle_url('/user/profile.php', ['id' => $this->data->id]);
+
// Display the mandatory header and footer.
 
+
// And display the form, and its validation errors if there are any.
        return [
+
echo $OUTPUT->header();
            'profileurl' => $profileurl->out(false),
+
$form->display();
            'statuses' => $statuses
+
echo $OUTPUT->footer();
        ];
 
    }
 
}
 
 
</code>
 
</code>

Revision as of 07:47, 16 December 2016

Moodle 3.3 If you are creating persistent entries from forms, we've got something for you. You can use the class core\form\persistent as a base for your form instead of moodleform. Our persistent form class comes handy tools, such as automatic validation.

_Note: for more information about forms themselves,._

Defining your form

First of all, let

First, here is some code to create a form for the persistent we worked in the persistent documentation:


Linking to a persistent

In order for the form class to know what persistent we'll be dealing with, we must declare the protected static $persistentclass variable. The latter contains the fully qualified name of the persistent class.

/** @var string Persistent class name. */
protected static $persistentclass = 'example\\status';

Defining the form fields

Unfortunately this is not automatically done for us, so let's add our fields to the definition() method like you would do for any form.

/**
 * Define the form.
 */
public function definition() {
    $mform = $this->_form;
 
    // User ID.
    $mform->addElement('hidden', 'userid');
    $mform->setType('userid', PARAM_INT);
    $mform->setConstant('userid', $this->_customdata['userid']);
 
    // Message.
    $mform->addElement('editor', 'message', 'Message');
    $mform->setType('message', PARAM_RAW);
 
    // Location.
    $mform->addElement('text', 'location', 'Location');
    $mform->setType('location', PARAM_ALPHANUMEXT);
 
    $this->add_action_buttons();
}

All of this is pretty standard, except for the userid. When creating a new 'status', we do not want our users to be in control of this value. Therefore we define it as a hidden value which we lock (using setConstant) to the value we created our form with. All mandatory fields of the persistent need to be defined, but if they are not customised by the user, then they must be hidden and combined with setConstant.

Also note that the id property is not included. It is not required, nor recommended, to add it to your fields it will be handled automatically.

Using the form

When instantiating the form, there are two little things that you need to pay attention to.

Firstly you should always pass the URL of the current page, including the different query parameters it included. We need this to be able to display the form with its validation errors without affecting anything else.

Secondly, the persistent instance must be provided to the form through the custom data. That instance will be used to populate the form with initial data, typically when you are editing an object. When you don't have a persistent instance yet, probably because your user will be creating a new one, then simply pass null.

$customdata = [
    'persistent' => $persistent,  // An instance, or null.
    'userid' => $USER->id         // For the hidden userid field.
];
$form = new status_form($PAGE->url->out(false), $customdata);

Just like any other form, we will be using get_data() to validate the form. The only difference is that to determine whether we are editing an object, or creating a new one, we will check if the id value was returned to us. The peristent form will return the ID value from the persistent we gave it. Then it's up to you to decide how to apply the data, most likely you will defer the logic to another part of your code, one that ensures that all capability checks are fulfilled.

// Get the data. This ensures that the form was validated.
if (($data = $form->get_data())) {
 
    if (empty($data->id)) {
        // If we don't have an ID, we know that we must create a new record.
        // Call your API to create a new persistent from this data.
        // Or, do the following if you don't want capability checks (discouraged).
        $persistent = new status(null, $data);
        $persistent->create();
    } else {
        // We had an ID, this means that we are going to update a record.
        // Call your API to update the persistent from the data.
        // Or, do the following if you don't want capability checks (discouraged).
        $persistent->from_record($data);
        $persistent->update();
    }
 
    // We are done, so let's redirect somewhere.
    redirect(new moodle_url('/'));
}

Additional validation

There are times where the built-in validation of the persistent is not enough. Usually you would use the method validation(), but as the form persistent class does some extra stuff to make it easier for you, you must use the extra_validation() method. The latter works almost just like the validation() one.

/**
 * Extra validation.
 *
 * @param  stdClass $data Data to validate.
 * @param  array $files Array of files.
 * @param  array $errors Currently reported errors.
 * @return array of additional errors, or overridden errors.
 */
protected function extra_validation($data, $files, array &$errors) {
    $newerrors = array();
 
    if ($data->location === 'SFO') {
        $newerrors['location'] = 'San-Francisco Airport is not accepted from the form.';
    }
 
    return $newerrors;
}

The typical additional validation will return an array of errors, those will override any previous error. Rarely you will need to remove previously reported errors, hence the reference to $errors given, which you can modify directly. Do not abuse it though, this should only be used when you have no other choice.

Foreign fields

By default, the form class tries to be smart at detecting foreign fields such as the submit button. Failure to do so will cause trouble during validation or other else. So when your form becomes more complex, if it includes more submit buttons, or when it deals with other fields for example files, we must specify it.

Fields to ignore completely

The fields to remove are never validated and they are not returned when calling get_data(). By default the submit button is added to this list so that when we call get_data() we only get the persistent-related fields. To remove more fields, re-declare the protected static $fieldstoremove class variable.

/** @var array Fields to remove when getting the final data. */
protected static $fieldstoremove = array('submitbutton', 'areyouhappy');

Do not forget to add the submitbutton back in there.

Fields to validate

What about when we have a legit but that does not belong to the persistent? We still want to validate it ourselves, but we don't want it to be validated by the persistent as it will cause an error. In that case we define it in the protected static $foreignfields class variable.

/** @var array Fields to remove from the persistent validation. */
protected static $foreignfields = array('updatedelay');

Now the persistent will not validate this field, and we will get the updatedelay value when we call get_data(). Just don't forget to remove it before you feed the data to your persistent.

if (($data = $form->get_data())) {
    $updatedelay = $data->updatedelay;
    unset($data->updatedelay);
    $newpersistent = new status(0, $data);
}

This method is particularily useful when dealing with file managers.

Examples

Minimalist

class status_form extends \core\form\persistent {
 
    /** @var string Persistent class name. */
    protected static $persistentclass = 'example\\status';
 
    /**
     * Define the form.
     */
    public function definition() {
        $mform = $this->_form;
 
        // User ID.
        $mform->addElement('hidden', 'userid');
        $mform->setType('userid', PARAM_INT);
        $mform->setConstant('userid', $this->_customdata['userid']);
 
        // Message.
        $mform->addElement('editor', 'message', 'Message');
        $mform->setType('message', PARAM_RAW);
 
        // Location.
        $mform->addElement('text', 'location', 'Location');
        $mform->setType('location', PARAM_ALPHANUMEXT);
 
        $this->add_action_buttons();
    }
 
}

More advanced

class status_form extends \core\form\persistent {
 
    /** @var string Persistent class name. */
    protected static $persistentclass = 'example\\status';
 
    /** @var array Fields to remove when getting the final data. */
    protected static $fieldstoremove = array('submitbutton', 'areyouhappy');
 
    /** @var array Fields to remove from the persistent validation. */
    protected static $foreignfields = array('updatedelay');
 
    /**
     * Define the form.
     */
    public function definition() {
        $mform = $this->_form;
 
        // User ID.
        $mform->addElement('hidden', 'userid');
        $mform->setType('userid', PARAM_INT);
        $mform->setConstant('userid', $this->_customdata['userid']);
 
        // Message.
        $mform->addElement('editor', 'message', 'Message');
        $mform->setType('message', PARAM_RAW);
 
        // Location.
        $mform->addElement('text', 'location', 'Location');
        $mform->setType('location', PARAM_ALPHANUMEXT);
 
        // Status update delay.
        $mform->addElement('duration', 'updatedelay', 'Status update delay');
 
        // Are you happy?
        $mform->addElement('selectyesno', 'areyouhappy', 'Are you happy?');
 
        $this->add_action_buttons();
    }
 
    /**
     * Extra validation.
     *
     * @param  stdClass $data Data to validate.
     * @param  array $files Array of files.
     * @param  array $errors Currently reported errors.
     * @return array of additional errors, or overridden errors.
     */
    protected function extra_validation($data, $files, array &$errors) {
        $newerrors = array();
 
        if ($data->location === 'SFO') {
            $newerrors['location'] = 'San-Francisco Airport is not accepted from the form.';
        }
 
        return $newerrors;
    }
}

Using the form

Consider the following code to be a page you users will access at '/example.php'.

require 'config.php';
 
// Check if we go an ID.
$id = optional_param('id', null, PARAM_INT);
 
// Set the PAGE URL (and mandatory context). Note the ID being recorded, this is important.
$PAGE->set_context(context_system::instance());
$PAGE->set_url(new moodle_url('/example.php', ['id' => $id]));
 
// Instantiate a persistent object if we received an ID. Typically receiving an ID
// means that we are going to be updating an object rather than creating a new one.
$persistent = null;
if (!empty($id)) {
    $persistent = new status($id);
}
 
// Create the form instance. We need to use the current URL and the custom data.
$customdata = [
    'persistent' => $persistent,
    'userid' => $USER->id         // For the hidden userid field.
];
$form = new status_form($PAGE->url->out(false), $customdata);
 
// Get the data. This ensures that the form was validated.
if (($data = $form->get_data())) {
 
    if (empty($data->id)) {
        // If we don't have an ID, we know that we must create a new record.
        // Call your API to create a new persistent from this data.
        // Or, do the following if you don't want capability checks (discouraged).
        $persistent = new status(null, $data);
        $persistent->create();
    } else {
        // We had an ID, this means that we are going to update a record.
        // Call your API to update the persistent from the data.
        // Or, do the following if you don't want capability checks (discouraged).
        $persistent->from_record($data);
        $persistent->update();
    }
 
    // We are done, so let's redirect somewhere.
    redirect(new moodle_url('/'));
}
 
// Display the mandatory header and footer.
// And display the form, and its validation errors if there are any.
echo $OUTPUT->header();
$form->display();
echo $OUTPUT->footer();