There are several considerations when implementing a REST server. The answers to these three questions will then let you define your REST service:
Application\Web\AbstractHttpApplication\Web\RequestApplication\Web\ReceivedApplication\Web\Response response class, based on AbstractHttp. The primary difference between this class and the others is that it accepts an instance of Application\Web\Request as an argument. The primary work is accomplished in the __construct() method. It's also important to set the Content-Type header and status:namespace Application\Web;
class Response extends AbstractHttp
{
public function __construct(Request $request = NULL,
$status = NULL, $contentType = NULL)
{
if ($request) {
$this->uri = $request->getUri();
$this->data = $request->getData();
$this->method = $request->getMethod();
$this->cookies = $request->getCookies();
$this->setTransport();
}
$this->processHeaders($contentType);
if ($status) {
$this->setStatus($status);
}
}
protected function processHeaders($contentType)
{
if (!$contentType) {
$this->setHeaderByKey(self::HEADER_CONTENT_TYPE,
self::CONTENT_TYPE_JSON);
} else {
$this->setHeaderByKey(self::HEADER_CONTENT_TYPE,
$contentType);
}
}
public function setStatus($status)
{
$this->status = $status;
}
public function getStatus()
{
return $this->status;
}
}Application\Web\Rest\Server class. You may be surprised at how simple it is. The real work is done in the associated API class:namespace Application\Web\Rest;
use Application\Web\ { Request, Response, Received };
class Server
{
protected $api;
public function __construct(ApiInterface $api)
{
$this->api = $api;
}listen() method that serves as a target for the request. The heart of the server implementation is this line of code:$jsonData = json_decode(file_get_contents('php://input'),true);public function listen()
{
$request = new Request();
$response = new Response($request);
$getPost = $_REQUEST ?? array();
$jsonData = json_decode(
file_get_contents('php://input'),true);
$jsonData = $jsonData ?? array();
$request->setData(array_merge($getPost,$jsonData));We have also added a provision for authentication. Otherwise, anybody could make requests and obtain potentially sensitive data. You will note that we do not have the server class performing authentication; rather, we leave it to the API class:
if (!$this->api->authenticate($request)) {
$response->setStatus(Request::STATUS_401);
echo $this->api::ERROR;
exit;
}GET, PUT, POST,and DELETE:$id = $request->getData()[$this->api::ID_FIELD] ?? NULL;
switch (strtoupper($request->getMethod())) {
case Request::METHOD_POST :
$this->api->post($request, $response);
break;
case Request::METHOD_PUT :
$this->api->put($request, $response);
break;
case Request::METHOD_DELETE :
$this->api->delete($request, $response);
break;
case Request::METHOD_GET :
default :
// return all if no params
$this->api->get($request, $response);
}$this->processResponse($response); echo json_encode($response->getData()); }
processResponse() method sets headers and makes sure the result is packaged as an Application\Web\Response object:protected function processResponse($response)
{
if ($response->getHeaders()) {
foreach ($response->getHeaders() as $key => $value) {
header($key . ': ' . $value, TRUE,
$response->getStatus());
}
}
header(Request::HEADER_CONTENT_TYPE
. ': ' . Request::CONTENT_TYPE_JSON, TRUE);
if ($response->getCookies()) {
foreach ($response->getCookies() as $key => $value) {
setcookie($key, $value);
}
}
}get(), put(), and so on are represented, and that all such methods accept request and response objects as arguments. You might notice that we have added a generateToken() method that uses the PHP 7 random_bytes() function to generate a truly random series of 16 bytes:namespace Application\Web\Rest;
use Application\Web\ { Request, Response };
abstract class AbstractApi implements ApiInterface
{
const TOKEN_BYTE_SIZE = 16;
protected $registeredKeys;
abstract public function get(Request $request,
Response $response);
abstract public function put(Request $request,
Response $response);
abstract public function post(Request $request,
Response $response);
abstract public function delete(Request $request,
Response $response);
abstract public function authenticate(Request $request);
public function __construct($registeredKeys, $tokenField)
{
$this->registeredKeys = $registeredKeys;
}
public static function generateToken()
{
return bin2hex(random_bytes(self::TOKEN_BYTE_SIZE));
}
}namespace Application\Web\Rest;
use Application\Web\ { Request, Response };
interface ApiInterface
{
public function get(Request $request, Response $response);
public function put(Request $request, Response $response);
public function post(Request $request, Response $response);
public function delete(Request $request, Response $response);
public function authenticate(Request $request);
}AbstractApi. This class leverages database classes defined in Chapter 5, Interacting with a Database:namespace Application\Web\Rest;
use Application\Web\ { Request, Response, Received };
use Application\Entity\Customer;
use Application\Database\ { Connection, CustomerService };
class CustomerApi extends AbstractApi
{
const ERROR = 'ERROR';
const ERROR_NOT_FOUND = 'ERROR: Not Found';
const SUCCESS_UPDATE = 'SUCCESS: update succeeded';
const SUCCESS_DELETE = 'SUCCESS: delete succeeded';
const ID_FIELD = 'id'; // field name of primary key
const TOKEN_FIELD = 'token'; // field used for authentication
const LIMIT_FIELD = 'limit';
const OFFSET_FIELD = 'offset';
const DEFAULT_LIMIT = 20;
const DEFAULT_OFFSET = 0;
protected $service;
public function __construct($registeredKeys,
$dbparams, $tokenField = NULL)
{
parent::__construct($registeredKeys, $tokenField);
$this->service = new CustomerService(
new Connection($dbparams));
}getDataByKey() to retrieve data items. The actual database interaction is performed by the service class. You might also notice that in all cases, we set an HTTP status code to inform the client of success or failure. In the case of get(), we look for an ID parameter. If received, we deliver information on a single customer only. Otherwise, we deliver a list of all customers using limit and offset:public function get(Request $request, Response $response)
{
$result = array();
$id = $request->getDataByKey(self::ID_FIELD) ?? 0;
if ($id > 0) {
$result = $this->service->
fetchById($id)->entityToArray();
} else {
$limit = $request->getDataByKey(self::LIMIT_FIELD)
?? self::DEFAULT_LIMIT;
$offset = $request->getDataByKey(self::OFFSET_FIELD)
?? self::DEFAULT_OFFSET;
$result = [];
$fetch = $this->service->fetchAll($limit, $offset);
foreach ($fetch as $row) {
$result[] = $row;
}
}
if ($result) {
$response->setData($result);
$response->setStatus(Request::STATUS_200);
} else {
$response->setData([self::ERROR_NOT_FOUND]);
$response->setStatus(Request::STATUS_500);
}
}put() method is used to insert customer data:public function put(Request $request, Response $response)
{
$cust = Customer::arrayToEntity($request->getData(),
new Customer());
if ($newCust = $this->service->save($cust)) {
$response->setData(['success' => self::SUCCESS_UPDATE,
'id' => $newCust->getId()]);
$response->setStatus(Request::STATUS_200);
} else {
$response->setData([self::ERROR]);
$response->setStatus(Request::STATUS_500);
}
}post() method is used to update existing customer entries:public function post(Request $request, Response $response)
{
$id = $request->getDataByKey(self::ID_FIELD) ?? 0;
$reqData = $request->getData();
$custData = $this->service->
fetchById($id)->entityToArray();
$updateData = array_merge($custData, $reqData);
$updateCust = Customer::arrayToEntity($updateData,
new Customer());
if ($this->service->save($updateCust)) {
$response->setData(['success' => self::SUCCESS_UPDATE,
'id' => $updateCust->getId()]);
$response->setStatus(Request::STATUS_200);
} else {
$response->setData([self::ERROR]);
$response->setStatus(Request::STATUS_500);
}
}delete() removes a customer entry:public function delete(Request $request, Response $response)
{
$id = $request->getDataByKey(self::ID_FIELD) ?? 0;
$cust = $this->service->fetchById($id);
if ($cust && $this->service->remove($cust)) {
$response->setData(['success' => self::SUCCESS_DELETE,
'id' => $id]);
$response->setStatus(Request::STATUS_200);
} else {
$response->setData([self::ERROR_NOT_FOUND]);
$response->setStatus(Request::STATUS_500);
}
}authenticate() to provide, in this example, a low-level mechanism to protect API usage:public function authenticate(Request $request)
{
$authToken = $request->getDataByKey(self::TOKEN_FIELD)
?? FALSE;
if (in_array($authToken, $this->registeredKeys, TRUE)) {
return TRUE;
} else {
return FALSE;
}
}
}Define the following classes, which were discussed in the previous recipe:
Application\Web\AbstractHttpApplication\Web\RequestApplication\Web\ReceivedYou can then define the following classes, described in this recipe, summarized in this table:
|
Class Application\Web\* |
Discussed in these steps |
|---|---|
|
|
2 |
|
|
3 - 8 |
|
|
9 |
|
|
10 |
|
|
11 - 16 |
You are now free to develop your own API class. If you choose to follow the illustration Application\Web\Rest\CustomerApi, however, you will need to also be sure to implement these classes, covered in Chapter 5, Interacting with a Database:
Application\Entity\CustomerApplication\Database\ConnectionApplication\Database\CustomerServiceYou can now define a chap_07_simple_rest_server.php script that invokes the REST server:
<?php $dbParams = include __DIR__ . '/../../config/db.config.php'; require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\Web\Rest\Server; use Application\Web\Rest\CustomerApi; $apiKey = include __DIR__ . '/api_key.php'; $server = new Server(new CustomerApi([$apiKey], $dbParams, 'id')); $server->listen();
You can then use the built-in PHP 7 development server to listen on port 8080 for REST requests:
php -S localhost:8080 chap_07_simple_rest_server.php
To test your API, use the Application\Web\Rest\AbstractApi::generateToken() method to generate an authentication token that you can place in an api_key.php file, something like this:
<?php return '79e9b5211bbf2458a4085707ea378129';
You can then use a generic API client (such as the one described in the previous recipe), or a browser plugin such as RESTClient by Chao Zhou (see http://restclient.net/ for more information) to generate sample requests. Make sure you include the token for your request, otherwise the API as defined will reject the request.
Here is an example of a POST request for ID 1, which sets the balance field to a value of 888888:

There are a number of libraries that help you implement a REST server. One of my favorites is an example implementing a REST server in a single file: https://www.leaseweb.com/labs/2015/10/creating-a-simple-rest-api-in-php/
Various frameworks, such as CodeIgniter and Zend Framework, also have REST server implementations.