Difference between revisions of "Creating a web service and a web service function"

Jump to: navigation, search
(Context validation)
(Implement the external function)
Line 221: Line 221:
 
         $params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));
 
         $params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));
  
         $transaction = $DB->start_delegated_transaction();
+
         $transaction = $DB->start_delegated_transaction(); //If an exception is thrown in the below code, all DB queries in this code will be rollback.
  
 
         $groups = array();
 
         $groups = array();

Revision as of 04:43, 2 November 2011

Moodle 2.0


Simple example

Have a look to the web service template. It is a simple hello_world function.

Complex example

We will write a web service function create_groups($groups). This function create a group into a Moodle course. - actually the function already exists into Moodle. You can browse Github repository to have a look to the latest code.

Write the function specification documentation

functional

create_groups($groups) will take a list of group object as parameters and it will return the same groups with their newly created id. If ever one group creation fails, the function will throw an exception, and no creation will happen. Note: we now tend to go away from stopping execution when we write bulk operations. When an error occurs, we recommend to send an error message string instead to throw an exception (for new web service bulk functions).

technical

  • the core functions: groups_create_group() from /group/lib.php.
  • the parameter types: a list of object. This object are groups, with id/name/courseid.
  • the returned value types: a list of group object with their id.
  • the user capabilities: moodle/course:managegroups

Write a simple test client

It is helpfull to start writing a web service test client. You will often discover use cases that you didn't think about. See How to create a web service client.

Declare the web service function

Web service functions are delcared/mapped in db/services.php files. create_groups() is a core external function, so it goes to /lib/db/services.php.

$functions = array(
    'core_group_create_groups' => array(         //web service function name
        'classname'   => 'core_group_external',  //class containing the external function
        'methodname'  => 'create_groups',          //external function name
        'classpath'   => 'group/externallib.php',  //file containing the class/external function
        'description' => 'Creates new groups.',    //human readable description of the web service function
        'type'        => 'write',                  //database rights of the web service function (read, write)
    ),
);

Web service functions should match the naming convention.

Define the web service description

Some web service protocol need a full description of the web service function parameters and return value. For example we need to generate a full WSDL for SOAP if we expect a JAVA/.Net client to call our function. This WSDL need a complete and fully detailed description of the parameters.
Moreover we implemented a web service parameters validation process that also requires a full description of these parameters.
In conclusion, you must write these descriptions. They are not applied to the web service function but the external function, remember that the web service function is just a kind of redirection.

Descriptions are usually located in a class inside a file named externallib.php in the component folder. You probably remember that we referenced this moodle_group_external class into /lib/db/services.php. Let's take a minute to talk about the /group/externallib.php that we need to create.

This file will contain all external functions. They are all related to each other. In our case the externallib.php file will contain all group related functions. For each external function, we will have:

  • the external function, calling a core function.
  • a function describing the parameters of the external function
  • a function describing the return values of the external function

Now we'll have look to the description of the create_group($groups) parameters. All parameter descriptions are included in a function called externalfunctionname_parameters(). In our case it will be create_groups_parameters() :

require_once("$CFG->libdir/externallib.php");
 
class moodle_group_external extends external_api {
 
    /**
     * Returns description of method parameters
     * @return external_function_parameters
     */
    public static function create_groups_parameters() {
        return new external_function_parameters(
            array(
                'groups' => new external_multiple_structure(
                    new external_single_structure(
                        array(
                            'courseid' => new external_value(PARAM_INT, 'id of course'),
                            'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                            'description' => new external_value(PARAM_RAW, 'group description text'),
                            'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
                        )
                    )
                )
            )
        );
    }

Ok you are probably already here :) We have to understand this function very well. There are different approaches to understand this function, here is my personal one.
The function xxx_parameters() always returns an external_function_parameters object. This object is initialized with an associative array. This array contains the parameter descriptions. If the external function has no parameters, this array will be empty (but present). If the external function has three parameters, it will have three elements, the keys being the parameters names. Each of its element is a parameter description. A parameter can be a list (external_multiple_structure), an object (external_single_structure) or a primary type (external_value).

Our create_groups() function expects one parameter named groups, so we will first write:

/**
     * Returns description of method parameters
     * @return external_function_parameters
     */
    public static function create_groups_parameters() {
        return new external_function_parameters(
            array(
                'groups' => ...
 
            )
        );
    }

This groups parameter is a list of group. So we will write :

'groups' => new external_multiple_structure(
                    ...
                )

An external_multiple_structure object (list) can be initialized with external_single_structure (object), external_value (primary type) or another external_multiple_structure (list). You will respectively obtain a list of a unique object type, a list of a unique primary type, or a list of a list. These last case being probably bad design.
In our case we want a list of group, a group being a object. Have a look on how would describe a group :

new external_single_structure(
                        array(
                            'courseid' => ...,
                            'name' => ...,
                            'description' => ...,
                            'enrolmentkey' => ...,
                        )
                    )

From this code you can guess that an external_single_structure is initialized with an associative array containing the object attibutes.
Thus a list of group will be defined :

'groups' => new external_multiple_structure(
                    new external_single_structure(
                        array(
                            'courseid' => ...,
                            'name' => ...,
                            'description' => ...,
                            'enrolmentkey' => ...,
                        )
                    )
                )

Each group attribute is a primary type. courseid is an integer. name is a string, description is a string, and enrolmentkey is also a string. To define a primary type we use the external_value object. This external_value is initialized with a non associative array containing three elements:

  1. the primary type
  2. a human readable description
  3. is the parameter optional (if the parameter is mandatory we don't need this third element)

For example for the courseid and name attribut we would define:

'courseid' => new external_value(PARAM_INT, 'id of course'),
                            'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),


So our list of object description will be :

'groups' => new external_multiple_structure(
                    new external_single_structure(
                        array(
                            'courseid' => new external_value(PARAM_INT, 'id of course'),
                            'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                            'description' => new external_value(PARAM_RAW, 'group description text'),
                            'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
                        )
                    )
                )

Now you should be able to understand this parameter description function. You also should be able to deduce how to write the return value description function.

Return value function always returns one primary or complex value, so we don't need to return external_function_parameters object. In our case we want to return the exact same list of groups. The only change being that we want the group to have an id attribute.

public static function create_groups_returns() {
        return new external_multiple_structure(
            new external_single_structure(
                array(
                    'id' => new external_value(PARAM_INT, 'group record id'),
                    'courseid' => new external_value(PARAM_INT, 'id of course'),
                    'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                    'description' => new external_value(PARAM_RAW, 'group description text'),
                    'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
                )
            )
        );
    }

Note that external_single_structure describes an associative array. If your function returns an object, you must first convert it to an array.

The returned value cannot be null, and if it is an array, none of its values can be null. If your function returns for example a list of database records with $DB->get_records(), you must check each field of each row to replace null values, and convert each row to an associative array.

Here ends the most difficult part of the web service infrastructure implementation. Congratulation!

Mandatory, Optional or Default

An attribute can be defined MANDATORY, OPTIONAL, or DEFAULT.

'yearofstudy' => new external_value(PARAM_INT, 'year of study',VALUE_DEFAULT, 1979),

The third parameter VALUE_DEFAULT indicates that 'yearofstudy' is set to a default value if the attribute is not supply. When you set VALUE_DEFAULT in the description, you must also write a default value as fourth parameter. Here the default value of 'yearofstudy' is 1979.

The three values can be understood like this :

  • VALUE_REQUIRED - if the parameter is not supplied, then the server throws an exception
  • VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
  • VALUE_DEFAULT - if the parameter is not supplied, then the default value is used

Implement the external function

We declared our web service function and we defined the external function parameters and return values. We will now implement the external function:

/**
     * Create groups
     * @param array $groups array of group description arrays (with keys groupname and courseid)
     * @return array of newly created groups
     */
    public static function create_groups($groups) { //Don't forget to set it as static
        global $CFG, $DB;
        require_once("$CFG->dirroot/group/lib.php");
 
        $params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));
 
        $transaction = $DB->start_delegated_transaction(); //If an exception is thrown in the below code, all DB queries in this code will be rollback.
 
        $groups = array();
 
        foreach ($params['groups'] as $group) {
            $group = (object)$group;
 
            if (trim($group->name) == '') {
                throw new invalid_parameter_exception('Invalid group name');
            }
            if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) {
                throw new invalid_parameter_exception('Group with the same name already exists in the course');
            }
 
            // now security checks
            $context = get_context_instance(CONTEXT_COURSE, $group->courseid);
            self::validate_context($context);
            require_capability('moodle/course:managegroups', $context);
 
            // finally create the group
            $group->id = groups_create_group($group, false);
            $groups[] = (array)$group;
        }
 
        $transaction->allow_commit();
 
        return $groups;
    }

Parameter validation

$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));

This validate_parameters function validates the external function parameters against the description. It will return an exception if some required parameters are missing, if parameters are not well-formed, and check the parameters validity. It is essential that you do this call to avoid potential hack.

Important: the parameters of the external function and their declaration in the description must be the same order. In this example we have only one parameter named $groups, so we don't need to worry about the order.

Context and Capability checks

In a long term, in Moodle 2.0, all context/capability checks will disappear from the core functions and will be located into the external functions. Don't forget them.

/// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);

Database transaction

If an exception is thrown in the below code, all DB queries in this code will be rollback.

$transaction = $DB->start_delegated_transaction();
 //
 //some code for bluk operation ....
 //
 $transaction->allow_commit();


You can throws exception. There are automatically handle by Moodle web service servers.

throw new invalid_parameter_exception('Group with the same name already exists in the course');

Correct return values

The return values will be validated by the Moodle web service servers:

  • return values contain some values not described => these values will be skipped.
  • return values miss some required values (VALUE_REQUIRED) => the server will return an error.
  • return values types don't match the description (int != PARAM_ALPHA) => the server will return an error

Add the web service function into a service

You can pre-build a service including any functions, so the Moodle administrator doesn't need to do it. Add into /lib/db/services.php:

$services = array(
      'groupservice' => array(                                                //the name of the web service
          'functions' => array ('moodle_add_groups', 'moodle_create_groups'), //web service functions of this service
          'requiredcapability' => 'some/capability:specified',                //if set, the web service user need this capability to access 
                                                                              //any function of this service                   
          'restrictedusers' = >1,                                             //if enabled, the Moodle administrator must link some user to this service
                                                                              //into the administration
          'enabled'=>0,                                                       //if enabled, the service can be reachable on a default installation
       )
  );

Note: it is not possible for an administrator to add/remove any function from a pre-built service.

See also

Specification: