おそらく、記事の準備および執筆中に、RESTful APIを作成するための組み込みフレームワークを備えたYii2のリリースとの関連性がわずかに失われました。 しかし、この記事は、Yii2にまだ慣れていない人や、既存のアプリケーション用の本格的なAPIを迅速かつ簡単に実装する必要がある人にとっては依然として有用です。
まず、既存の拡張機能を使用する際にサーバーAPIを完全に操作する必要がなかったいくつかの機能のリストを示します。
- 最初に遭遇した問題の1つは、1つのテーブルにさまざまなエンティティを格納することでした。 このようなレコードを取得するには、たとえばここに示すように、モデル名を単に示すだけでは不十分です 。 このようなメカニズムの1つの例は、RBACメカニズムのフレームワークで使用される
AuthItems
テーブルです(誰かがそれに慣れていない場合、このトピックに関するすばらしい記事があります)。type
フラグによって決定されるロール、操作、およびタスクが含まれており、異なるURLを使用したいAPIを介してこれらのエンティティを操作します。
GET: /api/authitems/?type=0 -
GET: /api/authitems/?type=1 -
GET: /api/authitems/?type=2 -
しかし、そのような:
GET: /api/operations -
GET: /api/tasks -
GET: /api/roles -
同意して、2番目のオプションは、特にフレームワークとその中のRBACデバイスに精通していない人にとって、より明白で理解しやすいように見えます。
- 2番目の重要な機会は、条件を設定し、ルールを結合する機能を備えたデータ検索およびフィルタリングメカニズムです。 たとえば、私はそのようなリクエストの類似物を実行できるようにしたかった:
SELECT * FROM users WHERE (age>25 AND first_name LIKE '%alex%') OR (last_name='shepard');
- コレクションを作成、更新、削除するだけでは不十分な場合があります。 つまり 再度検索とフィルタリングを使用して、1つのクエリでn番目のレコード数を変更します。 たとえば、あらゆる条件に該当するすべてのレコードを頻繁に削除または更新する必要があり、個々のクエリを使用するには費用がかかりすぎます。
- もう1つの重要なポイントは、関連データを受信できることです。 たとえば、これらの役割とそのすべてのタスクと操作を取得します。
- もちろん、受信したレコードの数を制限する(
limit
)、選択の開始をシフトする(offset
)、レコードのソート順を指定する(order by
)ことができないと、少なくとも何らかの形でAPIを操作することはできません。 また、グループ化(group by
)できると便利です。
- 各操作のユーザー権限を確認できることが重要です(
checkAccess
メソッドcheckAccess
まだ同じRBACにあります)。
- そして最後に、全体を何らかの形でテストする必要があります。
ほぼそのような「ウィッシュリスト」のリストを分析した結果、このすばらしいフレームワークでのAPI実装の私のバージョンが生まれました!
まず、APIがクライアントを探す方法。
同じRBACコンポーネントの例を見てみましょう。
レコードを取得する
すべてがいつも通りです:
GET: /roles -
GET: /roles/42 - id=42
検索とフィルター
それらのメカニズムはほぼ同じですが、唯一の違いは、検索時、部分的に一致するレコードが選択に含まれる場合と、完全にフィルタリングされる場合です。 フィールドとその値の組み合わせは、JSON形式で指定されます。 この機能を実装するのに最も便利だと思われたのは彼でした。 例:
{"name":"alex", "age":"25"}
-次の形式のクエリに一致します:
WHERE name='alex' AND age=25
[{"name":"alex"}, {"age":"25"}]
-次の形式のクエリに一致:
WHERE name='alex' OR age=25
つまり 1つのオブジェクトに渡されるパラメーターはAND条件に対応し、オブジェクトの配列で指定されるパラメーターはOR条件に対応します。
ANDおよびOR条件に加えて、値の前に必要な次の条件を指定できます。
-
<:
私 -
>:
もっと -
<=:
より小さいか等しい -
>=:
以上 -
<>:
等しくない -
=:
等しい
いくつかの例:
GET: /users?filter={"name":"alex"}
-alex
という名前のユーザー
GET: /users?filter={"name":"alex", "age":">25"}
-alex
という名前のユーザー25
歳以上
GET: /users?filter=[{"name":"alex"}, {"name":"dmitry"}]
-名前がalex
またはdmitry
ユーザー
GET: /users?search={"name":"alex"}
-サブストリングalex
(alexey、alexander、alexなど)を含む名前を持つユーザー
関連データを操作する
多くの場合、関連データを操作するための次の構文を見つけることができます。
GET: /roles/42/operations
-id = 42
ロールに属するすべての操作を取得します
最初はこの特定のアプローチを使用しましたが、その過程でいくつかの欠点があることに気付きました。
1対多
関係が1対多の場合、上記のフィルターアプローチを使用できます。
GET: operations?filter={"role_id":"42"}
-id = 42
ロールに属するすべての操作を取得します
多対多
多くの場合、通信テーブルはフィールド
parent_id
および
child_id
限定されないため、通信の操作は多くの場合、個別のエンティティとしてはるかに便利です。 製品(
products
)とその特性(
features
)の例を考えてみましょう。 通信テーブルには、少なくとも2つのフィールド
product_id
および
feature_id
です。 ただし、製品カードの特性リストのソート順を設定する必要がある場合は、
ordering
フィールドをテーブルに追加する必要が
value
、同じ特性の値の
value
も追加する必要があります。
フォームのURLを使用:
POST: /products/42/feature/1
製品42
を製品特性1
関連付けます
GET: /products/42/feature/1
製品1
の特性を取得します(features
表から入力)
同じソート順と特性値(通信テーブルからの入力)を取得する方法はありません。 個人的な経験から、このような種類の関係では、
productfeatures
などの別のエンティティを使用する方がよいと確信しました。
したがって、次のようになります。
POST: /productfeatures
要求の本文にproduct_id
、feature_id
、ordering
、およびvalue
のパラメーターを渡すと、値とソート順を示す特性と製品がリンクされます。
GET: /productfeatures?filter={"product_id":"42"}
-特性を持つ製品のすべてのリンクを取得します。 答えは次のようになります。
[ {"id":"12","feature_id":"1","product_id":"42","value":"33"}, {"id":"13","feature_id":"2","product_id":"42","value":"54"} ]
PUT: /productfeatures/12
-id=12
リンクを変更します
もちろん、このアプローチにも欠陥がないわけではありません。たとえば、2回の追加リクエストなしでは製品名と特性名を取得できないからです。 ここで、関連データを取得するメカニズムが役立ちます。
関連データの取得
GET: /productfeatures/12?with=product,feature
製品と特性とともに接続を取得します。 サーバー応答の例:
{ "id":"12", "feature_id":"1", "product_id":"42", "value":"33", "feature":{"id":"1","name":"","unit":""}, "product":{"id":"42","name":"", ...}, }
同様に、商品のすべての特性を取得できます。
GET: /products/42?with=features
-id=42
製品データと配列内のすべての特性を受け取ります。 サーバー応答の例:
{ "id":"42", "name":"", "features":[{"id":"1","name":"","unit":""}, {"id":"2","name":"","unit":""}], ... }
今後は、
with
を使用する
with
、関連するテーブルからデータを取得できるだけでなく、値を持つ配列を簡単に記述できると言います。 これは、たとえば、ステータスの可能な値のリストを製品データとともに送信する必要がある場合に役立ちます。 製品のステータスは
status
フィールドに保存され
status
が、受信した
status:0
値はあまりわかりません。 これを行うには、製品データと一緒に、その説明で可能なステータスを取得できます。
{ ..., "status":1, "statuses":{0:" ", 1:" ", 2:" "}, ..., }
データ削除
DELETE: /role/42 - id=42
DELETE: /role -
削除するときは、検索とフィルタリングも使用できます。
DELETE: /role?filter={"name":"admin"} - "admin"
データ作成
POST: /role -
単一のリクエストで、たとえば次の形式のリクエスト本文のデータの配列を転送することにより、単一のレコードとコレクションの両方を作成できます。
[ {"name":"admin"}, {"name":"guest"} ]
このようにして、対応する名前を持つ2つのロールが作成されます。 この場合のサーバー応答も、作成されたレコードの配列になります。
データ変更
作成と同様に、URLでidパラメーターを指定する必要があるのはもちろん、PUTだけです:
PUT: /role/42 - 42
複数のエントリを変更します。
PUT: /role
リクエストボディを渡す
[ {"id":"1","name":"admin"}, {"id":"2","name":"guest"} ]
ID 1および2のエントリが変更されます。
フィルターで見つかったレコードを変更します。
PUT: /user?filter={"role":"guest"}' - role=guest
レコードの制限、オフセット、順序
部分サンプリングでは、通常の
limit
と
offset
ます。
offset
ゼロから始まるオフセット
limit
-エントリ数
order
-ソート順
GET: /users/?offset=10&limit=10
GET: /users/?order=id DESC
GET: /users/?order=id ASC
以下を組み合わせることができます。
GET: /users/?order=parent_id ASC,ordering ASC
応答に制限とオフセットがどのように表示されるかについて言及することが重要です。 たとえば、応答本文でデータを送信するなど、いくつかのオプションを検討しました。
{ data:[ {id:1, name:"Alex", role:"admin"}, {id:2, name:"Dmitry", role:"guest"} ], meta:{ total:2, offset:0, limit:10 } }
クライアント側では、AngularJSを使用しました。 私には、
$resource
メカニズムを実装すると非常に便利に思えました。 その機能については詳しく説明しませんが、実際に使用するには、不必要な情報なしでクリーンなデータを取得する方が良いという事実です。 したがって、選択したレコードの数に関するデータはヘッダーに移動されました。
GET: roles?limit=5
Content-Range:items 0-4/10 - 0 4, 10.
上記の見出しは、4つのレコードが受信されたのではなく、5(ゼロベース)が受信されたことを示していることに注意することが重要です。 つまり 10個すべてのエントリを受け取ると、タイトルは次の形式になります。
Content-Range:items 0-9/10 - 0 9 10.
クライアントでこのようなヘッダーを解析することは難しくありません。また、応答本文に「余分な」データが詰まることはありません。
サーバーでの実装。
最初のステップは、モジュールを作成することです。 もちろん、これは必須の要件ではありませんが、モジュールはこれに最適です。 モジュール名にAPIバージョンを含めることもできます。
次に、アプリケーションの構成で、URLとリクエストメソッドに従って適切なルーティングのためのいくつかのルールを追加します。
array('api/<controller>/list', 'pattern'=>'api/<controller:\w+>', 'verb'=>'GET'), array('api/<controller>/view', 'pattern'=>'api/<controller:\w+>/<id:\d+>', 'verb'=>'GET'), array('api/<controller>/create', 'pattern'=>'api/<controller:\w+>', 'verb'=>'POST'), array('api/<controller>/update', 'pattern'=>'api/<controller:\w+>/<id:\d+>', 'verb'=>'PUT'), array('api/<controller>/update', 'pattern'=>'api/<controller:\w+>', 'verb'=>'PUT'), array('api/<controller>/delete', 'pattern'=>'api/<controller:\w+>/<id:\d+>', 'verb'=>'DELETE'), array('api/<controller>/delete', 'pattern'=>'api/<controller:\w+>', 'verb'=>'DELETE'),
フレームワークに少なくともある程度精通している人にとっては、説明することは何もないと思います。
次に、
ApiController.php
、
Controller.php
および
ApiRelationProvider.php
便利な方法で接続し
ApiRelationProvider.php
。
APIモジュールコントローラー
すべてのAPIモジュールコントローラーは、
ApiController
クラスを拡張する必要があります。
ルーターの設定から、次のメソッド(
actions
)をコントローラーに実装する必要があることは明らかです。
actionView()
-レコードを取得する
actionList()
-レコードのリストを取得する
actionCreate()
-レコードを作成します
actionUpdate()
-レコードの変更
actionDelete()
-レコードを削除する
ユーザーロールコントローラーの例を考えてみましょう。 前述したように、RBACフレームワークメカニズムは、すべてのエンティティ(ロール、操作、タスク)を1つのテーブル(
authitem
)に
authitem
ます。 エンティティのタイプは、このテーブルの
type
フラグによって決定されます。 つまり
RolesController
、
OperationsController
、
TasksController
は、1つのモデル(
AuthItems
)で動作する必要がありますが、それらのスコープは、対応する
type
値を持つレコードのみに制限する必要があります。
コントローラーコード:
class RolesController extends ApiController { public function __construct($id, $module = null) { $this->model = new AuthItem('read'); $this->baseCriteria = new CDbCriteria(); $this->baseCriteria->addCondition('type='.AuthItem::ROLE_TYPE); parent::__construct($id, $module); } public function actionView(){ if(!Yii::app()->user->checkAccess('getRole')){ $this->accessDenied(); } $this->getView(); } public function actionList(){ if(!Yii::app()->user->checkAccess('getRole')){ $this->accessDenied(); } $this->getList(); } public function actionCreate(){ if(!Yii::app()->user->checkAccess('createRole')){ $this->accessDenied(); } $this->model->setScenario('create'); $this->priorityData = array('type'=>AuthItem::ROLE_TYPE); $this->create(); } public function actionUpdate( ){ if(!Yii::app()->user->checkAccess('updateRole')){ $this->accessDenied(); } $this->model->setScenario('update'); $this->priorityData = array('type'=>AuthItem::ROLE_TYPE); $this->update(); } public function actionDelete( ){ if(!Yii::app()->user->checkAccess('deleteRole')){ $this->accessDenied(); } $this->model->setScenario('delete'); $this->delete(); } public function getRelations() { return array( 'roleoperations'=>array( 'relationName'=>'operations', 'columnName'=>'operations', 'return'=>'array' ) ); } }
まず、コンストラクターメソッドで、コントローラーが動作するモデルを指定し、モデルのインスタンスをコントローラーの
model
プロパティに割り当てます。
baseCriteria
プロパティを指定し、その条件(
addCondition('type='.AuthItem::ROLE_TYPE)
)を割り当てること
baseCriteria
、クライアントから受信したすべてのデータについて、この条件を満たす必要があると判断します。 したがって、データを受信、更新、削除するためのレコードを選択する場合、条件
type=2
一致するレコード
type=2
目的の
id
値を持つレコードがテーブルに存在するが、
type
が
baseCriteria
指定されたものと異なる場合
baseCriteria
クライアントは404エラーを受け取ります。
actionCreate()
メソッドは、
priorityData
プロパティの値
actionCreate()
設定します。このプロパティは、クライアントからリクエスト本文で受信したデータをオーバーライドするデータセットを指定します。 つまり、クライアントがリクエスト本文の
type
プロパティを42として指定した場合でも、
AuthItem::ROLE_TYPE
(2)の値に再定義され、ロール以外のエンティティの作成は許可されません。
操作を実行する前に、
checkAccess()
メソッドを使用してユーザー権限がチェックされ、モデルに応じてシナリオが示されます。これは、検証ルールまたはトリガーがシナリオに応じてモデルロジックで定義できるためです。
すべてのアクションメソッド(
getView()
、
getList()
、
create()
、
update()
、
delete()
)は、デフォルトでユーザーにデータを送信し、アプリケーションを終了します。 最初のパラメーター
false
を受け取ったメソッドは、応答を配列として返します。 これは、ユーザーに送信する前にモデルから受信したデータの一部の属性(パスワードなど)をクリアする必要がある場合に役立ちます。 この場合、応答コードは
statusCode
プロパティから取得できます。このプロパティは、メソッドの実行後に入力されます。
最後のコントローラーメソッド
getRelations()
は、モデルの関係を構成するために使用されます。 このメソッドは、関係のセットを記述する配列を返す必要があります。 この場合、
...?with=roleoperations
パラメーターをURLで指定すると、ロールデータとともに、それに割り当てられたすべての操作を受け取ります。
{ bizrule: null description: "Administrator" id: "1" name: "admin" operations: [{...}, {...},...] type: "2" }
getRelations()
メソッドによって返される配列では、配列キーはGETパラメーターに対応するリンクの名前です(この場合、
roleoperations
)。
接続を構成する配列の要素の値:
relationName
| string
| モデル内のリンクの名前。 モデルにそれぞれの接続がない場合。 フレームワークメカニズムは、その名前のプロパティを取得するか、 get
に置き換えてメソッドを実行しようとします。 たとえば、モデルメソッドは接続としても機能します。このため、接続の名前 getPossibleValues()
possibleValues
など getPossibleValues()
を指定し、データの配列を返すモデルで getPossibleValues()
メソッドを作成する getPossibleValues()
あります。 |
columnName
| string
| 見つかったレコードがサーバー応答に追加される属性の名前。
|
return
| string ('array' | 'object')
| オブジェクトの配列(モデル)または値の配列を返します。
|
ほとんどの場合、コントローラーは上記よりもはるかにシンプルに見えると言わなければなりません。 私のプロジェクトの1つのコントローラーの例を次に示します。
<?php class TagController extends ApiController { public function __construct($id, $module = null) { $this->model = new Tag('read'); parent::__construct($id, $module); } public function actionView(){ $this->getView(); } public function actionList(){ $this->getList(); } public function actionCreate(){ if(!Yii::app()->user->checkAccess('createTag')){ $this->accessDenied(); } $this->create(); } public function actionUpdate(){ if(!Yii::app()->user->checkAccess('updateTag')){ $this->accessDenied(); } $this->update(); } public function actionDelete(){ if(!Yii::app()->user->checkAccess('deleteTag')){ $this->accessDenied(); } $this->delete(); } }
ApiController
クラスの簡単な説明:
プロパティ:
物件
| 種類
| 説明
|
---|---|---|
data
| 配列
| リクエスト本文からのデータ。 Content-Type: x-www-form-urlencoded
を使用し、 Content-Type: application/json
を使用するリクエストからのデータは、配列に入ります |
priorityData
| 配列
| データの作成および変更操作を実行するときに、要求本文からのデータ(データ)で置換または補足されるデータ。
|
model
| CActiveRecord
| データを操作するためのモデルインスタンス。
|
statusCode
| 整数
| サーバー応答コード。 初期値は200
です。 |
criteriaParams
| 配列
| 初期選択パラメーター( limit
、 offset
、 order
)。 GET要求パラメーターから取得した値は、配列内の対応する値をオーバーライドします。 元の値:
|
contentRange
| 配列
| 選択したレコードの数に関するデータ。 例:
|
sendToEndUser
| ブール値
| 操作の完了(表示、作成、変更、削除)後にデータをユーザーに送信するか、アクションの結果を配列の形式で返すか。
|
criteria
| CDbCriteria
| データを取得するためのCDbCriteria
クラスのインスタンス。 要求からのデータ(制限、オフセット、順序、フィルター、検索など)に基づいて構成されます |
baseCriteria
| CDbCriteria
| データを取得するためのCDbCriteria
クラスの基本インスタンス。 オブジェクト条件は criteria
条件よりも優先され criteria
。 |
notFoundErrorResponse
| 配列
| エントリが見つからない場合のサーバーの応答。
|
方法
- getView()
GETパラメーターに従ってレコードを検索します。 書き込みパラメーターの配列を返すか、ユーザーにデータを送信します。 レコードが見つからない場合、404の応答コードを含むエラーメッセージをユーザーに送信するか、対応するエラー情報を含む配列を返します。 リクエストの実行後にstatusCode
プロパティを適切な値に設定します。
getView(boolean $sendToEndUser = true, integer $id)
$ sendToEndUser
ブール値
操作が完了した後にユーザーにデータを送信するか、アクションの結果を配列の形式で返すか。
$ id
整数
エントリのidパラメータ。 渡されない場合、GETパラメーターから取り込まれます。
- getList()
GETパラメーターに従ってレコードを検索します。 見つかったレコードの配列または空の配列を返します。
getList(boolean $sendToEndUser = true)
$ sendToEndUser
ブール値
操作が完了した後にユーザーにデータを送信するか、アクションの結果を配列の形式で返すか。
- 作成()
要求本文から取得したデータを使用して、新しいレコードを作成します。 属性の配列がリクエストボディで送信される場合、対応するレコード数が認識されます。 新しいレコードの属性を含む配列を返します。
例:
array( 'name'=>'Alex', 'age'=>'25' ) // .
array( array( 'name'=>'Alex', 'age'=>'25' ), array( 'name'=>'Dmitry', 'age'=>'33' ) ) // .
create(boolean $sendToEndUser = true)
$ sendToEndUser
ブール値
操作が完了した後にユーザーにデータを送信するか、アクションの結果を配列の形式で返すか。
- 更新()
受信したGETパラメーターに従って見つかったレコードを更新します。 書き込みパラメーターの配列を返すか、ユーザーにデータを送信します。 レコードが見つからない場合、404の応答コードを含むエラーメッセージをユーザーに送信するか、対応するエラー情報を含む配列を返します。 レコードの配列がリクエストボディで転送される場合、対応するレコード数が変更され、それらの値を持つ配列が返されます。
例:
PUT: users/1
array( 'name'=>'Alex', 'age'=>'25' ) // GET
PUT: users
array( array( 'id'=>1, 'name'=>'Alex', 'age'=>'25' ), array( 'id'=>2, 'name'=>'Dmitry', 'age'=>'33' ) ) // id . url
update(boolean $sendToEndUser = true, integer $id)
$ sendToEndUser
ブール値
操作が完了した後にユーザーにデータを送信するか、アクションの結果を配列の形式で返すか。
$ id
整数
エントリのidパラメータ。 渡されない場合、GETパラメーターから取り込まれます。
- 削除()
受信したGETパラメーターに従って見つかったレコードを削除します。 リモートレコードパラメーターの配列を返すか、ユーザーにデータを送信します。 レコードが見つからない場合、応答コード404のエラーメッセージをユーザーに送信するか、対応するエラー情報を含む配列を返します。 idパラメーターが受信されない場合、すべてのレコードが削除されます。
delete(boolean $sendToEndUser = true, integer $id)
$ sendToEndUser
ブール値
操作が完了した後にユーザーにデータを送信するか、アクションの結果を配列の形式で返すか。
$ id
整数
エントリのidパラメータ。 渡されない場合、GETパラメーターから取り込まれます。
テスト中
APIのテストの問題を調査して、多くのアプローチを検討しました。 単体テストではなく機能テストを使用することをお勧めします。 しかし、セレンを使用してフォームを作成し、入力フィールドを追加し、データを入力し、送信ボタンをクリックしてサーバーの応答を分析して送信するなどの信じられない方法で、機能テストのいくつかの方法を試しました( SeleniumおよびPhantomJsを使用)この方法ではテストに何年もかかります!
検索をさらに深く掘り下げ、他の開発者の経験を分析して、curlを使用してAPIをテストするためのクラスを作成しました。それを使用するには、クラス
ApiTestCase
を接続し、そこからテストクラスを拡張する必要があります。
APIのテスト中に最初に遭遇した問題は、アクセス許可の問題でした。テスト中に、テストベースが使用されます。したがって、RBACが使用するテーブルに常に最新のデータがあることを常に監視する必要があります。そうでない場合、エンティティの作成をテストしようとする
{"error":{"access":"You do not have sufficient permissions to access."}}
と、コード403で応答を取得できます。 APIコントローラーのアクションで。この問題を解決するために、コンポーネントが機能するための作業ベースを使用することにしました。
authManager
、テスト環境の構成ファイル(config / test.php)で以下を指定することにより、アクセス権を処理します。
... 'proddb'=>array( 'class'=>'CDbConnection', 'connectionString' => 'mysql:host=localhost;dbname=yiirestmodel', 'emulatePrepare' => true, 'username' => '', 'password' => '', 'charset' => 'utf8', ), // 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=yiirestmodel-test', ), // 'authManager'=>array( 'connectionID'=>'proddb', // ), ...
このアプローチの唯一の制限は、ユーザー管理者がテストベース
id=1
で作業管理者ロールに割り当てられている場合
id=42
、コンポーネントはそのようなユーザーを管理者と見なさないため、ユーザーテーブルの許可ユーザーのid値が両方のデータベースで同じであることを確認する必要があることです!
テスト例:
class UsersControllerTest extends ApiTestCase { public $fixtures = array( 'users'=>'User' ); public function testActionView(){ $user = $this->users('admin'); $response = $this->get('api/users/'.$user->id, array(), array('cookies'=>$this->getAuthCookies())); $this->assertEquals($response['code'], 200); $this->assertNotNull($response['decoded']); $this->assertEquals($response['decoded']['id'], $user->id); $this->assertArrayNotHasKey('password', $response['decoded']); $this->assertArrayNotHasKey('guid', $response['decoded']); } public function testActionList(){ $response = $this->get('api/users', array(), array('cookies'=>$this->getAuthCookies())); $this->assertEquals($response['code'], 200); $this->assertEquals(count($response['decoded']), User::model()->count()); } public function testActionCreate(){ $response = $this->post( 'api/users', array( 'first_name' => 'new_first_name', 'middle_name' => 'new_middle_name', 'last_name' => 'new_last_name', 'password' => 'new_user_psw', 'password_repeat' => 'new_user_psw', 'role' => 'guest', ), array('cookies'=>$this->getAuthCookies()) ); $this->assertEquals($response['code'], 200); $this->assertNotNull($response['decoded']); $this->assertArrayHasKey('id', $response['decoded']); $this->assertArrayNotHasKey('password', $response['decoded']); $this->assertNotNull( User::model()->findByPk($response['decoded']['id']) ); } }
最初に、テストで使用されるフィクスチャを示します。次に、テストメソッドで、メソッドを使用してリクエストを作成し
ApiTestCase::get()
(GETメソッドを使用してリクエストを実行)、URLを渡し、メソッドを呼び出して受信した承認Cookieを渡し
ApiTestCase::getAuthCookies()
ます。これらのCookieを取得するには、パラメーター
$loginUrl
とを指定する必要があります
$loginData
。
ApiTestCase
テストの各クラスでそれらを登録しないように、クラスでそれらを直接指定します:
public $loginUrl = 'api/login'; public $loginData = array('login'=>'admin', 'password'=>'admin');
このメソッド
ApiTestCase::getAuthCookies()
は、呼び出しごとに認証要求を行うのではなく、キャッシュされたデータを返すのに十分スマートであると言わなければなりません。要求を再実行するには、最初のパラメーターを渡すことができます
true
。
方法ApiTestCase :: GET()(として
ApiTestCase::post()
、
ApiTestCase::put()
、
ApiTestCase::delete()
)次の構造を使用してクエリを実行し、データの配列を返します。
body
| ひも
| サーバー応答
| ||
code
| 整数
| 応答コード
| ||
cookies
| 配列
| 応答で受信したCookieの配列
| ||
headers
| 配列
| 応答で受信したヘッダーの配列(ヘッダー名=>ヘッダー値)例:
| ||
decoded
| 配列
| デコードされた(json_decode)サーバー応答の配列
|
このデータは、サーバー応答の完全なテストと分析に十分です。
要求への応答を受信した後、さまざまなアサートがチェックされますが、これは非常に明白であり、コメントを必要としません。もちろん、これはエンティティの完全なテストコードとはほど遠いですが、この例はクラスでの作業の原則を理解するのに十分
ApiTestCase
です。
短いクラスの説明
ApiTestCase
:
プロパティ:
物件
| 種類
| 説明
|
---|---|---|
authCookies
| 配列
| 認証後に受信したCookie(メソッド呼び出しApiTestCase::getAuthCookies()
) |
loginUrl
| ひも
| 認証Cookieを受信するための認証リクエストを完了するためのアドレス。
|
loginData
| 配列()
| . :
|
:
- getAuthCookies()
.
getAuthCookies(boolean $reload = false)
$reload
boolean
.
- get()
GET. .
get( string $url, array $params = array(), array $options = array()){
$url
string
Url
$params
array
GET
$options
array
, curl_setopt_array
.cookies
, (=>) .
- post()
POST. .
post( string $url, array $params = array(), array $options = array()){
$url
string
Url
$params
array
リクエストボディで渡されるリクエストパラメータの配列
$オプション
配列
メソッドに置換されるオプションを要求します curl_setopt_array
。また、配列にはcookies
、リクエストヘッダーで送信するために、値がCookieの配列(名前=>値)である必要がある要素がある場合があります。
- put()
PUTメソッドを使用してリクエストを実行します。サーバー応答パラメーターを含む配列を返します。
パラメータの説明は、ApiTestCase :: post()を参照してください
- delete()
DELETEメソッドを使用してクエリを実行します。サーバー応答パラメーターを含む配列を返します。
パラメーターの説明は、ApiTestCase :: post()を参照してください
githubへのリンク。
おわりに
もちろん、データを処理するために使用されるため、負荷が高いと問題が発生する可能性があります
ActiveRecord
。これはキャッシングによって部分的に解決できると思います(Yiiにはこれに必要なものがすべて揃っているため)。
拡張機能全体ではなくても役に立つと思う開発者がいることを願っています。その場合、一部の拡張機能やアイデアだけが使用されます。
将来の計画はより多くのさまざまな改善と変更であるため、コメントや提案に感謝します。
PS
記事は大きく(意図されたものの半分を説明することはできませんでしたが)、やや「引き裂かれた」ことが判明しました。情報が将来役に立つ場合は、さらにいくつかのポイントを説明したいと思います。たとえば、承認の実装方法、コレクションの受信方法(リクエストを1つにまとめる)など。また、AngularJSツールを使用してクライアント側APIと対話する方法、および検索エンジンに優しい単一ページアプリケーション(PhantomJsによるページレンダリングを使用)を作成する方法についても話したいと思います。