Время на прочтение
9 мин
Количество просмотров 152K
С чего все началось
Разрабатывая проект, я столкнулся с необходимостью организации клиент-серверного взаимодействия приложений на платформах 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");
}
Как видно — здесь есть подмена заголовка для вывода графического контента.
Результат будет такой
Есть над чем работать
Для дальнейшего развития необходимо сделать авторизация пользователей, чтобы ввести разграничение прав на вызов запросов — какие-то оставить свободными, а какие-то только при авторизации пользователя.
Ссылки
Для тестирования выложил все файлы на github — simpleAPI
Хороший API — это немаловажная часть успеха. Бывает, что это единственная причина, почему выбрана именно эта библиотека или этот продукт. Создать хороший API не так-то просто, и не существует однозначного пути для его создания, потому что для каждого отдельного случая нужен свой подход. Те, кому нередко приходилось использовать различные библиотеки, наверняка уже неплохо представляют, что именно представляет собой хороший API. Постараемся собрать все точки зрения воедино.
1. Универсальный, но удобный
Универсальность – это понятно каждому, но вот, что значит удобно, понятно не всем. К некоторым элементам приходится обращаться чаще, чем к другим. Поэтому ключом к разработке грамотного API может стать создание выдержки из полной документации для справки по часто используемых методам.
Например, давайте рассмотрим простой класс Person. У него есть:
- Вам нужен пустой конструктор, который создает пустой объект, поля которого будут заполнены позже;
- Необходимы set/get методы для работы с именем и фамилией;
- И методы для обработки дополнительных полей имени/фамилии (у некоторых людей бывает больше двух слов в имени).
Из описания всех этих методов можно составить полный API. А для удобства давайте отдельно выделим документацию по таким элементам:
- Конструктор с двумя параметрами: именем и фамилией, т.к. такая комбинация наиболее часто встречающаяся в мире;
- Set/get методы для дополнительных имен, т.к. в некоторых культурах мира большая часть людей имеет более одного имени. Если же приложение создается специально для таких стран, то в конструктор из первого пункта имеет смысл добавить еще параметры.
Основная идея здесь состоит не в создании полного API, к которому можно было бы обратиться в любом случае, а в создании выделенной документации, содержащей ссылки на справку по наиболее часто используемым методам.
2. Применимый в любом случае, но не слишком большой
Этот пункт, пожалуй, можно было бы объединить с первым, но хотелось бы все же уделить ему больше внимания.
Большая часть документации строится по принципу «А что, если…» Хотя здесь, пожалуй, необходимо себя ограничивать, потому что нет определенных границ для рассмотрения всех возможных случаев. Точно не скажешь, от чего именно стоит избавиться, ибо все зависит от конкретной ситуации. Между тем, вот несколько советов:
- То, на чем многое завязано, что не менялось в течение долгого времени, следует обновлять постепенно, желательно, предупреждая об этом заранее.
Например, IPv6. Мы так долго пересаживаемся на новый протокол, т.к. большое количество программного обеспечения строго привязано к IPv4. Правильно ли это? Пожалуй, да. Сейчас у программиста имеется выбор, использовать ли современную технологию или же даже реализовать одновременную поддержку различных протоколов. Стоит ли это потраченного времени? Решать вам.
- Удобство классов и методов сохраняется до тех пор, пока понятно, что эти классы или методы из себя представляют и за что отвечают.
3. Названия методов должны быть осмысленными, доступными для понимания, структурированные и по возможности короткие
Грамотное именование является очень важной частью составления хорошего API. И хотя очень важно давать методам именно осмысленные имена, это далеко не единственное правило. И хотя большую часть работы программиста составляет чтение кода, все же код приходится еще и писать, и логично названные методы могут серьезно повысить производительность кодера, не говоря уже об автодополнении.
Структурирование API также имеет большое значение, если документация достаточно обширна. Говоря короче, должны существовать какие-то правила именования с использованием суффиксов/префиксов, которые отделят ваш API от остальной документации.
Кроме того, логичным название метода можно сделать за счет целостности. Правила именования должны быть общими для всего API, и желательно сходными с правилами именования в документации аналогичных областей. Лучше подстраиваться сразу под стиль именования языка, для которого вы создаете свой модуль и пишете API.
И наконец, не стоит придумывать слишком длинные имена. Конечно, чем длиннее, тем, обычно, яснее смысл, но все же всегда существует некоторая граница, переход через которую приведет к тавтологии. Например, Integer_Maximum_Value бесполезно длиннее, чем INT_MAX, а обладает той же смысловой нагрузкой.
4. Ориентирован на повышение производительности, но без потери удобства
К некоторым API не приходится обращаться часто, другие же должны всегда быть под рукой, а потому не пользование ими не должно снижать производительности программиста. И при этом никогда нельзя забывать об удобстве. Например, API Windows – пример того, как удобством пожертвовали в пользу производительности. Ему не хватает функций, заполняющих структуры данных значениями по умолчанию. Раздражает обнулять каждый элемент отдельно. С другой стороны, некоторые функции можно было бы назвать лишними: иногда несколько функций очевидно стоило бы заменить на одну. К примеру, FindFirstFile() и FindNextFile() можно было бы объединить, а вот ZeroMemory() стоило бы заменить на более эффективную memset().
5. Удобные константы
Чем реже разработчику приходится сужать область применения методов, классов и т.п. для лучшего их понимания, тем лучше. В большинстве случаев…
Константы должны также иметь логичные названия.
C другой стороны, неплохо также позволить задавать константы специфичной области применения.
6. Правильно выбирайте формат файла для представления вашего API
Коротко говоря – не останавливайтесь слепо на XML.
Если вы работаете над веб-сервисом, то задумайтесь, в каком виде представить документацию: это может быть XML, JSON или что-то еще. Подумайте о тех, кто будет пользоваться вашим API. Заставлять людей использовать XML это… ну, ладно, используйте XML, если ничто другое вас не устраивает.
7. Настраиваемый, но в меру
Здесь речь идет о фреймворках. Многие из них позволяют пользователю изменять поведение некоторых элементов за счет изменения параметров конфигурации. Это хорошо, но надо знать меру. Во-первых, необходимо понимать, что какие-то конфигурации совершенно бестолковы. Чем больше параметров поддается изменению, тем сложнее учесть все возможные конфигурации, не говоря уже о том, что какие-то значения параметров могут оказаться несовместимы и неблаготворно скажутся на работоспособности.
«Абсолютно все можно подключить, расширить и настроить» – не самый верный подход. Лучше не иметь возможности настроить, чем настраивать так, чтобы ничего не работало.
В особенных случаях можно ограничить возможности конфигурирования «белым ящиком»: вместо увеличения количества комбинаций параметров по принципу «черного ящика», можно разбить все настройки на группы заранее выставленных значений, и пользователь будет подстраивать фреймворк под себя, комбинируя эти наборы настроек.
8. Одновременное выполнение vs. Однопоточность
Если речь идет об операциях, требующих больших затрат по времени, то лучше отказаться от синхронного выполнения нескольких задач. В идеале, конечно же, все долгие операции должны минимизировать синхронизацию.
Но в таком случае у несинхронируемого приложения должна быть альтернатива с поддержкой одновременного выполнения нескольких задач. Странно? Поясним. Не имеет смысла синхронизировать выполнение операций, если у вас разработан приличный пользовательский интерфейс, т.к. синхронизация может снизить скорость отклика приложения, а вот если вы решили отказаться от пользовательского интерфейса, то дабы избежать задержек, вам стоит реализовать синхронизацию выполнения операций.
К сожалению, вы не можете предугадать, какую из версий предпочтет пользователь, поэтому имеет смысл создать обе и предоставить право выбора.
9. Не все знают английский язык
Если ваш API поддерживает пользовательский интерфейс, то обязательно реализуйте настройку локализации. Не забывайте также и про сообщения об ошибках: прописывайте коды исключений, отслеживающие параметры локализации, или локализуйте сообщения об ошибках сами по себе. НО если вы имеете слабое представление о локализации, то обязательно проконсультируйтесь у кого-то.
10. Совместимость со старыми и новыми версиями
В идеале, любая новая версия должна быть совместима со всеми предыдущими. Но на практике это, пожалуй, невозможно. Вот несколько советов:
- Изначально разрабатывайте API так, чтобы его можно было расширить в будущем. Особенно будьте осторожны с аргументами логического типа (их можно заменить нумерованным типом);
- Если о каких-то фичах, которые будут введены в будущем, вам известно заранее, то вы можете сразу же включить их в API для обеспечения совместимости с будущими версиями;
- Внимательно следите за сохранением совместимости с предыдущими версиями, но не реализовывайте рефлексию своего API, потому что в будущем вам придется учитывать и совместимость с механизмом самообработки API;
- Лучше делайте большие перерывы между выпусками новых версий API, чем выпускайте частые обновления. Конечно, никто не любит подобных перерывов, но вот необходимость часто обновляться, кажется, особенно раздражает разработчиков;
- Не пренебрегайте альфа/бета-тестированиями, потому что лучший путь развития – это развитие в соответствии с откликами пользователей.
11. Излишнее удобство не оправдано
Нельзя не придавать достаточного значения времени разработчика. Если программист пишет меньше кода, это не значит, что он мало работает.
Когда вещи работают авто-магически, их бывает трудно изменить в соответствии с выполняемой задачей.
Заключение
Хорошие API не могут быть созданы за закрытой дверью несколькими, пусть даже и очень умными и опытными людьми. Общение с потребителями, особенно, если такой контакт налажен с самого начала работы — это ключ к грамотной реализации.
В данной статье вы узнаете, как создать простой REST API в PHP.
- 1. Обзор проекта
- 1.1 Что такое REST API?
- 1.2 Зачем нужен REST API?
- 1.3 Где используется REST API?
- 2. Файловая структура
- 3. Настройка базы данных
- 3.1 Создание таблицы категорий
- 3.2 Дамп данных для таблицы категорий
- 3.3 Создание таблицы товаров
- 3.4 Дамп данных для таблицы товаров
- 3.5 Подключение к базе данных
- 4. Получение товаров
- 4.1 Создание объекта Product
- 4.2 Создание файла для чтения товаров
- 4.3 Подключение к базе данных и таблице товаров
- 4.4 Чтение товаров из базы данных
- 4.5 Создание метода read()
- 4.6 Уведомление пользователя о том, что товары не найдены
- 5. Создание товаров
- 5.1 Создание файла create.php
- 5.2 Создание метода create()
- 6. Получение одного товара
- 6.1 Создание файла read_one.php
- 6.2 Создание метода readOne()
- 7. Обновление товара
- 7.1 Создание файла update.php
- 7.2 Создание метода update()
- 8. Удаление товара
- 8.1 Создание файла delete.php
- 8.2 Создание метода delete()
- 9. Поиск товаров
- 9.1 Создание файла search.php
- 9.2 Создание метода search()
- 10. Пагинация товаров
- 10.1 Создание файла read_paging.php
- 10.2 Создание файла core.php
- 10.3 Создание метода readPaging()
- 10.4 Создание метода count()
- 10.5 Получение массива пагинации
- 11. Получение категорий
- 11.1 Создание класса Category
- 11.2 Создание метода readAll()
- 11.3 Создание файла read.php
1. Обзор проекта
1.1 Что такое REST API?
REST — это концепция (архитектура) для организации взаимодействия между независимыми объектами (приложениями) посредством протокола HTTP. Включает в себя набор принципов (рекомендаций) взаимодействия клиент-серверных приложений. Обычно он представлен в формате JSON.
API — интерфейс взаимодействия с каким-либо объектом (программой, приложением), включающий в себя набор правил, которые позволяют одному приложению общаться с другим. Эти «правила» могут включать в себя операции создания, чтения, обновления и удаления. Примером API может служить всем известная библиотека jQuery.
REST API позволяет вашему приложению взаимодействовать с одним или несколькими различными приложениями, используя концепции REST.
1.2 Зачем нужен REST API?
Во многих приложениях REST API необходим, потому что это самый легкий способ создания, чтения, обновления или удаления информации между различными приложениями через Интернет или протокол HTTP. Эта информация представляется пользователю в одно мгновение, особенно если вы используете JavaScript для отображения данных на веб-странице.
1.3 Где используется REST API?
REST API может использоваться любым приложением, которое может подключаться к Интернету. Если данные из приложения могут быть созданы, прочитаны, обновлены или удалены с помощью другого приложения, это обычно означает, что используется REST API.
2. Файловая структура
- api/
- config/
- core.php
- database.php
- objects/
- product.php
- category.php
- product/
- create.php/
- delete.php/
- read.php/
- read_paging.php/
- read_one.php/
- update.php/
- search.php/
- category/
- read.php
- shared/
- utilities.php
- config/
3. Настройка базы данных
Используя PhpMyAdmin, создайте новую базу данных api_db. После этого выполните следующие SQL-запросы, чтобы создать новые таблицы с образцами данных.
3.1 Создание таблицы категорий
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(256) NOT NULL,
`description` text NOT NULL,
`created` datetime NOT NULL,
`modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=19 ;
3.2 Дамп данных для таблицы категорий
INSERT INTO `categories` (`id`, `name`, `description`, `created`, `modified`) VALUES
(1, "Fashion", "Category for anything related to fashion.", "2014-06-01 00:35:07", "2014-05-30 17:34:33"),
(2, "Electronics", "Gadgets, drones and more.", "2014-06-01 00:35:07", "2014-05-30 17:34:33"),
(3, "Motors", "Motor sports and more", "2014-06-01 00:35:07", "2014-05-30 17:34:54"),
(5, "Movies", "Movie products.", "2019-05-20 10:22:05", "2019-08-20 10:30:15"),
(6, "Books", "Kindle books, audio books and more.", "2018-03-14 08:05:25", "2019-05-20 11:29:11"),
(13, "Sports", "Drop into new winter gear.", "2016-01-09 02:24:24", "2016-01-09 01:24:24");
3.3 Создание таблицы товаров
CREATE TABLE IF NOT EXISTS `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
`description` text NOT NULL,
`price` decimal(10,0) NOT NULL,
`category_id` int(11) NOT NULL,
`created` datetime NOT NULL,
`modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=65 ;
3.4 Дамп данных для таблицы товаров
INSERT INTO `products` (`id`, `name`, `description`, `price`, `category_id`, `created`, `modified`) VALUES
(1, "LG P880 4X HD", "My first awesome phone!", "336", 3, "2014-06-01 01:12:26", "2014-05-31 17:12:26"),
(2, "Google Nexus 4", "The most awesome phone of 2013!", "299", 2, "2014-06-01 01:12:26", "2014-05-31 17:12:26"),
(3, "Samsung Galaxy S4", "How about no?", "600", 3, "2014-06-01 01:12:26", "2014-05-31 17:12:26"),
(6, "Bench Shirt", "The best shirt!", "29", 1, "2014-06-01 01:12:26", "2014-05-31 02:12:21"),
(7, "Lenovo Laptop", "My business partner.", "399", 2, "2014-06-01 01:13:45", "2014-05-31 02:13:39"),
(8, "Samsung Galaxy Tab 10.1", "Good tablet.", "259", 2, "2014-06-01 01:14:13", "2014-05-31 02:14:08"),
(9, "Spalding Watch", "My sports watch.", "199", 1, "2014-06-01 01:18:36", "2014-05-31 02:18:31"),
(10, "Sony Smart Watch", "The coolest smart watch!", "300", 2, "2014-06-06 17:10:01", "2014-06-05 18:09:51"),
(11, "Huawei Y300", "For testing purposes.", "100", 2, "2014-06-06 17:11:04", "2014-06-05 18:10:54"),
(12, "Abercrombie Lake Arnold Shirt", "Perfect as gift!", "60", 1, "2014-06-06 17:12:21", "2014-06-05 18:12:11"),
(13, "Abercrombie Allen Brook Shirt", "Cool red shirt!", "70", 1, "2014-06-06 17:12:59", "2014-06-05 18:12:49"),
(26, "Another product", "Awesome product!", "555", 2, "2014-11-22 19:07:34", "2014-11-21 20:07:34"),
(28, "Wallet", "You can absolutely use this one!", "799", 6, "2014-12-04 21:12:03", "2014-12-03 22:12:03"),
(31, "Amanda Waller Shirt", "New awesome shirt!", "333", 1, "2014-12-13 00:52:54", "2014-12-12 01:52:54"),
(42, "Nike Shoes for Men", "Nike Shoes", "12999", 3, "2015-12-12 06:47:08", "2015-12-12 05:47:08"),
(48, "Bristol Shoes", "Awesome shoes.", "999", 5, "2016-01-08 06:36:37", "2016-01-08 05:36:37"),
(60, "Rolex Watch", "Luxury watch.", "25000", 1, "2016-01-11 15:46:02", "2016-01-11 14:46:02");
3.5 Подключение к базе данных
Приведенный ниже код показывает учетные данные базы данных и метод для получения подключения к базе данных с помощью PDO.
Создайте папку api и откройте её. Создайте папку config и в ней создайте файл database.php со следующим кодом.
<?php
class Database
{
// укажите свои учетные данные базы данных
private $host = "localhost";
private $db_name = "api_db";
private $username = "root";
private $password = "";
public $conn;
// получаем соединение с БД
public function getConnection()
{
$this->conn = null;
try {
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
$this->conn->exec("set names utf8");
} catch (PDOException $exception) {
echo "Ошибка подключения: " . $exception->getMessage();
}
return $this->conn;
}
}
4. Получение товаров
4.1 Создание объекта Product
Код ниже содержит класс с именем Product и несколько свойств. Также показан метод конструктора, который принимает соединение с базой данных.
Мы будем использовать этот класс для чтения данных из базы. Откройте папку api. Создайте папку objects. Откройте папку её и создайте файл product.php. Поместите в него следующий код.
<?php
class Product
{
// подключение к базе данных и таблице "products"
private $conn;
private $table_name = "products";
// свойства объекта
public $id;
public $name;
public $description;
public $price;
public $category_id;
public $category_name;
public $created;
// конструктор для соединения с базой данных
public function __construct($db)
{
$this->conn = $db;
}
// здесь будет метод read()
}
4.2 Создание файла для чтения товаров
Код ниже содержит заголовки о том, кто может читать этот файл и какой тип содержимого он будет возвращать.
В данном случае наш файл read.php может быть прочитан кем угодно (звездочка * означает все) и вернет данные в формате JSON.
Откройте папку api. Создайте в ней папку product. Откройте её и создайте файл read.php со следующим кодом.
<?php
// необходимые HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// подключение к базе данных будет здесь
4.3 Подключение к базе данных и таблице товаров
В приведенном ниже коде мы подключаем файлы database.php и product.php. Это файлы, которые мы создали ранее.
Нам нужно использовать метод getConnection() класса Database для получения соединения с базой данных. Мы передаем это подключение классу Product.
Замените комментарий // подключение к базе данных будет здесь в файле read.php следующим кодом.
// подключение базы данных и файл, содержащий объекты
include_once "../config/database.php";
include_once "../objects/product.php";
// получаем соединение с базой данных
$database = new Database();
$db = $database->getConnection();
// инициализируем объект
$product = new Product($db);
// чтение товаров будет здесь
4.4 Чтение товаров из базы данных
В приведенном ниже коде мы используем метод read() класса Product для получения данных из базы. Через переменную $num мы проверяем, найдены ли записи.
Если найдены записи, мы перебираем их с помощью цикла while, и добавляем каждую запись в массив $products_arr, устанавливаем код ответа 200 OK и показываем его пользователю в формате JSON.
Замените комментарий // чтение товаров будет здесь в файле read.php следующим кодом.
// запрашиваем товары
$stmt = $product->read();
$num = $stmt->rowCount();
// проверка, найдено ли больше 0 записей
if ($num > 0) {
// массив товаров
$products_arr = array();
$products_arr["records"] = array();
// получаем содержимое нашей таблицы
// fetch() быстрее, чем fetchAll()
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// извлекаем строку
extract($row);
$product_item = array(
"id" => $id,
"name" => $name,
"description" => html_entity_decode($description),
"price" => $price,
"category_id" => $category_id,
"category_name" => $category_name
);
array_push($products_arr["records"], $product_item);
}
// устанавливаем код ответа - 200 OK
http_response_code(200);
// выводим данные о товаре в формате JSON
echo json_encode($products_arr);
}
// "товары не найдены" будет здесь
4.5 Создание метода read()
Мы использовали метод read() в предыдущем разделе, но он пока ещё не существует в классе Product. Нам нужно добавить этот метод. С помощью кода ниже, мы делаем запрос для получения записей из базы данных.
Откройте папку objects. Откройте файл product.php. Поместите следующий код в класс Product перед последней закрывающей фигурной скобкой вместо комментария // здесь будет метод read().
// метод для получения товаров
function read()
{
// выбираем все записи
$query = "SELECT
c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
FROM
" . $this->table_name . " p
LEFT JOIN
categories c
ON p.category_id = c.id
ORDER BY
p.created DESC";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// выполняем запрос
$stmt->execute();
return $stmt;
}
4.6 Уведомление пользователя о том, что товары не найдены
Если переменная $num имеет нулевое или отрицательное значение, это означает, что из базы данных не возвращено никаких записей. Мы должны сообщить пользователю об этом.
В приведенном ниже коде мы устанавливаем код ответа 404 — Не найдено и сообщение, что Товары не найдены.
Замените комментарий // «товары не найдены» будет здесь в файле read.php следующим кодом.
else {
// установим код ответа - 404 Не найдено
http_response_code(404);
// сообщаем пользователю, что товары не найдены
echo json_encode(array("message" => "Товары не найдены."), JSON_UNESCAPED_UNICODE);
}
5. Создание товаров
5.1 Создание файла create.php
Откройте папку product и создайте в ней файл create.php со следующим содержимым.
<?php
// необходимые HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// получаем соединение с базой данных
include_once "../config/database.php";
// создание объекта товара
include_once "../objects/product.php";
$database = new Database();
$db = $database->getConnection();
$product = new Product($db);
// получаем отправленные данные
$data = json_decode(file_get_contents("php://input"));
// убеждаемся, что данные не пусты
if (
!empty($data->name) &&
!empty($data->price) &&
!empty($data->description) &&
!empty($data->category_id)
) {
// устанавливаем значения свойств товара
$product->name = $data->name;
$product->price = $data->price;
$product->description = $data->description;
$product->category_id = $data->category_id;
$product->created = date("Y-m-d H:i:s");
// создание товара
if ($product->create()) {
// установим код ответа - 201 создано
http_response_code(201);
// сообщим пользователю
echo json_encode(array("message" => "Товар был создан."), JSON_UNESCAPED_UNICODE);
}
// если не удается создать товар, сообщим пользователю
else {
// установим код ответа - 503 сервис недоступен
http_response_code(503);
// сообщим пользователю
echo json_encode(array("message" => "Невозможно создать товар."), JSON_UNESCAPED_UNICODE);
}
}
// сообщим пользователю что данные неполные
else {
// установим код ответа - 400 неверный запрос
http_response_code(400);
// сообщим пользователю
echo json_encode(array("message" => "Невозможно создать товар. Данные неполные."), JSON_UNESCAPED_UNICODE);
}
5.2 Создание метода create()
Откройте папку objects. Откройте файл product.php и добавьте следующий код внутри класса Product (objects / product.php).
// метод для создания товаров
function create()
{
// запрос для вставки (создания) записей
$query = "INSERT INTO
" . $this->table_name . "
SET
name=:name, price=:price, description=:description, category_id=:category_id, created=:created";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// очистка
$this->name = htmlspecialchars(strip_tags($this->name));
$this->price = htmlspecialchars(strip_tags($this->price));
$this->description = htmlspecialchars(strip_tags($this->description));
$this->category_id = htmlspecialchars(strip_tags($this->category_id));
$this->created = htmlspecialchars(strip_tags($this->created));
// привязка значений
$stmt->bindParam(":name", $this->name);
$stmt->bindParam(":price", $this->price);
$stmt->bindParam(":description", $this->description);
$stmt->bindParam(":category_id", $this->category_id);
$stmt->bindParam(":created", $this->created);
// выполняем запрос
if ($stmt->execute()) {
return true;
}
return false;
}
6. Получение одного товара
6.1 Создание файла read_one.php
Откройте папку product, создайте в ней файл read_one.php со следующим содержимым.
<?php
// необходимые HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: access");
header("Access-Control-Allow-Methods: GET");
header("Access-Control-Allow-Credentials: true");
header("Content-Type: application/json");
// подключение файла для соединения с базой и файл с объектом
include_once "../config/database.php";
include_once "../objects/product.php";
// получаем соединение с базой данных
$database = new Database();
$db = $database->getConnection();
// подготовка объекта
$product = new Product($db);
// установим свойство ID записи для чтения
$product->id = isset($_GET["id"]) ? $_GET["id"] : die();
// получим детали товара
$product->readOne();
if ($product->name != null) {
// создание массива
$product_arr = array(
"id" => $product->id,
"name" => $product->name,
"description" => $product->description,
"price" => $product->price,
"category_id" => $product->category_id,
"category_name" => $product->category_name
);
// код ответа - 200 OK
http_response_code(200);
// вывод в формате json
echo json_encode($product_arr);
} else {
// код ответа - 404 Не найдено
http_response_code(404);
// сообщим пользователю, что такой товар не существует
echo json_encode(array("message" => "Товар не существует"), JSON_UNESCAPED_UNICODE);
}
6.2 Создание метода readOne()
Откройте папку objects. Откройте файл product.php и добавьте следующий код внутри класса Product.
// метод для получения конкретного товара по ID
function readOne()
{
// запрос для чтения одной записи (товара)
$query = "SELECT
c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
FROM
" . $this->table_name . " p
LEFT JOIN
categories c
ON p.category_id = c.id
WHERE
p.id = ?
LIMIT
0,1";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// привязываем id товара, который будет получен
$stmt->bindParam(1, $this->id);
// выполняем запрос
$stmt->execute();
// получаем извлеченную строку
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// установим значения свойств объекта
$this->name = $row["name"];
$this->price = $row["price"];
$this->description = $row["description"];
$this->category_id = $row["category_id"];
$this->category_name = $row["category_name"];
}
7. Обновление товара
7.1 Создание файла update.php
Откройте папку product, создайте в ней файл update.php и поместите в него следующий код.
<?php
// HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// подключаем файл для работы с БД и объектом Product
include_once "../config/database.php";
include_once "../objects/product.php";
// получаем соединение с базой данных
$database = new Database();
$db = $database->getConnection();
// подготовка объекта
$product = new Product($db);
// получаем id товара для редактирования
$data = json_decode(file_get_contents("php://input"));
// установим id свойства товара для редактирования
$product->id = $data->id;
// установим значения свойств товара
$product->name = $data->name;
$product->price = $data->price;
$product->description = $data->description;
$product->category_id = $data->category_id;
// обновление товара
if ($product->update()) {
// установим код ответа - 200 ok
http_response_code(200);
// сообщим пользователю
echo json_encode(array("message" => "Товар был обновлён"), JSON_UNESCAPED_UNICODE);
}
// если не удается обновить товар, сообщим пользователю
else {
// код ответа - 503 Сервис не доступен
http_response_code(503);
// сообщение пользователю
echo json_encode(array("message" => "Невозможно обновить товар"), JSON_UNESCAPED_UNICODE);
}
7.2 Создание метода update()
В папке objects откройте файл product.php и добавьте новый метод update() внутри класса Product.
// метод для обновления товара
function update()
{
// запрос для обновления записи (товара)
$query = "UPDATE
" . $this->table_name . "
SET
name = :name,
price = :price,
description = :description,
category_id = :category_id
WHERE
id = :id";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// очистка
$this->name = htmlspecialchars(strip_tags($this->name));
$this->price = htmlspecialchars(strip_tags($this->price));
$this->description = htmlspecialchars(strip_tags($this->description));
$this->category_id = htmlspecialchars(strip_tags($this->category_id));
$this->id = htmlspecialchars(strip_tags($this->id));
// привязываем значения
$stmt->bindParam(":name", $this->name);
$stmt->bindParam(":price", $this->price);
$stmt->bindParam(":description", $this->description);
$stmt->bindParam(":category_id", $this->category_id);
$stmt->bindParam(":id", $this->id);
// выполняем запрос
if ($stmt->execute()) {
return true;
}
return false;
}
8. Удаление товара
8.1 Создание файла delete.php
Откройте папку product и создайте файл delete.php со следующим содержимым.
<?php
// HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// подключим файл для соединения с базой и объектом Product
include_once "../config/database.php";
include_once "../objects/product.php";
// получаем соединение с БД
$database = new Database();
$db = $database->getConnection();
// подготовка объекта
$product = new Product($db);
// получаем id товара
$data = json_decode(file_get_contents("php://input"));
// установим id товара для удаления
$product->id = $data->id;
// удаление товара
if ($product->delete()) {
// код ответа - 200 ok
http_response_code(200);
// сообщение пользователю
echo json_encode(array("message" => "Товар был удалён"), JSON_UNESCAPED_UNICODE);
}
// если не удается удалить товар
else {
// код ответа - 503 Сервис не доступен
http_response_code(503);
// сообщим об этом пользователю
echo json_encode(array("message" => "Не удалось удалить товар"));
}
8.2 Создание метода delete()
В папке objects откройте файл product.php и добавьте новый метод в класс Product.
// метод для удаления товара
function delete()
{
// запрос для удаления записи (товара)
$query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// очистка
$this->id = htmlspecialchars(strip_tags($this->id));
// привязываем id записи для удаления
$stmt->bindParam(1, $this->id);
// выполняем запрос
if ($stmt->execute()) {
return true;
}
return false;
}
9. Поиск товаров
Теперь реализуем поиск записей в базе данных, в нашем случае товаров.
9.1 Создание файла search.php
В папке product создайте файл search.php со следующим кодом.
<?php
// HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// подключение необходимых файлов
include_once "../config/core.php";
include_once "../config/database.php";
include_once "../objects/product.php";
// создание подключения к БД
$database = new Database();
$db = $database->getConnection();
// инициализируем объект
$product = new Product($db);
// получаем ключевые слова
$keywords = isset($_GET["s"]) ? $_GET["s"] : "";
// запрос товаров
$stmt = $product->search($keywords);
$num = $stmt->rowCount();
// проверяем, найдено ли больше 0 записей
if ($num > 0) {
// массив товаров
$products_arr = array();
$products_arr["records"] = array();
// получаем содержимое нашей таблицы
// fetch() быстрее чем fetchAll()
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// извлечём строку
extract($row);
$product_item = array(
"id" => $id,
"name" => $name,
"description" => html_entity_decode($description),
"price" => $price,
"category_id" => $category_id,
"category_name" => $category_name
);
array_push($products_arr["records"], $product_item);
}
// код ответа - 200 OK
http_response_code(200);
// покажем товары
echo json_encode($products_arr);
} else {
// код ответа - 404 Ничего не найдено
http_response_code(404);
// скажем пользователю, что товары не найдены
echo json_encode(array("message" => "Товары не найдены."), JSON_UNESCAPED_UNICODE);
}
9.2 Создание метода search()
В папке objects откройте product.php и добавьте метод search().
// метод для поиска товаров
function search($keywords)
{
// поиск записей (товаров) по "названию товара", "описанию товара", "названию категории"
$query = "SELECT
c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
FROM
" . $this->table_name . " p
LEFT JOIN
categories c
ON p.category_id = c.id
WHERE
p.name LIKE ? OR p.description LIKE ? OR c.name LIKE ?
ORDER BY
p.created DESC";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// очистка
$keywords = htmlspecialchars(strip_tags($keywords));
$keywords = "%{$keywords}%";
// привязка
$stmt->bindParam(1, $keywords);
$stmt->bindParam(2, $keywords);
$stmt->bindParam(3, $keywords);
// выполняем запрос
$stmt->execute();
return $stmt;
}
10. Пагинация товаров
10.1 Создание файла read_paging.php
В папке product создайте файл read_paging.php со следующим кодом.
<?php
// установим HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// подключение файлов
include_once "../config/core.php";
include_once "../shared/utilities.php";
include_once "../config/database.php";
include_once "../objects/product.php";
// utilities
$utilities = new Utilities();
// создание подключения
$database = new Database();
$db = $database->getConnection();
// инициализация объекта
$product = new Product($db);
// запрос товаров
$stmt = $product->readPaging($from_record_num, $records_per_page);
$num = $stmt->rowCount();
// если больше 0 записей
if ($num > 0) {
// массив товаров
$products_arr = array();
$products_arr["records"] = array();
$products_arr["paging"] = array();
// получаем содержимое нашей таблицы
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// извлечение строки
extract($row);
$product_item = array(
"id" => $id,
"name" => $name,
"description" => html_entity_decode($description),
"price" => $price,
"category_id" => $category_id,
"category_name" => $category_name
);
array_push($products_arr["records"], $product_item);
}
// подключим пагинацию
$total_rows = $product->count();
$page_url = "{$home_url}product/read_paging.php?";
$paging = $utilities->getPaging($page, $total_rows, $records_per_page, $page_url);
$products_arr["paging"] = $paging;
// установим код ответа - 200 OK
http_response_code(200);
// вывод в json-формате
echo json_encode($products_arr);
} else {
// код ответа - 404 Ничего не найдено
http_response_code(404);
// сообщим пользователю, что товаров не существует
echo json_encode(array("message" => "Товары не найдены"), JSON_UNESCAPED_UNICODE);
}
10.2 Создание файла core.php
Этот файл содержит нашу базовую конфигурацию, такую как базовый URL и переменные пагинации.
Откройте папку config и создайте в ней файл core.php со следующим содержимым.
<?php
// показывать сообщения об ошибках
ini_set("display_errors", 1);
error_reporting(E_ALL);
// URL домашней страницы
$home_url = "http://localhost/api/";
// страница указана в параметре URL, страница по умолчанию одна
$page = isset($_GET["page"]) ? $_GET["page"] : 1;
// установка количества записей на странице
$records_per_page = 5;
// расчёт для запроса предела записей
$from_record_num = ($records_per_page * $page) - $records_per_page;
10.3 Создание метода readPaging()
В папке objects откройте файл product.php и добавьте метод readPaging(). Этот метод вернет список записей, ограниченный тем, что мы установили в $records_per_page файла core.php.
// получение товаров с пагинацией
public function readPaging($from_record_num, $records_per_page)
{
// выборка
$query = "SELECT
c.name as category_name, p.id, p.name, p.description, p.price, p.category_id, p.created
FROM
" . $this->table_name . " p
LEFT JOIN
categories c
ON p.category_id = c.id
ORDER BY p.created DESC
LIMIT ?, ?";
// подготовка запроса
$stmt = $this->conn->prepare($query);
// свяжем значения переменных
$stmt->bindParam(1, $from_record_num, PDO::PARAM_INT);
$stmt->bindParam(2, $records_per_page, PDO::PARAM_INT);
// выполняем запрос
$stmt->execute();
// вернём значения из базы данных
return $stmt;
}
10.4 Создание метода count()
Так же в классе Product (файл product.php) добавьте метод count() для подсчёта количества товаров.
// данный метод возвращает кол-во товаров
public function count()
{
$query = "SELECT COUNT(*) as total_rows FROM " . $this->table_name . "";
$stmt = $this->conn->prepare($query);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row["total_rows"];
}
10.5 Получение массива пагинации
В корне создайте папку shared, в ней файл utilities.php со следующим кодом.
<?php
class Utilities
{
public function getPaging($page, $total_rows, $records_per_page, $page_url)
{
// массив пагинации
$paging_arr = array();
// кнопка для первой страницы
$paging_arr["first"] = $page > 1 ? "{$page_url}page=1" : "";
// подсчёт всех товаров в базе данных для подсчета общего количества страниц
$total_pages = ceil($total_rows / $records_per_page);
// диапазон ссылок для показа
$range = 2;
// отображать диапазон ссылок вокруг текущей страницы
$initial_num = $page - $range;
$condition_limit_num = ($page + $range) + 1;
$paging_arr["pages"] = array();
$page_count = 0;
for ($x = $initial_num; $x < $condition_limit_num; $x++) {
// убедимся, что $x > 0 И $x <= $total_pages
if (($x > 0) && ($x <= $total_pages)) {
$paging_arr["pages"][$page_count]["page"] = $x;
$paging_arr["pages"][$page_count]["url"] = "{$page_url}page={$x}";
$paging_arr["pages"][$page_count]["current_page"] = $x == $page ? "yes" : "no";
$page_count++;
}
}
// кнопка для последней страницы
$paging_arr["last"] = $page < $total_pages ? "{$page_url}page={$total_pages}" : "";
// формат json
return json_encode(
$paging_arr);
}
}
11. Получение категорий
11.1 Создание класса Category
Откройте папку objects и создайте новый файл category.php со следующим кодом.
<?php
class Category
{
// соединение с БД и таблицей "categories"
private $conn;
private $table_name = "categories";
// свойства объекта
public $id;
public $name;
public $description;
public $created;
public function __construct($db)
{
$this->conn = $db;
}
// здесь будет метод для получение всех категорий товаров
}
11.2 Создание метода readAll()
С помощью данного метода мы получим все категории товаров из базы данных. замените комментарий // здесь будет метод для получение всех категорий товаров следующим кодом:
// метод для получения всех категорий товаров
public function readAll()
{
$query = "SELECT
id, name, description
FROM
" . $this->table_name . "
ORDER BY
name";
$stmt = $this->conn->prepare($query);
$stmt->execute();
return $stmt;
}
11.3 Создание файла read.php
Создайте новую папку category в корне, и в ней файл read.php со следующим кодом.
<?php
// установим HTTP-заголовки
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// подключение файлов для соединения с БД и файл с объектом Category
include_once "../config/database.php";
include_once "../objects/category.php";
// создание подключения к базе данных
$database = new Database();
$db = $database->getConnection();
// инициализация объекта
$category = new Category($db);
// получаем категории
$stmt = $category->readAll();
$num = $stmt->rowCount();
// проверяем, найдено ли больше 0 записей
if ($num > 0) {
// массив для записей
$categories_arr = array();
$categories_arr["records"] = array();
// получим содержимое нашей таблицы
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// извлекаем строку
extract($row);
$category_item = array(
"id" => $id,
"name" => $name,
"description" => html_entity_decode($description)
);
array_push($categories_arr["records"], $category_item);
}
// код ответа - 200 OK
http_response_code(200);
// покажем данные категорий в формате json
echo json_encode($categories_arr);
} else {
// код ответа - 404 Ничего не найдено
http_response_code(404);
// сообщим пользователю, что категории не найдены
echo json_encode(array("message" => "Категории не найдены"), JSON_UNESCAPED_UNICODE);
}
API готов к использованию!
Frontend часть данного приложения (продолжение) — jQuery + AJAX + JSON.
Если вам понравилась данная статья, рекомендую к прочтению создание регистрации и авторизации в php с использованием JWT.
Перевод статьи «Best Practices: API Design».
Хорошо спроектированные API = довольные разработчики.
Прикладные программные интерфейсы (Application Programming Interfaces, API) — это интерфейсы, облегчающие использование в приложениях данных и ресурсов других приложений. Они жизненно необходимы для успеха продукта или даже компании.
Без API большинства ваших любимых программ попросту не существовало бы. Например, API Google Maps позволяет вам использовать карты Google в вашем мобильном или веб-приложении. Без него вам пришлось бы разрабатывать вашу собственную базу данных с картами! Только представьте, сколько времени это заняло бы.
Почему мы используем API?
- API обеспечивают сторонний доступ к вашим ресурсам.
- API расширяют возможности вашего приложения.
- API позволяют разработчикам повторно использовать логику приложения.
- API «платформонезависимы», т. е., они могут доставлять данные, не оглядываясь на особенности запрашивающей платформы.
В большинстве жизненных сценариев модель данных уже будет существовать, но поскольку мы собираемся обсудить лучшие подходы к проектированию API, давайте начнем с самого начала.
Моделирование и структурирование данных
Моделирование данных с оглядкой на будущий API это первый шаг на пути к его легкому созданию, поддержке и обновлению.
При проектировании API всегда старайтесь использовать общие термины, а не сложную бизнес-терминологию, которая может быть неизвестна за пределами вашей организации.
Ваши API могут использоваться внутри вашей компании, но могут и быть открыты для стороннего использования, чтобы другие разработчики могли применять их в своих проектах. Используя общие термины, исходите из того, что они должны быть понятны этим «другим разработчикам», чтобы они могли побыстрее разобраться с вашим API и его интеграцией в свой продукт.
Представьте, что вы строите портал, где пользователи смогут просматривать информацию по книгам различных авторов. В вашей компании могут использоваться свои, специфические термины, например, Storytellers, creations, series (букв. авторы рассказов, произведения, серии), при том, что имеются в виду Authors, books и series (авторы, книги, серии). Для простоты и чтобы сторонние разработчики могли использовать ваши API, имеет смысл придерживаться более общей терминологии при создании путей API.
https://api.domain.com/authors https://api.domain.com/authors/{id}/books
Таким образом любой разработчик сможет куда быстрее разобраться, что из себя представляет ваш API.
Написание ресурсо-ориентированных API
Приложения, использующие ваши API, хотят получить доступ к вашим ресурсам. Поддержка иерархии ресурсов поможет лучше структурировать ваш API. Каждый узел в вашем пути, содержащий какой-либо ресурс или коллекцию ресурсов, должен занимать определнное место в иерархии.
Ресурсом может быть отдельный кусочек данных, скажем, информация об авторе (если продолжать использовать пример, приведенный выше). Коллекцией ресурсов в нашем случае может быть список книг, написанных отдельным автором.
Иерархия ресурсов при этом может выглядеть следующим образом:
Base Path -> Authors (collection) -> profile (resource) Base Path -> Authors (collection) -> books (collection) -> book (resource)
В иерархии должно поддерживаться постоянство, чтобы у разработчиков не возникало лишних вопросов при интеграции вашего API и их приложения.
Вот несколько советов по поддержке постоянства и простоты:
- В именах коллекций и ресурсов используйте американский английский (например, color, а не colour).
- Следите за правописанием.
- Используйте более простые, широко употребимые слова, чтобы все было максимально ясно (например, delete, а не remove).
- Если используете те же ресурсы, что и в другом API, используйте ту же терминологию.
- Для коллекций используйте множественные формы (например, authors, books и т. п.).
REST
REST (Representational State Transfer) — это наиболее широко используемый стандарт для отправки HTTP-запросов к API. Суть этого подхода в том, что каждый URL представляет объект.
У API может быть одно из следующих назначений:
- Создание данных.
- Чтение данных.
- Обновление данных.
- Удаление данных.
Да, вы угадали, это CRUD!
Назначения API регулируются набором HTTP-слов, определяющих природу запроса и то, что он должен делать.
Слово GET
ассоциируется с получением данных от API. Оно запрашивает представление данных. Запрос GET может включать параметры запроса для фильтрации результатов, полученных от API.
Слово POST
связано с отправкой информации к API, в результате чего в базе данных будет создан ресурс.
Слово PUT
обычно используется для обновления уже имеющихся на сервере ресурсов.
Слово DELETE
служит для удаления ресурса с сервера.
Разбираемся с минорными и мажорными обновлениями
Когда вы не вносите изменений, способных сломать приложение вашего клиента, можно обойтись минорными обновлениями. Речь идет о таких вещах как добавление опциональных полей или включение поддержки дополнительных параметров. В таких случаях вы инкрементируете номер минорной версии вашего API.
Мажорные обновления с большей вероятностью способны сломать приложение. К таким обновлениям можно отнести добавление нового обязательного параметра в полезную нагрузку запроса или изменения полей в ответе.
Есть несколько способов версионирования ваших API.
Самый распространенный способ — включение версии в URI.
https://api.domain.com/v1.0/authors
В основе следующего способа лежит дата. При этом в URI включается дата выпуска версии. Преимущество этого способа в том, что любой разработчик сможет сразу увидеть, насколько часто вы обновляете ваш API.
https://api.domain.com/2020-06-15/authors
Третий способ — включение версии API в заголовки.
https://api.domain.com/authors x-api-version: v1
Самый рекомендуемый и приемлемый вариант версионирования — использовать имя версии в URI.
Разбивка на страницы
В мире все возрастающего количества точек данных становится нереальным отображать все данные на одном экране одновременно. Поэтому важно дать возможность пользователям получать определенное количество документов, прежде чем запрашивать новые. Это называется разбивкой на страницы (или пагинацией), а возвращаемый набор данных называется страницей.
Чтобы сделать возможной разбивку на страницы в вашем API, рекомендуется использовать особые фразы в полезной нагрузке запроса и ответа:
- STRING
page_token
(отправляется в запросе) - STRING
next_page_token
(возвращается API) - INT
page_size
(отправляется в запросе)
page_token
указывает, какую именно страницу должен вернуть API. Обычно это строка. Для первого вызова API page_token = “1”
.
page_size
определяет, сколько записей должно вернуться в ответе. Например, page_size = 100
вернет максимум 100 записей по одному вызову API.
next_page_token
определяет следующий токен в серии. Если после page_token="1"
есть дополнительные данные, возвращается значение next_page_token=”2”
. Если же данных больше нет, т. е., пользователь исчерпал их до конца, возвращается пустое значение next_page_token=””
.
Вот и все! Это были лучшие подходы, благодаря которым ваши API будут надежными, последовательными и простыми в интеграции с другими приложениями.
29 июля 2021
7 945
0
Время чтения ≈ 31 минута
Содержание:
- Что такое REST API
- Архитектура REST
- Исходные данные
- Настройка бэкенда REST API
- Создание пользовательского модуля
- Создание модуля «auth»
- Создание middleware-компонентов разрешений и валидации
- Запуск и тестирование REST API с Insomnia
- Следующие этапы
Интерфейсы прикладного программирования или API (Application Programming Interface) применяются в разработке повсеместно. Они позволяют одним программам последовательно взаимодействовать с другими — внутренними или внешними — программными компонентами. Это является ключевым условием масштабируемости, не говоря уже о возможности повторного использования приложений.
В настоящее время довольно распространены онлайн-сервисы, использующие общедоступные API. Они дают возможность другим разработчикам легко интегрировать такие функции, как авторизация через соцсети, платежи кредитной картой и отслеживание поведения.
Применяемый при этом стандарт де-факто называется «передачей состояния представления» (REpresentational State Transfer) или сокращённо REST. Простыми словами, REST API — это набор правил, по которым серверные приложения взаимодействуют с клиентскими.
Для создания простого, но безопасного бэкенда на основе REST API может быть задействовано множество платформ и языков программирования, например ASP.NET Core, Laravel (PHP) или Bottle (Python).
В этой же статье будет использоваться следующий инструментарий:
- js — как пример распространённой кроссплатформенной среды выполнения JavaScript.
- Express, который значительно упрощает выполнение основных задач веб-сервера в Node.js и является стандартным инструментом для создания серверной части на основе REST API.
- Mongoose, который будет соединять наш бэкенд с базой данных MongoDB.
Помимо вышеперечисленного, пользователям данного руководства необходимо уверенно владеть навыками работы с терминалом (или командной строкой).
Обратите внимание! В рамках инструкции не будет рассмотрена работа с кодовой базой фронтенда. Тем не менее, можно легко использовать код (например, объектные модели на стороне сервера и клиента), так как наш бэкенд написан на JavaScript.
Архитектура REST
Чтобы понять, как работает REST API, нужно подробнее рассмотреть, что представляет собой стиль архитектуры программного обеспечения REST.
REST API используются для доступа к данными и их обработки с помощью стандартного набора операций без сохранения состояния. Эти операции являются неотъемлемой частью протокола HTTP. Они представляют собой основные функции создания («create»), чтения («read»), модификации («update») и удаления («delete») и обозначаются акронимом CRUD.
Операциям REST API соответствуют, хотя и не полностью идентичны, следующие методы HTTP:
- POST (создание ресурса или предоставление данных в целом).
- GET (получение индекса ресурсов или отдельного ресурса).
- PUT (создание и замена ресурса).
- PATCH (обновление/изменение ресурса).
- DELETE (удаление ресурса).
С помощью этих HTTP-методов и имени ресурса в качестве адреса, мы можем построить REST API, создав конечную точку для каждой операции. В результате мы получим стабильную и легко понятную основу, которая позволит быстро дорабатывать код и осуществлять его дальнейшее сопровождение.
Та же основа будет применяться для интеграции сторонних функций, о которых было сказано чуть выше. Большинство из них тоже использует REST API, что ускоряет такую интеграцию.
Исходные данные
В этом руководстве мы создадим самый простой REST API с нуля для ресурса под названием «users» с помощью Node.js.
У ресурса «users» будет следующая базовая структура:
- id (UUID, что значит «автоматически сгенерированный универсальный уникальный идентификатор»).
- firstName (Имя)
- lastName (Фамилия)
- email (Электронная почта)
- password (Пароль)
- permissionLevel (что разрешено делать данному пользователю).
Для этого ресурса будут созданы следующие операции:
- «POST» в конечной точке «/users» (создание нового пользователя).
- «GET» в конечной точке «/users» (вывод списка всех пользователей).
- «GET» в конечной точке «/users/:userId» (получение конкретного пользователя).
- «PATCH» в конечной точке «/users/:userId» (обновление данных для конкретного пользователя).
- «DELETE» в конечной точке «/users/:userId» (удаление конкретного пользователя).
Кроме того, будет использоваться стандарт «JSON web token» для создания токенов доступа.
Поэтому мы создадим ещё один ресурс под названием «auth», который будет принимать адрес электронной почты и пароль пользователя. А в ответ — генерировать токен для аутентификации при определённых операциях.
Настройка бэкенда REST API
Прежде всего убедитесь, что у вас установлена самая последняя версия Node.js. Здесь будем использовать версию 14.9.0, однако могут подойти и более старые.
Затем позаботьтесь о том, чтобы у вас была установлена MongoDB. Не будем подробно расписывать особенности используемых здесь Mongoose и MongoDB.
Просто запустите сервер в интерактивном режиме (введя в командной строке «mongo»), а не как сервис. Дело в том, что в какой-то момент нам нужно будет взаимодействовать с MongoDB напрямую, а не через код Node.js.
Обратите внимание! С MongoDB нет необходимости создавать конкретную базу данных, как это бывает в сценариях некоторых реляционных СУБД. Первый вызов «insert» из кода Node.js инициирует её создание автоматически.
Данная статья не содержит всего кода, необходимого для рабочего проекта. Предполагается, что вы клонируете дополнительный репозиторий и просто следуете за основными пунктами, по мере чтения этого руководства. Если хотите, можете при необходимости копировать конкретные файлы и фрагменты кода из этого репозитория.
Перейдите к появившейся папке «rest-api-tutorial/» в терминале. Здесь находятся три папки модулей проекта:
- «common» — все общие сервисы и информация, совместно используемая пользовательскими модулями.
- «users» — всё, что касается пользователей.
- «auth» — обработка генерации токенов JWT (JSON Web Token) и поток авторизации.
Теперь запустите «npm install» (или «yarn», если он у вас есть).
Поздравляем, теперь у вас есть все зависимости и настройки, необходимые для запуска простого бэкенда на базе REST API.
Создание пользовательского модуля
Мы будем использовать Mongoose — ODM-библиотеку для MongoDB. С её помощью создадим модель «user» (пользователь) в рамках схемы «user» (пользователь).
Первым делом нужно создать схему для библиотеки Mongoose в «/users/models/users.model.js»:
const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });
После определения схемы можно легко присоединить её к модели «user».
const userModel = mongoose.model('Users', userSchema);
Теперь можем использовать эту модель для реализации всех операций «CRUD», которые нам нужны в конечных точках Express.
Начнём с операции создания пользователя, определив маршрут в «users/routes.config.js»:
app.post('/users', [ UsersController.insert ]);
Этот код добавляется в основной файл «index.js» приложения на Express. Объект «UsersController» импортируется из нашего контроллера, где мы соответствующим образом хешируем пароль, определённый в «/users/controllers/users.controller.js»:
exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest("base64"); req.body.password = salt + "$" + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };
Теперь мы можем протестировать нашу модель Mongoose, запустив сервер («npm start») и отправив запрос «POST» в «/users» с данными в формате JSON:
{ "firstName" : "Marcos", "lastName" : "Silva", "email" : "marcos.henrique@toptal.com", "password" : "s3cr3tp4sswo4rd" }
Для этого есть соответствующий инструментарий. Во-первых, это Insomnia (его рассмотрим далее) и Postman — популярные инструменты с графическим интерфейсом, а также curl — утилита командной строки.
Можно даже использовать один только JavaScript. Например, из встроенной консоли средств разработки вашего браузера:
fetch('http://localhost:3600/users', { method: 'POST', headers: { "Content-type": "application/json" }, body: JSON.stringify({ "firstName": "Marcos", "lastName": "Silva", "email": "marcos.henrique@toptal.com", "password": "s3cr3tp4sswo4rd" }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); });
Здесь результатом валидного «POST» будет только лишь идентификатор «id» от созданного пользователя:
{ "id": "5b02c5c84817bf28049e58a3" }
Нам также нужно добавить метод «createUser» к модели в «users/models/users.model.js»:
exports.createUser = (userData) => { const user = new User(userData); return user.save(); };
Теперь надо убедиться, что пользователь существует. Для этого реализуем функцию «get user by id» для следующей конечной точки, а именно «users/:userId».
Первым делом создаём маршрут в «/users/routes/config.js»:
app.get('/users/:userId', [ UsersController.getById ]);
Затем создаём контроллер в «/users/controllers/users.controller.js»:
exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };
И, наконец, добавляем метод «findById» к модели в «/users/models/users.model.js»:
exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };
Ответ будет примерно таким:
{ "firstName": "Marcos", "lastName": "Silva", "email": "marcos.henrique@toptal.com", "password": "Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }
Обратите внимание! Здесь мы видим хешированный пароль. В данном руководстве мы показываем пароль, но лучше никогда этого не делать, даже если пароль хеширован. Ещё у нас здесь есть «permissionLevel», который мы будем использовать для обработки разрешений пользователя.
Повторяя изложенный выше порядок действий, теперь можно добавить функциональность для обновления пользователя. Будем использовать операцию «PATCH», позволяющую отправлять только те поля, которые мы хотим изменить. Таким образом, мы будем отправлять любые поля, которые захотим изменить, по маршруту от «PATCH» к «/users/:userid».
Кроме того, потребуется провести дополнительную проверку. Ведь изменения должны затрагивать только соответствующего пользователя или администратора. И только администратор должен иметь возможность менять «permissionLevel» (уровень допуска).
Пока что оставим эту часть и вернёмся к ней после того, как реализуем модуль «auth». Сейчас наш контроллер будет выглядеть так:
exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); req.body.password = salt + "$" + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); };
Здесь мы по умолчанию будем отправлять HTTP-код «204» без тела ответа, чтобы показать, что запрос был успешным.
И нужно будет добавить к модели метод «patchUser»:
exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };
Список пользователей будет реализован как «GET» в «/users/» следующим контроллером:
exports.list = (req, res) => { let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : 10; let page = 0; if (req.query) { if (req.query.page) { req.query.page = parseInt(req.query.page); page = Number.isInteger(req.query.page) ? req.query.page : 0; } } UserModel.list(limit, page).then((result) => { res.status(200).send(result); }) };
Соответствующим методом модели будет:
exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); };
Ответ в результирующем списке будет иметь следующую структуру:
[ { "firstName": "Marco", "lastName": "Silva", "email": "marcos.henrique@toptal.com", "password": "z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }, { "firstName": "Paulo", "lastName": "Silva", "email": "marcos.henrique2@toptal.com", "password": "wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==", "permissionLevel": 1, "id": "5b02d038b653603d1ca69729" } ]
Теперь реализуем последнюю часть, «DELETE» в «/users/:userId». Наш контроллер для удаления:
exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };
Контроллер снова вернёт HTTP-код «204» с пустым телом ответа в качестве подтверждения.
Соответствующий метод модели должен выглядеть следующим образом:
exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };
Теперь у нас есть все необходимые операции для манипулирования ресурсом «user» (пользователь) и готов контроллер пользователя.
Приведённый здесь код должен сформировать представление об основных принципах использования REST. К этому коду нам предстоит ещё вернуться для проведения проверок и изменения разрешений, но сначала нужно обеспечить безопасность. Для этого мы создадим модуль аутентификации.
Создание модуля «auth»
Прежде чем обезопасить модуль «users» с помощью добавления middleware-компонентов (ПО промежуточной обработки) для обеспечения разрешений и валидации, нужно создать валидный токен текущего пользователя.
В качестве токена используем JSON web token (JWT). Это стандарт для создания токенов доступа, основанный на формате JSON, который можно использовать для безопасного выполнения пользователем нескольких запросов без повторной проверки. Мы сгенерируем JWT в ответ на предоставление пользователем валидного адреса электронной почты и пароля.
Обычно у токена есть определённое время действия: для обеспечения безопасности передачи данных каждые несколько минут создаётся новый токен. Но в данном руководстве мы воздержимся от обновления токена. Для простоты ограничимся одним-единственным токеном на каждую авторизацию.
Во-первых, создадим конечную точку для POST-запросов к ресурсу «/auth». Тело запроса будет содержать адрес электронной почты пользователя и пароль:
{ "email" : "marcos.henrique2@toptal.com", "password" : "s3cr3tp4sswo4rd2" }
Прежде чем задействовать контроллер, нужно проверить пользователя в «/authorization/middlewares/verify.user.middleware.js»:
exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };
После этого можно перейти к контроллеру и сгенерировать JWT:
exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64"); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };
Несмотря на то, что токен у нас в статье обновляться не будет, контроллер мы настроили таким образом, чтобы такую генерацию легко можно было реализовывать в последующей разработке.
Теперь нам нужно лишь создать маршрут и вызвать соответствующий middleware-компонент в «/authorization/routes.config.js»:
app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);
В ответе будет сгенерированный JWT (в поле «accessToken»):
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY", "refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==" }
Создав токен, мы можем использовать его внутри заголовка «Authorization» с помощью формы «Bearer ACCESS_TOKEN».
Создание middleware-компонентов разрешений и валидации
Первым делом нужно определить, кто может использовать ресурс «users». Вот сценарии, с которыми нам нужно будет иметь дело:
- Общедоступный для создания пользователей (процесс регистрации). В этом сценарии JWT использоваться не будет.
- Закрытый для авторизовавшегося пользователя и для администраторов, обновляющих этого пользователя.
- Закрытый для администратора только для удаления учётных записей пользователей.
Определившись со сценариями, перейдём к middleware-компонентам, которые всегда проверяют, использует ли пользователь валидный JWT.
Добавить middleware компонент в «/common/middlewares/auth.validation.middleware.js» можно также легко, как в примере ниже:
if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };
Для обработки ошибок запроса будем использовать коды ошибок HTTP:
- «HTTP 401» для недопустимого запроса.
- «НТТР 403» для валидного запроса с неверным токеном или для валидного токена с недопустимыми разрешениями.
Для управления разрешениями можно использовать побитовый оператор «И» (AND, &). Если возвести каждое требуемое разрешение во вторую степень, можно рассматривать каждый бит 32-битного целого числа как одно разрешение.
Администратор может получить все разрешения, установив их значение равным «2147483647». В данном случае этот пользователь будет иметь возможность получить доступ к любому маршруту.
Другой пример: пользователь, значение разрешения которого равно семи, будет иметь разрешения к правам доступа, отмеченным битами для значений 1, 2 и 4 (два в нулевой, первой и второй степени).
Middleware-компонент будет выглядеть следующим образом:
exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };
Компоненты middleware универсальны. Если уровень разрешений пользователя и необходимый уровень разрешений совпадают хотя бы в одном бите, результат будет больше нуля и действие может продолжаться. В противном случае будет возвращён HTTP-код «403».
Теперь нужно добавить middleware для аутентификации к маршрутам модуля «user» в «/users/routes.config.js»:
app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);
На этом завершается основная часть разработки REST API. Остаётся только всё это протестировать.
Запуск и тестирование REST API с Insomnia
В Insomnia есть хорошая бесплатная версия клиента REST. Будем использовать здесь данный инструмент как приложение, помогающее понять, что происходит с нашим API.
Лучше всего, конечно, сразу внедрять в проект тестирования кода и точную отчётность об ошибках. Но, когда данные службы недоступны, сторонний клиент REST отлично справляется с этой задачей.
Чтобы создать пользователя, нужно просто отправить с помощью «POST» необходимые поля в соответствующую конечную точку и сохранить сгенерированный идентификатор ID для последующего использования.
В ответ с API придёт идентификатор пользователя:
Теперь с помощью конечной точки «/auth/» можно сгенерировать JWT:
В ответе мы должны получить токен:
Берём «accessToken», ставим впереди него «Bearer» (не забываем о пробеле) и добавляем его в заголовки запроса внутри «Authorization»:
Если не сделать этого, то на каждый запрос, кроме регистрации, будет возвращаться HTTP-код «401». Тем более сейчас, когда мы внедрили middleware для разрешений. Но у нас есть валидный токен, поэтому от «/users/:userId» мы получаем следующий ответ:
Ранее здесь уже упоминалось, что в учебных целях и ради простоты понимания мы показываем все поля. На практике пароль (хешированный или другой) никогда не должен быть виден в ответе.
Попробуем получить список пользователей:
Неожиданно мы получаем ответ «403».
У нашего пользователя нет разрешений на доступ к этой конечной точке. Придётся поменять «permissionLevel» пользователя с 1 до 7 (даже «5» будет достаточно, так как наши бесплатные и платные уровни разрешений представлены значениями «1» и «4» соответственно.)
Это можно сделать вручную. Например, в интерактивной консоли MongoDB (идентификатор ID поменяйте на свой):
db.users.update({"_id" : ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}})
Затем нужно сгенерировать новый JWT. После чего мы получим нужный ответ:
Теперь протестируем обновлённый функционал, отправив запрос «PATCH» с произвольными полями в нашу конечную точку «/users/:userId»:
В ответ должен прийти «204» в качестве подтверждения успешности операции. Однако мы можем ещё раз запросить пользователя, чтобы убедиться наверняка.
И, наконец, нужно удалить пользователя. Потребуется создать нового пользователя, как мы это уже делали выше. Не забудьте обозначить его ID и позаботьтесь, чтобы для пользователя-администратора имелся соответствующий JWT.
Новый пользователь должен будет иметь разрешения со значением «2053» (т. е. «2048» — ADMIN — плюс наши предыдущие «5»), чтобы иметь возможность также выполнить операцию удаления.
После того как всё будет готово и сгенерирован новый JWT, нужно будет обновить заголовок запроса «Authorization»:
Отправив запрос «DELETE» на «/users/:userId», мы должны получить ответ «204» в качестве подтверждения. Можно дополнительно убедиться в удалении, отправив запрос ««/users/»» для получения списка всех имеющихся пользователей.
Следующие этапы создания REST API
Теперь с помощью инструментов и методов, рассмотренных в этом руководстве, вы сможете создавать простые и безопасные REST API на Node.js. Многое из того, что не является существенным для работы с REST API, было опущено. Так что не забывайте:
- Проводить проверки валидности формы (например, чтобы электронная почта пользователя была уникальной).
- Внедрять модульное тестирование и отчётность об ошибках.
- Не допускать изменения уровня разрешений самими пользователями.
- Не допускать возможности администраторам удалять самих себя.
- Не допускать разглашения конфиденциальной информации (например, хешированных паролей).
- Помещать пароли к токену и ключи авторизации из «common/config/env.config.js» в механизм их распределения, находящийся во внешнем хранилище.
В заключение читатель может попробовать перейти от использования в кодовой базе промисов JavaScript к применению «async/await».
Нужна надёжная база для разработки веб-приложений? Выбирайте виртуальные сервера от Eternalhost с технической поддержкой 24/7 и бесплатной защитой от DDoS!
Автор оригинала: Marcos Henrique da Silva
Оцените материал:
[Всего голосов: 0 Средний: 0/5]
Это вторая версия статьи на тему создания REST API с дополнительными комментариями по исходной статье для перевода.
При работе с проектами по интеграции, для получения данных на сайт клиента, в CRM или мобильное приложение из базы данных под управлением MS SQL — реализуем стандартный REST API.
Проще всего создать такую интеграцию используя Node.js и два популярных npm-модуля Express (оснастка веб-сервера) и mssql (MS SQL Server клиент для Node.js).
Сначала создаем таблицы sales и invoices в базе данных SQL-сервера, процедуру для добавления записей в таблицу invoices и заполняем таблицу sales несколькими тестовыми записями:
create table sales(
id int primary key identity(1,1),
title varchar(255) null,
amount dec(12,2) not null,
clientId int not null,
date_create datetime DEFAULT (getdate())
);
go
create table invoices(
id int primary key identity(1,1),
idSale int not null REFERENCES sales(id),
date_create datetime DEFAULT (getdate())
);
go
create procedure addInvoices
(
@idSales int
) as
begin
insert into invoices (idSale)
output inserted.*
select id from sales where id = @idSales
end
go
insert into sales (title, amount, clientId) values
(‘Заказ 1’, 100.80, 1),
(‘Заказ 2’, 120.30, 2),
(‘Заказ 3’, 78.11, 1);
go
Проверяем на SQL-сервере созданную таблицу и добавленные тестовые данные:
select * from sales
Переходим к созданию приложения в файле server.js добавляем код.
var express = require(‘express’); // оснастка веб сервера
var app = express();
var sql = require(‘mssql’); // клиент для MS SQL Server
// строка для подключения к базе данных.
var sqlConfig = {
user: ‘UserName’,
password: ‘mot de passe’,
server: ‘localhost’,
database: ‘DatabaseName’
}
// сервер для http://localhost:8081/
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log(«сервер доступен по url http://%s:%s», host, port)
});
После сохранения файла server.js проверим работоспособность сервера и выполнения файла скрипта:
node server.js
сервер должен вернуть сообщение о доступности для выполнения запросов, например:
сервер доступен по url http://localhost:8081
Добавим в файл server.js код обработки запроса к web-серверу для получения всех данных из таблицы SQL сервера sales.
app.get(‘/sales’, function (req, res) {
sql.connect(sqlConfig, function() {
var request = new sql.Request();
request.query(‘select * from sales’, function(err, resp) {
if(err) console.log(err);
res.json(resp.recordset); // результат в формате JSON
sql.close(); // закрываем соединение с базой данных
});
});
});
Сохраним файл, перезапустим сервер и проверим запрос в Postman, вернется JSON-объект с данными из таблицы SQL-сервера:
Усложняем запрос, добавим в обработчик параметр из URL для выборки по таблице invoices только запись с id = 2.
Передачу значения параметра из URL-запроса реализуем специальным отдельным методом — для исключения проблемы SQL-инъекций.
app.get(‘/sales/:id’, function (req, res) {
sql.connect(sqlConfig, function() {
var request = new sql.Request();
request.input(‘input_parameter’, sql.Int, Number(req.params.id)) // защита от SQL-инъекций и преобразование к числовому типу
.query(‘select * from sales where id = @input_parameter’, function(err, resp) {
if(err) console.log(err);
res.json(resp.recordset); // результат в формате JSON
sql.close(); // закрываем соединение с базой данных
});
});
});
Результат выполнения запроса, возврат отдельной записи из таблицы в формате JSON:
В следующем обработчике запроса добавим в таблицу “invoices” запись с новым заказом, для REST это должен быть метод HTTP, тип запроса POST :
app.post(‘/sales/:id/invoices’, function (req, res) {
sql.connect(sqlConfig, function() {
var request = new sql.Request();
request.input(‘idSales’, sql.Int, Number(req.params.id)) // защита от SQL-инъекций
.execute(‘addInvoices’, function(err, resp, returnValue, affected) {
if(err) console.log(err);
res.json(resp.recordset); // результат в формате JSON
sql.close(); // закрываем соединение с базой данных
});
});
});
Для тестирования запросов типа POST требуется установка в браузер дополнения, использования отдельного приложения, например Postman, или же запрос возможно выполнить при помощи curl, используя командную строку:
В результате выполнения запроса получаем json с данными о добавленной записи в таблице invoices. При повторном выполнении новый ID и дата добавления записи.
В результате скрипт приложения server.js следующего содержания:
var express = require(‘express’); // оснастка веб сервера
var app = express();
var sql = require(‘mssql’); // клиент для MS SQL Server
// строка для подключения к базе данных.
var sqlConfig = {
user: ‘UserName’,
password: ‘mot de passe’,
server: ‘localhost’,
database: ‘DatabaseName’
}
// сервер для http://localhost:8081/
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log(«сервер доступен по url http://%s:%s», host, port)
});
app.get(‘/sales’, function (req, res) {
sql.connect(sqlConfig, function() {
var request = new sql.Request();
request.query(‘select * from sales’, function(err, resp) {
if(err) console.log(err);
res.json(resp.recordset); // результат в формате JSON
sql.close(); // закрываем соединение с базой данных
});
});
});
app.get(‘/sales/:id’, function (req, res) {
sql.connect(sqlConfig, function() {
var request = new sql.Request();
request.input(‘input_parameter’, sql.Int, Number(req.params.id)) // защита от SQL-инъекций и преобразование к числовому типу
.query(‘select * from sales where id = @input_parameter’, function(err, resp) {
if(err) console.log(err);
res.json(resp.recordset); // результат в формате JSON
sql.close(); // закрываем соединение с базой данных
});
});
});
app.post(‘/sales/:id/invoices’, function (req, res) {
sql.connect(sqlConfig, function() {
var request = new sql.Request();
request.input(‘idSales’, sql.Int, Number(req.params.id)) // защита от SQL-инъекций
.execute(‘addInvoices’, function(err, resp, returnValue, affected) {
if(err) console.log(err);
res.json(resp.recordset); // результат в формате JSON
sql.close(); // закрываем соединение с базой данных
});
});
});
Дальнейшее дополнение скрипта приложения server.js это — включение в код обработчика ошибок, подключение к SQL через организацию пула соединений, обработка JSON встроенными функциями SQL-сервера.
Ссылка на источник для перевода и корректировки исходного кода статьи. Дополнительная информация по технологиям интеграции систем с использованием MS SQL-сервер — на сайте voInfo.ru.
Не так давно один из моих посетителей мне задал вопрос по e-mail: «Как создать свой API на сайте?«. Я решил, что это будет весьма полезно другим пользователям, тем более, что на кажущуюся сложность процесса, всё очень и очень просто. Необходимо лишь обладать самыми элементарными знаниями PHP.
Если Вы вдруг не понимаете, о чём идёт речь, то прочитайте сначала статью: что такое API. Идём дальше. Давайте разберём, а для каких сайтов нужен вообще API:
- Социальные сети (Facebook и другие). Здесь требуется API для получения информации о различных данных пользователя: его друзьях, личных сообщениях и прочей информации.
- Почтовые сервисы (например, mail.ru). В первую очередь, для получения писем. Иногда для отправки.
- Различные сервисы для создания Интернет-магазинов. Например, получить список новых заказов или список всех товаров в заданной категории.
- И много-много других сайтов.
Первое, что необходимо усвоить — это то, что API нужен далеко не каждому сайту (даже если он принадлежит одной из вышеуказанных групп).
Если же Вы считаете, что API на Вашем сайте необходим, то давайте разберём пример того, как он создаётся. Пусть у нас будет такая задача: есть ЭПС (как, например, WebMoney). И мы хотим, чтобы пользователь мог из своего кода, пользуясь нашим API, узнать свой баланс на счёте.
Создадим файл (например, api.php), который у нас будет принимать GET-запросы от пользователей на получение различной информации. Напишем в этом обработчике такой код:
<?php
if ($_GET['action'] == "getbalance") {
$balance;
//Узнаём из базы данных баланс аккаунта и записываем в переменную balance
echo $balance;
}
?>
Теперь разработчики API должны дать информацию пользователям о том, как надо отправлять запрос, чтобы пользователь мог узнать свой баланс на аккаунте:
http://mysite.ru/api.php?action=getbalance&key=fa9sgwlgjs9gdsjlgjdsjglsdlgs
Этот запрос пользователи формируют в своих скриптах (например, через cURL). Параметр key — это уникальный ключ каждого пользователя. И ответом этого запроса будет число, отвечающее за баланс пользователя. Аналогично создаются и все другие возможности API. Можно добавлять другие различные параметры: например, получить список операций пополнения счёта с одной даты по другую. Желательно, сами списки возвращать в формате JSON.
Вот так легко и просто создаётся API для сайта. Безусловно, нужен он Вам или нет — решать только Вам, а всю самую необходимую информацию я Вам только что дал.
-
Создано 02.07.2011 14:21:21
-
Михаил Русаков
Копирование материалов разрешается только с указанием автора (Михаил Русаков) и индексируемой прямой ссылкой на сайт (http://myrusakov.ru)!
Добавляйтесь ко мне в друзья ВКонтакте: http://vk.com/myrusakov.
Если Вы хотите дать оценку мне и моей работе, то напишите её в моей группе: http://vk.com/rusakovmy.
Если Вы не хотите пропустить новые материалы на сайте,
то Вы можете подписаться на обновления: Подписаться на обновления
Если у Вас остались какие-либо вопросы, либо у Вас есть желание высказаться по поводу этой статьи, то Вы можете оставить свой комментарий внизу страницы.
Если Вам понравился сайт, то разместите ссылку на него (у себя на сайте, на форуме, в контакте):
-
Кнопка:
Она выглядит вот так:
-
Текстовая ссылка:
Она выглядит вот так: Как создать свой сайт
- BB-код ссылки для форумов (например, можете поставить её в подписи):