📜 ⬆️ ⬇️

Convenient embedding of a RESTful API into a project

It's no secret that having an API benefits any project. But often, in case of an error in the system architecture or adding it to the finished project, the overhead of support and testing takes a lot of time.

I want to introduce the community to our implementation of a RESTful architecture, without duplicating the code and with minimal changes to the existing business logic. Or How to add an API to a project in five minutes?

To implement this approach, we have written an extension for the Yii Framework , but the approach itself can be used in any MVC architecture.

Let's imagine that we have a RestUserController controller with methods:

We also have a RestUser model, which is the ActvieRecord of the rest_users table.
')
Consider the actionCreate method, whose task is to create a new RestUser user,
 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-  } ... } 

Everything is clear here - if we simply request /restUser/create - the html-form for adding a new user is displayed, if we send a POST request to this address, then the validation and addition logic works, then either redirects us to the user’s view, or displays the html-form c errors.

Now, let's say we want to make a mobile application that will be able to create new users from its interface. The correct way is to create a server API.
Since we are talking about RESTful style, the interaction of the server and mobile applications, in the example of a request through curl , will look like this:

Request

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

Answer

 < 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" } 

Here, authorization via HTTP basic auth demo login with the password demo takes place, and the required parameters email and password are transmitted, in response, if everything is correct, we get the JSON object of the new user.

The whole idea of ​​our approach is to add the ability of action-s to respond to API requests correctly, only by changing the redirect and render methods, and also by adding rules for rendering models.
Of course, it is also necessary to implement the interception of errors and exams of the application, as well as errors when creating the model itself, for a correct response to the API client, but this does not require changes to the business logic of the controller's action s .

In our extension, we implemented the proposed approach by intercepting the onException and onError , as well as adding additional functionality to the CActiveRecord base model and the onError CController using behaviors .
As a result, the code that returns the desired answer, when requested via the API, and the html form with a normal request, will look like this:
 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 } ... } 

The important difference between the new code and the previous one is the transfer to the redirect method as an id parameter, not $model->id , but the $model object, in order for the created object to be returned to the client. Also, the third parameter was added response code 201 - this is necessary to meet the standard, because along with the response, the Location header is sent, containing the address of the created object. 3xx HTTP codes are not allowed in the response.
Another difference is the added fourth parameter in the render method, it contains an enumeration of fields from the $data array passed to the client in response. If the parameter is null then the entire $data array is returned.

Now, if the request is incorrect, the data that would normally be displayed in the html form will return in the following format:

Request

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

Answer

 < 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" } } 

Great, now you need to somehow protect the sensitive data of the model - our RestUser a password field. To do this, we define in the rule list of returned fields.
The display rule for the model will be in the rules method
 class RestUser extends CModel { public function rules() { return array( ... array('id, email, name', 'safe', 'on' => 'render'), ); } } 

This rule will then be taken into account in the getRenderAttributes method added to the model, which will return all the attributes available for display by an array, recursively traversing the object's connections if they are specified in the rule.

In conclusion, I want to talk a little about the possibilities of authentication and display.
The extension core is built around the \rest\Service component (service), which deals with basic event handling and the correct display of data. This service has two groups of auth and renderer adapters.
auth has authentication adapters - the default basic auth HTTP adapter is available.
The renderer contains adapters that perform data mapping — by default, two JSON and XML adapters are available.

Expansion


Briefly about the settings

An example of the configuration file 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 ) ), ), ); 

Add behavior to the controller and redefine the methods.
 /** * @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); } } } 

All these methods can and should be added to the parent controller in order not to implement them separately in each controller.

Add behavior to the model to make the rendering rules work.
 /** * @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') ); } } 


Links


GitHub repository - github.com/paysio/yii-rest-api
Description of installation and configuration - github.com/paysio/yii-rest-api#installation
All the code above is github.com/paysio/yii-rest-api/tree/master/demo

Source: https://habr.com/ru/post/155021/


All Articles