tunecino / yii2-nested-rest
为Yii RESTful API框架添加嵌套资源路由支持以及相关的动作和关系处理器
Requires
- yiisoft/yii2: ~2.0.13
README
为Yii RESTful API框架添加嵌套资源路由支持以及相关的动作和关系处理器。
工作原理
此扩展不替换任何内置的REST组件。它是一个辅助动作集合和自定义的UrlRule
类,旨在与默认的类一起使用。
'rules' => [ [ // Yii defaults REST UrlRule class 'class' => 'yii\rest\UrlRule', 'controller' => ['team','player','skill'], ], [ // The custom UrlRule class 'class' => 'tunecino\nestedrest\UrlRule', 'modelClass' => 'app\models\Team', 'relations' => ['players'], ], [ 'class' => 'tunecino\nestedrest\UrlRule', 'modelClass' => 'app\models\Player', 'relations' => ['team','skills'], ], ]
为了解释它是如何工作的,让我们通过一个示例来更好地了解。
如果在之前的配置中,我们期望team
和player
之间存在一对多的关系,而player
和skill
在连接表中存在多对多的关系,并且该连接表有一个额外的名为level
的列,那么这个扩展可能有助于实现以下HTTP请求。
# get the players 2, 3 and 4 from team 1 GET /teams/1/players/2,3,4 # list all skills of player 5 GET /players/5/skills # put the players 5 and 6 in team 1 PUT /teams/1/players/5,6 # create a new player and put him in team 1 POST /teams/1/players {name: 'Didier Drogba', position: 'FC'} # create a new skill called 'dribble' and assign it to player 9 # with a related level of 10 ('level' should be stored in the junction table) POST /players/9/skills {name: 'dribble', level: 10} # update the 'level' attribute in the junction table related to player 9 and skill 2 PUT /players/9/skills/2 {level: 11} # unlink skill 3 and player 2 DELETE /players/2/skills/3 # get all players out of team 2 DELETE /teams/2/players
安装
安装此扩展的首选方法是通过composer。
运行以下命令:
$ composer require tunecino/yii2-nested-rest
或者将以下内容添加到您的composer.json
文件的require
部分:
"tunecino/yii2-nested-rest": "*"
配置
默认情况下,此扩展中使用的自定义UrlRule类的所有属性都将用于生成内置的yii\rest\UrlRule的多个实例,所以基本上这两个类共享相似的配置。
以下是在应用程序配置文件中设置给UrlManager的所有可能的配置。
'rules' => [ [ /** * the custom UrlRule class */ 'class' => 'tunecino\nestedrest\UrlRule', /* required */ /** * the model class name */ 'modelClass' => 'app\models\Player', /* required */ /** * relations names to be nested with this model * they should be already defined in the model's Active Record class. * check the below section for more about advanced configurations. */ 'relations' => ['team','skills'], /* required */ /** * used to generate the 'prefix'. * default: the model name pluralized */ 'resourceName' => 'players', /* optional */ /** * also used with 'prefix'. is the expected foreign key. * default: $model_name . '_id' */ 'linkAttribute' => 'player_id', /* optional */ /** * building related rules using 'controller => ['teams' => 'v1/team']' * instead of 'controller => ['team']' */ 'modulePrefix' => 'v1', /* optional */ /** * the default list of tokens that should be replaced for each pattern. */ 'tokens' => [ /* optional */ '{id}' => '<id:\\d[\\d,]*>', '{IDs}' => '<IDs:\\d[\\d,]*>', ], /** * The Regular Expressions Syntax used to parse the id of the main resource from url. * For example, in the following final rule, $linkAttributePattern is default to that `\d+` to parse $brand_id value: * * GET,HEAD v1/brands/<brand_id:\d+>/items/<IDs:\d[\d,]*> * * While that works fine with digital IDs, in a system using a different format, like uuid for example, * you may use $linkAttributePattern to define different patterns. Something like this maybe: * * [ * // Nested Rules Brand * 'class' => 'tunecino\nestedrest\UrlRule', * 'modelClass' => 'app\modules\v1\models\Brand', * 'modulePrefix' => 'v1', * 'resourceName' => 'v1/brands', * 'relations' => ['items'], * 'tokens' => [ * '{id}' => '<id:[a-f0-9]{8}\\-[a-f0-9]{4}\\-4[a-f0-9]{3}\\-(8|9|a|b)[a-f0-9]{3}\\-[a-f0-9]{12}>', * '{IDs}' => '<IDs:([a-f0-9]{8}\\-[a-f0-9]{4}\\-4[a-f0-9]{3}\\-(8|9|a|b)[a-f0-9]{3}\\-[a-f0-9]{12}(?:,|$))*>', * ], * 'linkAttributePattern' => '[a-f0-9]{8}\\-[a-f0-9]{4}\\-4[a-f0-9]{3}\\-(8|9|a|b)[a-f0-9]{3}\\-[a-f0-9]{12}', * ], */ 'linkAttributePattern' => '\d+', /* optional */ /** * the default list of patterns. they may all be overridden here * or just edited within $only, $except and $extraPatterns properties */ 'patterns' => [ /* optional */ 'GET,HEAD {IDs}' => 'nested-view', 'GET,HEAD' => 'nested-index', 'POST' => 'nested-create', 'PUT {IDs}' => 'nested-link', 'DELETE {IDs}' => 'nested-unlink', 'DELETE' => 'nested-unlink-all', '{id}' => 'options', '' => 'options', ], /** * list of acceptable actions. */ 'only' => [], /* optional */ /** * actions that should be excluded. */ 'except' => [], /* optional */ /** * supporting extra actions in addition to those listed in $patterns. */ 'extraPatterns' => [] /* optional */ ], ]
如您所注意到的,默认情况下,$patterns
指向6个新的动作,这些动作与附加到ActiveController类的基本CRUD动作不同。这些是包含在此扩展中的辅助动作,您需要在控制器内部或从所有其他控制器继承的BaseController
内部手动声明它们。此外,请注意,默认情况下,我们期望一个附加到相关控制器的OptionsAction。对于扩展ActiveController或其子控制器的任何控制器都应该是这种情况。否则,您也应该实现\yii\rest\OptionsAction
。
以下是一个控制器::actions()函数完整实现的示例:
public function actions() { $actions = parent::actions(); $actions['nested-index'] = [ 'class' => 'tunecino\nestedrest\IndexAction', /* required */ 'modelClass' => $this->modelClass, /* required */ 'checkAccess' => [$this, 'checkAccess'], /* optional */ ]; $actions['nested-view'] = [ 'class' => 'tunecino\nestedrest\ViewAction', /* required */ 'modelClass' => $this->modelClass, /* required */ 'checkAccess' => [$this, 'checkAccess'], /* optional */ ]; $actions['nested-create'] = [ 'class' => 'tunecino\nestedrest\CreateAction', /* required */ 'modelClass' => $this->modelClass, /* required */ 'checkAccess' => [$this, 'checkAccess'], /* optional */ /** * the scenario to be assigned to the new model before it is validated and saved. */ 'scenario' => 'default', /* optional */ /** * the scenario to be assigned to the model class responsible * of handling the data stored in the juction table. */ 'viaScenario' => 'default', /* optional */ /** * expect junction table related data to be wrapped in a sub object key in the body request. * In the example we gave above we would need to do : * POST {name: 'dribble', related: {level: 10}} * instead of {name: 'dribble', level: 10} */ 'viaWrapper' => 'related' /* optional */ ]; $actions['nested-link'] = [ 'class' => 'tunecino\nestedrest\LinkAction', /* required */ 'modelClass' => $this->modelClass, /* required */ 'checkAccess' => [$this, 'checkAccess'], /* optional */ /** * the scenario to be assigned to the model class responsible * of handling the data stored in the juction table. */ 'viaScenario' => 'default', /* optional */ ]; $actions['nested-unlink'] = [ 'class' => 'tunecino\nestedrest\UnlinkAction', /* required */ 'modelClass' => $this->modelClass, /* required */ 'checkAccess' => [$this, 'checkAccess'], /* optional */ ]; $actions['nested-unlink-all'] = [ 'class' => 'tunecino\nestedrest\UnlinkAllAction', /* required */ 'modelClass' => $this->modelClass, /* required */ 'checkAccess' => [$this, 'checkAccess'], /* optional */ ]; return $actions; }
您需要知道的内容
1. 这不支持复合键。事实上,我在构建此扩展时的主要担忧之一是找到一种干净的方法来避免为映射连接表的相关模型(如复合键)构建资源。有关更多详细信息,请参阅第8.节中提供的示例。
2. 在配置文件中定义关系名称时,它们应与您模型内部实现的方法名称匹配(请参阅 Yii 指南中的 声明关系 部分,获取更多详细信息)。此扩展将执行检查,并在名称不匹配时抛出 InvalidConfigException 异常。但由于性能原因(请参阅 此处),并且当您已经正确设置了关系列表时,继续在每个请求中进行相同的验证没有意义,因此此扩展在应用程序处于 生产 模式时不会再次进行数据库模式解析。换句话说,仅在 YII_DEBUG
为 true 时进行验证。
3. 默认情况下,当您在 $relation
属性中指定关系 'abc' 时,预期在 URL 端点中使用的相关名称应为 'abcs'(复数形式),其控制器预期为 AbcController
。您可以通过配置 $relation
属性来显式指定如何将端点 URL 中使用的名称映射到其相关控制器 ID。例如,如果我们有一个在 Team
模型类中的 getJuniorCoaches()
方法内定义的关系,我们可以这样做:
// GET /players/1/junior-coaches => should route to 'JuniorCoachController' 'relations' => ['players','juniorCoaches'] // how it works by default // GET /players/1/junior-coaches => should route to 'JuniorCoachesController' 'relations' => [ 'players', 'juniorCoaches' => 'junior-coaches' // different controller name ] // GET /players/1/juniors => should route to 'JuniorCoachesController' 'relations' => [ 'players', 'juniorCoaches' => ['juniors' => 'junior-coaches'] // different endpoint name and different controller name ]
4. 当涉及到通过连接表中的额外列将多对多关系链接起来时,强烈建议使用 via() 而不是 viaTable(),这样中间类就可以被此扩展用来验证相关属性,而不是使用 link() 并在不执行适当验证的情况下保存数据。有关详细信息,请参阅 Yii 指南中的 通过连接表的关系 部分。
5. 当您执行
POST /players/9/skills
{name: 'dribble', level: 10}
并且期望 'name' 属性与创建的新模型一起加载和保存,而 'level' 应该添加到相关连接表中。那么您应该知道这个
-
如果两个模型之间的关系在 via() 中定义,则使用 load() 方法将
Yii::$app->request->bodyParams
填充到两个模型中。$model->load($bodyParams); $viaModel->load($bodyParams); /* Scenarios can also be assigned to both models. when attaching actions. see configuration section */
-
如果关系是在 viaTable() 中定义的,则脚本将尝试进行一些猜测。
因此,当出现意外结果或模型类和连接相关类中属性名称相似时,建议设置 viaWrapper
属性。有关更多详细信息,请参阅配置部分的 'nested-create' 动作。
6. 在断开数据连接时,如果两个模型之间的关系类型为 many_to_many,则将在连接表中删除相关行。否则,相关的外键属性将被设置为数据库中相关列的 NULL。
7. 当成功进行链接或解除链接请求时,应期望收到一个 204
响应,而一个 304
响应应表明没有进行任何更改,例如在请求链接两个已链接的模型时。当你尝试链接两个具有 many_to_many
关系的模型,并且这两个模型已经链接时,不会向相关连接表中添加额外的行:如果 bodyRequest
为空,你将收到一个 304
响应;否则,将使用 bodyRequest
的内容来更新连接表中发现的多余属性,并返回一个 204
响应头。
8. 当执行任何HTTP请求时;例如,假设为示例 GET /players/9/skills/2
;自定义的 UrlRule
默认将其重定向到路由 skill/nested-view
(或其他取决于你的模式),并将这四个额外的属性添加到 Yii::$app->request->queryParams
中。
relativeClass = 'app/models/player'; // the class name of the relative model relationName = 'skills'; // the one you did set in rules configuration. linkAttribute = 'player_id'; // the foreign key attribute name. player_id = 9; // the foreign key attribute and its value
这些可能在构建自己的动作或执行额外操作时很有用,例如,如果我们将以下内容添加到 app/models/skill
中:
// junction table related method. usually auto generated by gii. public function getSkillHasPlayers() { return $this->hasMany(SkillHasPlayer::className(), ['skill_id' => 'id']); } protected function getSharedData() { $params = Yii::$app->request->queryParams; $player_id = empty($params['player_id']) ? null : $params['player_id']; return ($player_id) ? $this->getSkillHasPlayers() ->where(['player_id' => $player_id ]) ->select('level') ->one() : null; } public function fields() { $fields = parent::fields(); if (!empty(Yii::$app->request->queryParams['player_id'])) { $fields['_shared'] = 'sharedData'; } return $fields; }
类似于 GET /players/9/skills
或 GET /players/9/skills/2
的请求也将输出两个模型之间存储在相关连接表中的相关数据。
GET /players/9/skills/2 # outputs: { "id": 2, "name": "dribble", "_shared": { "level": 11 } }