Пишем свой API для сайта с использованием Apache, PHP и MySQL
Разрабатывая проект, я столкнулся с необходимостью организации клиент-серверного взаимодействия приложений на платформах 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 вида ]
Путь к папке 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 будет принимать он.
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 и их методов.
//Конструктор //$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
Данный класс используется только для хранения констант.
MySQLiWorker.php
Класс-одиночка для работы с базой. В прочем это обычный одиночка — таких примеров в сети очень много.
//Чтобы нельзя было создать через клонирование 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("Подключение невозможно: %s\n", 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 в системе.
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 следующего содержания
function helloAPI() < $retJSON = $this->createDefaultJson(); $retJSON->withoutParams = 'It\'s method called without parameters'; return $retJSON; > //http://www.example.com/api/?apitest.helloAPIWithParams= 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= 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 вызова
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"); >
Как видно — здесь есть подмена заголовка для вывода графического контента.
Результат будет такой
Есть над чем работать
Для дальнейшего развития необходимо сделать авторизация пользователей, чтобы ввести разграничение прав на вызов запросов — какие-то оставить свободными, а какие-то только при авторизации пользователя.
Ссылки
Для тестирования выложил все файлы на github — simpleAPI