(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Удобное встраивание RESTful API в проект

Источник: habrahabr
volovikov

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

Я хочу представить сообществу нашу реализацию RESTful архитектуры, без дублирования кода и с минимальными изменениями существующей бизнес-логики. Или Как добавить в проект API за пять минут?

Для реализации данного подхода, нами было написано расширение для Yii Framework, но сам подход может быть использован в любой MVC архитектуре.

Давайте представим, что у нас есть контроллер RestUserController с методами:

  • actionIndex - список пользователей
  • actionView - просмотр пользователя
  • actionCreate - создание пользователя
  • actionUpdate - обновление пользователя
  • actionDelete - удаление пользователя

Также у нас есть модель RestUser, которая представляет из себя  ActvieRecord  таблицы  rest_users .

Рассмотрим метод actionCreate, задача которого, создание нового пользователя RestUser,

class RestUserController extends Controller
{
    ...
    public function actionCreate()
    {
        $model = new RestUser();

        if (isset($_POST) && ($data = $_POST)) { // проверяем отправлен ли POST запрос 
            $model->attributes = $data; // пишем в модель новые атрибуты
            if ($model->save()) { // проверяем атрибуты, если валидны - то сохраняем
                $this->redirect(array('view', 'id' => $model->id));
            }
        }
        $this->render('create', array('model' => $model)); // отображаем html-форму добавления
    }
    ...
}

Тут все понятно - еcли просто запрашиваем /restUser/create - отображается html-форма добавления нового пользователя, если отправляем  POST-запрос  на этот адрес, то отрабатывает логика валидации и добавления, затем либо перенаправляет нас на просмотр пользователя, либо отображает html-форму c ошибками.

Теперь, допустим, мы хотим сделать мобильное приложение которое будет иметь возможность из своего интерфейса создавать новых пользователей. Правильный путь - создать серверное API. 
Т.к. мы говорим о RESTful стиле, то взаимодействие серверного и мобильного приложения, на примере запроса через  curl , будет выглядеть примерно так:

Запрос

curl http://test.local/api/users    -u demo:demo    -d email="user@test.local"    -d password="passwd"

Ответ

< HTTP/1.1 201 Created
< Content-Type: application/json
< WWW-Authenticate: Basic realm="App"
< Location: http://test.local/api/users/TEST_ID
{
    "object":"rest_user",
    "id":"TEST_ID",
    "email":"user@test.local",
    "name":"Test REST User"
}

Здесь происходит авторизация через  HTTP basic auth  логина demo с паролем demo, и передаются обязательные параметры email и password, в ответ, если все правильно, получаем JSON-объект нового пользователя.

Вся идея нашего подхода заключается в том, чтобы добавить  action-ам  возможность правильно отвечать на API-запросы, только изменением методов redirect и render, а также добавлением правил рендеринга моделей.
Конечно, необходима также реализовать перехват ошибок и эксепшенов приложения, а также ошибок при создании самой модели, для корректного ответа API-клиенту, но для этого не потребуется изменения самой бизнес-логики  action-ов  контроллера.

В нашем расширении мы реализовали предложенный подход перехватом событий onException и onError, а также добавлением дополнительной функциональности к базовой модели CActiveRecord и контроллеруCController при помощи поведений.
В результате, код, возвращающий нужный ответ, при запросе через API, и html-форму при обычном запросе, будет выглядеть так:

class RestUserController extends Controller
{
    ...
    public function actionCreate()
    {
        $model = new RestUser();

        if ($this->isPost() && ($data = $_POST)) { // добавился метод isPost наряду с isPut и isDelete
            $model->attributes = $data;
            if ($model->save()) {
                $this->redirect(array('view', 'id' => $model), true, 201); // возвращаем объект
            }
        }
        $this->render('create', array('model' => $model), false, array('model')); // в ответе только model
    }
    ...
}

Важное отличие нового кода от предидущего - это передача в метод redirect в качестве параметра idне $model->id, а объекта $model, для того чтобы созданный объект был возвращен клиенту. Также, третьим параметром добавлен код ответа 201 - это необходимо для соответсвия стандарту, т.к. вместе с ответом передается заголовок  Location , содержащий адрес созданного объекта. HTTP-коды 3xx в ответе не позволяются.
Ещё одним отличием является добавленный четвертый параметр в методе render, в нем содержится перечисление полей из массива $data, передаваемых в ответе клиенту. Если праметр null то возвращается весь массив $data.

Теперь при неверном запросе, данные, которые в обычном режиме отобразились бы в html-форме, вернутся в следующем формате:

Запрос

curl http://test.local/api/users    -u demo:demo    -d email="user@test.local" 

Ответ

< HTTP/1.1 400 Bad Request
< Content-Type: application/json
< WWW-Authenticate: Basic realm="App"
{
    "error":{
        "params":[
            {
                "code":"required",
                "message":"Password cannot be blank.",
                "name":"password"
            }
        ],
        "type":"invalid_param_error",
        "message":"Invalid data parameters"
    }
}

Отлично, теперь нужно как-то защитить чувствительные данные модели - у нашего RestUser это полеpassword. Для этого определим в правиле список возвращаемых полей.
Правило отображения для модели будет находится в методе rules
class RestUser extends CModel
{
    public function rules()
    {
        return array(
            ...
            array('id, email, name', 'safe', 'on' => 'render'),
        );
    }
}

Это правило затем будет учтено в методе getRenderAttributes, добавленном в модель, который будет возвращать массивом все доступные для отображения атрибуты, рекурсивно проходя по связям объекта, если они указаны в правиле.

В заключении хочу рассказать немного о возможностях аутентификации и отображения.
Ядро расширения построено вокруг компонента (сервиса) \rest\Service, который занимается основной обработкой событий и правильным отображением данных. У данного сервиса есть две группы адаптеровauth и renderer
В auth находятся адаптеры, осуществляющие аутентификацию - по умолчанию доступен адаптер  HTTP basic auth .
В renderer находятся адаптеры, осуществляющие отображение данных - по умолчанию доступны два адаптера  JSON  и  XML .

Расширение


Коротко о настройках

Пример конфигурационного файла main.php
YiiBase::setPathOfAlias('rest', realpath(__DIR__ . '/../extensions/yii-rest-api/library/rest'));

return array(
    'basePath' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '..',
    'name' => 'My Web Application',

    'preload' => array('restService'),

    'import' => array(
        'application.models.*',
        'application.components.*',
    ),

    'components' => array(
        'restService' => array(
            'class'  => '\rest\Service',
            'enable' =>strpos($_SERVER['REQUEST_URI'], '/api/') !== false, // для примера
        ),

        'urlManager' => array(
            'urlFormat'      => 'path',
            'showScriptName' => false,
            'baseUrl'        => '',
            'rules'          => array(
                array('restUser/index',  'pattern' => 'api/v1/users', 
                      'verb' => 'GET',   'parsingOnly' => true),
                array('restUser/create', 'pattern' => 'api/v1/users', 
                      'verb' => 'POST', 'parsingOnly' => true),
                array('restUser/view',   'pattern' => 'api/v1/users/<id>', 
                      'verb' => 'GET', 'parsingOnly' => true),
                array('restUser/update', 'pattern' => 'api/v1/users/<id>',
                       'verb' => 'PUT', 'parsingOnly' => true),
                array('restUser/delete', 'pattern' => 'api/v1/users/<id>',
                       'verb' => 'DELETE', 'parsingOnly' => true),

                array('restUser/index2',  'pattern' => 'api/v2/users', 
                      'verb' => 'GET', 'parsingOnly' => true), // к примеру, если нужно будет сменить версию API
            )
        ),
    ),
);

Добавим в контроллер поведение и переопределим методы
/**
 * @method bool isPost()
 * @method bool isPut()
 * @method bool isDelete()
 * @method string renderRest(string $view, array $data = null, bool $return = false, array $fields = array())
 * @method void redirectRest(string $url, bool $terminate = true, int $statusCode = 302)
 * @method bool isRestService()
 * @method \rest\Service getRestService()
 */
class RestUserController extends Controller
{
    public function behaviors()
    {
        return array(
            'restAPI' => array('class' => '\rest\controller\Behavior')
        );
    }
    // если поле  $fields не определено, есть возвращаемые поля по умолчанию
    public function render($view, $data = null, $return = false, array $fields = array('count', 'model', 'data'))
    {
        if (($behavior = $this->asa('restAPI')) && $behavior->getEnabled()) {
            if (isset($data['model']) && $this->isRestService() && 
                count(array_intersect(array_keys($data), $fields)) == 1) {
                $data = $data['model']; // по логике нашего API, возвращаемый объект мы никак не оборачиваем, но детали конкретной реализации - на ваше усмотрение
                $fields = null;
            }
            return $this->renderRest($view, $data, $return, $fields);
        } else {
            return parent::render($view, $data, $return);
        }
    }

    public function redirect($url, $terminate = true, $statusCode = 302)
    {
        if (($behavior = $this->asa('restAPI')) && $behavior->getEnabled()) {
            $this->redirectRest($url, $terminate, $statusCode);
        } else {
            parent::redirect($url, $terminate, $statusCode);
        }
    }
}

Все эти методы можно и нужно добавить в родительский контроллер, чтобы не имплементировать в каждом контроллере по отдельности.

Добавим поведение в модель для того, чтобы заработали правила рендеринга

/**
 * @method array getRenderAttributes(bool $recursive = true)
 * @method string getObjectId()
 */
class RestUser extends CActiveRecord
{
    /**
     * @return array
     */
    public function behaviors()
    {
        return array(
            'renderModel' => array('class' => '\rest\model\Behavior')
        );
    }
}

Ссылки

GitHub репозиторий - github.com/paysio/yii-rest-api
Описание установки и настройки - github.com/paysio/yii-rest-api#installation
Весь код, приведенный выше - github.com/paysio/yii-rest-api/tree/master/demo

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 09.11.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Nero Basic Burning ROM 2018 VL 5 - 9 License corporate
Allround Automation PL/SQL Developer - Unlimited license
Microsoft 365 Business Basic (corporate)
Delphi Professional Named User
NERO 2016 Classic ESD. Электронный ключ
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
СУБД Oracle "с нуля"
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Новые материалы
Adobe Photoshop: алхимия дизайна
Corel DRAW - от идеи до реализации
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100