Webservice protocol plugin for Moodle enabling developers to use a RESTful API.
The default protocols included in Moodle are RPC-like protocols, even the rest
one, and thus do not use verbose URLs, HTTP methods or HTTP status codes. This plugin attempts to remedy this by mapping URLs and HTTP methods to existing external functions.
GET /webservice/restful/index.php/courses/2
HTTP/1.1 200 OK
Content-Length: 4211
Content-Type: application/json
{
"id": 2,
"fullname": "An Awesome Course",
"shortname": "AwesomeCourse",
...
}
This version has only been tested with the following but could work in different configurations.
- Moodle 3.4
- PHP 7.0
NOT PRODUCTION READY: This plugin is yet in alpha, do not use it in production!
- Place the content of this repository in the folder
webservice/restful
. - Navigate to
Site Administration > Notifications
to install the plugin.
The root of all the following is webservice/restful/index.php
.
Route | Method | Comment |
---|---|---|
/courses | GET | List all courses |
/courses | POST | Create a new course |
/courses/ID | GET | Get a course |
/courses/ID | PATCH | Update a course |
/courses/ID | DELETE | Delete a course |
/courses/ID/duplicate | POST | Duplicate a course |
Enable everything needed as for any other webservice protocol, and create a token.
- The requests are made at
/webservice/restful/index.php/API_ROUTE
- The header
Content-Type
should readapplication/json
- The request body must be in JSON
- The token must be passed using the header
Authorization: Bearer TOKEN
GET /webservice/restful/index.php/courses/2 HTTP/1.1
Accept: */*
Authorization: Bearer 10787a782d5cea26d69e103729d594f7
Host: localhost
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 4211
Content-Type: application/json
Date: Thu, 28 Sep 2017 11:47:55 GMT
Keep-Alive: timeout=5, max=100
Server: Apache/2.4.25 (Ubuntu)
{
"id": 2,
"fullname": "An Awesome Course",
"shortname": "AwesomeCourse",
...
}
With HTTPie:
http https://example.com/webservice/restful/index.php/courses/2 Authorization:'Bearer 10787a782d5cea26d69e103729d594f7'
With curl:
curl localhost/ws/webservice/restful/index.php/courses/2 -H 'Authorization: Bearer 10787a782d5cea26d69e103729d594f7'
POST /webservice/restful/index.php/courses HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Authorization: Bearer 10787a782d5cea26d69e103729d594f7
Content-Length: 89
Content-Type: application/json
{
"categoryid": "1",
"fullname": "Another Awesome Course",
"shortname": "AnotherAwesome"
}
HTTP/1.1 201 Created
Connection: Keep-Alive
Content-Length: 39
Content-Type: application/json
Date: Thu, 28 Sep 2017 11:54:17 GMT
Keep-Alive: timeout=5, max=100
Server: Apache/2.4.25 (Ubuntu)
{
"id": 12,
"shortname": "AnotherAwesome"
}
With HTTPie:
http POST localhost/ws/webservice/restful/index.php/courses \
Authorization:'Bearer 10787a782d5cea26d69e103729d594f7' \
fullname="Another Awesome Course" \
shortname="AnotherAwesome" \
categoryid=1
With curl:
curl -X POST localhost/ws/webservice/restful/index.php/courses \
-H "Authorization: Bearer 10787a782d5cea26d69e103729d594f7" \
-H 'Content-Type: application/json' \
-d '{"fullname": "Another Awesome Course", "shortname": "AnotherAwesome", "categoryid": 1}'
This plugin is still in a very early stage, and thus the API design could drastically change by the time it is deemed production ready. Also note that performance have not been considered at all at this stage. However, if you are interested in learning how to define a new route, here is some documentation.
The plugin works by analysing the URL and mapping it to a route. A route is constituted of a regex, and a list of methods that can be handled. Each method contains a handler function which is responsible for returning a response.
There are no interfaces, classes, or strong types defined. Rather functions are used to construct data in the expected format. For instance, a response will be an associative array containing the response code, its text and maybe some data. To construct such a response, you would use the function make_response
, or a more appropriate one such as content_created
.
Let's create a new route to get a category.
[
'regex' => '/categories/([0-9]+)',
]
Notice that the regex does not include the usual start and end delimiters (^$
), those will be added automatically later. And notice that we are capturing the category ID.
Now, we will add the GET
method, which will use the existing external function core_course_get_categories
. There is a function to help us create a pipeline for handling requests going through to the external API: external_api_method
. Its first argument is the name of the external function, its second argument will be an associative array containing modifying functions, but let's leave that aside for now.
[
'regex' => '/categories/([0-9]+)',
'methods' => [
'GET' => external_api_method('core_course_get_categories')
]
]
If we test this like this, and request /categories/1
, the function will return all the categories. That is because the external function core_course_get_categories
did not receive any parameter, in which case it returns all the categories. How do we map the ID from the route to the call of the external function then? By using the argsmapper
function to provide to external_api_method
. In argsmapper
you will construct the arguments expected by the external function.
The function core_course_get_categories
expects an array containing criteria
under which there are rows of criterion, each containing key
and value
, for us it looks a bit like this:
[
'criteria' => [
[
'key' => 'id',
'value' => CATEGORY_ID
]
]
]
So let's add our argsmapper
function:
[
'regex' => '/categories/([0-9]+)',
'methods' => [
'GET' => external_api_method('core_course_get_categories', [
'argsmapper' => function($routeargs, $request, $options) {
return [
'criteria' => [
[
'key' => 'id',
'value' => $routeargs[0]
]
]
];
}
])
]
]
Notice that the argsmapper
function receives three parameters: the route arguments from our regex, and the request and options which we will not cover now. It's important that you note that the $routeargs
are indexed from 0, not 1.
And now it works!
GET /categories/1
HTTP/1.1 200 OK
[
{
"id": 1,
"name": "Miscellaneous",
...
}
]
However, there is still a quirk here. The category is returned in the form of an array of categories, that is not what we want when we only expect to get one. Also, when we request a category that does not exist, we still get a 200 OK
with an empty array.
GET /categories/123456
HTTP/1.1 200 OK
[]
So, what we need to do now, is to add the resultmapper
function. Its purpose is to manipulate the result in order to return what we need, the same way we mapped the arguments to the function. It will receive the same arguments as argsmapper
, except that it will also receive the result. Here is how it looks like.
[
'regex' => '/categories/([0-9]+)',
'methods' => [
'GET' => external_api_method('core_course_get_categories', [
'argsmapper' => function($routeargs, $request, $options) {
return [
'criteria' => [
[
'key' => 'id',
'value' => $routeargs[0]
]
]
];
},
'resultmapper' => function($result, $args, $request, $options) {
return reset($result) ?: null;
}
])
]
]
That's it! Now /categories/1
just returns the category object. And when we request the category that does not exist, we get a 404 Not Found
response without a body. Oh, and if you are wondering what ?: null
means, well that's simply to ensure that the return value is null
when $result
is empty. When null
is returned from a GET
request, external_api_method
automatically assumes that it is a Not Found
.
Are you validating the parameters?
Yes, the function external_api_method
takes care of validating both the parameters, and the return values of the external functions.
What if I want to return another status code?
You can define the function responsemaker
and return your own reponse.
Can I capture exceptions raised by the external function?
The function errorhandler
can be specified to handle different type of responses based on the exceptions captured from the external function execution. Exceptions triggered at other stages of the request will be handled separately.
Do I need to configure Apache to rewrite URLs?
No, this relies on slasharguments
like a lot of other things in Moodle.
- Add lots of other routes
- Write tests, lots of tests
- Document the API (routes, verbs, parameters)
- Propertly handle request and header content types
- Use HTTP headers for altering
external_settings
parameters - Check caching-related response headers
Licensed under the GNU GPL License.