User:Frédéric Massart

Revision as of 08:15, 14 December 2016 by Frédéric Massart (talk | contribs)

Jump to: navigation, search

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
            ),
        );
    }
 
}