#55 - PHP API with Versioning
Data: 2019-04-06 12:00 - PHP
PHP class to define an API with versioning.
<?php
class API {
const removedEndpoint = -1;
private static $_endpoints = array();
private static $_currentEndpointVersions;
private static $_input;
private static $_lastVersion = 0;
public static function getEndpoints() {
return self::$_endpoints;
}
public static function endpoint($name, $version, $function) {
if (!array_key_exists($name, self::$_endpoints))
self::$_endpoints[$name] = array();
if (array_key_exists($version, self::$_endpoints[$name]))
trigger_error("Endpoint '$name' has multiple '$version' versions", E_USER_ERROR);
self::$_endpoints[$name][$version] = $function;
self::$_lastVersion = max(self::$_lastVersion, $version);
}
private static function bestVersion() {
$match = preg_match('/^application\/vnd\.' . API_CONTENT_TYPE_NAME . '\.([\d]+)\+json$/', $_SERVER['HTTP_ACCEPT'], $matches);
if ($match !== 0 && intval($matches[1]) == $matches[1])
return min(self::$_lastVersion, $matches[1]);
return self::$_lastVersion;
}
public static function process() {
$uri = substr($_SERVER['REQUEST_URI'], strlen(API_BASE_URL));
if (($query_start = strpos($uri, '?')) !== false)
$uri = substr($uri, 0, $query_start);
$cleanURI = str_replace(chr(0), '', trim($uri, '/'));
$split = preg_split('/\//', $cleanURI, NULL, PREG_SPLIT_NO_EMPTY);
try {
if (empty($split))
throw new MethodMissingException();
if (count($split) > 1)
$input = array_slice($split, 1);
else {
$rawInput = file_get_contents("php://input");
$input = @json_decode($rawInput, true);
if ($rawInput !== '' && $input === NULL && json_last_error() !== JSON_ERROR_NONE)
throw new MalformedRawInputException();
}
$version = self::bestVersion();
$endpoint = $split[0];
$result = API::handle($endpoint, $version, $input);
header('Content-Type: application/vnd.' . API_CONTENT_TYPE_NAME . '.' . $version . '+json');
echo json_encode($result);
exit();
} catch (APIException $e) {
http_response_code($e->getHTTPCode());
echo json_encode(['error' => $e->getMessage(), 'code' => $e->getCode()]);
exit();
}
http_response_code(500);
echo json_encode(['error' => 'unknown server error', 'code' => 0]);
exit();
}
public static function handle($name, $version, $input) {
if (!array_key_exists($name, self::$_endpoints)) {
throw new UnknownMethodException();
}
self::$_currentEndpointVersions = &self::$_endpoints[$name];
ksort(self::$_currentEndpointVersions);
self::$_currentEndpointVersions = array_reverse(self::$_currentEndpointVersions, true);
reset(self::$_currentEndpointVersions);
while (key(self::$_currentEndpointVersions) > $version) {
$next = next(self::$_currentEndpointVersions);
if ($next === FALSE)
throw new UnknownMethodException();
}
self::$_input = $input;
$function = current(self::$_currentEndpointVersions);
if ($function === self::removedEndpoint)
throw new UnknownMethodException();
return $function();
}
private static function totalKey($inputKey, $key) {
return $inputKey === '' ? $key : ($inputKey . '.' . $key);
}
private static function verifyAgainst($structure, $input, $inputKey) {
if (!is_array($input)) {
if ($inputKey === '')
$input = array();
else
throw new ParameterMalformedException($inputKey);
}
foreach ($structure as $key => $value) {
if (!array_key_exists($key, $input))
throw new ParameterMissingException(self::totalKey($inputKey, $key));
$inputValue = $input[$key];
if (is_array($value))
self::verifyAgainst($value, $inputValue, self::totalKey($inputKey, $key));
else if (!is_string($inputValue) && !is_numeric($inputValue))
throw new MalformedInputException();
else if (preg_match('%^'. $value . '$%', $inputValue) === 0)
throw new ParameterMalformedException(self::totalKey($inputKey, $key));
}
}
public static function verify($structure) {
self::verifyAgainst($structure, self::$_input, '');
}
public static function input($key = NULL) {
if ($key !== NULL) {
if (!array_key_exists($key, self::$_input))
trigger_error("'$key' does not exist in input. missing verification?", E_USER_ERROR);
return self::$_input[$key];
}
return self::$_input;
}
public static function older() {
$function = next(self::$_currentEndpointVersions);
if ($function === FALSE)
trigger_error("Endpoint does not have any older version.", E_USER_ERROR);
$foundRemoved = false;
if ($function === self::removedEndpoint) {
$foundRemoved = true;
$function = next(self::$_currentEndpointVersions);
if ($function === FALSE)
trigger_error('Endpoint has removed method as oldest version.', E_USER_ERROR);
if ($function === self::removedEndpoint)
trigger_error('Endpoint has multiple removed methods in a row.', E_USER_ERROR);
}
$result = $function();
prev(self::$_currentEndpointVersions);
if ($foundRemoved)
prev(self::$_currentEndpointVersions);
return $result;
}
}