Взаимодействие сервисов и REST API
В современной веб-разработке принято разделять backend-разработку (то, что выполняется на сервере – например, приложение на PHP) от frontend-разработки (то, что выполняется в браузере пользователя – JavaScript). Frontend выполняет запросы на backend и отрисовывает данные, которые backend ему возвращает. Но каким образом происходит этот обмен? Чем они обмениваются? Как выглядят данные, которые передаются между бэкендом и фронтендом? Об этом и пойдёт речь в данном уроке.
JSON
В уроке про composer мы с вами уже сталкивались с форматом JSON. И я вам в том уроке советовал погуглить об этом формате. Еще не сделали этого? Тогда сейчас – самое время.
Вжух!
Итак, вы уже знаете о формате JSON. Так вот, этот формат – это номер 1 среди форматов для обмена между современными приложениями. При этом бэкенд, который обменивается с клиентом в формате JSON, называется API (англ. application programming interface - программный интерфейс приложения). API принимает в качестве запроса JSON и отвечает тоже JSON-ом. Ну, точнее, не всегда именно JSON-ом, он может работать и в другом формате – XML, например. Но вся суть API в том, что он работает не с HTML, который красиво рендерится в браузере и приятен для восприятия человеком. API работает в формате, с которым удобно работать другим программам. Одна программа передаёт JSON в API, и получает от него ответ в формате JSON.
Пишем API
В этом уроке мы с вами напишем простейшее API для работы со статьями.
Первое, что нам следует сделать – это создать новый фронт-контроллер, который будет предназначен специально для работы в формате JSON.
Создаём в папке www папку api. А внутри нее – файл .htaccess:
www/api/.htaccess
RewriteEngine On
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^(.*)$ ./index.php?route=$1 [QSA,L]
И рядом с ним файл index.php
www/api/index.php
<?php
echo 123;
Проверяем, что всё работает, перейдя по адресу: http://myproject.loc/api/
Теперь попробуем вывести что-нибудь в формате json.
В PHP есть встроенные функции для работы с json. Нас будут интересовать прежде всего две: json_encode() и json_decode(). Первая позволяет представить какую-то сущность в json-формате.
www/api/index.php
<?php
$entity = [
'kek' => 'cheburek',
'lol' => [
'foo' => 'bar'
]
];
echo json_encode($entity);
Обновим страничку и увидим следующее:
Кроме того, когда сервер отвечает в фомате JSON, стоит отправлять соответствующий заголовок клиенту:
www/api/index.php
<?php
require __DIR__ . '/../../vendor/autoload.php';
$entity = [
'kek' => 'cheburek',
'lol' => [
'foo' => 'bar'
]
];
header('Content-type: application/json; charset=utf-8');
echo json_encode($entity);
Теперь поставьте в свой браузер расширение JSON formatter.
И снова обновите страничку. Вы увидите, что ответ сервера стало гораздо проще читать – это расширение добавляет форматирование, чтобы ответ было легче воспринимать человеку.
Теперь давайте сделаем наш API в ООП-стиле. Мы будем использовать ту же архитектуру MVC, в которой компонент View вместо рендеринга HTML-шаблонов будет выводить JSON. Давайте сделаем у View метод для вывода JSON-а.
src/MyProject/View/View.php
public function displayJson($data, int $code = 200)
{
header('Content-type: application/json; charset=utf-8');
http_response_code($code);
echo json_encode($data);
}
Теперь создадим контроллер, который позволит работать со статьями через API. Создаём сначала папку Api внутри Controllers, а затем добавляем наш новый контроллер:
src/MyProject/Controllers/Api/ArticlesApiController.php
<?php
namespace MyProject\Controllers\Api;
use MyProject\Controllers\AbstractController;
use MyProject\Exceptions\NotFoundException;
use MyProject\Models\Articles\Article;
class ArticlesApiController extends AbstractController
{
public function view(int $articleId)
{
$article = Article::getById($articleId);
if ($article === null) {
throw new NotFoundException();
}
$this->view->displayJson([
'articles' => [$article]
]);
}
}
Теперь создаём отдельный роутинг для API:
src/routes_api.php
<?php
return [
'~^articles/(\d+)$~' => [\MyProject\Controllers\Api\ArticlesApiController::class, 'view'],
];
И, наконец, пишем фронт-контроллер для API.
www/api/index.php
<?php
require __DIR__ . '/../../vendor/autoload.php';
try {
$route = $_GET['route'] ?? '';
$routes = require __DIR__ . '/../../src/routes_api.php';
$isRouteFound = false;
foreach ($routes as $pattern => $controllerAndAction) {
preg_match($pattern, $route, $matches);
if (!empty($matches)) {
$isRouteFound = true;
break;
}
}
if (!$isRouteFound) {
throw new \MyProject\Exceptions\NotFoundException('Route not found');
}
unset($matches[0]);
$controllerName = $controllerAndAction[0];
$actionName = $controllerAndAction[1];
$controller = new $controllerName();
$controller->$actionName(...$matches);
} catch (\MyProject\Exceptions\DbException $e) {
$view = new \MyProject\View\View(__DIR__ . '/../templates/errors');
$view->displayJson(['error' => $e->getMessage()], 500);
} catch (\MyProject\Exceptions\NotFoundException $e) {
$view = new \MyProject\View\View(__DIR__ . '/../templates/errors');
$view->displayJson(['error' => $e->getMessage()], 404);
} catch (\MyProject\Exceptions\UnauthorizedException $e) {
$view = new \MyProject\View\View(__DIR__ . '/../templates/errors');
$view->displayJson(['error' => $e->getMessage()], 401);
}
Всё, теперь можно зайти на наш API и проверить как выводится статья: http://myproject.loc/api/articles/1
Но вот незадача – вместо полей статьи мы видим только две фигурные скобки - {}. А всё потому, что функция json_encode не умеет преобразовывать в JSON объекты. Однако, можно её «научить». Для этого нужно чтобы класс реализовывал специальный интерфейс – JsonSerializable и содержал метод jsonSerialize(). Этот метод должен возвращать представление объекта в виде массива. Я предлагаю сделать такой метод на уровне ActiveRecordEntity, чтобы все его наследники автоматически могли преобразовываться в JSON.
Добавляем реализацию интерфейса:
src/MyProject/Models/ActiveRecordEntity.php
abstract class ActiveRecordEntity implements \JsonSerializable
и добавляем метод, который представит объект в виде массива:
public function jsonSerialize()
{
return $this->mapPropertiesToDbFormat();
}
Обновляем страничку http://myproject.loc/api/articles/1 и вуаля - статья в JSON-формате!
Postman
Но что, если мы захотим изменить нашу статью с помощью API? Для этого нам нужно отправить в API запрос в формате JSON. В реальном приложении для этого используется фронтенд на JS. А в целях разработки – специальные инструменты, позволяющие отпралять такие запросы. Одним из таких инструментов является приложение Postman. Скачайте, установите и запустите.
В контроллере добавим еще один метод:
src/MyProject/Controllers/Api/ArticlesApiController.php
public function add()
{
$input = json_decode(
file_get_contents('php://input'),
true
);
var_dump($input);
}
Здесь php://input – это входной поток данных. Именно из него мы и будем получать JSON из запроса. file_get_contents – читает данные из указанного места, в нашем случае из входного потока. А json_decode декодирует json в структуру массива. После чего мы просто выводим массив с помощью var_dump().
Добавляем для него роут:
src/routes_api.php
<?php
return [
'~^articles/(\d+)$~' => [\MyProject\Controllers\Api\ArticlesApiController::class, 'view'],
'~^articles/add$~' => [\MyProject\Controllers\Api\ArticlesApiController::class, 'add'],
];
И заполняем Postman данными, как на скриншоте:
После этого жмём кнопку Send. Прокручиваем ниже до ответа и выбираем вкладку Preview.
Тут мы видим вывод var_dump той структуры, которую мы отправили в POST-запросе в формате JSON.
Давайте вынесем функционал чтения входных данных в абстрактный контроллер:
src/MyProject/Controllers/AbstractController.php
protected function getInputData()
{
return json_decode(
file_get_contents('php://input'),
true
);
}
И теперь во всех контроллерах мы сможем получать входные данные вот так:
src/MyProject/Controllers/Api/ArticlesApiController.php
public function add()
{
$input = $this->getInputData();
var_dump($input);
}
Давайте теперь сделаем функционал, который позволит сохрянять в базу данных статью, пришедшую в формате JSON.
src/MyProject/Controllers/Api/ArticlesApiController.php
public function add()
{
$input = $this->getInputData();
$articleFromRequest = $input['articles'][0];
$authorId = $articleFromRequest['author_id'];
$author = User::getById($authorId);
$article = Article::createFromArray($articleFromRequest, $author);
$article->save();
header('Location: /api/articles/' . $article->getId(), true, 302);
}
Разумеется, здесь также стоит добавить авторизацию и проверять, является ли авторизованный пользователь тем, кто указан в авторе статьи. Но это учебный и упрощенный пример, который показывает сам принцип работы с JSON-API.
Снова возвращаемся в Postman и повторно жмем Send.
Прокручиваем вниз до ответа, но на этот раз переходим во вкладку Pretty.
Как видим, статья успешно добавилась и выводится в формате JSON по адресу http://myproject.loc/api/articles/id_статьи.
REST API
То что мы сейчас с вами написали – это простейший учебный пример API. Есть более сложные системы для реализации API. Они позволяют привязывать роутинг к конкретному типу запроса. Например, POST-запрос по адресу http://myproject.loc/api/articles/1 вызовет в контроллере экшн update, который будет обновлять статью с id=1. А GET-запрос по тому же адресу будет вызывать экшн view, который будет просто возвращать статью.
То есть для одного и того же адреса мы отправляем разные типы запросов – POST, GET, PUT, DELETE. И в зависимости от типа запроса будут вызваны разные экшены. В рамках текущего курса мы этого делать не будем – ограничимся простым примером, чтобы вы просто понимали концепцию.
При этом структура запроса и ответа как правило одинаковые – мы можем посмотреть статью в формате JSON. Чтобы обновить её – мы тоже отправляем статью в формате JSON, с теми же полями.
Вот этот стиль взаимодействия с API в формате JSON, когда мы используем одну и ту же структуру данных для запроса и ответа, и используем разные типы запросов для разных действий – называется REST API. Запомните это, об этом могут спросить на собеседовании: «Что такое REST API». И вы скажете, что это когда:
- Запрос и ответ имеют одинаковую структуру
- Используются разные типы запросов (GET, POST, PUT, DELETE и другие).
- Используется формат, с которым удобно работать другим программам (чаще всего JSON, но могут быть и другие – например, XML).
Заключение
Стоит отметить, что API используется не только для взаимодействия между фронтендом и бэкендом, но еще и для взаимодействия между разными сервисами на бэкенде. В одном проекте может быть несколько приложений на бэкенде, которые общаются между собой по API. Один сервис отправляет в другой сервис сообщение в JSON-формате. Тот его принимает и преобразует JSON в данные для работы.
Конечно, тут все зависит от компании – где-то вообще не используют API и рендерят HTML-шаблоны, а где-то наоборот – на бэкенде ни одного HTML-тега. В любом случае, основы HTML вы уже знаете, а большего вам, как бэкендеру, о фронтенде и знать ничего не нужно. Многие когда начинают проходить мои курсы спрашивают - а будет ли курс по CSS. И я отвечаю - нет. Большую часть работы вы будете писать код на PHP, скорее всего разрабатывая API и вообще не касаясь фронтенда.
Но в целом все нормальные компании стремятся сейчас к разделению фронтенда и бэкенда - команда узкоспециализированных профи работает куда круче такой же кучки "мастеров на все руки". Те, кто это понимают - обгоняют команды с фуллстеками, потому что человеческий мозг гораздо лучше работает с чем-то одним, а не всем подряд.
Комментарии