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

от автора

Ни для кого не секрет, что наличие 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

ссылка на оригинал статьи http://habrahabr.ru/post/155021/


Комментарии

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

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