Как писать api на php
Перейти к содержимому

Как писать api на php

  • автор:

Пример простого REST API на PHP.

В данной заметке пример самого простого REST API на PHP без использования какого-либо фреймворка и других средств. Целью есть предоставить общую картину — как это все работает.
Недавно я уже опубликовал статью, в которой описан процесс создания REST API для проекта на Yii2.

Т.к. никакие фреймворки с маршрутизаторами в примере использоваться не будут, нужно начать с перенаправления всех запросов на «точку входа» — файл index.php. Для сервера на Apache это можно сделать в файле .htaccess который должен располагаться в корне проекта:

Options +FollowSymLinks IndexIgnore */* RewriteEngine on # Перенаправление с ДОМЕН на ДОМЕН/api RewriteCond % ^/$ RewriteRule ^(.*)$ /api/$1 [R=301] #Если URI начинается с api/ то перенаправлять все запросы на index.php RewriteEngine On RewriteCond % !-f RewriteCond % !-d RewriteRule ^api/(.*)$ /index.php

Согласно правил, ссылка должна начинаться на /api и ,например, для API работающего с таблицей users должна иметь такой вид:
ДОМЕН/api/users

Пример файла index.php

run(); > catch (Exception $e) < echo json_encode(Array('error' =>$e->getMessage())); >

Как видно из кода — будем работать с объектом usersApi, т.е. с пользователями (таблица users). Т.к. для простоты примера я не использую тут Composer или другой механизм для автозагрузки классов, просто подключим файл класса с помощью

require_once 'UsersApi.php';

Кроме пользователей, может потребоваться сделать api и для других сущностей, поэтому все классы различных API должны иметь один общий костяк, который будет определять метод запроса, действие для выполнения и тд. Создаем файл Api.php c абстрактным классом Api:

requestUri = explode('/', trim($_SERVER['REQUEST_URI'],'/')); $this->requestParams = $_REQUEST; //Определение метода запроса $this->method = $_SERVER['REQUEST_METHOD']; if ($this->method == 'POST' && array_key_exists('HTTP_X_HTTP_METHOD', $_SERVER)) < if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'DELETE') < $this->method = 'DELETE'; > else if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'PUT') < $this->method = 'PUT'; > else < throw new Exception("Unexpected Header"); >> > public function run() < //Первые 2 элемента массива URI должны быть "api" и название таблицы if(array_shift($this->requestUri) !== 'api' || array_shift($this->requestUri) !== $this->apiName) < throw new RuntimeException('API Not Found', 404); >//Определение действия для обработки $this->action = $this->getAction(); //Если метод(действие) определен в дочернем классе API if (method_exists($this, $this->action)) < return $this->action>(); > else < throw new RuntimeException('Invalid Method', 405); >> protected function response($data, $status = 500) < header("HTTP/1.1 " . $status . " " . $this->requestStatus($status)); return json_encode($data); > private function requestStatus($code) < $status = array( 200 =>'OK', 404 => 'Not Found', 405 => 'Method Not Allowed', 500 => 'Internal Server Error', ); return ($status[$code])?$status[$code]:$status[500]; > protected function getAction() < $method = $this->method; switch ($method) < case 'GET': if($this->requestUri) < return 'viewAction'; >else < return 'indexAction'; >break; case 'POST': return 'createAction'; break; case 'PUT': return 'updateAction'; break; case 'DELETE': return 'deleteAction'; break; default: return null; > > abstract protected function indexAction(); abstract protected function viewAction(); abstract protected function createAction(); abstract protected function updateAction(); abstract protected function deleteAction(); >

Осталось реализовать абстрактные методы и свойство $apiName, которое уникально для каждого отдельного API. Для этого создаем файл UsersApi.php:

getConnect(); $users = Users::getAll($db); if($users)< return $this->response($users, 200); > return $this->response('Data not found', 404); > /** * Метод GET * Просмотр отдельной записи (по id) * http://ДОМЕН/users/1 * @return string */ public function viewAction() < //id должен быть первым параметром после /users/x $id = array_shift($this->requestUri); if($id)< $db = (new Db())->getConnect(); $user = Users::getById($db, $id); if($user)< return $this->response($user, 200); > > return $this->response('Data not found', 404); > /** * Метод POST * Создание новой записи * http://ДОМЕН/users + параметры запроса name, email * @return string */ public function createAction() < $name = $this->requestParams['name'] ?? ''; $email = $this->requestParams['email'] ?? ''; if($name && $email)< $db = (new Db())->getConnect(); $user = new Users($db, [ 'name' => $name, 'email' => $email ]); if($user = $user->saveNew())< return $this->response('Data saved.', 200); > > return $this->response("Saving error", 500); > /** * Метод PUT * Обновление отдельной записи (по ее id) * http://ДОМЕН/users/1 + параметры запроса name, email * @return string */ public function updateAction() < $parse_url = parse_url($this->requestUri[0]); $userId = $parse_url['path'] ?? null; $db = (new Db())->getConnect(); if(!$userId || !Users::getById($db, $userId))< return $this->response("User with not found", 404); > $name = $this->requestParams['name'] ?? ''; $email = $this->requestParams['email'] ?? ''; if($name && $email)< if($user = Users::update($db, $userId, $name, $email))< return $this->response('Data updated.', 200); > > return $this->response("Update error", 400); > /** * Метод DELETE * Удаление отдельной записи (по ее id) * http://ДОМЕН/users/1 * @return string */ public function deleteAction() < $parse_url = parse_url($this->requestUri[0]); $userId = $parse_url['path'] ?? null; $db = (new Db())->getConnect(); if(!$userId || !Users::getById($db, $userId))< return $this->response("User with not found", 404); > if(Users::deleteById($db, $userId))< return $this->response('Data deleted.', 200); > return $this->response("Delete error", 500); > >

Методы связанные с базой данных и получением данных из нее просто для примера.

Автор: Сергей Дата публикации: 05.10.2018

  • Использование YouTube Data API для своего WEB-сервиса. Выборка нужных видеозаписей, получение информации.
  • Определение страны и города посетителя по его IP. Расширение GeoIP.
  • Установка, настройка и базовое использование фреймворка для тестирования «Codeception».
  • Основы PHPUnit — 1 часть.
  • Использование событий в PHP.

Создание современного API на PHP в 2020 году

Итак, на примере этого API, я хочу показать современную PHP архитектуру для высоконагруженных проектов. Когда проект еще в самом начале, и не то, что бизнеслогика (взаимоотношения с базой данных) не прописана, но и сама бизнес модель не очень ясна, построение эффективной IT архитектуры может идти только одним путем: необходимо жестко разделить frontend и backend.

Что обычно делали в таких ситуациях два-три года назад? Брался монолитный фрейворк типа Laravel или Yii2, вся бизнес модель разбивалась, худо-бедно, на блоки, а эти блоки уже имплементировались как модули фреймворка. В итоге еще через 3 года получалась огромная не поворотная машина, которая сама по себе медленная, а становилась почти невыносимо медленной, в которой фронтенд рендится через бэкенд посредством классической MVC архитеркутуры (пользователь отправил запрос, контроллер его подхватил, вызвал модель, та в свою очередь чего-то там натворила с базой данных, вернула все контроллеру, а тот наконец-то вызвал вьювер, вставил туда данные из модели и отдал это все пользователю, который уже успел открыть очередную банку пива. ). А… ну еще особо продвинутые ребята, они не просто вьюверели Tweeter Bootstrap, а во вьювер вкручивали на самом деле очень хорошие библиотеки типа JQuery или вместо вьювера использовали какой-нибудь фронтенд фреймворк. В итоге поддерживать такой БеЛаЗ становилось все сложнее, а ввести нового программиста в команду было очень сложно, ибо не все рождаются Энштейнами. Добавим сюда тотальное отсутствие документации разработчика (камменты в 9000 файлах почитаешь — там все есть!) и в итоге, смотря на это все, становилось по-настоящему грустно…

Но тут произошел ряд событий, который в корне изменил ситуацию. Во-первых, наконец-то, вышли стандарты PSR и Symfony внезапно перестал быть единственным модульным фремворком, во-вторых, вышел ReactJS, который позволил полноценно разделить фронтенд от бэкенда и заставить их общаться через API. И добивая последний гвоздь в крышку гроба старой системы разработки (MVC — это наше все!) выходит OpenAPI 3.0, собственно, который и регулирует стандарты этого общения через API между фронтендом и бэкендом.

И в мире PHP стало возможно делать следующее:

  1. Разделить бизнес модель на сервисы и микросервисы, и не поднимать для этого весь БеЛаЗ, а обслуживать запросы микросервисов буквально в пару строк кода — самый банальный пример: похожие товары (отдельный запрос GET — отдельный, малюпасенький API, который его обработал и вернул, и мгновенный вывод этой информации ReactJS в браузере пользователя. Основной БеЛаЗ даже и не узнал о том, что произошло.
  2. Писать API стандартизировано по стандарту OpenAPI 3.0 ( swagger.io ) в виде YAML или JSON файла, когда каждый программист не лезет в грязных сапогах в ядро системы, а например, культурно дописывает свою часть в общем YAML файле, тем самым устраняя вероятность ошибки от человека к человеку и уменьшая количество седых волос у тестеровщиков. Просто потом из готового YAML сгенерировал полностью рабочий и даже с middleware сервер. На каком угодно языке и фреймфорке.
  3. Теперь не надо стало нанимать кого-то, чтобы он, этот кто-то писал для вашего API библиотеки, которыми ваши клиенты будут обращаться к вашему API: github.com/OpenAPITools/openapi-generator — я насчитал генерацию более 40 серверов для API и даже не стал считать библиотеки для доступа к ним, ибо единственный язык программирования который я там не нашел — Dlang.

Теперь становятся вопросы, точнее два вопроса, а что у нас перед API и соответственно, что у нас «под хвостом» после API.

1.«Там вдали за рекой», далеко перед API у нас цветет, расползается на новую функциональность и картинки — ФРОНТЕНД (Предпочтительнее ReactJS, но Vue тоже сойдет. Хотя там из коробки всего столько много, что утяжелять процесс он будет, а вот насколько в реальной жизни это понадобится — не совсем понятно и зависит напрямую от бизнес модели). И НЕТ! Я к этому зверю даже близко подходить не буду, ибо с детства у меня на него аллергия. Тут нужен отдельный специалист. Я не фулстак и не пишу фронтенд.

2. Прямо вот перед самим API у нас… НЕ УГАДАЛИ… не NGINX, а RoadRunner roadrunner.dev/features. Заходим, читаем, понимаем, что это быстрее и вокеры расписываются по количеству процессоров, а посему никогда не будет таблички «МЫ НА ПРОФИЛАКТИКЕ», ибо просто надо вокеры переключить.

И на этом моменте хочу остановиться подробнее. Ибо в моем понимании есть три пути «как мух ловить», запросы то бишь…

1. В случае если весь API написан уже, и будет написан в дальнейшем, на PHP — голову ломать не зачем, ставим RoadRunner с prometheus.io

2. В случае, если система собирается из разных кусков, разные сервисы написаны на разных языках и дальше тоже не понятно на чем их писать будут:

2.1. Ставим NGINX UNIT — пользуемся поддерживаемыми языками.

2.2. Поднимаем ВСЕ РАВНО КАКУЮ систему контейнеров, Docker, LXC, LXD. Выбор опять же зависит от размера проекта — поддерживать сборку PROXMOX-LXC на хостинге в 12 процессоров, c 32Гб памяти, за 40 евро в месяц будет в разы дешевле, чем Docker сборки на Google Cloud Platform. В каждый контейнер ставим подходящий к языку сервер, и связываем все это HAProxy www.haproxy.org. HAProxy — шикарный балансер и прокси сервер который в корпоративной среде, не менее популярен, чем NGINX. Что он делает, а чего нет, читаем тут cbonte.github.io/haproxy-dconv/2.3/intro.html пункт 3.1. При такой архитектуре сервисы или микросервисы могут писаться на чем угодно и никто не зависит от ограничений накладываемыми RoadRunner или NGINX UNIT.

3. «Под хвостом» — Cycle ORM. Не ленимся, смотрим видео, что будет стоять за ней конкретно MySQL или PostgreSQL- опять, я бы оставил на после того, как будет понятна бизнес схема проекта. MySQL проще масштабируется, в PostgreSQL — больше бизнес логики перенесенной внутрь самой базы.

4. Пример, который можно посмотреть и пощупать. Там за основу взято тестовое задание. Все самые полезные вещи находятся в папке EXTRAS. Там уже есть jar file генератора, YAML swagger файл API, сгенерированный API stub через OpenAPITools в SLIM4. Даже с аутентификацией и middleware. Документация на API сгенрированная, правда swagger, не OpenAPITools. Предполагается, что некоторые юзеры залогинены и им выдан токен. Там уже стоит RoadRunner впереди. Стек — PHP 7.4.10, PostgreSQL 12.4.

После Git clone, composer install в файле /bootstrap.php прописываем юзера и пароль к базе, которую вначале создаем, потому что это PostgreSQL, по умолчанию сервер слушает локальный порт 8888, если нужно — меняем в файле /.rr.yaml, и выполняем команду: composer run-script fill-database. Все — никаких миграций. Пользуемся.

P.S. Всем залогиненым пользователям присвоен одинаковый токен. Вообще в примере нет никаких валидаций ввода пользователя и почти нет защит — это пример в основном нацеленный на архитектуру.

С уважением,
Кирилл Лапчинский
api-studio.com
mail@api-studio.com

Пример простого создания REST API на PHP

В данной статье рассказывается пример простого REST API реализация которого на PHP без использования каких-либо фреймворков. Из-за того, что никак фреймворки применяться не будут, необходимо начать с того, что перенаправления всех запросов на точку входа, это index.php . Сервер где используется Apache , это можно реализовать в файле htaccess который должен находится в корне проекта:

Options +FollowSymLinks IndexIgnore */* RewriteEngine on # Перенаправление с domain.by на domain.by/api RewriteCond % ^/$ RewriteRule ^(.*)$ /api/$1 [R=301] #Если адрес начинается с api/ , то перенаправлять все запросы на index.php RewriteEngine On RewriteCond % !-f RewriteCond % !-d RewriteRule ^api/(.*)$ /index.php 

Исходя из правил, ссылка должна начинаться с /api , допустим, для API работающего с таблицей people должна иметь такой вид: domain.by/api/people Пример файла index.php :

require_once 'PeopleApi.php'; try < $api = new usersApi(); echo $api->run(); > catch (Exception $e) < echo json_encode(Array('error' =>$e->getMessage())); > 

Как видно из примера, будет идти работа с объектом peopleApi , в примере показано подключение файла c классом непосредственно через require_once .

require_once 'PeopleApi.php'; 

Кроме людей еще возможно нужно будет сделать API и по другим сущностям, и поэтому все классы различных API должны иметь один общий костяк, который будет определять метод запроса, действие для выполнения и так далее. Создаем файл API с абстрактным классом Api :

abstract class Api < public $apiName = ''; //people protected $method = ''; //GET|POST|PUT|DELETE public $requestUri = []; public $requestParams = []; protected $action = ''; //Название метода для выполнения public function __construct() < header("Access-Control-Allow-Orgin: *"); header("Access-Control-Allow-Methods: *"); header("Content-Type: application/json"); //Массив GET параметров разделенных / $this->requestUri = explode('/', trim($_SERVER['REQUEST_URI'],'/')); $this->requestParams = $_REQUEST; //Определение метода запроса $this->method = $_SERVER['REQUEST_METHOD']; if ($this->method == 'POST' && array_key_exists('HTTP_X_HTTP_METHOD', $_SERVER)) < if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'DELETE') < $this->method = 'DELETE'; > else if ($_SERVER['HTTP_X_HTTP_METHOD'] == 'PUT') < $this->method = 'PUT'; > else < throw new Exception("Unexpected Header"); >> > public function run() < //Первые 2 элемента массива URI должны быть "api" и название таблицы if(array_shift($this->requestUri) !== 'api' || array_shift($this->requestUri) !== $this->apiName) < throw new RuntimeException('API Not Found', 404); >//Определение действия для обработки $this->action = $this->getAction(); //Если метод(действие) определен в дочернем классе API if (method_exists($this, $this->action)) < return $this->action>(); > else < throw new RuntimeException('Invalid Method', 405); >> protected function response($data, $status = 500) < header("HTTP/1.1 " . $status . " " . $this->requestStatus($status)); return json_encode($data); > private function requestStatus($code) < $status = array( 200 =>'OK', 404 => 'Not Found', 405 => 'Method Not Allowed', 500 => 'Internal Server Error', ); return ($status[$code])?$status[$code]:$status[500]; > protected function getAction() < $method = $this->method; switch ($method) < case 'GET': if($this->requestUri) < return 'viewAction'; >else < return 'indexAction'; >break; case 'POST': return 'createAction'; break; case 'PUT': return 'updateAction'; break; case 'DELETE': return 'deleteAction'; break; default: return null; > > abstract protected function indexAction(); abstract protected function viewAction(); abstract protected function createAction(); abstract protected function updateAction(); abstract protected function deleteAction(); > 

Напоследок осталось реализовать абстрактные методы и свойство $apiName , которые будут уникальны для каждого отдельного API . Для примера создаем файл PeopleApi.php :

require_once 'Api.php'; require_once 'Db.php'; require_once 'People.php'; class PeopleApi extends Api < public $apiName = 'people'; /** * Метод GET * Вывод списка всех записей * https://domain.by/people * @return string */ public function indexAction() < $db = (new Db())->getConnect(); $people = People::getAll($db); if($people)< return $this->response($people, 200); > return $this->response('Data not found', 404); > /** * Метод GET * Просмотр отдельной записи (по id) * https://domain.by/people/1 * @return string */ public function viewAction() < //id должен быть первым параметром после /people/x $id = array_shift($this->requestUri); if($id)< $db = (new Db())->getConnect(); $user = People::getById($db, $id); if($user)< return $this->response($user, 200); > > return $this->response('Data not found', 404); > /** * Метод POST * Создание новой записи * https://domain.by/people + параметры запроса name, email * @return string */ public function createAction() < $name = $this->requestParams['name'] ?? ''; $email = $this->requestParams['email'] ?? ''; if($name && $email)< $db = (new Db())->getConnect(); $user = new People($db, [ 'name' => $name, 'email' => $email ]); if($user = $user->saveNew())< return $this->response('Data saved.', 200); > > return $this->response("Saving error", 500); > /** * Метод PUT * Обновление отдельной записи (по ее id) * https://domain.by/people/1 + параметры запроса name, email * @return string */ public function updateAction() < $parse_url = parse_url($this->requestUri[0]); $userId = $parse_url['path'] ?? null; $db = (new Db())->getConnect(); if(!$userId || !People::getById($db, $userId))< return $this->response("User with not found", 404); > $name = $this->requestParams['name'] ?? ''; $email = $this->requestParams['email'] ?? ''; if($name && $email)< if($user = People::update($db, $userId, $name, $email))< return $this->response('Data updated.', 200); > > return $this->response("Update error", 400); > /** * Метод DELETE * Удаление отдельной записи (по ее id) * https://domain.by/people/1 * @return string */ public function deleteAction() < $parse_url = parse_url($this->requestUri[0]); $userId = $parse_url['path'] ?? null; $db = (new Db())->getConnect(); if(!$userId || !People::getById($db, $userId))< return $this->response("User with not found", 404); > if(People::deleteById($db, $userId))< return $this->response('Data deleted.', 200); > return $this->response("Delete error", 500); > > 

Простое REST api для сайта на php хостинге

Иногда бывает необходимо развернуть не большое рест апи для своего сайта, сделанного по технологии СПА (Vue, React или др.) без использования каких-либо фреймворков, CMS или чего-то подобного, и при этом хочется воспользоваться обычным php хостингом с минимальными усилиями на внедрение и разработку. При этом там же желательно разместить и сам сайт СПА (в нашем случае на vue).

Использование php позволяет для построения ендпоинтов апи использовать даже статические php файлы, размещаемые просто в папках на хостинге, которые предоставляют результат при непосредственном обращении к ним. И хотя, видимо в своё время, такой подход послужил широкому распространению php мы рассмотрим далее более программистский подход к созданию апи, который очень похож на используемый в библиотеке Node.js Express и поэтому интуитивно понятен, и прост для освоения. Для это нам понадобиться библиотека «pecee/simple-router».

Далее мы предполагаем, что у вас уже есть среда для запуска кода локально (LAMP, XAMP, docker) или как-то иначе и у вас настроено перенаправление всех запросов на индексный файл (index.php). Кроме, того мы предполагаем, что вы можете устанавливать зависимости через composer.

Структура проекта

На Рис.1. представлена общая структура проекта. Точкой входа является файл

index.phpв папке web. Сама папка webявляется публично доступной папкой, и должна быть указана в настройках сервера как корневая. В папке configбудут находится настройки роутов наших ендпоинтов. В папке controllerбудут обработчики ендпоинтов маршрутов. В папке middlewaresмы разместим промежуточные обработчике роутов для выполнения авторизации перед началом основного кода ендпоинта. В папках exceptions, views и models будут соответственно исключения, html шаблон и объектные модели. Полный код проекта тут.

Инсталляция и запуск

Для работы необходимо инсталлировать следующее содержимое composer.json (composer install в корне проекта).

// composer.json < "require": < "pecee/simple-router": "*", "lcobucci/jwt": "^3.4", "ext-json": "*" >, "autoload": < "psr-4": < "app\\": "" >> > 

Обратите внимание, что ‘app\’ объявлено как префикс для namespace. Данный префикс будет использоваться при объявлении неймспейсов классов.

Запуск всего остального кода происходит вызовом статического метода Router::route() в файле index.php

Так же тут подключаются роуты определённые в файле config/routes.php.

Подключение SPA на Vue.js 2 к проекту на php

Если вы развёртываете сборку vue отдельно от апи, то этот раздел можно пропустить.

Рассмотрим теперь то, как подключить проект на vue в данной конфигурации с использованием соответствующих маршрутов. Для этого содержимое сборки необходимо поместить в папку web. В файле маршрутов (‘/config/routes.php’) прописываем два правила:

; Router::setDefaultNamespace('app\controllers'); Router::get('/', 'VueController@run'); // правило 1 Router::get('/controller', 'VueController@run') ->setMatch('/\/([\w]+)/'); // правило 2 

Для пустого (корневого) маршрута ‘/’ вызывается метод run класса VueController. Второе правило указывает что для любого явно незаданного пути будет тоже вызываться VueController, чтобы обработка маршрута происходила на стороне vue. Это правило всегда должно быть последним, чтобы оно срабатывало только тогда, когда другие уже не сработали. Метод run представляет собой просто рендеринг файла представления с помощью метода renderTemplate(), определённого в родительском классе контроллера. Здесь мы также устанавливаем префикс для классов методы которых используются в роутах с помощью setDefaultNamespace.

renderTemplate('../views/vue/vue_page.php'); > > 

В свою очередь представление vue_page.php тоже просто отрисовка индексного файла сборки vue.

Итого мы подключили проект на vue к проекту на php, который уже готов к развертыванию на хостинге. Данный подход можно использовать для любых проектов на php. Осталось только рассмотреть, что собой представляет родительский класс AbstractController.

request = Router::router()->getRequest(); $this->response = new Response($this->request); > public function renderTemplate($template) < ob_start(); include $template; return ob_get_clean(); >public function setCors() < $this->response->header('Access-Control-Allow-Origin: *'); $this->response->header('Access-Control-Request-Method: OPTIONS'); $this->response->header('Access-Control-Allow-Credentials: true'); $this->response->header('Access-Control-Max-Age: 3600'); > > 

В конструкторе класса AbstractController определяются поля $request и $response. В $request хранится распарсенный классом Pecee\Http\Router запрос. А $response будет использоваться для создания ответов на запросы к апи. Определённый здесь метод renderTemplate используется для рендеринга представлений (html страниц). Кроме того, здесь определён метод устанавливающий заголовки для работы с политикой CORS. Его следует использовать если запросы к апи происходят не с того же адреса, т.е. если сборка vue запускается на другом веб-сервере. Теперь перейдём непосредственно к созданию апи.

Создание REST API эндпоинтов

Для работы с апи нам нужно произвести дополнительную обработку входящего запроса, потому что используемая библиотека не производит парсинг сырых данных. Для этого создадим промежуточный слой ProccessRawBody и добавим его как middleware в роуты для запросов к апи.

 $value) < $request->$key = $value; > > catch (\Throwable $e) < >> > > 

Здесь мы считываем из входного потока и помещаем полученное в объект $request для дальнейшего доступа из кода в контроллерах. ProccessRawBody реализует интерфейс IMIddleware обязательный для всех middleware.

Теперь создадим группу роутов для работы с апи использующее данный промежуточный слой.

 'api/v1', 'middleware' => [ ProccessRawBody::class ] ], function () < Router::post('/auth/sign-in', 'AuthController@signin'); Router::get('/project', 'ProjectController@index'); >);

У этой группы определён префикс «api/v1» (т.е. полный путь запроса должен быть например ‘/api/v1/auth/sign-in’), и ранее определённое нами middleware ProccessRawBody::class, так что в контроллерах наследованных от AbstractController доступны входные переменные через $request. AuthController рассмотрим чуть позже сейчас же мы уже можем воспользоваться методами не требующими авторизации, как например ProjectController::index.

response->json([ [ 'name' => 'project 1' ], [ 'name' => 'project 2' ] ]); > >

Как видим, на входящий запрос, в ответе возвращаются данные о проектах.

Остальные роуты создаются аналогичным образом.

Авторизация по JWT токену

Теперь перейдём к роутам требующим авторизации. Но перед этим реализуем вход и получение jwt-токена. Для создания токена и его валидации мы будем использовать библиотеку “ lcobucci/jwt” Всё это будет у нас выполнятся по роуту определённому ранее ‘/auth/sign-in’. Соответственно в AuthController::singin у нас прописана логика выдачи jwt-токена после авторизации пользователя.

builder() // Configures the issuer (iss claim) ->issuedBy('http://example.com') // Configures the audience (aud claim) ->permittedFor('http://example.org') // Configures the id (jti claim) ->identifiedBy('4f1g23a12aa') // Configures the time that the token was issue (iat claim) ->issuedAt($now) // Configures the expiration time of the token (exp claim) ->expiresAt($now->modify('+2 minutes')) // Configures a new claim, called "uid" ->withClaim('uid', $user->id) // Configures a new header, called "foo" ->withHeader('foo', 'bar') // Builds a new token ->getToken($config->signer(), $config->signingKey()); return $this->response->json([ 'accessToken' => $token->toString() ]); > >

Здесь используется симметричная подпись для jwt с использованием секретного ключа ‘секретный_ключ’. По нему будет проверятся валидность токена при запросах к апи. Ещё можно использовать асимметричную подпись с использованием пары ключей.

Можно также отметить, что можно создавать сколько угодно клаймов ->withClaim(‘uid’, $user->id) и сохранять там данные которые можно будет потом извлекать из ключа. Например, id пользователя для дальнейшей идентификации запросов от этого пользователя. Токен выдан на 2 минуты (->expiresAt($now->modify(‘+2 minutes’))) после чего он становится не валидным. ->issuedBy и ->permittedFor используются для oath2.

Теперь создадим группу роутов защищённую авторизацией. Для этого определим для группы роутов промежуточный слой Authenticate::class.

 'api/v1', 'middleware' => [ ProccessRawBody::class ] ], function () < Router::post('/auth/sign-in', 'AuthController@signin'); Router::get('/project', 'ProjectController@index'); Router::group([ 'middleware' =>[ Authenticate::class ] ], function () < // authenticated routes Router::post('/project/create', 'ProjectController@create'); Router::post('/project/update/', 'ProjectController@update') ->where(['id' => '[\d]+']); >); >); 

Как видите, группа с авторизацией объявлена внутри группы с префиксом “api/v1 ”. Рассмотрим роут ‘/project/update/’. Здесь объявлен параметр id который определён как число. В метод update, контроллера Projectcontroller будет передана переменная $id содержащая значение этого параметра. Ниже приведён пример запроса и ответ.

 > */ public function update(int $id): string < // код обновляющий проект return $this->response->json([ [ 'response' => 'OK', 'request' => $this->request->project, 'id' => $id ] ]); > > 

Вернёмся теперь к промежуточному слою Authenticate::class с помощью которого происходит авторизация запросов к апи.

parser()->parse($tokenString); if ( !$config->validator()->validate( $token, new SignedWith( new Sha256(), InMemory::plainText('секретный_ключ') ), new ValidAt(new FrozenClock(new DateTimeImmutable())) ) ) < throw new NotAuthorizedHttpException('Токен доступа не валиден или просрочен'); >$userId = $token->claims()->get('uid'); $request['uid'] = $userId; > > 

Здесь, считывается заголовок ‘Authorization: Bearer [token]’ (так называемая bearer авторизация) и извлекается оттуда токен, которые клиенты получают после логина и должны посылать со всеми запросами, требующими авторизацию. Далее с помощью парсера jwt-токен-строчка парсится. И дальше с помощью валидатора распарсенный токен валидируется. Метод validate() возвращает true or false. В случае не валидного токена выбрасывается исключение NotAuthorizedException. Если токен валидный, то мы извлекаем из него id пользователя $token->claims()->get(‘uid’) и сохраняем в переменную запроса $request, чтобы его можно было использовать дальше в контроллере. NotAuthorizedException определяется следующим образом:

В завершении рассмотрим ещё обработку ошибок. В файле routes.php запишем следующие строчки:

httpCode(401); break; > case Exception::class: < $response->httpCode(500); break; > > if (PROD) < return $response->json([]); > else < return $response->json([ 'status' => 'error', 'message' => $exception->getMessage() ]); > >);

В итоге файл routes.php будет выглядеть следующим образом:

Рис. 2. Итоговая структура проекта

; use app\middlewares\< Authenticate, ProccessRawBody >; use Pecee\< Http\Request, SimpleRouter\SimpleRouter as Router >; const PROD = false; Router::setDefaultNamespace('app\controllers'); Router::get('/', 'VueController@run'); Router::group([ 'prefix' => 'api/v1', 'middleware' => [ ProccessRawBody::class ] ], function () < Router::post('/auth/sign-in', 'AuthController@signin'); Router::get('/project', 'ProjectController@index'); Router::group([ 'middleware' =>[ Authenticate::class ] ], function () < // authenticated routes Router::post('/project/create', 'ProjectController@create'); Router::post('/project/update/', 'ProjectController@update') ->where(['id' => '[\d]+']); >); >); Router::get('/controller', 'VueController@run') ->setMatch('/\/([\w]+)/'); Router::error(function(Request $request, Exception $exception) < $response = Router::response(); switch (get_class($exception)) < case NotAuthorizedHttpException::class: < $response->httpCode(401); break; > case Exception::class: < $response->httpCode(500); break; > > if (PROD) < return $response->json([]); > else < return $response->json([ 'status' => 'error', 'message' => $exception->getMessage() ]); > >); 

Заключение

В итоге у нас получилось небольшое, простое REST api для небольших проектов которое можно использовать на обычном php хостинге с минимальными трудозатратами на его (хостинга) настройку. Полный код проекта тут.

Больше настроек роутов можно найти здесь. Вместо рассмотренной библиотеки «pecee/simple-router» можно использовать любую другую аналогичную библиотеку или даже микрофреймворк Slim.

Пс. Если вы используете публичный репозиторий или придерживаетесь бестпрактис, то не следует хранит секретный ключ в коде. Для этого можно использовать переменные среды или локальные файлы, которые не добавляются в репозиторий. Код работы с jwt токенами можно выделить в отдельный класс в папке services.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *