Взаимодействие сервисов и REST API

28.10.2018 в 14:52
30390
+760

В современной веб-разработке принято разделять 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/

Ответ API

Теперь попробуем вывести что-нибудь в формате json.

В PHP есть встроенные функции для работы с json. Нас будут интересовать прежде всего две: json_encode() и json_decode(). Первая позволяет представить какую-то сущность в json-формате.

www/api/index.php

<?php

$entity = [
    'kek' => 'cheburek',
    'lol' => [
        'foo' => 'bar'
    ]
];

echo json_encode($entity);

Обновим страничку и увидим следующее:

Ответ API JSON

Кроме того, когда сервер отвечает в фомате 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.

И снова обновите страничку. Вы увидите, что ответ сервера стало гораздо проще читать – это расширение добавляет форматирование, чтобы ответ было легче воспринимать человеку.

Форматированный JSON

Теперь давайте сделаем наш 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-формате!

Статья в формате 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 данными, как на скриншоте:
Postman

После этого жмём кнопку Send. Прокручиваем ниже до ответа и выбираем вкладку Preview.
Ответ API

Тут мы видим вывод 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.
Ответ API в формате JSON

Как видим, статья успешно добавилась и выводится в формате 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». И вы скажете, что это когда:

  1. Запрос и ответ имеют одинаковую структуру
  2. Используются разные типы запросов (GET, POST, PUT, DELETE и другие).
  3. Используется формат, с которым удобно работать другим программам (чаще всего JSON, но могут быть и другие – например, XML).

Заключение

Стоит отметить, что API используется не только для взаимодействия между фронтендом и бэкендом, но еще и для взаимодействия между разными сервисами на бэкенде. В одном проекте может быть несколько приложений на бэкенде, которые общаются между собой по API. Один сервис отправляет в другой сервис сообщение в JSON-формате. Тот его принимает и преобразует JSON в данные для работы.

Конечно, тут все зависит от компании – где-то вообще не используют API и рендерят HTML-шаблоны, а где-то наоборот – на бэкенде ни одного HTML-тега. В любом случае, основы HTML вы уже знаете, а большего вам, как бэкендеру, о фронтенде и знать ничего не нужно. Многие когда начинают проходить мои курсы спрашивают - а будет ли курс по CSS. И я отвечаю - нет. Большую часть работы вы будете писать код на PHP, скорее всего разрабатывая API и вообще не касаясь фронтенда.

Но в целом все нормальные компании стремятся сейчас к разделению фронтенда и бэкенда - команда узкоспециализированных профи работает куда круче такой же кучки "мастеров на все руки". Те, кто это понимают - обгоняют команды с фуллстеками, потому что человеческий мозг гораздо лучше работает с чем-то одним, а не всем подряд.

loader
28.10.2018 в 14:52
30390
+760
Логические задачи с собеседований