RBACおよびテストを使用したYiiフレームワーク上のRESTful API

YiiフレームワークにRESTFul APIを実装するための既製のソリューションは多数ありますが、これらのソリューションを実際のプロジェクトで使用する場合、すべてが犬とその所有者の例でのみ美しく見えることを理解しています。



おそらく、記事の準備および執筆中に、RESTful APIを作成するための組み込みフレームワークを備えたYii2のリリースとの関連性がわずかに失われました。 しかし、この記事は、Yii2にまだ慣れていない人や、既存のアプリケーション用の本格的なAPIを迅速かつ簡単に実装する必要がある人にとっては依然として有用です。



まず、既存の拡張機能を使用する際にサーバーAPIを完全に操作する必要がなかったいくつかの機能のリストを示します。



  1. 最初に遭遇した問題の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. 2番目の重要な機会は、条件を設定し、ルールを結合する機能を備えたデータ検索およびフィルタリングメカニズムです。 たとえば、私はそのようなリクエストの類似物を実行できるようにしたかった:

     SELECT * FROM users WHERE (age>25 AND first_name LIKE '%alex%') OR (last_name='shepard');
          
          





  3. コレクションを作成、更新、削除するだけでは不十分な場合があります。 つまり 再度検索とフィルタリングを使用して、1つのクエリでn番目のレコード数を変更します。 たとえば、あらゆる条件に該当するすべてのレコードを頻繁に削除または更新する必要があり、個々のクエリを使用するには費用がかかりすぎます。

  4. もう1つの重要なポイントは、関連データを受信できることです。 たとえば、これらの役割とそのすべてのタスクと操作を取得します。

  5. もちろん、受信したレコードの数を制限する( limit



    )、選択の開始をシフトする( offset



    )、レコードのソート順を指定する( order by



    )ことができないと、少なくとも何らかの形でAPIを操作することはできません。 また、グループ化( group by



    )できると便利です。

  6. 各操作のユーザー権限を確認できることが重要です( checkAccess



    メソッドcheckAccess



    まだ同じRBACにあります)。

  7. そして最後に、全体を何らかの形でテストする必要があります。



ほぼそのような「ウィッシュリスト」のリストを分析した結果、このすばらしいフレームワークでの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要求パラメーターから取得した値は、配列内の対応する値をオーバーライドします。

元の値:

 array( 'limit' => 100, 'offset' => 0, 'order' => 'id ASC' )
      
      





contentRange





配列

選択したレコードの数に関するデータ。 例:

 array( 'total'=>10, 'start'=>6, 'end'=>15 )
      
      





sendToEndUser





ブール値

操作の完了(表示、作成、変更、削除)後にデータをユーザーに送信するか、アクションの結果を配列の形式で返すか。

criteria





CDbCriteria

データを取得するためのCDbCriteria



クラスのインスタンス。 要求からのデータ(制限、オフセット、順序、フィルター、検索など)に基づいて構成されます

baseCriteria





CDbCriteria

データを取得するためのCDbCriteria



クラスの基本インスタンス。 オブジェクト条件はcriteria



条件よりも優先されcriteria





notFoundErrorResponse





配列

エントリが見つからない場合のサーバーの応答。



方法







テスト中



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





配列

応答で受信したヘッダーの配列(ヘッダー名=>ヘッダー値)例:

 array( 'Date' => "Fri, 23 May 2014 12:10:37 GMT" 'Server' =>"Apache/2.4.7 (Win32) OpenSSL/1.0.1e PHP/5.5.9" ... )
      
      





decoded





配列

デコードされた(json_decode)サーバー応答の配列



このデータは、サーバー応答の完全なテストと分析に十分です。



要求への応答を受信した後、さまざまなアサートがチェックされますが、これは非常に明白であり、コメントを必要としません。もちろん、これはエンティティの完全なテストコードとはほど遠いですが、この例はクラスでの作業の原則を理解するのに十分ApiTestCase



です。



短いクラスの説明ApiTestCase





プロパティ:

物件

種類

説明

authCookies





配列

認証後に受信したCookie(メソッド呼び出しApiTestCase::getAuthCookies()





loginUrl





ひも

認証Cookieを受信するための認証リクエストを完了するためのアドレス。

loginData





配列()

. :

 array('login'=>'admin', 'password'=>'admin');
      
      







:





githubへのリンク



おわりに



もちろん、データを処理するために使用されるため、負荷が高いと問題が発生する可能性がありますActiveRecord



。これはキャッシングによって部分的に解決できると思います(Yiiにはこれに必要なものがすべて揃っているため)。

拡張機能全体ではなくても役に立つと思う開発者がいることを願っています。その場合、一部の拡張機能やアイデアだけが使用されます。

将来の計画はより多くのさまざまな改善と変更であるため、コメントや提案に感謝します。



PS



記事は大きく(意図されたものの半分を説明することはできませんでしたが)、やや「引き裂かれた」ことが判明しました。情報が将来役に立つ場合は、さらにいくつかのポイントを説明したいと思います。たとえば、承認の実装方法、コレクションの受信方法(リクエストを1つにまとめる)など。また、AngularJSツールを使用してクライアント側APIと対話する方法、および検索エンジンに優しい単一ページアプリケーション(PhantomJsによるページレンダリングを使用)を作成する方法についても話したいと思います。



All Articles