(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
IBM Domino Messaging Client Access License Authorized User License + SW Subscription & Support 12 Months
erwin Data Modeler Workgroup Edition r9.7 - Product plus 1 Year Enterprise Maintenance Commercial
Quest Software. Toad for SQL Server Development Suite
IBM RATIONAL Clearcase Floating User License + Sw Subscription & Support 12 Months
ESET NOD32 Антивирус на 1 год для 3ПК или продление на 20 месяцев
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
СУБД Oracle "с нуля"
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Новые материалы
Утиль - лучший бесплатный софт для Windows
Adobe Photoshop: алхимия дизайна
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100