Method in place now to automatically format the response type based on content negotiation with the request. Started docs.

This commit is contained in:
Lonnie Ezell 2016-11-24 00:02:36 -06:00
parent 96b85c980c
commit f2c6cb2e82
No known key found for this signature in database
GPG Key ID: 88F86F2034554774
8 changed files with 403 additions and 10 deletions

View File

@ -0,0 +1,69 @@
<?php namespace Config;
use CodeIgniter\Config\BaseConfig;
class API extends BaseConfig
{
/*
|--------------------------------------------------------------------------
| Available Response Formats
|--------------------------------------------------------------------------
|
| When you perform content negotiation with the request, these are the
| available formats that your application supports. This is currently
| only used with the API\ResponseTrait. A valid Formatter must exist
| for the specified format.
|
| These formats are only checked when the data passed to the respond()
| method is an array.
|
*/
public $supportedResponseFormats = [
'application/json',
'application/xml'
];
/*
|--------------------------------------------------------------------------
| Formatters
|--------------------------------------------------------------------------
|
| Lists the class to use to format responses with of a particular type.
| For each mime type, list the class that should be used. Formatters
| can be retrieved through the getFormatter() method.
|
*/
public $formatters = [
'application/json' => \CodeIgniter\API\JSONFormatter::class,
'application/xml' => \CodeIgniter\API\XMLFormatter::class
];
//--------------------------------------------------------------------
/**
* A Factory method to return the appropriate formatter for the given mime type.
*
* @param string $mime
*
* @return mixed
*/
public function getFormatter(string $mime)
{
if (! array_key_exists($mime, $this->formatters))
{
throw new \InvalidArgumentException('No Formatter defined for mime type: '. $mime);
}
$class = $this->formatters[$mime];
if (! class_exists($class))
{
throw new \BadMethodCallException($class.' is not a valid Formatter.');
}
return new $class();
}
//--------------------------------------------------------------------
}

View File

@ -0,0 +1,91 @@
<?php namespace App\Controllers;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Controller;
use Config\Database;
class Checks extends Controller
{
use ResponseTrait;
public function index()
{
session()->start();
}
public function escape()
{
$db = Database::connect();
$db->initialize();
$jobs = $db->table('job')
->whereNotIn('name', ['Politician', 'Accountant'])
->get()
->getResult();
die(var_dump($jobs));
}
public function password()
{
$db = Database::connect();
$db->initialize();
$result = $db->table('misc')
->insert([
'key' => 'password',
'value' => '$2y$10$ErQlCj/Mo10il.FthAm0WOjYdf3chZEGPFqaPzjqOX2aj2uYf5Ihq'
]);
die(var_dump($result));
}
public function forms()
{
helper('form');
var_dump(form_open());
}
public function api()
{
$data = array(
"total_users" => 3,
"users" => array(
array(
"id" => 1,
"name" => "Nitya",
"address" => array(
"country" => "India",
"city" => "Kolkata",
"zip" => 700102,
)
),
array(
"id" => 2,
"name" => "John",
"address" => array(
"country" => "USA",
"city" => "Newyork",
"zip" => "NY1234",
)
),
array(
"id" => 3,
"name" => "Viktor",
"address" => array(
"country" => "Australia",
"city" => "Sydney",
"zip" => 123456,
)
),
)
);
return $this->respond($data);
}
}

View File

@ -0,0 +1,13 @@
<?php namespace CodeIgniter\API;
interface FormatterInterface
{
/**
* Takes the given data and formats it.
*
* @param $data
*
* @return mixed
*/
public function format(array $data);
}

View File

@ -0,0 +1,49 @@
<?php namespace CodeIgniter\API;
class JSONFormatter implements FormatterInterface
{
/**
* The error strings to use if encoding hits an error.
*
* @var array
*/
protected $errors = [
JSON_ERROR_NONE => 'No error has occurred',
JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
JSON_ERROR_SYNTAX => 'Syntax error',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
];
//--------------------------------------------------------------------
/**
* Takes the given data and formats it.
*
* @todo Handle converting entity classes from db results, since we handle loading/saving those...
*
* @param $data
*
* @return mixed
*/
public function format(array $data)
{
$options = ENVIRONMENT == 'production'
? JSON_NUMERIC_CHECK
: JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT;
$result = json_encode($data, 512, $options);
// If result is NULL, then an error happened.
// Let them know.
if ($result === null)
{
throw new \RuntimeException($this->errors[json_last_error()]);
}
return utf8_encode($result);
}
//--------------------------------------------------------------------
}

View File

@ -7,6 +7,7 @@
* consistent HTTP responses under a variety of common
* situations when working as an API.
*
* @property $request CodeIgniter\HTTP\Request
* @property $response CodeIgniter\HTTP\Response
*
* @package CodeIgniter\API
@ -100,7 +101,7 @@ trait ResponseTrait
{
if (! is_array($messages))
{
$message = [$messages];
$messages = [$messages];
}
$response = [
@ -279,9 +280,16 @@ trait ResponseTrait
return $data;
}
// @todo Implement a formatting library so we have other options
$this->setContentType('json');
return json_encode($data);
$config = new \Config\API();
// Determine correct response type through content negotiation
$format = $this->request->negotiate('media', $config->supportedResponseFormats);
$this->setContentType($format);
$formatter = $config->getFormatter($format);
return $formatter->format($data);
}
//--------------------------------------------------------------------
@ -296,14 +304,14 @@ trait ResponseTrait
{
switch ($type)
{
case 'html':
$this->response->setContentType('text/html');
case 'text/html':
$this->response = $this->response->setContentType('text/html');
break;
case 'json':
$this->response->setContentType('application/json');
case 'application/json':
$this->response = $this->response->setContentType('application/json');
break;
case 'xml':
$this->response->setContentType('text/xml');
case 'application/xml':
$this->response = $this->response->setContentType('text/xml');
break;
}
}

View File

@ -0,0 +1,64 @@
<?php namespace CodeIgniter\API;
class XMLFormatter implements FormatterInterface
{
/**
* Takes the given data and formats it.
*
* @param $data
*
* @return mixed
*/
public function format(array $data)
{
$result = null;
// SimpleXML is installed but default
// but best to check, and then provide a fallback.
if (! extension_loaded('simplexml'))
{
throw new \RuntimeException('The SimpleXML extension is required to format XML.');
}
$output = new \SimpleXMLElement("<?xml version=\"1.0\"?><response></response>");
$this->arrayToXML($data, $output);
return $output->asXML();
}
//--------------------------------------------------------------------
/**
* A recursive method to convert an array into a valid XML string.
*
* Written by CodexWorld. NOTE: waiting on confirmation if we can use....
* @see http://www.codexworld.com/convert-array-to-xml-in-php/
*
* @param array $data
* @param $output
*/
protected function arrayToXML(array $data, &$output)
{
foreach ($data as $key => $value)
{
if (is_array($value))
{
if (! is_numeric($key))
{
$subnode = $output->addChild("$key");
$this->arrayToXML($value, $subnode);
} else
{
$subnode = $output->addChild("item{$key}");
$this->arrayToXML($value, $subnode);
}
} else
{
$output->addChild("$key", htmlspecialchars("$value"));
}
}
}
//--------------------------------------------------------------------
}

View File

@ -0,0 +1,98 @@
##################
API Response Trait
##################
Much of modern PHP development requires building API's, whether simply to provide data for a javascript-heavy
single page application, or as a standalone product. CodeIgniter provides an API Response trait that can be
used with any controller to make common response types simple, with no need to remember which HTTP status code
should be returned for which response types.
.. contents:: Page Contents
:local:
*************
Example Usage
*************
The following example shows a common usage pattern within your controllers.
::
<?php namespace App\Controllers;
class Users extends \CodeIgniter\Controller
{
use CodeIgniter\API\ResponseTrait;
public function createUser()
{
$model = new UserModel();
$user = $model->save($this->request->getPost());
// Respond with 201 status code
return $this->respondCreated();
}
}
In this example, an HTTP status code of 201 is returned, with the generic status message, 'Created'. Methods
exist for the most common use cases::
// Generic response method
respond($data, 200);
// Generic failure response
fail($errors, 400);
// Item created response
respondCreated($data);
// Item successfully deleted
respondDeleted($data);
// Client isn't authorized
failUnauthorized($description);
// Forbidden action
failForbidden($description);
// Resource Not Found
failNotFound($description);
// Data did not validate
failValidationError($description);
// Resource already exists
failResourceExists($description);
// Resource previously deleted
failResourceGone($description);
// Client made too many requests
failTooManyRequests($description);
***********************
Handling Response Types
***********************
When you pass your data in any of these methods, they will determine the data type to format the results as based on
the following criteria:
* If $data is a string, it will be treated as HTML to send back to the client
* If $data is an array, it will try to negotiate the content type with what the client asked for, defaulting to JSON
if nothing else has been specified within Config\API.php, the ``$supportedResponseFormats`` property.
===============
Class Reference
===============
.. php:method:: respond($data[, $statusCode=200[, $message='']])
:param mixed $data: The data to return to the client. Either string or array.
:param int $statusCode: The HTTP status code to return. Defaults to 200
:param string $message: A custom "reason" message to return.
This is the method used by all other methods in this trait to return a response to the client.
The ``$data`` element can be either a string or an array. By default, a string will be returned as HTML,
while an array will be run through json_encode and returned as JSON, unless :doc:`Content Negotiation </libraries/content_negotiation>`
determines it should be returned in a different format.
If a ``$message`` string is passed, it will be used in place of the standard IANA reason codes for the
response status. Not every client will respect the custom codes, though, and will use the IANA standards
that match the status code.
.. note:: Since it sets the status code and body on the active Response instance, this should always
be the final method in the script execution.

View File

@ -5,6 +5,7 @@ Library Reference
.. toctree::
:titlesonly:
api_responses
benchmark
caching
cli