Note:

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

Talk:External services description

From MoodleDocs

About the different alternatives describing the available WS, by Eloy

Hi, after reading the different alternatives, I'm inclined to implement something like the 3rd one. Having our custom structures to define the interface to the available web services seems really better than using "free-form" structures like "raw" arrays and objects or using harcoded keywords like "multiple" and so on. It has various advantages and doesn't hurt simplicity when defining the parameters:

  • Better PHP structures <--> XML mapping
  • Extensibility:
    • custom/reusable validation methods.
    • built-in XML writing
    • enforce some sort of common response functionality (see my concerns about returned content below).

Anyway, I've some (small) concerns about the example code used in the 3rd alternative, so I'm writing here how it should look like ideally IMO (rationale about changes is after the code):

class moodle_group_external extends external_api {

   public static function add_member_parameters() {
       return new external_single_structure(
           'member', array(
               new external_param('groupid', PARAM_INT, 'some group id'),
               new external_param('userid', PARAM_INT, 'some user id')
           )
       );
   }
   public static function add_member($groupid, $userid) {
       $params = self::validate_prameters(self::add_member_parameters(), array('groupid'=>$groupid, 'userid'=>$userid));
       // all the parameter/behavioural checks and security constrainsts go here,
       // throwing exceptions if neeeded and and calling low level (grouplib)
       // add_member() function that will be one in charge of the functionality without
       // further checks.
   }
   public static function add_member_returns() { // Some concerns here. See below.
       return null;
   }


   public static function add_members_parameters() {
       return new external_multiple_structure(
           'membership', self::add_member_parameters()
       );
   }
   public static function add_members(array $membership) {
       foreach($membership as $one) { // simply one iterator over the "single" function if possible
           self::add_member($one->groupid, $one->userid);
       }
   }
   public static function add_members_returns() { // BIG concerns here. See below.
       return null;
   }


   public static function get_groups_parameters() {
       return new external_multiple_structure(
           'groups', new external_single_structure(
               'group', array(
                   new external_param('groupid', PARAM_INT, 'some group id')
               )
           )
       );
   }
   public static function get_groups(array $groupids) {
       $params = self::validate_prameters(self::get_groups_parameters(), array('groupids'=>$groupids));
       // all the parameter/behavioural checks and security constrainsts go here,
       // throwing exceptions if neeeded and and calling low level (grouplib)
       // get_groups() function that will be one in charge of the functionality without
       // further checks.
   }
   public static function get_groups_returns() { // BIG concerns here. See below.
       return new external_multiple_structure(
           'groups', new external_single_structure(
               'group', array(
                   new external_param('id', PARAM_INT, 'some group id'),
                   new external_param('name', PARAM_TEXT, 'multilang compatible name, course unique'),
                   new external_param('description', PARAM_RAW, 'just some text'),
                   new external_param('enrolmentkey', PARAM_RAW, 'group enrol secret phrase')
               )
           )
       );
   }

}


class moodle_user_external extends external_api {

   public static function create_users_parameters() {
       return new external_multiple_structure(
           'users', new external_single_structure(
               'user', array(
                   new external_param('username', PARAM_USERNAME, 'Username policy is defined in Moodle security config'),
                   new external_param('password', PARAM_RAW, 'Moodle passwords can consist of any character'),
                   new external_param('firstname', PARAM_NOTAGS, 'The first name(s) of the user'),
                   new external_param('lastname', PARAM_NOTAGS, 'The family name of the user'),
                   new external_param('email', PARAM_EMAIL, 'A valid and unique email address'),
                   new external_param('auth', PARAM_AUTH, 'Auth plugins include manual, ldap, imap, etc', false),
                   new external_param('confirmed', PARAM_NUMBER, 'Active user: 1 if confirmed, 0 otherwise', false),
                   new external_param('idnumber', PARAM_RAW, 'An arbitrary ID code number perhaps from the institution', false),
                   new external_param('emailstop', PARAM_NUMBER, 'Email is blocked: 1 is blocked and 0 otherwise', false),
                   new external_param('lang', PARAM_LANG, 'Language code such as "en_utf8", must exist on server', false),
                   new external_param('theme', PARAM_THEME, 'Theme name such as "standard", must exist on server', false),
                   new external_param('timezone', PARAM_ALPHANUMEXT, 'Timezone code such as Australia/Perth, or 99 for default', false),
                   new external_param('mailformat', PARAM_INTEGER, 'Mail format code is 0 for plain text, 1 for HTML etc', false),
                   new external_param('description', PARAM_TEXT, 'User profile description, as HTML', false),
                   new external_param('city', PARAM_NOTAGS, 'Home city of the user', false),
                   new external_param('country', PARAM_ALPHA, 'Home country code of the user, such as AU or CZ', false),
                   new external_multiple_structure(
                       'preferences', new external_single_structure(
                           'preference', array(
                               new external_param('type', PARAM_ALPHANUMEXT, 'The name of the preference'),
                               new external_param('value', PARAM_RAW, 'The value of the preference')
                           )
                       ), 'User preferences', false),
                   new external_multiple_structure(
                       'customfields', new external_single_structure(
                           'customfield', array(
                               new external_param('type', PARAM_ALPHANUMEXT, 'The name of the custom field'),
                               new external_param('value', PARAM_RAW, 'The value of the custom field')
                           )
                       ), 'User custom fields', false)
               )
           )
       );
   }
   public static function create_users(array $users) {
       $params = self::validate_prameters(self::create_users_parameters(), array('users'=>$users));
       foreach ($users as $user) {
           // all the parameter/behavioural checks and security constrainsts go here,
           // throwing exceptions if neeeded and and calling low level (userlib)
           // add_user() function that will be one in charge of the functionality without
           // further checks.
       }
   }
   public static function create_users_returns() { // BIG concerns here. See below.
       return new external_multiple_structure(
           'users', new external_single_structure(
               'user', array(
                   new external_param('userid', PARAM_INT, 'id of the created user'))
           )
       );
   }

}

Changes from original code are:

  • Small change in structure names. external_bulk_array renamed to external_multiple_structure and external_assoc_array to external_single_structure (note that structure can be changed to anything else). The important bits are the single/multiple ones vs. the assoc/bulk. Clear IMO.
  • Moved the "name" of the single structure and param classes to the constructor (no need for associative arrays). Clear syntax.
  • Small change in external_multiple_structure to add the missing "name" to its constructor. Matching previous point.
  • The xxxx_parameters() function will return directly one single or multiple structure, saving one nesting level from original code.
  • Reuse as much as possible:
    • The moodle_group_external is a clear example. Once we have the single service implemented (add_member), it's pretty easy to build its multiple alternative. Just reuse the single parameters definition and the single executor. IMO we should always enforce the single/multiple duality.
    • Also, although there isn't an example, I think we could have some "habitual" structures predefined somewhere, see for example, the preference or the customfield single structures (or the whole preferences or customfields multiple ones if you prefer). They are basically the same (name/value pairs). It could be useful to have one predefined "name_value" structure somewhere and use it where necessary (user preferences, user custom fields, various config options...).

Concerns about return functions:

In the example above we have a good example. There are some xxxx_return() functions returning NULLs, others returning multiple structures (and also is possible to return single structures or numbers or whatever).

IMO returned structures should be, always, encapsulated into something constant, call it 'external_response'. Then, within that, we can return some constant information (bool result, string error, whatever...) and some dynamic information (single/multiple structures).

Also, while it's really simple to return results for single structures (for example the add_member() above will return one simple result ), it's more complex to do so for multiple structures. For example, the add_members() above must return one different result for each groupid-userid added. Imagine you create one client sending 4 pairs (within a multiple structure well formed WS request).

And only one of them is added successfully, other fails because the userid is incorrect, another fails because it already exists and the 4th fails because the user cannot handle that group, or because the user isn't enrolled in the course the group belongs to or... whatever. How will the return information be formatted to inform about all those heterogeneous situations? How will the client know the exact status of each pair sent in the multiple request?

Should we introduce one mandatory "key" parameter in all the occurrences within one multiple request, in order to get the response properly identified by those keys? Or will the multiple request be rejected as a whole (I think this isn't possible).

Note I don't know if that has been resolved in other places of the WS documentation but I haven't been able to find it, hence the warn. ;-)

So, summarising, I like the 3rd alternative with the changes specified above (in order to make it more readable/simple, basically). And I think the "response" (return) part is at least as important as the "request" (parameters) part, and it still needs more work/definition/polishing. Shouldn't be difficult to achieve that.

Hope this helps, ciao --Eloy Lafuente (stronk7) 00:24, 18 September 2009 (UTC)

Offtopic: I hate the "external" keyword everywhere. :-D :-P

Note: The second revision to this alternative (adding new "name" parameter to all the classes vs old associative arrays) has been discussed/agreed with Petr. --Eloy Lafuente (stronk7) 20:39, 19 September 2009 (UTC)


I think the flexible return values are problematic - I personally prefer exception when anything goes wrong, getting complex result requires extra post processing of results. So my -1 here. Petr Škoda (škoďák)

But "client" must know what has been processed and what not. What if something breaks in the 4th element of a multiple request? How is the client informed about the 3 first elements already being processed and the rest not? Eloy Lafuente (stronk7) 09:10, 18 September 2009 (UTC)
Right, the methods working with bulk data should do that, I am just not sure single methods should return complex structures, also we could send this extra information through exception - the bulk methods should call simple ones and could return exception containing the original exception + progress info Petr Škoda (škoďák) 20:32, 19 September 2009 (UTC)