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

Jump to: navigation, search
Line 1: Line 1:
== Persistents ==
+
{{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 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.
+
== Introduction ==
  
Persistents extend the abstract class ''core\persistent''.
+
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.
  
=== Properties ===
+
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.
  
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.
+
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 inherit this added property.
 +
 
 +
== Defining properties ==
 +
 
 +
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>
 
<code php>
 
/**
 
/**
  * Return the definition of the properties of this model.
+
  * Return the list of properties.
 
  *
 
  *
 
  * @return array
 
  * @return array
Line 17: Line 21:
 
protected static function define_properties() {
 
protected static function define_properties() {
 
     return array(
 
     return array(
         'userid' => array(
+
         'id' => array(
             'type' => PARAM_INT,
+
             'type' => PARAM_INT
 +
        ),
 +
        'username' => array(
 +
            'type' => PARAM_ALPHANUMEXT
 
         ),
 
         ),
        'message' => array(
 
            'type' => PARAM_RAW,
 
        )
 
 
     );
 
     );
 
}
 
}
 
</code>
 
</code>
  
==== Properties attributes ====
+
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 with ''PARAM_*'' constants only.
 
 
;type
 
: 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>
 
'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');
 
    },
 
),
 
</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.
 
  
==== Mandatory properties ====
+
== Using an exporter ==
  
Four fields are always added to your persistent and should be reflected in your database table. You must not define those properties in ''define_properties()'':
+
Once we've got a [[#Minimalist|minimalist exporter]] set-up, here is how to use it.
  
;id (non-null integer)
+
=== Exporting the data ===
: 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.
 
  
=== Linking to a database table ===
+
<code php>
 +
$data = (object) ['id' => 123, 'username' => 'batman'];
  
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''.
+
// The only time we can give data to our exporter is when instantiating it.
 +
$ue = new user_exporter($data);
  
<code php>
+
// To export, we must pass the reference to a renderer.
/** Table name for the persistent. */
+
$data = $ue->export($OUTPUT);
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.
+
If we print the content of ''$data'', we will obtain this:
 
 
=== 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>
 
// Create a new object.
 
$persistent = new status();
 
 
 
// Get the user ID using persistent::get().
 
$userid = $persistent->get('userid');
 
 
 
// Get the user ID using the magic getter.
 
$userid = $persistent->get_userid();
 
  
// Get all the properties in an stdClass.
+
<code>
$data = $persistent->to_record();
+
stdClass Object
 +
(
 +
    [id] => 123
 +
    [username] => batman
 +
)
 
</code>
 
</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.
+
=== In external functions ===
  
It is, however, encouraged to add convenience methods such as the following:
+
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:
  
 
<code php>
 
<code php>
/**
+
public static function get_users_returns() {
* Returns the user object of the author.
+
     return external_multiple_structure(
*
+
        user_exporter::get_read_structure()
* @return stdClass
+
    );
*/
 
public function get_author() {
 
     return core_user::get_user($this->get('userid'));
 
 
}
 
}
 
</code>
 
</code>
  
=== Assigning values to properties ===
+
Now that this is done, we must use our exporter as shown above to export our users' data.
 
 
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!');
+
public static function get_users() {
 
+
    global $DB, $PAGE;
// Instantiates a new object with value for some properties.
+
    $output = $output = $PAGE->get_renderer('core');
$persistent = new status(0, $data);
+
    $users = $DB->get_records('user', null, '', 'id, username', 0, 10); // Get 10 users.
 
+
    $result = [];
// Is similar to.
+
    foreach ($users as $userdata) {
$persistent = new status();
+
        $exporter = new user_exporter($userdata);
$persistent->from_record($data);
+
        $result[] = $exporter->export($output);
 
+
    }
 +
    return $result;
 +
}
 
</code>
 
</code>
  
Or you can use the ''set()'' method on an instance.
+
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.
  
 
<code php>
 
<code php>
// Instantiates a blank object.
+
public static function create_user_parameters() {
$persistent = new status();
+
    return new external_function_parameters([
 
+
        'user' => user_exporter::get_create_structure()
// Assign a new value to the 'message' property.
+
    ]);
$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>
+
public static function create_user($user) {
/**
+
    // Mandatory parameter validation.
* Convenience method to set the user ID.
+
     $params = self::validate_parameters(self::create_user_parameters(), ['user' => $user]);
*
+
     $user = $params['user'];
* @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);
 
 
}
 
}
 
</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.
+
Important note: when used in the ''parameters'', the exporter's structure must always be included in an argument, above we used ''user''. Else this would not flexible, and may not generate a valid structure for some protocols.
  
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.
+
== Abiding to text formatting rules ==
  
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
+
If we had to pass the ''$OUTPUT'' during export as seen above, 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.
  
=== Read, save and delete entries ===
+
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 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.
 
 
 
Here are some code examples:
 
  
 
<code php>
 
<code php>
// Fetches an object from database based on its ID.
+
'description' => array(
$id = 123;
+
    'type' => PARAM_RAW,
$persistent = new status($id);
+
),
 
+
'descriptionformat' => array(
// Create previously instantiated object in the database.
+
    'type' => PARAM_INT,
$persistent->create();
+
),
 
 
// Load an object from the database, and update it.
 
$id = 123;
 
$persistent = new status($id);
 
$persistent->set_message('Hello new world!');
 
$persistent->update();
 
 
 
// Reset the instance to the values in the database.
 
$persistent->read();
 
 
 
// Permanently delete the object from the database.
 
$persistent->delete();
 
 
</code>
 
</code>
  
=== Fetching records ===
+
With the two above properties (not ''other properties'') added, let's see what happens when the user wrote their description on Markdown and we export it.
 
 
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.
+
$data = (object) [
$persistent = new status($id);
+
    'id' => 123,
 
+
    'username' => 'batman',
// Get one record from a set of conditions.
+
    'description' => 'Hello __world__!',
$persistent = status::get_record(['userid' => $userid, 'message' => 'Hello world!']);
+
    'descriptionformat' => FORMAT_MARKDOWN
 
+
];
// Get multiple records from a set of conditions.
+
$ue = new user_exporter($data);
$persistents = status::get_records(['userid' => $userid]);
+
$data = $ue->export($OUTPUT);
 
 
// 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'', ...).
+
Unsurprisingly, this is what comes out of it:
  
==== Custom fetching ====
+
<code>
 
+
stdClass Object
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.
+
(
 
+
     [id] => 123
<code php>
+
    [username] => batman
/**
+
     [description] => <p>Hello <strong>world</strong>!</p>
* Get all records by a user from its username
+
     [descriptionformat] => 1
*
+
)
* @param string $username The username.
 
* @return status[]
 
*/
 
public static function get_records_by_username($username) {
 
     global $DB;
 
 
 
    $sql = 'SELECT s.*
 
              FROM {' . static::TABLE . '} s
 
              JOIN {user} u
 
                ON u.id = s.userid
 
            WHERE u.username = :username';
 
 
 
     $persistents = [];
 
 
 
    $recordset = $DB->get_recordset_sql($sql, ['username' => $username]);
 
     foreach ($recordset as $record) {
 
        $persistents[] = new static(0, $record);
 
    }
 
    $recordset->close();
 
 
 
    return $persistents;
 
}
 
 
</code>
 
</code>
  
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:
+
Psst... If you're wondering where we get the ''context'' from, look at [[#Related_objects|related objects]].
 
 
;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 ===
+
== Additional properties ==
  
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.
+
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.
  
A validation method must always return either ''true'' or an instance of ''lang_string'' which contains the error message to send to the user.
+
_Other_ properties are only included in the _read_ structure of an object as they are dynamically generated. They are not required, nor needed, to _create_ or _update_ an object.
  
 
<code php>
 
<code php>
 
/**
 
/**
  * Validate the user ID.
+
  * Return the list of additional properties.
*
+
 
* @param int $value The value.
+
  * @return array
  * @return true|lang_string
 
 
  */
 
  */
protected function validate_userid($value) {
+
protected static function define_other_properties() {
     if (!core_user::is_real_user($value, true)) {
+
     return array(
         return new lang_string('invaliduserid', 'error');
+
        'profileurl' => array(
    }
+
            'type' => PARAM_URL
 
+
        ),
     return true;
+
         'statuses' => array(
 +
            'type' => status_exporter::read_properties_definition(),
 +
            'multiple' => true,
 +
            'optional' => true
 +
        ),
 +
     );
 
}
 
}
 
</code>
 
</code>
  
The above example ensures that the ''userid'' property contains a valid user ID.
+
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.
  
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.
+
=== Property attributes ===
  
==== Validation results ====
+
Each property is configured using the following attributes:
  
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.
+
;type
 +
: The only mandatory attribute. It must either be one of the many PARAM_* constants, or an array of properties.
 +
;default
 +
: The default value when the value was not provided. When not specified, a value is required.
 +
;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.
  
<code php>
+
== Related objects ==
// We can catch the invalid_persistent_exception.
 
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.
 
$persistent->get_errors();      // Array where keys are properties and values are errors.
 
 
// Validate the object.
 
$persistent->validate();        // Returns true, or an array of errors.
 
</code>
 
  
=== Hooks ===
+
== Common misconception ==
  
You can define the following methods to be notified prior to, or after, something happened:
+
# Exporters do not validate your data. They use the properties' attributes to generate the structure required by the [[External functions API]], the latter is responsive for using the structure to validate the data being exported.
  
;protected before_validate()
+
== Examples ==
: Do something before the object is validated.
 
;protected before_create()
 
: 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 ===
+
=== Minimalist ===
  
 
<code php>
 
<code php>
namespace example;
+
class user_exporter extends core\external\exporter {
 
 
use core\persistent;
 
use core_user;
 
use lang_string;
 
 
 
class status extends persistent {
 
 
 
    /** Table name for the persistent. */
 
    const TABLE = 'status';
 
  
 
     /**
 
     /**
     * Return the definition of the properties of this model.
+
     * Return the list of properties.
 
     *
 
     *
 
     * @return array
 
     * @return array
Line 368: Line 213:
 
     protected static function define_properties() {
 
     protected static function define_properties() {
 
         return array(
 
         return array(
             'userid' => array(
+
             'id' => array(
                 'type' => PARAM_INT,
+
                 '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(
+
             'username' => array(
                 'type' => PARAM_ALPHANUMEXT,
+
                 '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.
 
    *
 
    * @return stdClass
 
    */
 
    public function get_author() {
 
        return core_user::get_user($this->get('userid'));
 
    }
 
 
    /**
 
    * Convenience method to set the user ID.
 
    *
 
    * @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);
 
    }
 
 
    /**
 
    * 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;
 
    }
 
 
    /**
 
    * Get all records by a user from its username
 
    *
 
    * @param string $username The username.
 
    * @return status[]
 
    */
 
    public static function get_records_by_username($username) {
 
        global $DB;
 
 
        $sql = 'SELECT s.*
 
                  FROM {' . static::TABLE . '} s
 
                  JOIN {user} u
 
                    ON u.id = s.userid
 
                WHERE u.username = :username';
 
 
        $persistents = [];
 
 
        $recordset = $DB->get_recordset_sql($sql, ['username' => $username]);
 
        foreach ($recordset as $record) {
 
            $persistents[] = new static(0, $record);
 
        }
 
        $recordset->close();
 
 
        return $persistents;
 
 
     }
 
     }
  
 
}
 
}
 
 
</code>
 
</code>

Revision as of 08:15, 14 December 2016

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.

Introduction

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.

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 inherit this added property.

Defining properties

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.

/**
 * Return the list of properties.
 *
 * @return array
 */
protected static function define_properties() {
    return array(
        'id' => array(
            'type' => PARAM_INT
        ),
        'username' => array(
            'type' => PARAM_ALPHANUMEXT
        ),
    );
}

Although this is not a rule, it is recommended that the standard properties (by opposition to additional properties) only use the type attribute, and with PARAM_* constants only.

Using an exporter

Once we've got a minimalist exporter set-up, here is how to use it.

Exporting the data

$data = (object) ['id' => 123, 'username' => 'batman'];
 
// The only time we can give data to our exporter is when instantiating it.
$ue = new user_exporter($data);
 
// To export, we must pass the reference to a renderer.
$data = $ue->export($OUTPUT);

If we print the content of $data, we will obtain this:

stdClass Object
(
    [id] => 123
    [username] => batman
)

In external functions

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:

public static function get_users_returns() {
    return external_multiple_structure(
        user_exporter::get_read_structure()
    );
}

Now that this is done, we must use our exporter as shown above to export our users' data.

public static function get_users() {
    global $DB, $PAGE;
    $output = $output = $PAGE->get_renderer('core');
    $users = $DB->get_records('user', null, '', 'id, username', 0, 10); // Get 10 users.
    $result = [];
    foreach ($users as $userdata) {
        $exporter = new user_exporter($userdata);
        $result[] = $exporter->export($output);
    }
    return $result;
}

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.

public static function create_user_parameters() {
    return new external_function_parameters([
        'user' => user_exporter::get_create_structure()
    ]);
}
 
public static function create_user($user) {
    // Mandatory parameter validation.
    $params = self::validate_parameters(self::create_user_parameters(), ['user' => $user]);
    $user = $params['user'];
    ...
}

Important note: when used in the parameters, the exporter's structure must always be included in an argument, above we used user. Else this would not flexible, and may not generate a valid structure for some protocols.

Abiding to text formatting rules

If we had to pass the $OUTPUT during export as seen above, 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 on the content typically submitted by users, but also to convert it from a few given formats to HTML.

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().

'description' => array(
    'type' => PARAM_RAW,
),
'descriptionformat' => array(
    'type' => PARAM_INT,
),

With the two above properties (not other properties) added, let's see what happens when the user wrote their description on Markdown and we export it.

$data = (object) [
    'id' => 123,
    'username' => 'batman',
    'description' => 'Hello __world__!',
    'descriptionformat' => FORMAT_MARKDOWN
];
$ue = new user_exporter($data);
$data = $ue->export($OUTPUT);

Unsurprisingly, this is what comes out of it:

stdClass Object
(
    [id] => 123
    [username] => batman
    [description] => <p>Hello <strong>world</strong>!</p>
    [descriptionformat] => 1
)

Psst... If you're wondering where we get the context from, look at related objects.

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, 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 as they are dynamically generated. They are not required, nor needed, to _create_ or _update_ an object.

/**
 * Return the list of additional properties.
 
 * @return array
 */
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
        ),
    );
}

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.

Property attributes

Each property is configured using the following attributes:

type
The only mandatory attribute. It must either be one of the many PARAM_* constants, or an array of properties.
default
The default value when the value was not provided. When not specified, a value is required.
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.

Related objects

Common misconception

  1. Exporters do not validate your data. They use the properties' attributes to generate the structure required by the External functions API, the latter is responsive for using the structure to validate the data being exported.

Examples

Minimalist

class user_exporter extends core\external\exporter {
 
    /**
     * Return the list of properties.
     *
     * @return array
     */
    protected static function define_properties() {
        return array(
            'id' => array(
                'type' => PARAM_INT
            ),
            'username' => array(
                'type' => PARAM_ALPHANUMEXT
            ),
        );
    }
 
}