С чего все началось
Разрабатывая проект, я столкнулся с необходимостью организации клиент-серверного взаимодействия приложений на платформах iOS и Android с моим сайтом на котором хранилась вся информация — собственно БД на mysql, картинки, файлы и другой контент.
Задачи которые нужно было решать — достаточно простые:
регистрация/авторизация пользователя;
отправка/получение неких данных (например список товаров).
И тут-то мне захотелось написать свой API для взаимодействия с серверной стороной — большей своей частью для практического интереса.
Входные данные
В своем распоряжении я имел:
Сервер — Apache, PHP 5.0, MySQL 5.0
Клиент — Android, iOS устройства, любой браузер
Я решил, что для запросов к серверу и ответов от него буду использовать JSON формат данных — за его простоту и нативную поддержку в PHP и Android. Здесь меня огорчила iOS — у нее нет нативной поддержки JSON (тут пришлось использовать стороннюю разработку).
Так же было принято решение, что запросы можно будет отсылать как через GET так и через POST запросы (здесь помог $_REQUEST в PHP). Такое решение позволило проводить тестирование API через GET запросы в любом доступном браузере.
Внешний вид запросов решено было сделать таким:
http://[адрес сервера]/[путь к папке api]/?[название_api].[название_метода]=[JSON вида {«Hello»:«Hello world»}]
Путь к папке api — каталог на который нужно делать запросы, в корне которого лежит файл index.php — он и отвечает за вызов функций и обработку ошибок
Название api — для удобства я решил разделить API группы — пользователь, база данных, конент и тд. В таком случае каждый api получил свое название
Название метода — имя метода который нужно вызвать в указанном api
JSON — строковое представление JSON объекта для параметров метода
Скелет API
Скелет API на серверной стороне состоит из нескольких базовых классов:
index.php — индексный файл каталога в Apache на него приходятся все вызовы API, он осуществляет парсинг параметров и вызов API методов
MySQLiWorker — класс-одиночка для работы с базой MySQL через MySQLi
apiBaseCalss.php — родительский класс для всех API в системе — каждый API должен быть наследован от этого класса для корректной работы
apiEngine.php — основной класс системы — осуществляет разбор переданных параметров (после их предварительного парсинга в index.php) подключение нужного класса api (через require_once метод), вызов в нем нужного метода и возврат результата в JSON формате
apiConstants.php — класс с константами для api вызовов и передачи ошибок
apitest.php — тестовый api для тестирования новых методов перед их включением в продакшн версию
Весь механизм выглядит следующим образом:
Мы делаем запрос на сервер — к примеру www.example.com/api/?apitest.helloWorld={}
На серверной стороне файл index.php — производит парсинг переданных параметров. Index.php берет всегда только первый элемент из списка переданных параметров $_REQUEST — это значит что конструкция вида www.example.com/api/?apitest.helloWorld={}&apitest.helloWorld2 — произведет вызов только метода helloWorld в apitest. Вызова же метода helloWorld2 непроизойдет
Теперь подробней о каждом
Я попробовал достаточно документировать файлы, чтобы не занимать много место под текст. Однако в тех файлах где нет комментариев, я все ж приведу описание.
Index.php
Как уже говорил раньше это входной индексный файл для Apache а значит все вызовы вида www.example.com/api/ будет принимать он.
<?php
header('Content-type: text/html; charset=UTF-8');
if (count($_REQUEST)>0){
require_once 'apiEngine.php';
foreach ($_REQUEST as $apiFunctionName => $apiFunctionParams) {
$APIEngine=new APIEngine($apiFunctionName,$apiFunctionParams);
echo $APIEngine->callApiFunction();
break;
}
}else{
$jsonError->error='No function called';
echo json_encode($jsonError);
}
?>
Первым делом устанавливаем тип контента — text/html (потом можно сменить в самих методах) и кодировку — UTF-8.
Дальше проверяем, что у нас что-то запрашивают. Если нет то выводим JSON c ошибкой.
Если есть параметры запроса, то подключаем файл движка API — apiEngine.php и создаем класс движка с переданными параметрами и делаем вызов api метода.
Выходим из цикла так как мы решили что будем обрабатывать только один вызов.
apiEngine.php
Вторым по важности является класс apiEngine — он представляет собой движок для вызова api и их методов.
<?php
require_once('MySQLiWorker.php');
require_once ('apiConstants.php');
class APIEngine {
private $apiFunctionName;
private $apiFunctionParams;
//Статичная функция для подключения API из других API при необходимости в методах
static function getApiEngineByName($apiName) {
require_once 'apiBaseClass.php';
require_once $apiName . '.php';
$apiClass = new $apiName();
return $apiClass;
}
//Конструктор
//$apiFunctionName - название API и вызываемого метода в формате apitest_helloWorld
//$apiFunctionParams - JSON параметры метода в строковом представлении
function __construct($apiFunctionName, $apiFunctionParams) {
$this->apiFunctionParams = stripcslashes($apiFunctionParams);
//Парсим на массив из двух элементов [0] - название API, [1] - название метода в API
$this->apiFunctionName = explode('_', $apiFunctionName);
}
//Создаем JSON ответа
function createDefaultJson() {
$retObject = json_decode('{}');
$response = APIConstants::$RESPONSE;
$retObject->$response = json_decode('{}');
return $retObject;
}
//Вызов функции по переданным параметрам в конструкторе
function callApiFunction() {
$resultFunctionCall = $this->createDefaultJson();//Создаем JSON ответа
$apiName = strtolower($this->apiFunctionName[0]);//название API проиводим к нижнему регистру
if (file_exists($apiName . '.php')) {
$apiClass = APIEngine::getApiEngineByName($apiName);//Получаем объект API
$apiReflection = new ReflectionClass($apiName);//Через рефлексию получем информацию о классе объекта
try {
$functionName = $this->apiFunctionName[1];//Название метода для вызова
$apiReflection->getMethod($functionName);//Провераем наличие метода
$response = APIConstants::$RESPONSE;
$jsonParams = json_decode($this->apiFunctionParams);//Декодируем параметры запроса в JSON объект
if ($jsonParams) {
if (isset($jsonParams->responseBinary)){//Для возможности возврата не JSON, а бинарных данных таких как zip, png и др. контетнта
return $apiClass->$functionName($jsonParams);//Вызываем метод в API
}else{
$resultFunctionCall->$response = $apiClass->$functionName($jsonParams);//Вызыаем метод в API который вернет JSON обект
}
} else {
//Если ошибка декодирования JSON параметров запроса
$resultFunctionCall->errno = APIConstants::$ERROR_ENGINE_PARAMS;
$resultFunctionCall->error = 'Error given params';
}
} catch (Exception $ex) {
//Непредвиденное исключение
$resultFunctionCall->error = $ex->getMessage();
}
} else {
//Если запрашиваемый API не найден
$resultFunctionCall->errno = APIConstants::$ERROR_ENGINE_PARAMS;
$resultFunctionCall->error = 'File not found';
$resultFunctionCall->REQUEST = $_REQUEST;
}
return json_encode($resultFunctionCall);
}
}
?>
apiConstants.php
Данный класс используется только для хранения констант.
<?php
class APIConstants {
//Результат запроса - параметр в JSON ответе
public static $RESULT_CODE="resultCode";
//Ответ - используется как параметр в главном JSON ответе в apiEngine
public static $RESPONSE="response";
//Нет ошибок
public static $ERROR_NO_ERRORS = 0;
//Ошибка в переданных параметрах
public static $ERROR_PARAMS = 1;
//Ошибка в подготовке SQL запроса к базе
public static $ERROR_STMP = 2;
//Ошибка запись не найдена
public static $ERROR_RECORD_NOT_FOUND = 3;
//Ошибка в параметрах запроса к серверу. Не путать с ошибкой переданных параметров в метод
public static $ERROR_ENGINE_PARAMS = 100;
//Ошибка zip архива
public static $ERROR_ENSO_ZIP_ARCHIVE = 1001;
}
?>
MySQLiWorker.php
Класс-одиночка для работы с базой. В прочем это обычный одиночка — таких примеров в сети очень много.
<?php
class MySQLiWorker {
protected static $instance; // object instance
public $dbName;
public $dbHost;
public $dbUser;
public $dbPassword;
public $connectLink = null;
//Чтобы нельзя было создать через вызов new MySQLiWorker
private function __construct() { /* ... */
}
//Чтобы нельзя было создать через клонирование
private function __clone() { /* ... */
}
//Чтобы нельзя было создать через unserialize
private function __wakeup() { /* ... */
}
//Получаем объект синглтона
public static function getInstance($dbName, $dbHost, $dbUser, $dbPassword) {
if (is_null(self::$instance)) {
self::$instance = new MySQLiWorker();
self::$instance->dbName = $dbName;
self::$instance->dbHost = $dbHost;
self::$instance->dbUser = $dbUser;
self::$instance->dbPassword = $dbPassword;
self::$instance->openConnection();
}
return self::$instance;
}
//Определяем типы параметров запроса к базе и возвращаем строку для привязки через ->bind
function prepareParams($params) {
$retSTMTString = '';
foreach ($params as $value) {
if (is_int($value) || is_double($value)) {
$retSTMTString.='d';
}
if (is_string($value)) {
$retSTMTString.='s';
}
}
return $retSTMTString;
}
//Соединяемся с базой
public function openConnection() {
if (is_null($this->connectLink)) {
$this->connectLink = new mysqli($this->dbHost, $this->dbUser, $this->dbPassword, $this->dbName);
$this->connectLink->query("SET NAMES utf8");
if (mysqli_connect_errno()) {
printf("Подключение невозможно: %sn", mysqli_connect_error());
$this->connectLink = null;
} else {
mysqli_report(MYSQLI_REPORT_ERROR);
}
}
return $this->connectLink;
}
//Закрываем соединение с базой
public function closeConnection() {
if (!is_null($this->connectLink)) {
$this->connectLink->close();
}
}
//Преобразуем ответ в ассоциативный массив
public function stmt_bind_assoc(&$stmt, &$out) {
$data = mysqli_stmt_result_metadata($stmt);
$fields = array();
$out = array();
$fields[0] = $stmt;
$count = 1;
$currentTable = '';
while ($field = mysqli_fetch_field($data)) {
if (strlen($currentTable) == 0) {
$currentTable = $field->table;
}
$fields[$count] = &$out[$field->name];
$count++;
}
call_user_func_array('mysqli_stmt_bind_result', $fields);
}
}
?>
apiBaseClass.php
Ну вот мы подошли к одному из самых важных классов системы — базовый класс для всех API в системе.
<?php
class apiBaseClass {
public $mySQLWorker=null;//Одиночка для работы с базой
//Конструктор с возможными параметрами
function __construct($dbName=null,$dbHost=null,$dbUser=null,$dbPassword=null) {
if (isset($dbName)){//Если имя базы передано то будет установленно соединение с базой
$this->mySQLWorker = MySQLiWorker::getInstance($dbName,$dbHost,$dbUser,$dbPassword);
}
}
function __destruct() {
if (isset($this->mySQLWorker)){ //Если было установленно соединение с базой,
$this->mySQLWorker->closeConnection(); //то закрываем его когда наш класс больше не нужен
}
}
//Создаем дефолтный JSON для ответов
function createDefaultJson() {
$retObject = json_decode('{}');
return $retObject;
}
//Заполняем JSON объект по ответу из MySQLiWorker
function fillJSON(&$jsonObject, &$stmt, &$mySQLWorker) {
$row = array();
$mySQLWorker->stmt_bind_assoc($stmt, $row);
while ($stmt->fetch()) {
foreach ($row as $key => $value) {
$key = strtolower($key);
$jsonObject->$key = $value;
}
break;
}
return $jsonObject;
}
}
?>
Как видно данный класс содержит в себе несколько «утилитных» методов, таких как:
конструктор в котором осуществляется соединение с базой, если текущее API собирается работать с базой;
деструктор — следит за освобождением ресурсов — разрыв установленного соединения с базой
createDefaultJson — создает дефолтный JSON для ответа метода
fillJSON — если подразумевается что запрос вернет только одну запись, то данный метод заполнит JSON для ответа данными из первой строки ответа от БД
Создадим свой API
Вот собственно и весь костяк этого API. Теперь рассмотрим как же это все использовать на примере создания первого API под названием apitest. И напишем в нем пару простых функций:
одну без параметров
одну с параметрами и их же она нам и вернет, чтобы было видно, что она их прочитала
одну которая вернет нам бинарные данные
И так создаем класс apitest.php следующего содержания
<?php
class apitest extends apiBaseClass {
//http://www.example.com/api/?apitest.helloAPI={}
function helloAPI() {
$retJSON = $this->createDefaultJson();
$retJSON->withoutParams = 'It's method called without parameters';
return $retJSON;
}
//http://www.example.com/api/?apitest.helloAPIWithParams={"TestParamOne":"Text of first parameter"}
function helloAPIWithParams($apiMethodParams) {
$retJSON = $this->createDefaultJson();
if (isset($apiMethodParams->TestParamOne)){
//Все ок параметры верные, их и вернем
$retJSON->retParameter=$apiMethodParams->TestParamOne;
}else{
$retJSON->errorno= APIConstants::$ERROR_PARAMS;
}
return $retJSON;
}
//http://www.example.com/api/?apitest.helloAPIResponseBinary={"responseBinary":1}
function helloAPIResponseBinary($apiMethodParams){
header('Content-type: image/png');
echo file_get_contents("http://habrahabr.ru/i/error-404-monster.jpg");
}
}
?>
Для удобства тестирования методов, я дописываю к ним адрес по которому я могу сделать быстрый запрос для тестирования.
И так у нас три метода
helloAPI
function helloAPI() {
$retJSON = $this->createDefaultJson();
$retJSON->withoutParams = 'It's method called without parameters';
return $retJSON;
}
Это простой метод без параметров. Его адрес для GET вызова www.example.com/api/?apitest.helloAPI={}
Результатом выполнения будет вот такая страница (в браузере)
helloAPIWithParams
Этот метод принимает в параметры. Обязательным является TestParamOne, для него и сделаем проверку. Его его не передать, то будет выдан JSON с ошибкой
function helloAPIWithParams($apiMethodParams) {
$retJSON = $this->createDefaultJson();
if (isset($apiMethodParams->TestParamOne)){
//Все ок параметры верные, их и вернем
$retJSON->retParameter=$apiMethodParams->TestParamOne;
}else{
$retJSON->errorno= APIConstants::$ERROR_PARAMS;
}
return $retJSON;
}
Результат выполнения
helloAPIResponseBinary
И последний метод helloAPIResponseBinary — вернет бинарные данные — картинку хабра о несуществующей странице (в качестве примера)
function helloAPIResponseBinary($apiMethodParams){
header('Content-type: image/jpeg');
echo file_get_contents("http://habrahabr.ru/i/error-404-monster.jpg");
}
Как видно — здесь есть подмена заголовка для вывода графического контента.
Результат будет такой
Есть над чем работать
Для дальнейшего развития необходимо сделать авторизация пользователей, чтобы ввести разграничение прав на вызов запросов — какие-то оставить свободными, а какие-то только при авторизации пользователя.
Автор: NelepovDS