В блог

Разработка RESTful API

В каждом проекте, где есть связь с внешним миром, необходима разработка API (Application programming interface).

С помощью API мы можем обмениваться данными между нашим web-приложением и любой внешней системой.

Один из примеров – мобильные приложения. Чтобы в мобильном приложении появились какие-то данные: лента новостей, список заказов или просто график работы, необходимо разработать API.

Slim Framework

Для сложных реализаций API мы используем Slim Framework – микрофреймворк, позволяющий быстро и качественно разработать API.

Устанавливается при помощи composer:

php composer.phar create-project slim/slim-skeleton [app-name]

Далее вся работа будет основываться на двух вещах: маршруты (routes) и посредники (middleware).

Маршруты

Маршрутами мы определяем какой запрос надо выполнить в зависимости от URL.

Маршруты можно разграничить по HTTP-методам:

<?php

$app->get('/action/{id}', function ($request, $response, $args) {
    // Получаем информацию по акции $args['id']
    // Будет приниматься только GET-запрос
});

$app->post('/action', function ($request, $response, $args) {
    // Добавляем новую акцию
    // Будет приниматься только POST-запрос
});

$app->put('/action/{id}', function ($request, $response, $args) {
    // Обновляем акцию $args['id']
    // Будет приниматься только PUT-запрос
});

$app->delete('/action/{id}', function ($request, $response, $args) {
    // Удаляем акцию $args['id']
    // Будет приниматься только DELETE-запрос
});

$app->any('/action/{id}', function ($request, $response, $args) {
    // Будет доступно для любого HTTP-метода
});

Параметры запроса могут быть обязательными или не обязательными.

<?php

// {city} - обязательный
$app->get('/{city}/actions', function (Request $request, Response $response, array $args) {
    $items = [];

   // Подбираем акции в зависимости от города.
   // Если город не указан – выдаем ошибку.

   return $response->withJson(['response' => 'success', 'actions' => $items]);
});

// {city} - не обязательный
$app->get('/actions[/{city}]', function (Request $request, Response $response, array $args) {
    $items = [];

   // Выбираем все акции, если город отсутствует, либо фильтруем по городу, если он указан

   return $response->withJson(['response' => 'success', 'actions' => $items]);
});

$app->get('/actions/{id:[0-9]+}', function ($request, $response, $args) {
    // В качестве $args['id'] будут приниматься только числа
});

В Slim Framework есть штатный обработчик ошибок:

  • Если маршрут не найден – notFoundHandler
  • Если доступ к маршруту запрещен с данным HTTP-методом – notAllowedHandler
  • Если был выброшен Exception – errorHandler
  • Runtime-ошибка PHP (PHP 7.0+) – phpErrorHandler

Стандартно в Slim ошибки выдаются в формате text/html. Так как мы используем формат JSON, то нам необходимо переопределить стандартный вывод ошибок.

Для этого в файле settings.php дописываем:

'notFoundHandler' => function ($c) {
   return function ($request, $response, $exception) use ($c) {
      return $c['response']->withStatus(404)
         ->withJson(['response' => 'error', 'code' => 'METHOD_NOT_FOUND', 'description' => 'Method is not found']);
   };
},
'errorHandler' => function ($c) {
   return function ($request, $response, $exception) use ($c) {
      return $c['response']->withStatus(500)
         ->withJson(['response' => 'error', 'code' => 'INTERNAL_SERVER_ERROR', 'description' => 'Internal server error: ' . $exception->getMessage()]);
   };
},
'notAllowedHandler' => function ($c) {
   return function ($request, $response, $exception) use ($c) {
      return $c['response']->withStatus(405)
         ->withJson(['response' => 'error', 'code' => 'METHOD_NOT_ALLOWED', 'description' => 'Method is not allowed']);
   };
},
'phpErrorHandler' => function ($c) {
   return function ($request, $response, $exception) use ($c) {
      return $c['response']->withStatus(500)
         ->withJson(['response' => 'error', 'code' => 'INTERNAL_SERVER_ERROR', 'description' => 'Internal server error: ' . $exception->getMessage()]);
   };
},

Middleware

Middleware является посредников между получением запроса и его обработкой.

Например, мы можем защитить наш API с помощью Basic-авторизации.

Для этого устанавливаем composer-пакет:

php composer.phar require tuupola/slim-basic-auth
и в middleware.php добавляем:
<?php

$app->add(new Tuupola\Middleware\HttpBasicAuthentication([
    'users' => [
        'user_one' => 'Pa$s1',
        'user_two' => 'p^ss2'
    ]
]));

После этого для всех запросов будет запрошена авторизация.

Или можем добавить middleware только для одного метода, возьмем для примера так же авторизацию:

<?php

$middle = new Tuupola\Middleware\HttpBasicAuthentication([
    'users' => [
        'user_one' => 'Pa$s1',
        'user_two' => 'p^ss2'
    ]
]);

$app->get('/profile', function ($request, $response, $args) {
    
    $user = [];	
    // Получаем профиль авторизованного пользователя

    return $response->withJson(['response' => 'success', 'user' =>$user]);
})->add($middle);
Параметры из Middleware в маршрут:
<?php

$needAuth = function (ServerRequestInterface $request, ResponseInterface $response, $next) {
	$phone = $request->getHeader('X-Application-User');
	$hash = $request->getHeader('X-Application-Token');

	if (!$phone || !$hash) {
		return $response->withStatus(401)->withJson([
			'response'    => 'error',
			'code'        => 'UNAUTHORIZED',
			'description' => 'Unauthorized'
		]);
	}

	// Находим пользователя, которому соответствует номер телефона и специальный хэш

	$request = $request->withAttribute('userId', $userId);
	$response = $next($request, $response);

	return $response;
};

$app->get('/profile', function ($request, $response, $args) {
    
    $userId = $request->getAttribute('userId');

    // Получаем профиль авторизованного пользователя по $userId

    return $response->withJson(['response' => 'success', 'user' =>$user]);
})->add($needAuth);

Конечно, авторизация – не единственная задача, которую можно решить с помощью Middleware. Также можно реализовать добавление особых заголовков (например, CORS HTTP-ответ вашего приложения) или логирование всех HTTP-запросов.

Prominado: REST

UPD: С версии 1С-Битрикс 18.0 модуль Rest появился в коробке. Подробнее о том, как добавить свои методы – можно прочитать в документации.

Для разработки простого API для сайтов на базе 1С-Битрикс, нами был разработан модуль Prominado: REST.

С помощью модуля можно быстро разработать структуру API.

Модуль может:

  • Разграничить методы HTTP запроса (какие-то методы доступны только для POST запросов, какие-то только для GET, а какие-то разрешены и так и так)

  • Защищать запросы (например, чтобы показывать в приложении только заказы авторизованного пользователя, нам нужно защитить запрос с помощью авторизации)

Чтобы начать работу с модулем, после установки, необходимо зарегистрировать обработчик для события 1С-Битрикс.

Подробнее про то, как работают события и как зарегистрировать обработчик, написано в документации 1С-Битрикс.

<?php

$event = \Bitrix\Main\EventManager::getInstance();
$event->addEventHandler('prominado.rest', 'onRestMethodBuildDescription', 'restServiceDescription');

function restServiceDescription()
{
    return [
        'data.get' => [
            'allow_methods' => [],
            'callback' => ['\\Prominado\\Events\\Rest', 'dataGet']
        ],
        'data.update' => [
            'allow_methods' => ['POST'],
            'authenticator' => ['\\Prominado\\Events\\Rest', 'isAuthorized'],
            'callback' => ['\\Prominado\\Events\\Rest', 'dataUpdate']
        ],
    ];
}

В данном примере мы создаем 2 запроса: data.get, который будет доступен без авторизации и с любым методом HTTP запроса и data.update, который доступен только POST-запросом и только если будет пройдена авторизация.

Структура запросов единая: http://my_website.ru/rest/

Наши методы будут доступны по ссылкам: http://my_website.ru/rest/data.get и http://my_website.ru/rest/data.update.

Теперь нам необходимо обработать эти запросы на стороне сайта:

<?php

namespace Prominado\Events;

class Rest
{
    public function dataGet(\Prominado\Rest\Request $request)
    {
        // Если не указан id - выдаем ошибку
        $userId = $request->getQuery('id');

        if(!$userId) {
            throw new \Prominado\Rest\RestException('No user_id passed'); 
        }

        // Ответ сервера будет установлен 200 OK («хорошо»)
        $request->withStatus(200);
        // Устанавливаем заголовок ответа
        $request->withHeader('X-Token', 'prominado-web-access');

        // Возвращаем в теле ответа данные (будут преобразованы в JSON)
        return ['user' => ['NAME' => 'Prominado']];
    }

    public function dataUpdate(\Prominado\Rest\Request $request)
    {
        $userId = $request->getQuery('id');
        $fields = $request->getQuery('fields');

        // Проверяем на наличие id в запросе
        if(!$userId) {
            throw new \Prominado\Rest\RestException('No user_id passed'); 
        }

        // Проверяем на наличие fields в запросе
        if(!$fields) {
            throw new \Prominado\Rest\RestException('No fields passed'); 
        }

        $request->withStatus(200);
        $request->withHeader('X-Token', 'prominado-web-access');

        return ['user' => ['NAME' => 'Prominado']];
    }

    /**
    * Метод проверки авторизации
    *
    * Здесь можно реализовать любую логику авторизации
    * В качестве результата метод должен вернуть
    * true, если авторизация успешна
    * false, если авторизация не удалась
    */
    public function isAuthorized(Request $request) 
    {
        $server = $request->getServer();
    
        preg_match('/Bearer\s(.*)/', $server['REMOTE_USER'], $matches);
        if ($matches[1]) {
            return true;
        }

        return false;
    }
}

Важно: Файл с обработчиками событий REST должен быть подключен. Например, с помощью composer или напрямую в файле init.php.

Хочу проект
Закрыть