PHP Serving RESTful APIs
Creating a Resource
Problem
You want to let people add a new resource to the system.
Solution
Accept requests using POST. Read the POST body. Return success and the location of the new resource.
For a POST request to http://api.example.com/v1/jobs:
POST /v1/jobs HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 49
{
"position": {
"title": "PHP Developer"
}
}
Use this PHP code:
if ($_SERVER["REQUEST_METHOD"] == 'POST') {
$body = file_get_contents('php://input');
switch(strtolower($_SERVER['HTTP_CONTENT_TYPE'])) {
case "application/json":
$job = json_decode($body);
break;
case "text/xml":
// parsing here
break;
}
// Validate input
// Create new Resource
$id = create($job); // Returns id of 456
$json = json_encode(array('id' => $id));
http_response_code(201); // Created
$site = 'https://api.example.com';
header("Location: $site/" . $_SERVER['REQUEST_URI'] . "/$id");
header('Content-Type: application/json');
print $json;
}
To generate this output:
HTTP/1.1 201 Created
Location: https://api.example.com/jobs/456
Content-Type: application/json
Content-Length: 15
{
"id": 456
}
If the client is allowed to specify (and knows) the ID, use PUT instead:
PUT /v1/jobs/456 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 49
{
"position": {
"title": "PHP Developer"
}
}
Use this PHP code:
if ($_SERVER["REQUEST_METHOD"] == 'PUT') {
$body = file_get_contents('php://input');
switch(strtolower($_SERVER['HTTP_CONTENT_TYPE'])) {
case "application/json":
$job = json_decode($body);
break;
case "text/xml":
// parsing here
break;
}
// Validate input
// Create new Resource
$request = explode('/', substr($_SERVER['PATH_INFO'], 1));
$resource = array_shift($request);
$id = create($job, $request[0]); // Uses id from request
$json = json_encode(array('id' => $id));
http_response_code(201); // Created
$site = 'https://api.example.com';
header("Location: $site/" . $_SERVER['REQUEST_URI'] );
print $json;
}
Discussion
The standard way to add new records is by HTTP POSTing to a parent (or collection) resource. For example, to add a new job to the system, POST the data to /v1/jobs (in contrast to a specific resource such as /v1/jobs/123).
It’s the job of the server to parse the data, validate it, and assign an ID for the newly recreated record. For example:
if ($_SERVER["REQUEST_METHOD"] == 'POST') {
$body = file_get_contents('php://input');
switch(strtolower($_SERVER['HTTP_CONTENT_TYPE'])) {
case "application/json":
$job = json_decode($body);
break;
case "text/xml":
$job = simplexml_load_string($body);
break;
}
// Validate input
// Create new Resource
$id = create($job); // Returns id
}
PHP automatically parses standard HTML form data into $_POST. However, for most REST APIs, the POST body is in JSON (or XML or another format).
This requires you to read and parse the data yourself. The raw POST body is available using the special stream php://input; slurp it into a variable using file_get_contents().
Next, check the Content-Type HTTP header to learn what data format was sent. You do this via the $_SERVER['HTTP_CONTENT_TYPE'] superglobal variable. You may only support one format, such as JSON, but you should still confirm that the client is using that format.
Based on the Content-Type, use the appropriate function, such as json_decode() or simplexml_load_string(), to deserialize the data to PHP.
Now you can perform the necessary business logic to validate the input, add the resource, and generate a unique ID for that record.
If everything goes OK, signal success and return the location of the new resource:
http_response_code(201); // Created
$site = 'https://api.example.com';
header("Location: $site/" . $_SERVER['REQUEST_URI'] . "/$id");
print $json;
A status code of 201 signifies a resource has been created, which is preferable over the more generic 200 (OK). Additionally, it’s a best practice to return the location, either via the Location HTTP header or in the body. The first is more RESTful, but some clients find it easier to parse the results in a body than from a header. The Location HTTP must be an absolute URL.
If there’s a problem with how the request was sent, return a status code in the 4xx range. Whenever possible, you should return a message explaining how the client can fix her request.
For example, if a required field is missing or the document is otherwise well-formed but has an incorrect schema, return 422 (Unprocessable Entity):
http_response_code(422); // Unprocessable Entity
$error_body = [
"error" => "12",
"message" => "Missing required field: job title"
];
print json_encode($error_body);
If you cannot find a specific error code for the problem, then a response code of 400 (Bad Request) is always OK.
If your system cannot definitively say whether a request is or isn’t OK, return 202 (Accepted). This is the appropriate way to passive-agressively signal your noncommittal behavior. This is most frequently used when you process requests via an asynchronous queue, so the REST server is primarily handing off the request to another system, but that system doesn’t immediately return a response.
When the client knows the ID associated with the new record (instead of having you assign one), have them PUT directly to the location (instead of to the parent resource). For example:
PUT /v1/jobs/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 49
{
"position": {
"title": "PHP Developer"
}
}
if ($_SERVER["REQUEST_METHOD"] == 'PUT') {
$body = file_get_contents('php://input');
switch(strtolower($_SERVER['HTTP_CONTENT_TYPE'])) {
case "application/json":
$job = json_decode($body);
break;
case "text/xml":
// parsing here
break;
}
// Validate input
// Create new Resource
$request = explode('/', substr($_SERVER['PATH_INFO'], 1));
$resource = array_shift($request);
$id = create($job, $request[0]); // Uses id from request
$json = json_encode(array('id' => $id));
http_response_code(201); // Created
$site = 'https://api.example.com';
header("Location: $site/" . $_SERVER['REQUEST_URI'] . "/$id");
print $json;
}
Regardless whether the request is a PUT or a POST, the same set of responses are appropriate. A PUT to an already existing resource (for instance, if /v1/job/123was already defined) will overwrite what’s there. In that case, return a 200 OK response code instead of 201 Created (unless you’re using some form of versioning to protect against this). If you do have a versioning conflict, return a 409 Conflict response code.
POST requests are not safe, so they are allowed to have side effects. Additionally, they are not idempotent, so making the same request multiple times causes multiple resources to be re-created. This is in contrast to PUT requests, which are not safe, but are idempotent. Making the same PUT request more than once is equivalent to making it one time.
No comments:
Post a Comment