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

Jump to: navigation, search
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
== Persistents ==
+
{{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.
  
Moodle persistents are equivalent to models (or active records). They represent an object stored in the database and provide the methods to create, read, update, and delete those objects. Additionally, the persistents validate its own data against automatic and custom validation rules.
+
''Note: for more information about forms themselves, [[:Category:Formslib|head this way]].''
  
Persistents extend the abstract class ''core\persistent''.
+
== Linking to a persistent ==
  
=== Properties ===
+
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.
 
 
Defining properties can be done by defining the method ''protected static define_properties()''. It returns an array where the keys are the names of the fields (and database columns), and their details. The ''type'' of each field must be specified using one of the ''PARAM_*'' constants. This type will be used to automatically validate the property's value.
 
  
 
<code php>
 
<code php>
/**
+
/** @var string Persistent class name. */
* Return the definition of the properties of this model.
+
protected static $persistentclass = 'example\\status';
*
 
* @return array
 
*/
 
protected static function define_properties() {
 
    return array(
 
        'userid' => array(
 
            'type' => PARAM_INT,
 
        ),
 
        'message' => array(
 
            'type' => PARAM_RAW,
 
        )
 
    );
 
}
 
 
</code>
 
</code>
  
==== Properties attributes ====
+
== Defining the form fields ==
  
;type
+
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.
: The only mandatory attribute. It must be one of the many ''PARAM_*'' constants.
 
;default
 
: The default value to attach to the property when it hasn't been provided. Alternatively this can be a ''Closure'' returning the default value.
 
;null
 
: Either of the constants ''NULL_ALLOWED'' or ''NULL_NOT_ALLOWED'' determining if the null value is accepted. This defaults to ''NULL_NOT_ALLOWED''.
 
;message
 
: The default error message (as a ''lang_string'' instance) to return when the validation of this field fails.
 
;choices
 
: An array of values which the property must fall in.
 
  
 
<code php>
 
<code php>
'messageformat' => array(
+
/**
    'type' => PARAM_INT,
+
* Define the form.
    'default' => FORMAT_PLAIN,
+
*/
    'choices' => array(FORMAT_PLAIN, FORMAT_HTML, FORMAT_MOODLE, FORMAT_MARKDOWN)
+
public function definition() {
),
+
     $mform = $this->_form;
'location' => array(
 
    'type' => PARAM_ALPHANUMEXT,
 
     'null' => NULL_ALLOWED,
 
    'message' => new lang_string('invaliddata', 'error'),
 
    'default' => function() {
 
        return get_config('default_location');
 
    },
 
),
 
</code>
 
  
Note that you should always use a ''Closure'' for the ''default'' value when you cannot guarantee that it will not change since the start of the request. The list of properties and their attributes is cached, and so failure to use a ''Closure'' can result in using an outdated default value.
+
    // User ID.
 +
    $mform->addElement('hidden', 'userid');
 +
    $mform->setType('userid', PARAM_INT);
 +
    $mform->setConstant('userid', $this->_customdata['userid']);
  
==== Mandatory properties ====
+
    // Message.
 +
    $mform->addElement('editor', 'message', 'Message');
 +
    $mform->setType('message', PARAM_RAW);
  
Four fields are always added to your persistent and should be reflected in your database table. You must not define those properties inThose are:
+
    // Location.
 +
    $mform->addElement('text', 'location', 'Location');
 +
    $mform->setType('location', PARAM_ALPHANUMEXT);
  
;id (non-null integer)
+
    $this->add_action_buttons();
: The primary key of the record.
+
}
;usermodified (non-null integer)
 
: The user who created/modified the object. It is automatically set.
 
;timecreated (non-null integer)
 
: The timestamp at which the record was modified. It is automatically set.
 
;timemodified (non-null integer)
 
: The timestamp at which the record was modified. It is automatically set, and defaults to 0.
 
 
 
=== Attaching to the database ===
 
 
 
While the persistent class is helpful for database interactions, it does not automatically fetch the properties from the database, nor does it create the tables. You will need to create the table yourself, as well as pointing the persistent to it. This can be done by defining the constant ''TABLE''.
 
 
 
<code php>
 
/** Table name for the persistent. */
 
const TABLE = 'status';
 
 
</code>
 
</code>
  
The table name must not contain the Moodle prefix. Also it is common practice to always refer to your table name using the constant.
+
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 the mandatory fields (without a default value) of the persistent need to be added to the form. If your users cannot change their values, then they must be hidden and locked with ''setConstant''.
 
 
=== Reading property values ===
 
 
 
This can be done using the method ''get()'', or the magic method ''get_'' followed by the property name. Alternatively you can also use ''to_record()'' which exports the whole object.
 
  
<code php>
+
Also note that the ''id'' property is not included. It is not required, nor recommended, to add it to your fields as it will be handled automatically.
// Create a new object.
 
$persistent = new status();
 
  
// Get the user ID using persistent::get().
+
== Using the form ==
$userid = $persistent->get('userid');
 
  
// Get the user ID using the magic getter.
+
When instantiating the form, there are two little things that you need to pay attention to.  
$userid = $persistent->get_userid();
 
  
// Get all the properties in an stdClass.
+
Firstly you should always pass the URL of the current page, including its query parameters. We need this to be able to display the form with its validation errors without affecting anything else.
$data = $persistent->to_record();
 
</code>
 
  
You may override the magic getters to implements your own custom logic. For instance you could convert the data in another format automatically as a convenience for developers. However, use this sparingly as it may lead to confusion: what you get is not what is stored in the database. Note that you cannot guarantee that your getter will be used, remember that it is possible for developers to call the alternative ''get()'' which will not use your custom logic.
+
Secondly, the persistent instance must be provided to the form through the custom data. That persistent 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.
 
 
It is, however, encouraged to add convenience methods such as the following:
 
  
 
<code php>
 
<code php>
/**
+
$customdata = [
  * Returns the user object of the author.
+
    'persistent' => $persistent, // An instance, or null.
*
+
    'userid' => $USER->id        // For the hidden userid field.
* @return stdClass
+
];
*/
+
$form = new status_form($PAGE->url->out(false), $customdata);
public function get_author() {
 
    return core_user::get_user($this->get('userid'));
 
}
 
 
</code>
 
</code>
  
=== Assigning values to properties ===
+
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 persistent 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.  
 
 
There are a few methods to do so.
 
 
 
You use an object (''stdClass'') to assign a bunch of properties at once. Use it with the constructor, or the method ''from_record()''.
 
  
 
<code php>
 
<code php>
$data = (object) array('userid' => 2, 'message' => 'Hello world!');
+
// Get the data. This ensures that the form was validated.
 +
if (($data = $form->get_data())) {
  
// Instantiates a new object with value for some properties.
+
    if (empty($data->id)) {
$persistent = new status(0, $data);
+
        // 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.
// Is similar to.
+
        // Or, do the following if you don't want capability checks (discouraged).
$persistent = new status();
+
        $persistent = new status(null, $data);
$persistent->from_record($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('/'));
 +
}
 
</code>
 
</code>
  
Or you can use the ''set()'' method on an instance.
+
== Additional validation ==
 
 
<code php>
 
// Instantiates a blank object.
 
$persistent = new status();
 
  
// Assign a new value to the 'message' property.
+
There are times when 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.
$persistent->set('message', Hello new world!');
 
</code>
 
 
 
Finally you can use the magic setters ''set_'' followed by the property name.
 
 
 
<code php>
 
// Instantiates a blank object.
 
$persistent = new status();
 
 
 
// Assign a new value to the 'message' property.
 
$persistent->set_message('Hello new world!');
 
</code>
 
 
 
==== Defining a custom setter ====
 
 
 
Though you don't have to for the code to work, you can define your own ''setter'' methods which will override the magic setters. They are useful if you want to extract the data out of a more complex object prior to assigning it. Though note that those setters will then have to use the ''set()'' method to assign the values.
 
  
 
<code php>
 
<code php>
 
/**
 
/**
  * Convenience method to set the user ID.
+
  * Extra validation.
 
  *
 
  *
  * @param object|int $idorobject The user ID, or a user object.
+
  * @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.
 
  */
 
  */
public function set_userid($idorobject) {
+
protected function extra_validation($data, $files, array &$errors) {
     $userid = $idorobject;
+
     $newerrors = array();
     if (is_object($idorobject)) {
+
 
         $userid = $idorobject->id;
+
     if ($data->location === 'SFO') {
 +
         $newerrors['location'] = 'San-Francisco Airport is not accepted from the form.';
 
     }
 
     }
     $this->set('userid', $userid);
+
 
 +
     return $newerrors;
 
}
 
}
 
</code>
 
</code>
  
In the above example we will accept an object or an ID, as a convenience for developers we will extract the ID value out of the object passed if any.
+
The typical additional validation will return an array of errors, those will override any previously defined errors. Sometimes, though 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.
  
Just like custom getters, you cannot guarantee that they will be used. Developers can directly call the ''set()'' method. Therefore you must not use a custom setter to reliably transform the data added to a property. For instance do not add a custom setter to remove HTML tags out of a text field, it may not always happen.
+
== Foreign fields ==
  
You can obviously create your own setters which aren't based on any properties just as a convenience. For instance we could have created ''set_userid_from_user(object $user)'' which is more verbose and more predictable
+
By default, the form class tries to be smart at detecting foreign fields such as the submit button. Failure to do so will cause troubles during validation, or when getting the data. So when your form becomes more complex, if it includes more submit buttons, or when it deals with other fields, for example file managers, we must indicate it.
  
=== Read, save and delete entries ===
+
=== Fields to ignore completely ===
  
The methods to ''create'', ''read'', ''update'' and ''delete'' are eponymous. Your object will be validated before you ''create'' or ''update'' it. The ''update'', ''delete'' and ''read'' methods require your object to contain its ID. And you also won't be allowed to ''create'' an entry which already had an ID defined.
+
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.
 
 
Here are some code examples:
 
  
 
<code php>
 
<code php>
// Fetches an object from database based on its ID.
+
/** @var array Fields to remove when getting the final data. */
$id = 123;
+
protected static $fieldstoremove = array('submitbutton', 'areyouhappy');
$persistent = new status($id);
+
</code>
  
// Create previously instantiated object in the database.
+
Do not forget to add the ''submitbutton'' back in there.
$persistent->create();
 
  
// Load an object from the database, and update it.
+
=== Fields to validate ===
$id = 123;
 
$persistent = new status($id);
 
$persistent->set_message('Hello new world!');
 
$persistent->update();
 
  
// Reset the instance to the values in the database.
+
What about when we have a ''legit'' field but it 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.
$persistent->read();
 
  
// Permanently delete the object from the database.
+
<code php>
$persistent->delete();
+
/** @var array Fields to remove from the persistent validation. */
 +
protected static $foreignfields = array('updatedelay');
 
</code>
 
</code>
  
=== Fetching records ===
+
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.
 
 
Once you start using persistents you should never directly interact with the database outside of your class. The persistent class comes with a few handy methods allowing you to retrieve your objects.
 
  
 
<code php>
 
<code php>
// Use the constructor to fetch one object from its ID.
+
if (($data = $form->get_data())) {
$persistent = new status($id);
+
    $updatedelay = $data->updatedelay;
 
+
    unset($data->updatedelay);
// Get one record from a set of conditions.
+
    $newpersistent = new status(0, $data);
$persistent = status::get_record(['userid' => $userid, 'message' => 'Hello world!']);
+
}
 
 
// Get multiple records from a set of conditions.
 
$persistents = status::get_records(['userid' => $userid]);
 
 
 
// Count the records.
 
$count = status::count_records(['userid' => $userid]);
 
 
 
// Check whether a record exists.
 
$exists = status::record_exists($id);
 
 
</code>
 
</code>
  
Make sure to also check their additional parameters and their variants (''record_exists_select()'', ''count_records_select'', ''get_records_select'', ...).
+
This method is particularily useful when dealing with file managers.
  
==== Custom fetching ====
+
== Examples ==
  
It's always a good idea to add more complex queries directly within your persistent. By convention you should always return an instance of your persistent and never an stdClass. Here we add a custom method which allows to directly fetch all records by username.
+
=== Minimalist ===
  
 
<code php>
 
<code php>
/**
+
class status_form extends \core\form\persistent {
* Get all records by a user from its username
+
   
*
+
    /** @var string Persistent class name. */
* @param string $username The username.
+
    protected static $persistentclass = 'example\\status';
* @return status[]
 
*/
 
public static function get_records_by_username($username) {
 
    global $DB;
 
  
     $sql = 'SELECT s.*
+
     /**
              FROM {' . static::TABLE . '} s
+
    * Define the form.
              JOIN {user} u
+
    */
                ON u.id = s.userid
+
    public function definition() {
            WHERE u.username = :username';
+
        $mform = $this->_form;
  
    $persistents = [];
+
        // User ID.
 +
        $mform->addElement('hidden', 'userid');
 +
        $mform->setType('userid', PARAM_INT);
 +
        $mform->setConstant('userid', $this->_customdata['userid']);
  
    $recordset = $DB->get_recordset_sql($sql, ['username' => $username]);
+
        // Message.
    foreach ($recordset as $record) {
+
        $mform->addElement('editor', 'message', 'Message');
         $persistents[] = new static(0, $record);
+
         $mform->setType('message', PARAM_RAW);
    }
 
    $recordset->close();
 
  
    return $persistents;
+
        // Location.
}
+
        $mform->addElement('text', 'location', 'Location');
</code>
+
        $mform->setType('location', PARAM_ALPHANUMEXT);
  
If you need to join persistents together and be able to extract their respective properties in a single database query, you should have a look at the following methods:
+
        $this->add_action_buttons();
 
 
;get_sql_fields(string $alias, string $prefix = null)
 
: Returns the SQL statement to include in the SELECT clause to prefix columns.
 
;extract_record(stdClass $row, string $prefix = null)
 
: Extracts all the properties from a row based on the given prefix.
 
 
 
<code php>
 
// Minimalist example.
 
$sqlfields = status::get_sql_fields('s', 'statprops');
 
$sql = "SELECT $sqlfields, u.username
 
          FROM {" . status::TABLE . "} s
 
          JOIN {user} ON s.userid = u.id
 
        WHERE s.id = 1";
 
$row = $DB->get_record($sql, []);
 
$statusdata = status::extract_record($row, 'statprops');
 
$persistent = new status(0, $statusdata);
 
</code>
 
 
 
=== Validating ===
 
 
 
Basic validation of the properties values happens automatically based on their type (''PARAM_*'' constant), however this is not always enough. In order to implement your own custom validation, simply define a ''protected'' method starting with ''validate_'' followed with the property name. This method will be called whenever the model needs to be validated and will receive the data to validate.
 
 
 
A validation method must always return either ''true'' or an instance of ''lang_string'' which contains the error message to send to the user.
 
 
 
<code php>
 
/**
 
* Validate the user ID.
 
*
 
* @param int $value The value.
 
* @return true|lang_string
 
*/
 
protected function validate_userid($value) {
 
    if (!core_user::is_real_user($value, true)) {
 
        return new lang_string('invaliduserid', 'error');
 
 
     }
 
     }
  
    return true;
 
 
}
 
}
 
</code>
 
</code>
  
The above example ensures that the ''userid'' property contains a valid user ID.
+
=== More advanced ===
 
 
Note that the basic validation is always performed first, and thus your custom validation method will not be called when the value did not pass the basic validation.
 
 
 
==== Validation results ====
 
 
 
The validation of the object automatically happens upon ''create'' and ''update''. If the validation did not pass, an ''invalid_persisten_exception'' will be raised. You can validate the object prior to saving the object and get the validation results if you need to.
 
  
 
<code php>
 
<code php>
// We can catch the invalid_persistent_exception.
+
class status_form extends \core\form\persistent {
try {
 
    $persistent = new status();
 
    $persistent->create();
 
} catch (invalid_persistent_exception $e) {
 
    // Whoops, something wrong happened.
 
}
 
 
 
// Check whether the object is valid.
 
$persistent->is_valid();        // True or false.
 
  
// Get the validation errors.
+
    /** @var string Persistent class name. */
$persistent->get_errors();     // Array where keys are properties and values are errors.
+
    protected static $persistentclass = 'example\\status';
  
// Validate the object.
+
    /** @var array Fields to remove when getting the final data. */
$persistent->validate();       // Returns true, or an array of errors.
+
    protected static $fieldstoremove = array('submitbutton', 'areyouhappy');
</code>
 
  
=== Hooks ===
+
    /** @var array Fields to remove from the persistent validation. */
 +
    protected static $foreignfields = array('updatedelay');
  
You can define the following methods to be notified prior to, or after, something happened:
+
    /**
 
+
    * Define the form.
;protected before_validate()
+
    */
: Do something before the object is validated.
+
    public function definition() {
;protected before_create()
+
        $mform = $this->_form;
: Do something before the object is inserted in the database. Note that values assigned to properties are not longer validated at this point.
 
;protected after_create()
 
: Do something right after the object was added to the database.
 
;protected before_update()
 
: Do something before the object is updated in the database. Note that values assigned to properties are not longer validated at this point.
 
;protected after_update(bool $result)
 
: Do something right after the object was updated in the database.
 
;protected before_delete()
 
: Do something right before the object is deleted from the database.
 
;protected after_delete(bool $result)
 
: Do something right after the object was deleted from the database.
 
  
=== Full example ===
+
        // User ID.
 +
        $mform->addElement('hidden', 'userid');
 +
        $mform->setType('userid', PARAM_INT);
 +
        $mform->setConstant('userid', $this->_customdata['userid']);
  
<code php>
+
        // Message.
namespace example;
+
        $mform->addElement('editor', 'message', 'Message');
 +
        $mform->setType('message', PARAM_RAW);
  
use core\persistent;
+
        // Location.
use core_user;
+
        $mform->addElement('text', 'location', 'Location');
use lang_string;
+
        $mform->setType('location', PARAM_ALPHANUMEXT);
  
class status extends persistent {
+
        // Status update delay.
 +
        $mform->addElement('duration', 'updatedelay', 'Status update delay');
  
    /** Table name for the persistent. */
+
        // Are you happy?
    const TABLE = 'status';
+
        $mform->addElement('selectyesno', 'areyouhappy', 'Are you happy?');
  
    /**
+
        $this->add_action_buttons();
    * Return the definition of the properties of this model.
 
    *
 
    * @return array
 
    */
 
    protected static function define_properties() {
 
        return array(
 
            'userid' => array(
 
                'type' => PARAM_INT,
 
            ),
 
            'message' => array(
 
                'type' => PARAM_RAW,
 
            ),
 
            'messageformat' => array(
 
                'type' => PARAM_INT,
 
                'default' => FORMAT_PLAIN,
 
                'choices' => array(FORMAT_PLAIN, FORMAT_HTML, FORMAT_MOODLE, FORMAT_MARKDOWN)
 
            ),
 
            'location' => array(
 
                'type' => PARAM_ALPHANUMEXT,
 
                'null' => NULL_ALLOWED,
 
                'message' => new lang_string('invaliddata', 'error'),
 
                'default' => function() {
 
                    return get_config('default_location');
 
                },
 
            ),
 
        );
 
 
     }
 
     }
  
 
     /**
 
     /**
     * Returns the user object of the author.
+
     * Extra validation.
 
     *
 
     *
     * @return stdClass
+
    * @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.
 
     */
 
     */
     public function get_author() {
+
     protected function extra_validation($data, $files, array &$errors) {
         return core_user::get_user($this->get('userid'));
+
         $newerrors = array();
    }
 
  
    /**
+
         if ($data->location === 'SFO') {
    * Convenience method to set the user ID.
+
             $newerrors['location'] = 'San-Francisco Airport is not accepted from the form.';
    *
 
    * @param object|int $idorobject The user ID, or a user object.
 
    */
 
    public function set_userid($idorobject) {
 
        $userid = $idorobject;
 
         if (is_object($idorobject)) {
 
             $userid = $idorobject->id;
 
 
         }
 
         }
         $this->set('userid', $userid);
+
 
 +
         return $newerrors;
 
     }
 
     }
 +
}
 +
</code>
  
    /**
+
=== Using the form ===
    * Validate the user ID.
+
 
    *
+
Consider the following code to be a page you users will access at '/example.php'.
    * @param int $value The value.
+
 
    * @return true|lang_string
+
<code php>
    */
+
require 'config.php';
    protected function validate_userid($value) {
 
        if (!core_user::is_real_user($value, true)) {
 
            return new lang_string('invaliduserid', 'error');
 
        }
 
  
        return true;
+
// 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.
    * Get all records by a user from its username
+
$PAGE->set_context(context_system::instance());
    *
+
$PAGE->set_url(new moodle_url('/example.php', ['id' => $id]));
    * @param string $username The username.
 
    * @return status[]
 
    */
 
    public static function get_records_by_username($username) {
 
        global $DB;
 
  
        $sql = 'SELECT s.*
+
// Instantiate a persistent object if we received an ID. Typically receiving an ID
                  FROM {' . static::TABLE . '} s
+
// means that we are going to be updating an object rather than creating a new one.
                  JOIN {user} u
+
$persistent = null;
                    ON u.id = s.userid
+
if (!empty($id)) {
                WHERE u.username = :username';
+
    $persistent = new status($id);
 +
}
  
        $persistents = [];
+
// 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);
  
        $recordset = $DB->get_recordset_sql($sql, ['username' => $username]);
+
// Get the data. This ensures that the form was validated.
        foreach ($recordset as $record) {
+
if (($data = $form->get_data())) {
            $persistents[] = new static(0, $record);
 
        }
 
        $recordset->close();
 
  
         return $persistents;
+
    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();
 
</code>
 
</code>

Latest revision as of 07:58, 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, head this way.

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 the mandatory fields (without a default value) of the persistent need to be added to the form. If your users cannot change their values, then they must be hidden and locked with setConstant.

Also note that the id property is not included. It is not required, nor recommended, to add it to your fields as 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 its query parameters. 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 persistent 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 persistent 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 when 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 previously defined errors. Sometimes, though 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 troubles during validation, or when getting the data. So when your form becomes more complex, if it includes more submit buttons, or when it deals with other fields, for example file managers, we must indicate 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 field but it 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();