Yii上のRESTクライアントとサーバー

はじめに



開発でYiiフレームワークを使用する人は誰でも、データベースへのアクセスとして、ほとんどの場合、組み込みORMコンポーネントActiveRecordを使用することを知っています。 しかし、ある時点で、複数のリモートサーバーに物理的に配置されたデータを操作する必要があるという事実に直面しました。 これは、私が働いている会社の分散ネットワークでFTPおよびRadiusユーザーを管理するための集中型システムの開発であり、支店と中央オフィスを組み合わせていました。



実際、さまざまなネットワーク上のサーバーにあるデータを操作する必要がある状況は多くあります。 ちょっとした考えから、HTTPプロトコルとそれに基づくRESTアプローチを使用することになりました。 最初と主な2つの理由がありました。RESTを使用してサーバーとクライアントの両方のパーツを開発する方法を学ぶためです。 2つ目は、HTTPプロトコルを使用することの利便性です。私の場合、ほとんどのファイアウォールで開かれており、プロキシサーバーも使用できます。



ソースコードの一部を記事の本文に挿入する必要があったため、かなりの量になりました。



降りる



そのため、決定が下されました。 一見すると、奇妙な束ができます。 通常、REST APIはサイトではなくモバイルアプリケーションで使用されます。 ユーザーが私のアカウント管理ページにHTTPリクエストを行い、ページを提供するWebサーバーが、アカウントが直接配置されているサーバーにさらに別のHTTPリクエストを行うことがわかりました。 また、それらを管理するためのREST APIも実装しました。



サーバー側


既製のソリューションの1つ、たとえばrestfullyiiを使用することは可能でしたが、私は研究し、それがどのように機能するか、または内部から機能する必要があるかを理解したいと思っていました。 したがって、私たちは子供の天才の発明に従事します。



サーバー側を自分で作成する方法は、公式プロジェクトwikiで非常に詳しく説明されています 。 この決定は基礎として採用されました。



Yii RESTアプリケーションのメインマジックは、 protected / config / main.phpの urlManager設定で発生します。



'urlManager' => array( 'urlFormat' => 'path', 'showScriptName' => false, 'rules' => array( // REST patterns array('api/list', 'pattern' => 'api/<model:\w+>', 'verb' => 'GET'), array('api/view', 'pattern' => 'api/<model:\w+>/<id:\d+>', 'verb' => 'GET'), array('api/update', 'pattern' => 'api/<model:\w+>/<id:\d+>', 'verb' => 'PUT'), array('api/delete', 'pattern' => 'api/<model:\w+>/<id:\d+>', 'verb' => 'DELETE'), array('api/create', 'pattern' => 'api/<model:\w+>', 'verb' => 'POST'), // Other rules '<controller:\w+>/<id:\d+>'=>'<controller>/view', '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>', '<controller:\w+>/<action:\w+>'=>'<controller>/<action>', ), ),
      
      





次の形式のリクエストを送信するルールは次のとおりです。



POST api.domain.ru/api/users











POST api.domain.ru/api/create?model=users







この例で気に入らなかったのは、実際にモデルがスイッチブロックに読み込まれるときのアプローチです。 これは、プロジェクトに新しいモデルを追加し、コントローラーを変更する場合、より普遍的なソリューションを作成したかったことを意味します。 その結果、動作中のモデルを作成するために、フォームの構築が使用されました。



  if (isset($_GET['model'])) $_model = CActiveRecord::model(ucfirst($_GET['model']));
      
      





次に、私のケースで判明したコントローラーの完全なリストを提供します(クラスのヘルパーメソッドの実装を意図的に削除しました。これは、変更なしで上記のリンクから例から取られました。さらに、章の最後にYiiアプリケーションの完全なソースへのリンクがあります):



 <?php class ApiController extends Controller { Const APPLICATION_ID = 'ASCCPE'; private $format = 'json'; public function filters() { return array(); } public function actionList() { if (isset($_GET['model'])) $_model = CActiveRecord::model(ucfirst($_GET['model'])); if (isset($_model)) { $_data = $_model->summary($_GET)->findAll(); if (empty($_data)) $this->_sendResponse(200, sprintf('No items were found for model <b>%s</b>', $_GET['model'])); else { $_rows = array(); foreach ($_data as $_d) $_rows[] = $_d->attributes; $this->_sendResponse(200, CJSON::encode($_rows)); } } else { $this->_sendResponse(501, sprintf( 'Error: Mode <b>list</b> is not implemented for model <b>%s</b>', $_GET['model'])); Yii::app()->end(); } } public function actionView() { if (isset($_GET['model'])) $_model = CActiveRecord::model(ucfirst($_GET['model'])); if (isset($_model)) { $_data = $_model->findByPk($_GET['id']); if (empty($_data)) $this->_sendResponse(200, sprintf('No items were found for model <b>%s</b>', $_GET['model'])); else $this->_sendResponse(200, CJSON::encode($_data)); } else { $this->_sendResponse(501, sprintf( 'Error: Mode <b>list</b> is not implemented for model <b>%s</b>', $_GET['model'])); Yii::app()->end(); } } public function actionCreate() { $post = Yii::app()->request->rawBody; if (isset($_GET['model'])) { $_modelName = ucfirst($_GET['model']); $_model = new $_modelName; } if (isset($_model)) { if (!empty($post)) { $_data = CJSON::decode($post, true); foreach($_data as $var => $value) $_model->$var = $value; if($_model->save()) $this->_sendResponse(200, CJSON::encode($_model)); else { // Errors occurred $msg = "<h1>Error</h1>"; $msg .= sprintf("Couldn't create model <b>%s</b>", $_GET['model']); $msg .= "<ul>"; foreach($_model->errors as $attribute => $attr_errors) { $msg .= "<li>Attribute: $attribute</li>"; $msg .= "<ul>"; foreach($attr_errors as $attr_error) $msg .= "<li>$attr_error</li>"; $msg .= "</ul>"; } $msg .= "</ul>"; $this->_sendResponse(500, $msg); } } } else { $this->_sendResponse(501, sprintf( 'Error: Mode <b>create</b> is not implemented for model <b>%s</b>', $_GET['model'])); Yii::app()->end(); } } public function actionUpdate() { $post = Yii::app()->request->rawBody; if (isset($_GET['model'])) { $_model = CActiveRecord::model(ucfirst($_GET['model']))->findByPk($_GET['id']); $_model->scenario = 'update'; } if (isset($_model)) { if (!empty($post)) { $_data = CJSON::decode($post, true); foreach($_data as $var => $value) $_model->$var = $value; if($_model->save()) { Yii::log('API update -> '.$post, 'info'); $this->_sendResponse(200, CJSON::encode($_model)); } else { // Errors occurred $msg = "<h1>Error</h1>"; $msg .= sprintf("Couldn't update model <b>%s</b>", $_GET['model']); $msg .= "<ul>"; foreach($_model->errors as $attribute => $attr_errors) { $msg .= "<li>Attribute: $attribute</li>"; $msg .= "<ul>"; foreach($attr_errors as $attr_error) $msg .= "<li>$attr_error</li>"; $msg .= "</ul>"; } $msg .= "</ul>"; $this->_sendResponse(500, $msg); } } else Yii::log('POST data is empty'); } else { $this->_sendResponse(501, sprintf( 'Error: Mode <b>update</b> is not implemented for model <b>%s</b>', $_GET['model'])); Yii::app()->end(); } } public function actionDelete() { if (isset($_GET['model'])) $_model = CActiveRecord::model(ucfirst($_GET['model'])); if (isset($_model)) { $_data = $_model->findByPk($_GET['id']); if (!empty($_data)) { $num = $_data->delete(); if($num > 0) $this->_sendResponse(200, $num); //this is the only way to work with backbone else $this->_sendResponse(500, sprintf("Error: Couldn't delete model <b>%s</b> with ID <b>%s</b>.", $_GET['model'], $_GET['id']) ); } else $this->_sendResponse(400, sprintf("Error: Didn't find any model <b>%s</b> with ID <b>%s</b>.", $_GET['model'], $_GET['id'])); } else { $this->_sendResponse(501, sprintf('Error: Mode <b>delete</b> is not implemented for model <b>%s</b>', ucfirst($_GET['model']))); Yii::app()->end(); } } private function _sendResponse($status = 200, $body = '', $content_type = 'text/html') { ... } private function _getStatusCodeMessage($status) { ... } private function _checkAuth() { ... } }
      
      





このアプローチでは、適切なモデルの準備が必要です。 たとえば、 ActiveRecordに組み込まれた配列変数属性は、データベースのテーブル構造のみに基づいて形成されます。 選択に関連テーブルのフィールドまたは計算可能なフィールドを含める必要がある場合getAttributesメソッドと、必要に応じて、モデルのhasAttributeをオーバーロードする必要があります 。 例として、 getAttributesの実装:



  public function getAttributes($names = true) { $_attrs = parent::getAttributes($names); $_attrs['quota_limit'] = $this->limit['bytes_in_avail']; $_attrs['quota_used'] = $this->tally['bytes_in_used']; return $_attrs; }
      
      





また、ページネーションとソートが正しく機能するように、モデルに名前付きスコープサマリーを作成する必要があります。



  public function summary($_getvars = null) { $_criteria = new CDbCriteria(); if (isset($_getvars['count'])) { $_criteria->limit = $_getvars['count']; if (isset($_getvars['page'])) $_criteria->offset = ($_getvars['page']) * $_getvars['count']; } if (isset($_getvars['sort'])) $_criteria->order = str_replace('.', ' ', $_getvars['sort']); $this->getDbCriteria()->mergeWith($_criteria); return $this; }
      
      





モデルの全文:



 <?php /** * This is the model class for table "ftpuser". * * The followings are the available columns in table 'ftpuser': * @property string $id * @property string $userid * @property string $passwd * @property integer $uid * @property integer $gid * @property string $homedir * @property string $shell * @property integer $count * @property string $accessed * @property string $modified */ class Ftpuser extends CActiveRecord { // Additional quota parameters public $quota_limit; public $quota_used; /** * Returns the static model of the specified AR class. * @return ftpuser the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } /** * @return string the associated database table name */ public function tableName() { return 'ftpuser'; } /** * @return array validation rules for model attributes. */ public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( array('uid, gid, count', 'numerical', 'integerOnly' => true), array('userid, passwd, homedir', 'required'), array('userid, passwd', 'length', 'max' => 32), array('homedir', 'length', 'max' => 255), array('shell', 'length', 'max' => 16), array('accessed, modified, quota_limit, quota_used', 'safe'), //array('userid', 'unique'), // The following rule is used by search(). // Please remove those attributes that should not be searched. array('id, userid, passwd, uid, gid, homedir, shell, count, accessed, modified', 'safe', 'on' => 'search'), ); } /** * @return array relational rules. */ public function relations() { // NOTE: you may need to adjust the relation name and the related // class name for the relations automatically generated below. return array( 'limit' => array(self::HAS_ONE, 'FTPQuotaLimits', 'user_id'), 'tally' => array(self::HAS_ONE, 'FTPQuotaTallies', 'user_id'), ); } /** * @return array customized attribute labels (name=>label) */ public function attributeLabels() { return array( 'id' => 'Id', 'userid' => 'Userid', 'passwd' => 'Passwd', 'uid' => 'Uid', 'gid' => 'Gid', 'homedir' => 'Homedir', 'shell' => 'Shell', 'count' => 'Count', 'accessed' => 'Accessed', 'modified' => 'Modified', ); } /** * Retrieves a list of models based on the current search/filter conditions. * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions. */ public function search() { // Warning: Please modify the following code to remove attributes that // should not be searched. $criteria = new CDbCriteria; $criteria->compare('userid', $this->userid, true); $criteria->compare('homedir', $this->homedir, true); return new CActiveDataProvider('ftpuser', array( 'criteria' => $criteria, )); } public function summary($_getvars = null) { $_criteria = new CDbCriteria(); if (isset($_getvars['count'])) { $_criteria->limit = $_getvars['count']; if (isset($_getvars['page'])) $_criteria->offset = ($_getvars['page']) * $_getvars['count']; } if (isset($_getvars['sort'])) $_criteria->order = str_replace('.', ' ', $_getvars['sort']); $this->getDbCriteria()->mergeWith($_criteria); return $this; } public function getAttributes($names = true) { $_attrs = parent::getAttributes($names); $_attrs['quota_limit'] = $this->limit['bytes_in_avail']; $_attrs['quota_used'] = $this->tally['bytes_in_used']; return $_attrs; } protected function afterFind() { parent::afterFind(); $this->quota_limit = $this->limit['bytes_in_avail']; $this->quota_used = $this->tally['bytes_in_used']; } protected function afterSave() { parent::afterSave(); if ($this->isNewRecord && !empty($this->quota_limit)) { $_quota = new FTPQuotaLimits(); $_quota->user_id = $this->id; $_quota->name = $this->userid; $_quota->bytes_in_avail = $this->quota_limit; $_quota->save(); } } protected function beforeValidate() { if ($this->isNewRecord) { if (empty($this->passwd)) $this->passwd = $this->genPassword(); $this->homedir = Yii::app()->params['baseFTPDir'].$this->userid; } elseif ($this->scenario == 'update') { if (empty($this->quota_limit)) { FTPQuotaLimits::model()->deleteAllByAttributes(array('name' => $this->userid)); FTPQuotaTallies::model()->deleteAllByAttributes(array('name' => $this->userid)); } else { $_quota_limit = FTPQuotaLimits::model()->find('name = :name', array(':name' => $this->userid)); if (isset($_quota_limit)) { $_quota_limit->bytes_in_avail = $this->quota_limit; $_quota_limit->save(); } else { $_quota_limit = new FTPQuotaLimits(); $_quota_limit->name = $this->userid; $_quota_limit->user_id = $this->id; $_quota_limit->bytes_in_avail = $this->quota_limit; $_quota_limit->save(); } } } return parent::beforeValidate(); } protected function beforeDelete() { FTPQuotaLimits::model()->deleteAllByAttributes(array('name' => $this->userid)); FTPQuotaTallies::model()->deleteAllByAttributes(array('name' => $this->userid)); return parent::beforeDelete(); } private function genPassword($len = 6) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $count = mb_strlen($chars); for ($i = 0, $result = ''; $i < $len; $i++) { $index = rand(0, $count - 1); $result .= mb_substr($chars, $index, 1); } return $result; } }
      
      





完全な幸福のために欠けているのは、フォーム/ users / 156 / recordsのネストされたリクエストの処理を処理する方法がないことです。しかし、これはフレームワークであり、CMSではありません。 私の場合は簡単です、それは必要ありませんでした。



サーバー部分が完了したら、クライアントに移動します。 興味がある人のために、Yiiサーバーサイドアプリケーションの完全なソースコードをここに投稿します 。 リンクをより確実に配置するためのより実用的な提案がある場合、リンクがどのくらいの期間存続するかわかりません-コメントに注意してください。



クライアント部


私の自転車を書かないために、小さな検索が実行され、優れたActiveResource拡張機能が見つかりました。 著者によると、インスピレーションの源は、Ruby on RailsでのActiveResourceの実装でした。 このページには、インストール方法と使用方法の簡単な説明があります。



しかし、ほとんどすぐにActiveRecordと互換性のあるコンポーネントであることがわかりましたが 、YiiウィジェットGridViewまたはListViewで使用するには、 ActiveDataProviderと互換性のあるコンポーネント必要でした。 EActiveResourceDataProviderEActiveResourceQueryCriteriaを含む別のブランチで行われた改善、および拡張機能の作成者が参加したフォーラムスレッドでのそれらの議論に簡単な検索をもたらしました。 ESortおよびEActiveResourceDataProviderの改訂版公開されました



ソリューションのすべての優雅さにもかかわらず、ファイルは完全ではありませんでした。 問題は、グリッド内のページネーションコンポーネントの誤動作でした。 ソースの簡単な調査により、拡張機能で使用されるオフセットはレコード数で表される実際のオフセットであり、 GridViewのページネーションではページ番号が使用されることが示されました。 ページごとに10個のレコードを設定すると、ページ2に移動すると、ページ20にスローされたことが判明しました。コードを調べて編集します。 これを行うには、 buildQueryStringメソッドの本体の保護されたファイル/ extensions / EActiveResource / EActiveResourceQueryCriteria.phpで、次の編集を行います。



 if($this->offset>0) // array_push($parameters, $this->offsetKey.'='.$this->offset); array_push($parameters, $this->offsetKey.'='.$this->offset / $this->limit);
      
      





その後、 EActiveResourcePaginationからgetOffsetメソッドのオーバーロードを不要にする必要があります。



したがって、RESTデータソースを使用してアプリケーションを作成する場合、必要なモデルを手動で作成する必要があり、残りはGIIを介して問題なく作成されます。



また、いくつかのサーバーでの動作に注目したいと思います。 最初は、リモートREST APIへの接続は構成に記述されているため、デフォルトではサイトで1つの接続のみを使用できます。 接続情報をデータベーステーブルに保存し、 ActiveResourceコンポーネントで使用するには、そこからオーバーロードされたgetConnectionメソッドを使用して子孫を作成する必要がありました(FTPユーザーの場合、サーバーデータはFTPServersモデルで記述されたテーブルに保存されます):



 abstract class EActiveResourceSelect extends EActiveResource { /** * Returns the EactiveResourceConnection used to talk to the service. * @return EActiveResourceConnection The connection component as pecified in the config */ public function getConnection() { $_server_id = Yii::app()->session['ftp_server_id']; $_db_params = array(); if (isset($_server_id)) { $_srv = FTPServers::model()->findByPk($_server_id); if (isset($_srv)) $_db_params = $_srv->attributes; else Yii::log('info', "No FTP server with ID: $_server_id were found"); } else { $_srv = FTPServers::model()->find(); if (isset($_srv)) { $_db_params = $_srv->attributes; Yii::app()->session['ftp_server_id'] = $_srv->id; } else Yii::log("No FTP servers were found", CLogger::LEVEL_INFO); } self::$_connection = new EActiveResourceConnection(); self::$_connection->init(); self::$_connection->site = $_db_params['site']; self::$_connection->acceptType = $_db_params['acceptType']; self::$_connection->contentType = $_db_params['contentType']; if (isset($_db_params['username']) && isset($_db_params['password'])) { self::$_connection->auth = array( 'type' => 'basic', 'username' => $_db_params['username'], 'password' => $_db_params['password'], ); } return self::$_connection; } }
      
      





クライアント部分のさらなる開発は、通常のActiveRecordを使用した開発と大きな違いはありませんでした。これは、私の意見では、 ActiveResource拡張の主な魅力です。



この記事がお役に立てば幸いです。



All Articles