-
Notifications
You must be signed in to change notification settings - Fork 39
Building a REST API for use via AJAX
NOTE: See the new RESTful APIs page for the new and much-improved RESTful Elefant API.
Let's make a generic API based on the Mytable
model created on the Database API and models page. To start, create a new app:
cd /path/to/site
./conf/elefant build-app api
Open the apps/api/handlers/index.php
file and add the following to the top:
<?php
$page->layout = false;
?>
Any request sent to /api
or /api/*
will route to this handler and we can simply use echo json_encode()
to format our output.
It's a good idea to version your API if you're exposing it to the public, since you may not have control over updates to 3rd-party clients using it. To do this, we simply create a handler for each new version named v1.php
, v2.php
, etc. Then we can adjust our main handler to refer to the most current version like this:
<?php
$page->layout = false;
$latest = 'v1';
header ('Location: /api/' . $latest);
exit;
?>
Now whenever someone accesses our API, it will be at /api/v1/*
or /api/v2/*
making it easy to provide backwards compatibility. Now let's implement our API at v1.php
.
There are two ways we can implement our API: In one file with a switch statement, or separated into one file per method. We'll start with one file, then look at how we can split methods into separate handlers after. Note that you can use a switch for most handlers, and split only some into separate handlers depending on what works best in your case.
In v1.php
:
<?php
$page->layout = false;
switch ($this->params[0]) {
case 'all':
$m = new Mytable ();
$out = $m->fetch_orig ();
break;
case 'item':
$m = new Mytable ($this->params[1]);
$out = $m->orig ();
break;
}
echo json_encode ($out);
?>
We now have two API methods we can use to retrieve data:
Retrieve all items from mytable
.
Retrieve an individual item from mytable
.
If you want to handle errors gracefully, you might want to create a response wrapper with success, data, and error properties like this:
<?php
$page->layout = false;
$error = false;
switch ($this->params[0]) {
case 'all':
$m = new Mytable ();
$out = $m->fetch_orig ();
if (! $out) {
$error = $m->error;
}
break;
case 'item':
$m = new Mytable ($this->params[1]);
if ($m->error) {
$error = $m->error;
break;
}
$out = $m->orig ();
break;
}
$res = new StdClass;
if ($error) {
$res->success = false;
$res->error = $error;
} else {
$res->success = true;
$res->data = $out;
}
echo json_encode ($res);
?>
Now all of our responses will take the form:
{"success":true,"data":[data]}
Or on error:
{"success":false,"error":"Error message"}
As your API grows, you may want to move calls from one growing switch statement into their own handlers. For example, say we want to add an /item/add
method to our API. We can do that by creating a separate handler based on our original in apps/api/handlers/v1/item/add.php
like this:
<?php
$page->layout = false;
$error = false;
$m = new Mytable ($_POST);
if (! $m->put ()) {
$error = $m->error;
}
$res = new StdClass;
if ($error) {
$res->success = false;
$res->error = $error;
} else {
$res->success = true;
}
echo json_encode ($res);
?>
As you can see, this allows us to eliminate the switch altogether and simplify each handler because we already know the URL matches /api/v1/item/add
.
Note that we could also aggregate groups of methods into their own switch by creating a handler like
apps/api/handlers/v1/item.php
and anything matching/api/v1/item/*
will now go there.
You'll need to configure your web server to send PUT and DELETE requests to PHP, which Apache doesn't do by default. Alternately, you can simply use the X-HTTP-Method-Override
HTTP header to specify the real request method. Elefant has two convenience methods to help here:
<?php
// checks X-HTTP-Method-Override or the real request method
$real_request_method = $this->request_method ();
?>
And if the request method is PUT, you can fetch the PUT data from stdin via:
<?php
if ($this->request_method () == 'PUT') {
$data = $this->get_put_data ();
}
?>
In the last example, we simply added a new item without checking any of the $_POST
data. Obviously in a real-world example we would need to validate that first. We can use lib/Form.php
to help with that.
<?php
$form = new Form ('post', 'api/v1-item-add');
$form->verify_referrer = false;
$form->verify_csrf = false;
if (! $form->submit ()) {
$error = $form->error;
if (count ($form->failed) > 0) {
$error .= ' (' . join (', ', $form->failed) . ')';
}
}
?>
This will verify both the fields, and the request method. If it's an open API for 3rd-party clients, then we do want to disable the referrer validation however. Next we simply define an INI file for our validation rules and save it to apps/api/forms/v1-item-add.php
. The one from Forms and input validation should work here as well since we're using the same schema.
If we just need a single validation done, a full INI file might be overkill. In that case we can say:
<?php
if (! Form::verify_value ($_GET['id'], 'int')) {
$error = 'ID must be an integer.';
}
?>
If you just want an HTTP Basic wrapper around your API, you can add the following code to the top of your handlers:
<?php
$page->layout = false;
if (! simple_auth ()) {
$res = new StdClass;
$res->success = false;
$res->error = 'Authorization required.';
echo json_encode ($res);
return;
}
?>
See the Custom user authentication page for info on adding your own authentication mechanism to your API.