Версия: 3.x
Внешнее API

Модуль обеспечивает взаимодействие интернет-магазина с внешними приложениями посредством публичных методов API. Любой сторонний модуль может привнести собственные методы или откорректировать результат работы "чужого" метода с помощью обработки событий.

Модуль автоматически формирует справку на всех задействованных языках и по всем версиям доступных методов API на основе PHPDoc комментариев. Справка будет доступна по адресу, если соответствующая опция включена в настройках модуля:

http://ваш-домен.ру/api[-ВАШ API КЛЮЧ]/help/<br>
Пример 1(если API ключ пустой): http://full.readyscript.ru/api/help
Пример 2(если API ключ = 123): http://full.readyscript.ru/api-123/help

Чтобы обратиться к методу API, необходимо выполнить POST или GET запрос на адрес конкретного API метода.

http://ваш-домен.ру/api[-ВАШ API КЛЮЧ]/methods/группа метода.действие метода<br>
Пример 1(если API ключ пустой): http://full.readyscript.ru/api/methods/oauth.token?v=V&lang=LANGUAGE&PARAMETERS...
Пример 2(если API ключ = 123): http://full.readyscript.ru/api-123/methods/oauth.token?v=V&lang=LANGUAGE&PARAMETERS...

где:

  • V - необязательный, версия метода API. По умолчанию - 1
  • LANGUAGE - необязательный, двухсимвольный идентификатор языка. По умолчанию - ru
  • PARAMETERS - остальные параметры запроса. См описание метода API

В случае использования POST запроса, обязательно следует передавать заголовок:

Content-type: application/x-www-form-urlencoded

В ответ на запрос вы получите результат в формате JSON.

Настройка модуля Внешнее API

Для начала работы модуля, необходимо произвести его настройку в разделе Веб-сайт → Настройка модулей → Внешнее API. Модуль имеет следующие опции:

Вкладка "Основные"

  • Разрешить работу API только на следующем домене - с помощью данной опции, можно ограничить доступность API только на отдельном домене или поддомене. Пустое поле будет означать, что API будет работать на любом домене вашего сайта.
  • API ключ(придумайте его) - с помощью данной опции можно сделать URL к вашим API уникальным, тем самым сделав API не публичными. Во время разработки допустимо оставлять данное поле пустым.
  • Включить возможность видеть справку по внешнему API по ссылке /api[-API ключ]/help - удобно включать данную опцию во время разработки, чтобы разработчики приложений могли видеть справку по API.
  • Отображать детальную информацию по внутренним ошибкам при вызове API. - если данная опция включена, то при возникновении исключения с идентификатором inside, вместо фразы "Внутренняя ошибка" будет отображен реальный текст исключения с подробностями.
  • Время жизни авторизационного токена в секундах - время, в течении которого будет действителен выданный авторизационный токен. Рекомендуемое значение - 1-3 года.
  • Версия API по умолчанию - данная версия будет использована, если параметр v не будет передан в запросе к API
  • Включить логирование запросов - в случае включения данной опции на странице настроек модуля можно просматривать журнал обащений к API с детализацией запросов к API и ответов

Вкладка "Разрешенные методы API"

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

Заметки
Если ожидаемый метод отсутствует в списке, отключите КЭШ`ирование на время разработки в разделе Управление → Настройка системы, проверьте включен ли модуль, который должен привнести свои методы API.

Разработка собственного метода API

Рассмотрим пример создания собственного метода API news.getList на основе простейшего модуля ModuleName. Предположим, что наш метод API должен возвращать данные в формате JSON о новостях для авторизованного пользователя.

Модуль ModuleName должен быть создан и установлен предварительно. Для того, чтобы модуль ModuleName привнес в систему новый API метод, необходимо создать класс в заранее установленном пространстве имен. Имя API метода состоит из двух символьных идентификаторов Группы и Действия, например: order.getList или order.update. Общая маска имени класса метода API выглядит следующим образом:

\ModuleName\Model\ExternalApi\ГРУППА МЕТОДА\ДЕЙСТВИЕ МЕТОДА

Согласно данной маске, необходимо создать файл getlist.inc.php в папке /modules/modulename/model/externalapi/news, в котором мы разместим наш класс.

Базовые классы методов API

Перед созданием класса нашего метода API, необходимо выбрать подходящий родительский класс в зависимости от задач метода. ExternalApi предлагает несколько базовых классов для API (нажав на каждый базовый класс, вы перейдете в дерево наследования, из которого легко можно найти и посмотреть пример кода реальных классов-потомков):

  • ExternalApi::Model::AbstractMethods::AbstractMethod - базовый класс, всю реализацию метода и проверку прав в этом случае вы берете на себя. Является родителем всех классов-методов API.
    Обязательные для реализации методы
    • process(custom_param1, custom_param2, ...):array - метод, в котором ожидается вся логика выполнения метода

  • ExternalApi::Model::AbstractMethods::AbstractAuthorizedMethod - для выполнения такого метода требуется авторизационный токен либо обязательно либо опционально для доступа к расширенным данным.
    Обязательные для реализации методы

    • process(custom_param1, custom_param2, ...):array - метод, в котором ожидается вся логика выполнения метода
    • getRightTitles():array - должен возвращать ассоциативный массив, где в ключе будет константа прав доступа, а в значении Строковое пояснение прав

    Необязательные для реализации методы

    • getRunRights():array - должен возвращать какие права необходимы для начального запуска метода. По умолчанию - все, что вернет getRightTitles()

    Дополнительные свойства, влияющие на логику работы класса:

    • $token_require:bool=true, если указано false, то токен может быть не указан
    • $token - в данном свойстве будет доступен объект ExternalApi::Model::Orm::AuthorizationToken

  • ExternalApi::Model::AbstractMethods::AbstractGet - класс содержит набор необходимых функций для выборки одного ORM объекта по ID для авторизованного пользователя
    Обязательные для реализации методы
    • getOrmObject():RS::Orm::AbstractObject - должен возвращать ORM объект, который необходимо выбирать по ID

  • ExternalApi::Model::AbstractMethods::AbstractFilteredList - класс содержит набор необходимых функций для выборки списка объектов с фильтрацией(опционально). Не содержит реализованного метода process
    Обязательные для реализации методы
    • getDaoObject():RS::Module::AbstractModel::EntityList - должен возвращать DAO(Data Access Object) объект для выборки ORM объектов. Подробнее о таких объектах написано здесь.
    • process($token, custom_param1, custom_param2, ...):array - метод, в котором ожидается вся логика выполнения метода
    • getAllowableFilterKeys():array - метод, который должен вернуть массив с описанием поддерживаемых фильтров.

  • ExternalApi::Model::AbstractMethods::AbstractGetList - класс содержит набор необходимых функций для выборки списка ORM объектов. Частный случай AbstractFilteredList, только здесь использование фильтра обязательно.
    Обязательные для реализации методы
    • getDaoObject():RS::Module::AbstractModel::EntityList - должен возвращать DAO объект для выборки ORM объектов. Подробнее о таких объектах написано здесь.

  • ExternalApi::Model::AbstractMethods::AbstractGetTreeList - класс содержит набор необходимых функций для выборки дерева ORM объектов
    Обязательные для реализации методы
    • getDaoObject():RS::Module::AbstractModel::TreeList - должен возвращать DAO объект для выборки дерева ORM объектов. Подробнее о таких объектах написано здесь.

  • ExternalApi::Model::AbstractMethods::AbstractUpdate - класс содержит набор необходимых функций для обновления ORM объекта
    Обязательные для реализации методы
    • getOrmObject():RS::Orm::AbstractObject - должен возвращать ORM объект, который следует обновить
    • getUpdateDataScheme():array - должен возвращать схему валидации для класса в виде массива. Данный массив будет передан в конструктор класса ExternalApi::Model::Validator::ValidateArray

При необходимости, вы можете перегрузить стандартные методы API собственной логикой с помощью механизмов ООП. Например:

  • run($params, $version = null, $lang = 'ru'):bool - запускает выполнение метода
  • validateRights($params, $version):bool - проверяет права на запуск текущего метода. Либо возвращает true, либо бросает исключение ExternalApi::Model::Exception
  • validateParams($params, $version):bool - проверяет корректность значений в переданных параметрах к API. Либо возвращает true, либо бросает исключение Externalapi::Model::Exception
  • getAcceptRequestMethod():array - возвращает массив с константами(GET, POST, FILES), кторый означает из каких суперглобальных переменных будет строиться аргумент $params, передаваемый в метод API
  • prepareDocComment($text, $lang):string - обрабатывает PHPDoc комментарий, перед отображением в документации. Здесь можно реализовать собственную логику автозамены каких-либо переменных.

Теоретически, мы всегда можем выбрать в качестве родителя ExternalApi::Model::AbstractMethods::AbstractMethod, и самостоятельно реализовать всю логику проверки входящих данных и выборки новостей. Это выглядело бы примерно так:

namespace ModuleName\Model\ExternalApi\News;
use \ExternalApi\Model\Exception as ApiException;
/**
* Возвращает список новостей
*/
{
/**
* Валидирует значение фильтра
*
* @param array $filter
*/
protected function validateFilter($filter)
{
//Здесь логика проверки данных в переменной...
//Если есть ошибка, то бросаем исключение throw new ApiException('...', ApiException::ERROR_WRONG_PARAM_VALUE)
}
/**
* Валидирует значение сортировки
*
* @param string $sort
*/
protected function validateSort($sort)
{
//Здесь логика проверки данных в переменной...
//Если есть ошибка, то бросаем исключение throw new ApiException('...', ApiException::ERROR_WRONG_PARAM_VALUE)
}
/**
* Выполняет запрос на выборку новостей
*
* @param string $token Авторизационный токен
* @param array $filter Фильтр(справка будет сформирована автоматически). #filters-info
* @param string $sort Сортировка(справка будет сформирована автоматически). #sort-info
* @param integer $page Номер страницы, начиная с 1
* @param integer $pageSize Размер страницы
* ...
*/
protected function process($token,
$filter = array(),
$sort = 'id desc',
$page = "1",
$pageSize = "20")
{
//Начинаем валидировать переменные
$this->validateFilter();
$this->validateSort();
//page и pageSize автоматически приведены к integer
//Делаем запрос к БД
$api = new \Article\Model\Api();
$api->setFilter($filter);
$api->setOrder($sort);
$articles = $api->getList($page, $pageSize);
return array(
'response' => array(
'articles' => \ExternalApi\Model\Utils::extractOrmList($articles);
)
)
}
}

Но мы выберем более простой путь и воспользуемся готовым базовым классом, который уже все это делает за нас. Так как наш метод news.getList должен возвращать список новостей для авторизованных пользователей, наиболее подходящий родительский класс для нас является ExternalApi::Model::AbstractMethods::AbstractGetList.

Создадим файл в нашем модуле согласно указанной выше маске /modules/modulename/model/externalapi/news/getlist.inc.php со следующим содержанием:

namespace ModuleName\Model\ExternalApi\News;
/**
* Возвращает список новостей
*/
{
/**
* Возвращает DAO (Data Access Object) объект для обеспечения выборки данных
*
* @return \Article\Model\Api
*/
public function getDaoObject()
{
return new \Article\Model\Api();
}
/**
* Возвращает возможный ключи для фильтров.
* Фильтры можно будет задать в параметре filter[title]=...&filter[parent]=...
*
*
* @return [
* 'поле' => [
* 'title' => 'Описание поля. Если не указано, будет загружено описание из ORM Объекта'
* 'type' => 'тип значения',
* 'func' => 'постфикс для функции makeFilter в текущем классе, которая будет готовить фильтр, например eq',
* 'values' => [возможное значение1, возможное значение2]
* ]
* ]
*/
public function getAllowableFilterKeys()
{
return array(
'title' => array(
'title' => t('Название новости'),
'type' => 'string',
'func' => self::FILTER_TYPE_LIKE //Искать по названию с помощью like
),
'parent' => array(
'title' => t('Рубрика'),
'type' => 'integer',
'func' => self::FILTER_TYPE_EQ //Искать по рубрике с помощью точного соответствия(равенства)
)
);
}
/**
* Возвращает возможные значения для сортировки
*
* @return array
*/
public function getAllowableOrderValues()
{
return array('id', 'id desc');
}
/**
* Выполняет запрос на выборку новостей
*
* @param string $token Авторизационный токен
* @param array $filter Фильтр(справка будет сформирована автоматически). #filters-info
* @param string $sort Сортировка(справка будет сформирована автоматически). #sort-info
* @param integer $page Номер страницы, начиная с 1
* @param integer $pageSize Размер страницы
*
* @example
* GET /api/methods/news.getList?token=7eb2af87c85f3eb4e82b493c1cf9bcd9214a87f6&pageSize=1
* Ответ:
* <pre>
* {
* "response": {
* "summary": {
* "page": "1",
* "pageSize": "1",
* "total": "6"
* },
* "list": [
* {
* "id": "11",
* "title": "Молодежная - главная - об оплате",
* "alias": "molodezhnaya--glavnaya--ob-oplate",
* "content": "Подробный текст новости...",
* "parent": "3",
* "dateof": "2014-05-14 18:03:28",
* "image": null,
* "user_id": "1",
* "short_content": "",
* "meta_title": "",
* "meta_keywords": "",
* "meta_description": ""
* }
* ]
* }
* }
* </pre>
*/
protected function process($token,
$filter = array(),
$sort = 'id desc',
$page = "1",
$pageSize = "20")
{
//Воспользуемся полностью стандартной реалзацией метода.
//Метод вернет массив данных со статьями
return parent::process($token, $filter, $sort, $page, $pageSize);
}
}

Таким образом мы создали самый простейший метод API, для которого будет сформирована автоматическая справка. Данный метод невозможно будет вызвать, пока разрешение на его вызов не будет прописано в классе Приложения. Об этом будет сказано ниже. Пока сосредоточимся на некоторых мелочах именно классов методов API.

Успешный результат выполнения запроса к API

Метод process вашего класса API должен возвращать массив с корневой секцией response. Это будет являться признаком успешного выполнения запроса. Пример:

protected function process($token, $param1, $param2)
{
return array(
'response' => array(
... //Здесь любые данные
)
);
}

В ответе на API запрос эти данные будут преобразованы в JSON.

{
"response": {
... //Здесь любые данные
}
}

Возврат полей ORM объектов

Наиболее частой задачей для API является возврат некоторого ORM Объекта или целого списка ORM объектов. В ReadyScript имеются готовые методы для подготовки данных к возврату через API.

У каждого ORM объекта, при описании полей можно указать через флаг "appVisible" - видимость данного поля для внешних API. Если данный флаг не указан, то за видимость отвечает флаг visible, который по умолчанию равен true.

Пример:

use \RS\Orm\Type;
class MyOrmObject extends \RS\Orm\OrmObject
{
protected static $table = '...'
...
function _init()
{
$this->getPropertyIterator()->append(
'some_field_1' => new Type\Varchar(array(
'description' => t('Некое тестовое поле 1'),
'appVisible' => false //Не будет виден через API
)),
'some_field_2' => new Type\Varchar(array(
'description' => t('Некое тестовое поле 2'),
'visible' => false //Не будет виден в админ.панели и через API
)),
'some_field_3' => new Type\Varchar(array(
'description' => t('Некое тестовое поле 3'),
'visible' => false, //Не будет виден в админ. панели
'appVisible' => true //Будет виден через API
)),
'some_field_4' => new Type\Varchar(array(
'description' => t('Некое тестовое поле 4'),
//Будет видно в админ. панели и через API
))
);
}
...
}

Для получения значений полей ORM объекта в виде массива с учетом видимости поля в API, воспользуйтесь вспомогательными функциями:

Пример 1:

protected function process($token, $param1, $param2)
{
...
$my_object = new MyOrmObject(1); //Загрузим объект с ID = 1
return array(
'response' => array(
'my_object' => \ExternalApi\Model\Utils\extractOrm($my_object)
)
);
}
//Результат:
{
"response": {
"my_object": {
"some_field_3": "test value 3",
"some_field_4": "test value 4"
}
}
}

Пример 2:

protected function process($token, $param1, $param2)
{
...
$my_objects = \RS\Orm\Request()
->from(new MyOrmObject)
->limit(0,2)
->objects();
return array(
'response' => array(
'my_objects' => \ExternalApi\Model\Utils\extractOrmList($my_objects)
)
);
}
//Результат:
{
"response": {
"my_objects":
[
{
"some_field_3": "test value 3",
"some_field_4": "test value 4"
},
{
"some_field_3": "test value 3",
"some_field_4": "test value 4"
}
]
}
}

Ошибки

В случае, если во время выполнения запроса возникла ошибка, её необходимо отобразить пользователю в ответ на запрос к API. Для этого достаточно бросить исключение. Все исключения, потомки ExternalApi::Model::AbstractException, будут возвращены пользователю в ответе на запрос API в требуемом формате(JSON). При возникновении ошибки, ответ в формате JSON выглядит так:

{
"error": {
"code": "string_code(строковый идентификатор)",
"title": "Пояснение к ошибке"
}
}

Модуль ExternalApi предлагает класс с базовым набором исключений ExternalApi::Model::Exception.

/**
* Общие исключения, связанные с внешним API
*/
class Exception extends AbstractException
{
const
/**
* Внутренняя ошибка
*/
ERROR_INSIDE = 'inside',
/**
* Метод API не найден
*/
ERROR_METHOD_NOT_FOUND = 'method_not_found',
/**
* Недостаточно прав для вызова метода.
* Возможно в приложении client_id не установлены права на вызов данного метода
*/
ERROR_METHOD_ACCESS_DENIED = 'method_access_denied',
/**
* Неверный логин или пароль
* или превышен лимит попыток авторизации с одного IP
* или пользователь заблокирован по IP
*/
ERROR_BAD_AUTHORIZATION = 'bad_authorization',
/**
* Неизвестный client_id или client_secret.
* client_id и client_secret создаются в классе приложения,
* потомке от ExternalApi\Model\App\AbstractAppType
*/
ERROR_BAD_CLIENT_SECRET_OR_ID = 'bad_client_secret_or_id',
/**
* Доступ к приложению запрещен. Проверьте, состоит ли пользователь
* в группе, которая требуется приложению.
*/
ERROR_APP_ACCESS_DENIED = 'app_access_denied',
/**
* Неверные параметры, переданные в метод.
* Почитайте справку к методу /api/help
*/
ERROR_WRONG_PARAMS = 'wrong_params',
/**
* Неверное значение параметра, переданного для вызова метода API.
* Почитайте справку к методу /api/help
*/
ERROR_WRONG_PARAM_VALUE = 'wrong_param_value',
/**
* Запрашиваемый объект не найден
*/
ERROR_OBJECT_NOT_FOUND = 'object_not_found';
}

Класс исключений ExternalApi::Model::AbstractException имеет измененный конструктор, который в аргументе code_string принимает строковый идентификатор исключения, в отличие от стандартного класса исключения, где код должен быть числовым. Это необходимо, чтобы снизить вероятность конфликтов идентификаторов ошибок, так как любой модуль может привносить собственные коды ошибок.

Конструктор исключения:

/**
* Базовый класс исключений для внешних API.
*/
abstract class AbstractException extends \RS\Exception
{
function __construct($message = '', $code_string = '', Exception $previous = null, $extra_info = '')
{
...
}
...
}

Где:

  • message - Сообщение об ошибке
  • code_string - Строковый идентификатор ошибки
  • previous - Предыдущее исключение в цепочке
  • extra_info - Дополнительная информация об исключении

Если в процессе работы вашего метода API происходит ошибка, которая не присутствует в классе ExternalApi::Model::Exception, вы можете зарегистрировать собственный класс исключений с необходимыми ошибками с помощью обработки события externalapi.getexceptions.

Рассмотрим пример регистрации собственного класса и вызова исключения в процессе выполнения метода API. Модуль "Внешнее API" автоматически разбирает комментарии к константам, заданным в классе исключения, используя эти данные в качестве пояснения в справке к API. Названия констант с идентификаторами ошибок должны начинаться с префикса ERROR_.

Создадим собственный класс исключения в файле /modules/modulename/model/externalapiexception.inc.php

namespace ModuleName\Model;
class ExternalApiException extends \ExternalApi\Model\AbstractException
{
const
/**
* Комментарий для произвольной ошибки, будет отображен в справке
*/
ERROR_MY_CUSTOM_ERROR = 'Произвольная ошибка';
}

Зарегистрируем класс исключений для отображения в справке с помощью обработки события в файле /modules/modulename/config/handlers.inc.php

namespace ModuleName\Config;
class Handlers extends \RS\Event\HandlerAbstract
{
function init()
{
$this->bind('externalapi.getexceptions');
}
public static function externalapiGetExceptions($exceptions)
{
//Зарегистрируем наш кастомный класс исключений, для отображения ошибок нашего модуля в общей справке к API
$exceptions[] = new \ModuleName\Model\ExternalApiException();
return $exceptions;
}
}

Бросим исключение при выполнении метода API.

namespace ModuleName\Model\ExternalApi\News;
use \ExternalApi\Model\Exception as ApiException,
\ModuleName\Model\ExternalApiException;
{
...
public function process($token, $param1, $param2)
{
throw new ExternalApiException(t('Произошла некоторая ошибка'), ExternalApiException::ERROR_MY_CUSTOM_ERROR);
}
...
}

Пользователь получит следующие JSON данные:

{
"error": {
"code": "string_code(строковый идентификатор)",
"title": "Пояснение к ошибке"
}
}

Версионирование

Модуль "Внешнее API" поддерживает возможность версионности методов API. Это означает, что метод различных версии может иметь различный набор параметров, а также возвращать различный результат.

Пример вызова метода news.getList версии 1 /api/methods/news.getList?token=abcd...&v=1

Пример вызова метода news.getList версии 2 /api/methods/news.getList?token=abcd...&some_param=345&v=2

Рассмотрим подробнее, как создать несколько версий одного метода API. Версия API передается в запросе к методу через необязтальный параметр v. Если параметр v не передан, то используется версия по умолчанию (задается в настройках модуля). Модуль "Внешнее API" сперва пытается найти метод по маске processVerX, где X - номер версии. Если метода с таким именем в классе нет, то X понижается до предыдущей версии и происходит повторный поиск функции processVerX процесс повторяется до тех пор, пока цикл не дойдет до версии 1, что означает что будет вызван метод по умолчанию process.

Что дает такой подход? Рассмотрим на примере. Часто бывает, что в API только часть функций будет иметь отличную реализацию версии 2 и 3, остальные методы API должны возвращать одинаковый результат как в версии 3, так и 2, так и в версии 1. Рассмотрим детально работу системы версий на примере кода трех различных методов API.

Метод API news.getList 3х версий

1 namespace ModuleName\Model\ExternalApi\News;
2 class getList extends \ExternalApi\Model\AbstractMethods\AbstractGetList
3 {
4  ...
5 
6  // Возвращает 5 последних новостей
7  // Будет вызван при обращении к /api/methods/news.getList?v=1&....
8  // Справка версии 1 будет построена по комментарию к данной функции
9  protected function process($token)
10  {
11  //Здесь реализация API версии 1
12  }
13 
14  // Возвращает 5 последний новостей + информация об авторе каждого поста
15  // Будет вызван при обращении к /api/methods/news.getList?v=2&....
16  // Справка версии 2 будет построена по комментарию к данной функции
17  protected function processVer2($token, $param1)
18  {
19  //Здесь реализация API версии 2
20  }
21 
22 
23  // Возвращает 5 последний новостей + информация об авторе каждого поста + все комментарии к посту
24  // Будет вызван при обращении к /api/methods/news.getList?v=3&....
25  // Справка версии 3 будет построена по комментарию к данной функции
26  protected function processVer3($token, $param1)
27  {
28  //Здесь реализация API версии 3
29  }
30 }

Метод API order.get, имеющий реализацию только версии 1 и 2

{
...
// Возвращает заказ по ID
// Будет вызван при обращении к /api/methods/order.get?v=1&....
protected function process($token, $id)
{
//Здесь реализация API версии 1
}
// Возвращает заказ по ID
// Будет вызван при обращении к /api/methods/order.get?v=2&....
// Будет вызван при обращении к /api/methods/order.get?v=3&....
protected function processVer2($token, $id)
{
//Здесь реализация API версии 2
}
}

Метод API order.sellStatistic, имеющий реализацию только версии 1

{
...
// Возвращает заказ по ID
// Будет вызван при обращении к /api/methods/order.sellStatistic?v=1&....
// Будет вызван при обращении к /api/methods/order.sellStatistic?v=2&....
// Будет вызван при обращении к /api/methods/order.sellStatistic?v=3&....
protected function process($token, $id)
{
//Здесь реализация API версии 1
}
}

В комментариях видно в каком случае какие методы будут вызваны при обращении к API. Логика вызова ближайшей предыдущей версии очень удобна на практике, так как если вы обновили до третьей версии некоторые методы API, стороннее приложение может подставлять ?v=3 ко всем запросам к API и это не будет вызывать ошибку. Если какой-то метод будет иметь версию 3 будет вызван он, если у метода будет реализация только версии 2, то будет вызван он, во всех остальных случаях будет вызван метод по умолчанию, имеющий версию 1.

В справке /api/help, напротив каждого метода будет видно, какие версии он поддерживает. Эта информация автоматически собирается модулем "Внешнее API" с помощью Reflection.

Интернационализация

Язык, на котором следует вернуть результат передается через необязательный параметр lang (по умлчанию он равен ru). В случае, если параметр lang передан, перед выполнением метода API, модуль устанвливает указанный язык в качестве текущего с помощью системного метода RS::Language::Core::setSystemLang.

На странице справки по методам API (/api/help), параметр lang устанавливает язык, на котором будет отображена справка. Модуль "Внешнее API" автоматически парсит PHPDoc комментарии к методам process и формирует справку по каждому аргументу, а также по секциям:

  • @param - тип аргумента, а также комментарий к нему
  • @example - пример вызова метода
  • @return - комментарий к возвращаемым данным

Модуль внешнее API может автоматически формировать справку на нескольких языках, если использовать специальный синтаксис при создании phpDoc комментария. Рассмотрим подробнее пример.

namespace ModuleName\Model\ExternalApi\News;
{
...
/**
* Описание метода для справки на русском языке
* #lang-en: Method description in English
* #lang-de: Beschreibung des Verfahrens in Deutsch
*
* @param string $token Авторизационный токен
* #lang-en: Authorization token
* #lang-de: Autorisierungs-Token
*
* @param string $param1 Произвольный параметр
* #lang-en: Custom option
* #lang-de: Benutzerdefinierte Option
*
* @example
* GET /api/methods/news.getList?token=abcd&param1=1234
* <pre>
* {
* "response": {
* "news": [
* {
* "id": 1,
* "title": "Заголовок новости 1"
* },
* {
* "id": 2,
* "title": "Заголовок новости 2"
* },
* ]
* }
* }
* </pre>
*
* @return array массив со списком новостей. Описание ключей
* response.news.id - ID новости
* response.news.title - Название новости
*
* #lang-en: Array of news. Keys descriptions:
* response.news.id - ID of news
* response.news.title - Title of news
*
* #lang-de: Array von Nachrichten. Tastenbeschreibungen:
* response.news.id - ID der Nachrichten
* response.news.title - Titel der Nachrichten
*/
protected function process($token, $param1)
{
//...
}
}

Фраза #lang-[двухсимвольный идентификатор языка]: задает начало описания на указанном языке. С помощью данной фразы можно создавать описание для нескольких языков. По умолчанию используется русский язык. Как только вы создадите описание на новом языке, оно тут же появится на странице справки API в секции переключения языков.

Валидация входящих данных

Модуль "Внешнее API" использует возможности PHP Refrection, чтобы определить какие параметры ожидает метод API. Имена переменных читаются из аргументов функции process, тип переменных читается из PHPDoc комментариев. Пример:

{
/* Загружает объект "заказ"
*
* @param string $token Авторизационный токен
* @param integer $order_id ID заказа
* @param array $filter Фильтр
* ...
*/
protected function process($token, $order_id, $filter)
{
}
}

ReadyScript автоматически найдет параметры token и order_id, filter в суперглобальных массивах POST, GET, FILES приведет их к типам string, integer, array соответственно. Строковые типы будут пропущены через функцию экранирования. Строковые значения в массивах будут также рекурсивно пропущены через функцию экранирования. Подготовленные данные поступят на вход к методу API в параметре $params.

Часто требуется принимать сложную структуру данных в одном из параметров метода API, например, если дело касается запросов на обновление данных. В этом случае рекомендуется использовать массив с некой структурой вложенных секций.

Например, предположим, у нас должен быть метод order.update, который должен ожидать параметры:

  • token - string, авторизационный токен
  • order_id - integer, ID заказа
  • data - array, массив данных для обновления со следующей структурой:
1 [
2  //Поля, которые необходимо обновить у заказа
3  'fields' => [
4  'status' => ID статуса заказа
5  'payment' => ID способа оплаты
6  ],
7 
8  //ID товаров, которые следует удалить из заказа
9  'remove_items' => [
10  'ID_ITEM_1', 'ID_ITEM_2', 'ID_ITEM_3'
11  ]
12 ]

Так как на вход должен поступать сложный массив, необходимо его тщательно валидировать, точно подсказывать о наличии ошибок в нужном узле массива. ReadyScript предлагает готовый инструмент для валидации вложенной структуры массивов. Рассмотрим его подробнее.

Для валидации массивов предназначен класс ExternalApi::Model::Validator::ValidateArray. Класс имеет следующие следующие публичные методы:

  • __construct(array $schema) - конструктор, который в качестве аргумента принимает схему валидации массива
  • validate($param_name, $param_value, $full_data):array - функция, производит валидацию массива $param_value или бросает исключение , в случае наличия ошибок
  • getSchema():array - возвращает схему валидации, установленную в конструкторе
  • getParamInfoHtml():string - возвращает HTML с описанием ожидаемой структуры массива

Рассмотрим пример схемы валидации массива $schema:

array(
'@validate' => function($value, $all_parameters) {
//Общая функция валидации
}
'fields' => array( //Ожидаемая секция
'@title' => '...', //Название секции для справки
'@validate' => function($value, $full_data) { //Функция валидации
//Валидация всей секции fields
},
'status' => array( //Ожидаемая секция
'@require' => true, //Это обязательный элемент
'@title' => t('ID статуса'), //Название секции для справки
'@type' => 'integer', //Тип, к которому будет приведена переменная
'@validate_callback' => function($value, $full_data) { //Функция валидации
//Валидация поля статус
}
),
'payment' => array( //Ожидаемая секция
'@title' => t('ID типа платежей'), //Название секции для справки
'@type' => 'integer', //Тип, к которому будет приведена переменная
'@allowable_values' => array(1,2,3) //Валидация значений. Возможные значения перечислены
)
),
'remove_items' => array( //Ожидаемая секция
'@title' => t('Уникальные коды удаляемых из заказа товаров'), //Название секции для справки
'@type' => 'array', //Тип, к которому будет приведена переменная
'@arrayitemtype' => 'string', //Тип значений массива
)
);

Валидатор принимает ключи, начинающиеся с @ за технические инструкции. В настоящее время поддерживаются следующие ключи:

  • - название секции для справки
  • - обязательный элемент
  • - тип, к которому будет приведен элемент
  • - тип значения элемента массива. Имеет значение, если = array
  • - возможные значения в текущем ключе
  • - callback для валидации значения в данном ключе

Пример использования функций валидации в методе API.

{
private
$validator;
//...
/**
* Возвращает инициализированный объект валидатора
*
* @return \ExternalApi\Model\Validator\ValidateArray
*/
public function getUpdateDataValidator()
{
if ($this->validator === null) {
$this->validator = new \ExternalApi\Model\Validator\ValidateArray(array(
'fields' => array(
'status' => array(
'@title' => t('ID статуса'),
'@type' => 'integer',
'@validate_callback' => function($value) {
$status_api = new \Shop\Model\UserStatusApi();
return $status_api->getOneItem($value) !== false;
}
),
'payment' => array(
'@title' => t('ID способа оплаты'),
'@type' => 'integer',
'@validate_callback' => function($value) {
$status_api = new \Shop\Model\UserStatusApi();
return $status_api->getOneItem($value) !== false;
}
),
),
'remove_items' => array(
'@title' => t('Уникальные коды удаляемых из заказа товаров'),
'@type' => 'array',
'@arrayitemtype' => 'string',
)
));
}
return $this->validator;
}
/**
* Валидирует значения для обновления
*
* @param array $data
* @return Возвращает true,
*/
public function validateData($data)
{
return $this->getUpdateDataValidator()->validate('data', $data, $this->method_params);
}
/**
* Форматирует комментарий, полученный из PHPDoc
* Заменяет #data-info на справку по схеме данных в переменной
*
* @param string $text - комментарий
* @return string
*/
protected function prepareDocComment($text, $lang)
{
$text = parent::prepareDocComment($text, $lang);
$validator = $this->getUpdateDataValidator();
$text = preg_replace_callback('/\#data-info/', function() use($validator) {
return $validator->getParamInfoHtml();
}, $text);
return $text;
}
/**
* Обновляет один заказ
* @param integer $object_id ID заказа
* @param array $data #data-info
*
* ...
*/
protected function process($token, $object_id, $data)
{
$data = $this->validateData($data);
//здесь $data - уже после валидации
//... продолжение реализации метода
}
}

Автодокументирование

Модуль "Внешнее API" строит автоматическую документацию на основе PHPDoc комментариев функции process в методах API. Для построения качественной документации обязательно включать в описание функции следующие секции:

  • Общее описание метода
  • В конструкциях @param должны быть описаны все типы и описания аргументов
  • В конструкции @example обязательно должен быть приведен пример строки запроса и ответа
  • В конструкции @return желательно указывать тип возвращаемых данных и описание возвращаемых полей

Пример хорошо документированного метода:

use \ExternalApi\Model\Exception as ApiException;
/**
* Статистический отчет по суммам заказов
*/
{
/**
* Возвращает статистику по сумме и количеству заказов в разрезе годов и месяцев
*
* @param string $token Авторизационный токен
* @param string $order_type Тип заказа. Возможные значения: <b>all</b> и <b>success</b>
* @param string $y_axis Что выводить на графике? Сумму или количество заказов. Возможные значения <b>summ</b> или <b>num</b>
*
* @example GET /api/methods/order.sellstatistic?token=2bcbf947f5fdcd0f77dc1e73e73034f5735de486
* Ответ:
* <pre>
* {
* "response": {
* "statistic": [
* {
* "label": "2013",
* "data": [
* {
* "x": 1451610000000,
* "y": 0,
* "pointDate": 1356998400000,
* "total_cost": 0,
* "count": 0
* },
* {
* "x": 1454288400000,
* "y": 0,
* "pointDate": 1359676800000,
* "total_cost": 0,
* "count": 0
* }
* ]
* }
* ]
* }
* }
* </pre>
*
* @return array Возвращает данные для построения графика
* statistic[0].label - год
* statistic[0].data - данные одной точки
* statistic[0].data.x - координаты X на графике timestamp в миллисекундах
* statistic[0].data.y - сумма или количество заказов
* statistic[0].data.pointDate - дата на 1 число каждого месяца
* statistic[0].data.total_cost - сумма заказов
* statistic[0].data.count - количество заказов
*/
function process($token, $order_type = 'all', $y_axis = 'summ')
{
//Реализация метода API
}
}

Для описанного выше метода будет сформирована следующая документация:

api_autodoc.png
Пример справки к методу API

Приложение

Получить авторизационный токен (соответственно пользоваться методами API, требующими токен) можно только для определенного приложения. ID приложения передается в параметре client_id в запросе на авторизацию.

Приложением называется зарегистрированный с помощью события getapps класс, потомок от RS::RemoteApp::AbstractAppType. Приложением может являться "Desktop приложение для уведомлений", "Мобильное приложение Интернет-магазин", "Мобильное приложение для администраторов", иными словами любой программный продукт, требующий подключения извне к вашему магазину, должен быть оформлен в виде специального класса в ReadyScript. Если приложение должно иметь доступ к вншним API, его класс должен имплементировать интерфейс ExternalApi::Model::App::InterfaceHasApi. Рассмотрим интерфейс подробнее.

use \ExternalApi\Model\Orm\AuthorizationToken;
/**
* Интерфейс
*/
interface InterfaceHasApi
{
/**
* Возвращает true, если client_secret корректный
*
* @return string
*/
public function checkSecret($client_secret);
/**
* Метод возвращает массив, содержащий требуемые права доступа к json api для приложения
*
* @return [
* 'oauth/authorize' => [RIGHT_CODE_1, RIGHT_CODE_2,...] //Точно перечисленные права
* 'oauth/token' => FULL_RIGHTS //Полные права
* ]
*/
public function getAppRights();
/**
* Устанавливает/сбрасывает token, который может влиять на результат метода getAppRights
*
* @param AuthorizationToken $token
*/
public function setToken(AuthorizationToken $token = null);
/**
* Возвращает авторизационный token
*
* @return AuthorizationToken
*/
public function getToken();
/**
* Возвращает группы пользователей, которым доступно данное приложение
*
* @return ["group_id_1", "group_id_2", ...]
*/
public function getAllowUserGroup();
}

Для упрощения разработки, мы создали абстрактный класс ExternalApi::Model::App::AbstractAppType, который можно использовать в качестве базового для приложений, требующих доступ к API.

use \ExternalApi\Model\Orm\AuthorizationToken;
/**
* Класс описывает Приложение (Тип приложения), а также требуемые права для приложения.
*
* Идентификатор приложения нужно будет передавать для получения авторизационного токена вместе с логином и паролем.
* Для успешного получения авторизационного токена, пользователь должен иметь права к данному приложению.
*
* Токен всегда будет привязан к Приложению, что позволит по токену всегда понять какие права он имеет
*/
abstract class AbstractAppType extends \RS\RemoteApp\AbstractAppType implements InterfaceHasApi
{
const
FULL_RIGHTS = 'all';
private
/**
* Устанавливает/сбрасывает token, который может влиять на результат метода getAppRights
*
* @param \ExternalApi\Model\Orm\AuthorizationToken $token
*/
public function setToken(\ExternalApi\Model\Orm\AuthorizationToken $token = null)
{
$this->token = $token;
}
/**
* Возвращает token, установленный с помощью setToken
*
* @return \ExternalApi\Model\Orm\AuthorizationToken
*/
public function getToken()
{
return $this->token;
}
}

Теперь рассмотрим пример регистрации в системе приложения, которое будет получать доступ к API news.getList из которого будет осуществляться авторизация. Сперва создадим класс нашего вымышленного приложения по отображению новостей.

namespace ModuleName\Model\AppTypes;
/**
* Desktop приложение, отображающее 5 последнийх новостей
*/
class MyCustomApp extends \ExternalApi\Model\App\AbstractAppType
{
/**
* Возвращает строковый идентификатор приложения
*
* @return string
*/
public function getId()
{
return 'mycustomapp'; //это будет client_id
}
/**
* Возвращает SHA1 от секретного ключа client_secret, который должен
* передаваться вместе с client_id в момент авторизации
*
* @return string
*/
public function checkSecret($client_secret)
{
//В реальных приложения нужно сверять хэш sha1( $client_secret ) == 'f8ebe8f6e99f...';
return $client_secret == '123456';
}
/**
* Метод возвращает название приложения
*
* @return string
*/
public function getTitle()
{
return t('Desktop приложение отображающее новости');
}
/**
* Метод возвращает массив, содержащий требуемые права доступа к json api для приложения
*
* @return [
* [
* 'method' => 'метод',
* 'right_codes' => [код действия, код действия, ...]
* ],
* ...
* ]
*/
public function getAppRights()
{
return array(
'news.getList' => self::FULL_RIGHTS,
);
}
/**
* Возвращает группы пользователей, которым доступно данное приложение.
* Сведения загружаются из настроек текущего модуля
*
* @return ["group_id_1", "group_id_2", ...]
*/
public function getAllowUserGroup()
{
return array('supervisor', 'admins');
}
}

Зарегистрируем наш класс приложения с помощью обработки события getapps в файле handlers.inc.php.

namespace ModuleName\Config;
class Handlers extends \RS\Event\HandlerAbstract
{
function init()
{
$this->bind('getapps');
}
/**
* Привносим наше приложение в список приложений ReadyScript
*
* @param [] $app_types
* @return []
*/
public static function getApps($app_types)
{
$app_types[] = new \ModuleName\Model\AppTypes\MyCustomApp();
return $app_types;
}
}

После регистрации приложения, его client_id можно использовать в методе авторизации oauth.token, выданный токен будет иметь права на обращение к методу news.getList.

Права доступа к методам API

Приложение может иметь доступ к различным методам API с различными уровнями доступа. Рассмотрим более сложный пример реализации метода getAppRights класса некоторого абстрактного приложения "StoreManagement".

Предположим, что приложение предназначено для курьера и для администратора. В случае, если пользователь, авторизовавшийся в приложении состоит в группе курьеров, то API должно отдавать только предназначенные для него заказы, а если авторизовавшийся пользователь администратор, то API должны отдавать все заказы.

Как это реализовать? С помощью метода getToken() в классе приложения мы можем получить объект авторизационного токены, из которго мы можем получить пользователя, у которого мы можем проверить группы, в которых он состоит. В зависимости от группы, метод getAppRights может возвращать различные права доступа к API. Главное, чтобы сам метод API имел поддержку различной выдачи в зависимости от прав пользователя, реализованную в методе process.

Пример сложной реализации метода getAppRights.

namespace MobileManagerApp\Model\AppTypes;
use \Shop\Model\ExternalApi\Order;
use \ExternalApi\Model\App\AbstractAppType;
use \ExternalApi\Model\Orm\AuthorizationToken;
/**
* Приложение - управление магазином
*/
class StoreManagement extends AbstractAppType
{
const
COURIER_GROUP = 'courier'; //Группа для курьеров
//...
/**
* Метод возвращает массив, содержащий требуемые права доступа к json api для приложения
*
* @return [
* [
* 'method' => 'oauth/authorize',
* 'right_codes' => [код действия, код действия, ...]
* ],
* ...
* ]
*/
public function getAppRights()
{
$courier_rights = array();
if (isset($this->token)) {
//Проверяем состоит ли пользователь в группе курьеров
if (in_array(self::COURIER_GROUP, $this->token->getUser()->getUserGroups())) {
$courier_rights[] = Order\Get::RIGHT_COURIER;
}
}
return array(
'order.get' => array_merge(array(Order\Get::RIGHT_LOAD), $courier_rights),
'status.getList' => self::FULL_RIGHTS,
//...
);
}
}

Далее в методе API можно реализовать логику проверки прав

use ExternalApi\Model\Exception as ApiException;
{
const
protected function process($token, $order_id, ...)
{
$object = $this->getOrmObject();
if ($object->load($order_id)) {
if ($this->checkAccessError(self::RIGHT_COURIER) === false) {
if ($object['courier_id'] != $this->token['user_id']) {
throw new ApiException(t('Курьеры могут загружать только назначенны им заказы'), ApiException::ERROR_METHOD_ACCESS_DENIED);
}
}
}
}
}

События

Модуль "Внешнее API" привносит в систему следующие события:

Идентификатор события Правила формирования идентификатора Тип параметра Когда происходит
externalapi.getexceptions - array of ExternalApi::Model::AbstractException. Массив классов исключений Вызывается во время рендеринга справочной страницы по API
api.МЕТОД.success МЕТОД - полный идентификатор метода API. Например: api.order.get.success или api.order.getlist.success. Все знаки обязательно в нижнем регистре. array. Элементы:
  • result - array результат выполнения метода API
  • version - string версия, переданная в параметре v
  • lang - string двухсимвольный идентификатор языка, переданный в параметре lang
  • method - object объект метода API
  • params - array все параметры, переданные в метод
Вызывается после выполнения метода API. Параметр result может быть изменен, это повлияет на результат выполнения API.

Авторизация (oauth.token)

ReadyScript предлагает способ авторизации через API метод oauth.token, входящий в стандарт OAuth 2.0 (https://tools.ietf.org/html/draft-ietf-oauth-v2-13#section-4.3) Метод позволяет получать авторизационный токен, который наделен правами, необходимыми для приложения client_id.

Метод обрабатывает только POST запрос со следующими параметрами:

  • grant_type - должен быть password
  • client_id - идентификатор вашего приложения
  • client_secret - секретный идентификатор вашего приложения
  • username - логин пользователя
  • password - пароль пользователя

Пример запроса:

POST /api/methods/oauth.token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=password&client_id=myapp&client_secret=myappsecret&username=demo_example.com&password=xxxxxxx

В случае успешной авторизации, метод вернет следующий результат:

{
'response': {
'auth': {
'token' => '38b83885448a8ad9e2fb4f789ec6b0b690d50041',
'expire' => '1504785044',
},
'user': {
'id' => 123,
'full_name' => 'Иванов Иван Иванович',
'groups' => 'admins, guest, clients'
}
}
}

Описание полей результата запроса.

  • response.auth.token - авторизационный token
  • response.auth.expire - срок истечения токена
  • response.user.id - ID Пользователя
  • response.user.full_name - Полное имя файла
  • response.user.groups - Группы, в которых состоит пользователь

Разбор часто возникающих ошибок:

  • Пользователь не имеет права доступа к приложению (app_access_denied).
    Причина: Метод getAllowUserGroup() класса вашего приложения вернул массив групп, ни в одной из которых не состоит пользователь.
  • Приложения с таким client_id не существует или оно не поддерживает работу с API (bad_client_secret_or_id)
    Причина: Либо указан некорректный client_id, либо ваш класс приложения не имплементирует интерфейс ExternalApi::Model::App::InterfaceHasApi
  • Приложения с таким client_id не существует или неверный client_secret (bad_client_secret_or_id).
    Причина: Функция checkSecret() класса вашего приложения вернула false

Объект авторизационного токена

После успешной регистрации, модуль Внешнее API создает в базе данных запись, которая содержит авторизационный токен, ID пользователя, ID приложения, запросившего токен, время протухания токена, дату создания, а также другие сведения.

Время жизни токена присваивается при создании токена на основе настроек модуля "Внешнее API".

Как и для всех записей в базе, ReadyScript предлагает ORM объект для авторизационного токена ExternalApi::Model::Orm::AuthorizationToken. Используйте объект авторизационного токена в ваших методах API, через свойство $this->token, чтобы получить id пользователя ($this->token->getUser()->id), обратившегося к API.

Заметки
В случае изменения пароля пользователем, все связанные с ним авторизационные токены автоматически удаляются.

Для удобства разработки, все авторизационные токены можно просмотреть в админстративной панел в разделе Веб-сайт → Настройка модулей → Внешнее API нажав на ссылку "Авторизационные токены".

Отладка

Если в настройках модуля "Внешнее API" включена опция "Включить логирование запросов", то система будет созранять все запросы и ответы, связанные с API. Просмотреть каждый запрос можно в административной панели в разделе Веб-сайт → Настройка модулей → Внешнее API нажав на ссылку "Журнал запросов к API".

Заметки
Рекомендуем включать логирование запросов только на время отладки, так как объем данных в БД может стремительно увеличиваться.