Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Exporter: Difference between revisions

From MoodleDocs
m (Text replacement - "<code php>" to "<syntaxhighlight lang="php">")
 
(22 intermediate revisions by 7 users not shown)
Line 8: Line 8:


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.
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.
Exporters are expected to be subclasses of the <tt>core\external\exporter</tt> base class:
<syntaxhighlight lang="php">
/**
* User data exporter.
*/
class user_exporter extends core\external\exporter {
}
</syntaxhighlight>
If you are creating an exporter for a persistent class see the [[#Exporters and persistent|Exporters and persistent]] section below!


== Defining properties ==
== Defining properties ==
Line 13: Line 26:
The method ''define_properties()'' returns a list of the properties expected when instantiating your exporter, and the ones it will export at the same time.
The method ''define_properties()'' returns a list of the properties expected when instantiating your exporter, and the ones it will export at the same time.


<code php>
<syntaxhighlight lang="php">
/**
/**
  * Return the list of properties.
  * Return the list of properties.
Line 20: Line 33:
  */
  */
protected static function define_properties() {
protected static function define_properties() {
     return array(
     return [
         'id' => array(
         'id' => [
             'type' => PARAM_INT
             'type' => PARAM_INT
         ),
         ],
         'username' => array(
         'username' => [
             'type' => PARAM_ALPHANUMEXT
             'type' => PARAM_ALPHANUMEXT
         ),
         ],
     );
     ];
}
}
</code>
</syntaxhighlight>


These properties will allow us to generate a ''create'' or ''update'' structure for external functions, [[#In_external_functions|more on this later]]. Oh, and if you are using persistent you do not need to do this: [[#Exporters_and_persistent|check this out]].
These properties will allow us to generate a ''create'' or ''update'' structure for external functions, [[#In_external_functions|more on this later]]. Oh, and if you are using persistent you do not need to do this: [[#Exporters_and_persistent|check this out]].
Line 56: Line 69:
=== Exporting the data ===
=== Exporting the data ===


<code php>
<syntaxhighlight lang="php">
$data = (object) ['id' => 123, 'username' => 'batman'];
$data = (object) ['id' => 123, 'username' => 'batman'];


Line 64: Line 77:
// To export, we must pass the reference to a renderer.
// To export, we must pass the reference to a renderer.
$data = $ue->export($OUTPUT);
$data = $ue->export($OUTPUT);
</code>
</syntaxhighlight>


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


<code>
<syntaxhighlight lang="php">
stdClass Object
stdClass Object
(
(
Line 74: Line 87:
     [username] => batman
     [username] => batman
)
)
</code>
</syntaxhighlight>


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]].
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]].
Line 82: Line 95:
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:
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>
<syntaxhighlight lang="php">
public static function get_users_returns() {
public static function get_users_returns() {
     return external_multiple_structure(
     return external_multiple_structure(
Line 88: Line 101:
     );
     );
}
}
</code>
</syntaxhighlight>


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


<code php>
<syntaxhighlight lang="php">
public static function get_users() {
public static function get_users() {
     global $DB, $PAGE;
     global $DB, $PAGE;
Line 104: Line 117:
     return $result;
     return $result;
}
}
</code>
</syntaxhighlight>


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 field 'username' to be passed in the key 'user'. The ''create'' and ''update'' structures include all of the ''standard'' properties, none of the ''[[#Additional_properties|other]]'' ones. Note that the ''create'' structure does not include the ''id'' property. Use '' user_exporter::get_update_structure()'' if to update a user and thus receive the ID.
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 field 'username' to be passed in the key 'user'. The ''create'' and ''update'' structures include all of the ''standard'' properties, none of the ''[[#Additional_properties|other]]'' ones. Note that the ''create'' structure does not include the ''id'' property. Use '' user_exporter::get_update_structure()'' if to update a user and thus receive the ID.


<code php>
<syntaxhighlight lang="php">
public static function create_user_parameters() {
public static function create_user_parameters() {
     return new external_function_parameters([
     return new external_function_parameters([
Line 121: Line 134:
     ...
     ...
}
}
</code>
</syntaxhighlight>


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 be flexible, and may not generate a valid structure for some webservice protocols.
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 be flexible, and may not generate a valid structure for some webservice protocols.
Line 129: Line 142:
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.
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.


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()''.
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()''. If you are not familiar with the rules for outputting text in Moodle see [[Output functions]] and [[lib/formslib.php_Form_Definition#Most_Commonly_Used_PARAM_.2A_Types| Most Commonly Used PARAM_* Types]]


<code php>
<syntaxhighlight lang="php">
'description' => array(
'description' => [
     'type' => PARAM_RAW,
     'type' => PARAM_RAW,
),
],
'descriptionformat' => array(
'descriptionformat' => [
     'type' => PARAM_INT,
     'type' => PARAM_INT,
),
],
</code>
</syntaxhighlight>


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.
With the two above properties added, let's see what happens when the user's description is in the Markdown format and we export it.


<code php>
<syntaxhighlight lang="php">
$data = (object) [
$data = (object) [
     'id' => 123,
     'id' => 123,
Line 149: Line 162:
     'descriptionformat' => FORMAT_MARKDOWN
     'descriptionformat' => FORMAT_MARKDOWN
];
];
$ue = new user_exporter($data);
$ue = new user_exporter($data, ['context' => context_user::instance(123)]);
$data = $ue->export($OUTPUT);
$data = $ue->export($OUTPUT);
</code>
</syntaxhighlight>


Unsurprisingly, this is what comes out of it:
Unsurprisingly, this is what comes out of it:


<code>
<syntaxhighlight lang="php">
stdClass Object
stdClass Object
(
(
Line 163: Line 176:
     [descriptionformat] => 1  // Corresponds to FORMAT_HTML.
     [descriptionformat] => 1  // Corresponds to FORMAT_HTML.
)
)
</code>
</syntaxhighlight>
 
Psst... we've cheating a bit above. Did you notice that we've passed a context to the exporter? We passed a [[#Related_objects|related object]], more on them later.
 
=== Formatting parameters ===
 
Formatting requires a context. If you've defined a [[#Related_objects|related object]] with the name ''context'' we will automatically use this one. But there are cases where you need multiple contexts, or you need to rewrite the pluginfile URLs. When that is the case, you will define a method called 'get_format_parameters_for_' followed with the name of the property. The latter also accepts the various options which both ''format_text'' and ''format_string'' support.
 
<syntaxhighlight lang="php">
/**
* Get the formatting parameters for the description.
*
* @return array
*/
protected function get_format_parameters_for_description() {
    return [
        'component' => 'core_user',
        'filearea' => 'description',
        'itemid' => $this->data->id
    ];
}
</syntaxhighlight>
 
For a complete list of options, refer to the PHP documentation of the method [https://github.com/moodle/moodle/blob/master/lib/classes/external/exporter.php core\external\exporter::get_format_parameters()].
 
Note that when you return a context from the above method, it is advised that you get it from the related objects. Doing so will prevent contexts from being loaded from the database from within the exporter and cause unwanted performance issues. The ''system context'' is an exception is this.


Psst... If you're wondering where we get the ''context'' from, look at [[#Related_objects|related objects]].
a
== Additional properties ==
== Additional properties ==


Line 173: Line 209:
''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.
''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>
<syntaxhighlight lang="php">
/**
/**
  * Return the list of additional properties.
  * Return the list of additional properties.
Line 180: Line 216:
  */
  */
protected static function define_other_properties() {
protected static function define_other_properties() {
     return array(
     return [
         'profileurl' => array(
         'profileurl' => [
             'type' => PARAM_URL
             'type' => PARAM_URL
         ),
         ],
         'statuses' => array(
         'statuses' => [
             'type' => status_exporter::read_properties_definition(),
             'type' => status_exporter::read_properties_definition(),
             'multiple' => true,
             'multiple' => true,
             'optional' => true
             'optional' => true
         ),
         ],
     );
     ];
}
}
</code>
</syntaxhighlight>


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.
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.
Line 197: Line 233:
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.
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.


<code php>
<syntaxhighlight lang="php">
/**
/**
  * Get the additional values to inject while exporting.
  * Get the additional values to inject while exporting.
Line 210: Line 246:
     ];
     ];
}
}
</code>
</syntaxhighlight>


Important note: ''additional properties'' cannot override ''standard'' properties, so make sure you the names do not conflict.
Important note: ''additional properties'' cannot override ''standard'' properties, so make sure the names do not conflict.


== Related objects ==
== Related objects ==
Line 222: Line 258:
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.
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.


<code php>
<syntaxhighlight lang="php">
    /**
/**
    * Returns a list of objects that are related.
* Returns a list of objects that are related.
    *
*
    * @return array
* @return array
    */
*/
    protected static function define_related() {
protected static function define_related() {
        return array(
    return [
            'context' => 'context',
        'context' => 'context',                     // Must be an instance of context.
            'statuses' => 'some\\namespace\\status[]',
        'statuses' => 'some\\namespace\\status[]', // Must be an array of status instances.
            'mother' => 'family\\mother?',
        'mother' => 'family\\mother?',             // Can be a mother instance, or null.
            'brothers' => 'family\\brother[]?',
        'brothers' => 'family\\brother[]?',         // Can be null, or an array of brother instances.
        );
    ];
    }
}
</code>
</syntaxhighlight>


We give the related objects to the ''exporter'' when we instantiate it, like this:
We give the related objects to the ''exporter'' when we instantiate it, like this:


<code php>
<syntaxhighlight lang="php">
$data = (object) ['id' => 123, 'username' => 'batman'];
$data = (object) ['id' => 123, 'username' => 'batman'];
$relateds = [
$relateds = [
Line 252: Line 288:
];
];
$ue = new user_exporter($data, $relateds);
$ue = new user_exporter($data, $relateds);
</code>
</syntaxhighlight>


Note that optional related must still be provided but as ''null''.
Note that optional relateds must still be provided but as ''null''. Related objects are accessible from within the exporter using ''$this->related['nameOfRelated']''.


== Exporters and persistent ==
== Exporters and persistent ==
Line 260: Line 296:
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()''.
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()''.


<code php>
<syntaxhighlight lang="php">
class status_exporter extends core\external\persistent {
class status_exporter extends \core\external\persistent_exporter {


     /**
     /**
Line 269: Line 305:
     */
     */
     protected static function define_class() {
     protected static function define_class() {
         return 'some\\namespace\\status';
         return \some\namespace\status::class; // PHP 5.5+ only
     }
     }


}
}
</code>
</syntaxhighlight>
   
   
And if you wanted to add more to it, there is always the [[#Additional_properties|other properties]].
And if you wanted to add more to it, there is always the [[#Additional_properties|other properties]].
Line 283: Line 319:
# When exporters are nested, the ''type'' to use should be ''other_exporter::read_properties_definition()'' and not ''other_exporter::get_read_structure()''.
# When exporters are nested, the ''type'' to use should be ''other_exporter::read_properties_definition()'' and not ''other_exporter::get_read_structure()''.
# Adding required relateds to an exporter once it has been released can cause ''coding_exceptions''. Each instance will require the new related, which it will not have in 3rd party code/plugins. Preferably an exporter's relateds should never change, but if they must, consider this:
# Adding required relateds to an exporter once it has been released can cause ''coding_exceptions''. Each instance will require the new related, which it will not have in 3rd party code/plugins. Preferably an exporter's relateds should never change, but if they must, consider this:
#* Consider creating a new exporter.
#* Creating a brand new exporter class, and leaving the other one unchanged.
#* Consider adding the related as ''optional''. This is strongly discouraged.
#* Adding the new related, and assigning it a default value in your ''constructor'' if undefined. Also print a debugging message for developers to fix their code.
#* Adding the new related and advertising that all of its usage must be updated.
#** Note: Adding the related as ''optional'' does not work. Optional relateds still need to be passed to the constructor, but as ''null''.
#* Adding the new related, and advertising that all of its usage must be updated.


== Examples ==
== Examples ==
Line 291: Line 328:
=== Minimalist ===
=== Minimalist ===


<code php>
<syntaxhighlight lang="php">
class user_exporter extends core\external\exporter {
class user_exporter extends core\external\exporter {


Line 300: Line 337:
     */
     */
     protected static function define_properties() {
     protected static function define_properties() {
         return array(
         return [
             'id' => array(
             'id' => [
                 'type' => PARAM_INT
                 'type' => PARAM_INT
             ),
             ],
             'username' => array(
             'username' => [
                 'type' => PARAM_ALPHANUMEXT
                 'type' => PARAM_ALPHANUMEXT
             ),
             ],
         );
         ];
     }
     }


}
}
</code>
</syntaxhighlight>


=== More advanced ===
=== More advanced ===


<code php>
<syntaxhighlight lang="php">
namespace example\external;
namespace example\external;


Line 330: Line 367:
     */
     */
     protected static function define_properties() {
     protected static function define_properties() {
         return array(
         return [
             'id' => array(
             'id' => [
                 'type' => PARAM_INT
                 'type' => PARAM_INT
             ),
             ],
             'username' => array(
             'username' => [
                 'type' => PARAM_ALPHANUMEXT
                 'type' => PARAM_ALPHANUMEXT
             ),
             ],
             'description' => array(
             'description' => [
                 'type' => PARAM_RAW,
                 'type' => PARAM_RAW,
             ),
             ],
             'descriptionformat' => array(
             'descriptionformat' => [
                 'type' => PARAM_INT,
                 'type' => PARAM_INT,
             ),
             ],
         );
         ];
     }
     }


Line 352: Line 389:
     */
     */
     protected static function define_other_properties() {
     protected static function define_other_properties() {
         return array(
         return [
             'profileurl' => array(
             'profileurl' => [
                 'type' => PARAM_URL
                 'type' => PARAM_URL
             ),
             ],
             'statuses' => array(
             'statuses' => [
                 'type' => status_exporter::read_properties_definition(),
                 'type' => status_exporter::read_properties_definition(),
                 'multiple' => true,
                 'multiple' => true,
                 'optional' => true
                 'optional' => true
             ),
             ],
         );
         ];
     }
     }


Line 370: Line 407:
     */
     */
     protected static function define_related() {
     protected static function define_related() {
         return array(
         return [
             'context' => 'context',
             'context' => 'context',
             'statuses' => 'some\\namespace\\status[]',
             'statuses' => 'some\\namespace\\status[]',
         );
         ];
    }
 
    /**
    * Get the formatting parameters for the description.
    *
    * @return array
    */
    protected function get_format_parameters_for_description() {
        return [
            'component' => 'core_user',
            'filearea' => 'description',
            'itemid' => $this->data->id
        ];
     }
     }


Line 397: Line 447:
     }
     }
}
}
</code>
</syntaxhighlight>
 
== See also ==
 
* [[Persistent]]
* [[Persistent form]]

Latest revision as of 13:33, 14 July 2021

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

Exporters are expected to be subclasses of the core\external\exporter base class:

/**
 * User data exporter.
 */
class user_exporter extends core\external\exporter {

}

If you are creating an exporter for a persistent class see the Exporters and persistent section below!

Defining properties

The method define_properties() returns a list of the properties expected when instantiating your exporter, and the ones it will export at the same time.

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

These properties will allow us to generate a create or update structure for external functions, more on this later. Oh, and if you are using persistent you do not need to do this: check this out.

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.

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

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
)

Now, I agree that this is not quite impressive. But wait until you read about automatically formatting text, and usage in external functions.

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 to export our users' data.

public static function get_users() {
    global $DB, $PAGE;
    $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. The following indicates that the external function requires the field 'username' to be passed in the key 'user'. The create and update structures include all of the standard properties, none of the other ones. Note that the create structure does not include the id property. Use user_exporter::get_update_structure() if to update a user and thus receive the ID.

public static function create_user_parameters() {
    return new external_function_parameters([
        'user' => user_exporter::get_create_structure()
    ]);
}

public static function create_user($user) {
    // Mandatory parameters 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 under another key, above we used user. Else this would not be flexible, and may not generate a valid structure for some webservice protocols.

Abiding to text formatting rules

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 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(). If you are not familiar with the rules for outputting text in Moodle see Output functions and Most Commonly Used PARAM_* Types

'description' => [
    'type' => PARAM_RAW,
],
'descriptionformat' => [
    'type' => PARAM_INT,
],

With the two above properties added, let's see what happens when the user's description is in the Markdown format and we export it.

$data = (object) [
    'id' => 123,
    'username' => 'batman',
    'description' => 'Hello __world__!',
    'descriptionformat' => FORMAT_MARKDOWN
];
$ue = new user_exporter($data, ['context' => context_user::instance(123)]);
$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   // Corresponds to FORMAT_HTML.
)

Psst... we've cheating a bit above. Did you notice that we've passed a context to the exporter? We passed a related object, more on them later.

Formatting parameters

Formatting requires a context. If you've defined a related object with the name context we will automatically use this one. But there are cases where you need multiple contexts, or you need to rewrite the pluginfile URLs. When that is the case, you will define a method called 'get_format_parameters_for_' followed with the name of the property. The latter also accepts the various options which both format_text and format_string support.

/**
 * Get the formatting parameters for the description.
 *
 * @return array
 */
protected function get_format_parameters_for_description() {
    return [
        'component' => 'core_user',
        'filearea' => 'description',
        'itemid' => $this->data->id
    ];
}

For a complete list of options, refer to the PHP documentation of the method core\external\exporter::get_format_parameters().

Note that when you return a context from the above method, it is advised that you get it from the related objects. Doing so will prevent contexts from being loaded from the database from within the exporter and cause unwanted performance issues. The system context is an exception is this.

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 (get_read_structure and read_properties_definition) 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 [
        'profileurl' => [
            'type' => PARAM_URL
        ],
        'statuses' => [
            '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.

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.

/**
 * Get the additional values to inject while exporting.
 *
 * @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) {
    $profileurl = new moodle_url('/user/profile.php', ['id' => $this->data->id]);
    return [
        'profileurl' => $profileurl->out(false)
    ];
}

Important note: additional properties cannot override standard properties, so make sure the names do not conflict.

Related objects

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. As much as possible you will want to avoid calling APIs, or querying the database upon export. Exporters can be used in loops and thus subsequent queries could dramatically affect performance, hence the need for related objects.

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.

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.

/**
 * Returns a list of objects that are related.
 *
 * @return array
 */
protected static function define_related() {
    return [
        'context' => 'context',                     // Must be an instance of context.
        'statuses' => 'some\\namespace\\status[]',  // Must be an array of status instances.
        'mother' => 'family\\mother?',              // Can be a mother instance, or null.
        'brothers' => 'family\\brother[]?',         // Can be null, or an array of brother instances.
    ];
}

We give the related objects to the exporter when we instantiate it, like this:

$data = (object) ['id' => 123, 'username' => 'batman'];
$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);

Note that optional relateds must still be provided but as null. Related objects are accessible from within the exporter using $this->related['nameOfRelated'].

Exporters and persistent

Great news, if you have a 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().

class status_exporter extends \core\external\persistent_exporter {

    /**
     * Returns the specific class the persistent should be an instance of.
     *
     * @return string
     */
    protected static function define_class() {
        return \some\namespace\status::class; // PHP 5.5+ only
    }

}

And if you wanted to add more to it, there is always the other properties.

Common pitfalls

  1. Exporters must not extend other exporters. They would become too unpredictable.
  2. 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.
  3. When exporters are nested, the type to use should be other_exporter::read_properties_definition() and not other_exporter::get_read_structure().
  4. Adding required relateds to an exporter once it has been released can cause coding_exceptions. Each instance will require the new related, which it will not have in 3rd party code/plugins. Preferably an exporter's relateds should never change, but if they must, consider this:
    • Creating a brand new exporter class, and leaving the other one unchanged.
    • Adding the new related, and assigning it a default value in your constructor if undefined. Also print a debugging message for developers to fix their code.
      • Note: Adding the related as optional does not work. Optional relateds still need to be passed to the constructor, but as null.
    • Adding the new related, and advertising that all of its usage must be updated.

Examples

Minimalist

class user_exporter extends core\external\exporter {

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

}

More advanced

namespace example\external;

use core\external\exporter;
use renderer_base;
use moodle_url;

class user_exporter extends exporter {

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

    /**
     * Return the list of additional properties.

     * @return array
     */
    protected static function define_other_properties() {
        return [
            'profileurl' => [
                'type' => PARAM_URL
            ],
            'statuses' => [
                'type' => status_exporter::read_properties_definition(),
                'multiple' => true,
                'optional' => true
            ],
        ];
    }

    /**
     * Returns a list of objects that are related.
     *
     * @return array
     */
    protected static function define_related() {
        return [
            'context' => 'context',
            'statuses' => 'some\\namespace\\status[]',
        ];
    }

    /**
     * Get the formatting parameters for the description.
     *
     * @return array
     */
    protected function get_format_parameters_for_description() {
        return [
            'component' => 'core_user',
            'filearea' => 'description',
            'itemid' => $this->data->id
        ];
    }

    /**
     * Get the additional values to inject while exporting.
     *
     * @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]);

        return [
            'profileurl' => $profileurl->out(false),
            'statuses' => $statuses
        ];
    }
}

See also