yii2tech / ar-role
为 Yii2 提供对 ActiveRecord 关联角色(表继承)组合的支持
Requires
- yiisoft/yii2: ~2.0.14
README
ActiveRecord 角色继承扩展 for Yii2
此扩展提供对 ActiveRecord 关联角色(表继承)组合的支持。
有关许可信息,请检查 LICENSE 文件。
安装
安装此扩展的首选方式是通过 composer。
运行以下命令:
php composer.phar require --prefer-dist yii2tech/ar-role
或
"yii2tech/ar-role": "*"
将以下内容添加到您的 composer.json 文件的 require 部分。
用法
此扩展提供对 ActiveRecord 关联角色组合的支持,这也被称为表继承。
例如:假设我们有一个大学的数据库。大学里有学生和教师。学生有学习小组和奖学金信息,而教师有职称和薪水。然而,学生和教师都有姓名、地址、电话号码等。因此,我们可以将它们的数据分成三个不同的表
- 'Human' - 存储常见数据
- 'Student' - 存储学生特殊数据和对 'Human' 记录的引用
- 'Instructor' - 存储教师特殊数据和对 'Human' 记录的引用
此类解决方案的 DDL 可能如下所示
CREATE TABLE `Human` ( `id` integer NOT NULL AUTO_INCREMENT, `role` varchar(20) NOT NULL, `name` varchar(64) NOT NULL, `address` varchar(64) NOT NULL, `phone` varchar(20) NOT NULL, PRIMARY KEY (`id`) ) ENGINE InnoDB; CREATE TABLE `Student` ( `humanId` integer NOT NULL, `studyGroupId` integer NOT NULL, `hasScholarship` integer(1) NOT NULL, PRIMARY KEY (`humanId`) FOREIGN KEY (`humanId`) REFERENCES `Human` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ) ENGINE InnoDB; CREATE TABLE `Instructor` ( `humanId` integer NOT NULL, `rankId` integer NOT NULL, `salary` integer NOT NULL, PRIMARY KEY (`humanId`) FOREIGN KEY (`humanId`) REFERENCES `Human` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ) ENGINE InnoDB;
此扩展引入了 [[\yii2tech\ar\role\RoleBehavior]] ActiveRecord 行为,它允许基于角色关系的 ActiveRecord 继承。为了使其工作,首先,您应该为基本表创建一个 ActiveRecord 类,在我们的例子中将是 'Human'
class Human extends \yii\db\ActiveRecord { public static function tableName() { return 'Human'; } }
然后您将能够组合 ActiveRecord 类,这些类使用 [[\yii2tech\ar\role\RoleBehavior]] 实现基于角色的继承。此类类组合有两种不同的方法
- 主角色继承
- 从属角色继承
主角色继承
这种方法假设角色 ActiveRecord 类是基本角色类的子类
class Student extends Human // extending `Human` - not `ActiveRecord`! { public function behaviors() { return [ 'roleBehavior' => [ 'class' => RoleBehavior::className(), // Attach role behavior 'roleRelation' => 'studentRole', // specify name of the relation to the slave table 'roleAttributes' => [ 'roleId' => Human::ROLE_STUDENT // mark 'Human' record as 'student' ], ], ]; } public function getStudentRole() { // Here `StudentRole` is and ActiveRecord, which uses 'Student' table : return $this->hasOne(StudentRole::className(), ['humanId' => 'id']); } }
此方法的主要优点是角色类直接从基本类继承所有方法、验证和其他逻辑。然而,您需要声明一个额外的 ActiveRecord 类,该类对应于角色表。另一个问题是您需要将 'Student' 记录和 'Instructor' 记录分开以进行搜索过程。如果没有以下代码,它将返回所有 'Human' 记录,包括 'Student' 和 'Instructor'
$students = Student::find()->all();
此问题的解决方案可能是引入 'Human' 表中的特殊列 'role' 并使用默认范围
class Student extends Human { // ... public static function find() { return parent::find()->where(['role' => 'student']); } }
如果大多数功能依赖于 'Human' 属性,则应选择此方法。
从属角色继承
这种方法假设角色 ActiveRecord 不扩展基本类,而是与其相关联
class Instructor extends \yii\db\ActiveRecord // do not extending `Human`! { public function behaviors() { return [ 'roleBehavior' => [ 'class' => RoleBehavior::className(), // Attach role behavior 'roleRelation' => 'human', // specify name of the relation to the master table 'isOwnerSlave' => true, // indicate that owner is a role slave - not master 'roleAttributes' => [ 'roleId' => Human::ROLE_STUDENT // will be applied to the 'Human' record ], ], ]; } public function getHuman() { return $this->hasOne(Human::className(), ['id' => 'humanId']); } }
这种方法不需要额外的ActiveRecord类即可正常工作,也不需要指定默认的作用域。它并不直接继承基ActiveRecord中声明的逻辑,然而任何在相关类中声明的自定义方法都可以通过魔法方法__call()
机制来访问。因此,如果类Human
有方法sayHello()
,您可以通过Instructor
实例来调用它。
如果大多数功能依赖于'Instructor'属性,应选择此方法。
访问角色属性
在被附加[[\yii2tech\ar\role\RoleBehavior]]之后,它提供了通过[[\yii2tech\ar\role\RoleBehavior::roleRelation]]指定的关系的模型绑定属性的访问,就像它们是主要的一样
$model = Student::findOne(1); echo $model->studyGroupId; // equals to $model->studentRole->studyGroupId $model = Instructor::findOne(2); echo $model->name; // equals to $model->human->name
如果相关模型不存在,例如,在新建记录的情况下,它将被自动实例化
$model = new Student(); $model->studyGroupId = 12; $model = new Instructor(); $model->name = 'John Doe';
访问角色方法
任何通过[[\yii2tech\ar\role\RoleBehavior::roleRelation]]关联的模型中声明的非静态方法都可以从主模型中访问
class Human extends \yii\db\ActiveRecord { // ... public function sayHello($name) { return 'Hello, ' . $name; } } class Instructor extends \yii\db\ActiveRecord { public function behaviors() { return [ 'roleBehavior' => [ 'class' => RoleBehavior::className(), // Attach role behavior // ... ], ]; } } $model = new Instructor(); echo $model->sayHello('John'); // outputs: 'Hello, John'
此功能允许在采用'奴隶'行为设置方法时从基角色模型继承逻辑。然而,这对于'主'和'奴隶'角色方法都适用。
验证
每次主要模型进行验证时,相关的角色模型也将进行验证,并且其错误将被附加到主要模型上
$model = new Student(); $model->studyGroupId = 'invalid value'; var_dump($model->validate()); // outputs "false" var_dump($model->hasErrors('studyGroupId')); // outputs "true"
您也可以为属于主要模型的属性指定验证规则
class Student extends Human { // ... public function rules() { return [ // ... ['studyGroupId', 'integer'], ['hasScholarship', 'boolean'], ]; } }
保存角色数据
当主要模型被保存时,相关的角色模型也将被保存
$model = new Student(); $model->name = 'John Doe'; $model->address = 'Wall Street, 12'; $model->studyGroupId = 14; $model->save(); // insert one record to the 'Human' table and one record - to the 'Student' table
当主要模型被删除时,相关的角色模型也将被删除
$student = Student::findOne(17); $student->delete(); // Deletes one record from 'Human' table and one record from 'Student' table
查询角色记录
[[\yii2tech\ar\role\RoleBehavior]]通过关系工作。因此,为了使角色属性功能正常工作,它将执行一个额外的查询来检索角色从属或主模型,这可能会在工作多个模型时产生性能影响。为了减少查询次数,您可以在角色关系上使用with()
$students = Student::find()->with('studentRole')->all(); // only 2 queries will be performed foreach ($students as $student) { echo $student->studyGroupId . '<br>'; } $instructors = Instructor::find()->with('human')->all(); // only 2 queries will be performed foreach ($instructors as $instructor) { echo $instructor->name . '<br>'; }
您可以将'with'应用于角色关系作为ActiveRecord查询的默认作用域
class Instructor extends ActiveRecord { // ... public static function find() { return parent::find()->with('human'); } }
提示:您可以命名从属表的主键与主键相同:使用'id'而不是'humanId'。在这种情况下,基于主键的条件将始终相同。然而,这个技巧在某些时候使用联合查询来处理角色关系时可能会导致额外的麻烦。
如果您需要基于两个实体中的字段指定搜索条件,并且您正在使用关系数据库,则可以使用joinWith()
方法
$students = Student::find() ->innerJoinWith('studentRole') ->andWhere(['name' => 'John']) // condition for 'Human' table ->andWhere(['hasScholarship' => true]) // condition for 'Student' table ->all();
提示:使用
joinWith()
仍然需要额外的SQL查询来检索关联数据。您可以使用yii2tech/ar-eagerjoin扩展来删除此额外查询。
创建角色设置Web界面
从字面上讲,[[\yii2tech\ar\role\RoleBehavior]]将两个ActiveRecord合并为一个。这意味着在创建它们的编辑Web界面时,您不需要任何特殊的东西。您可以使用标准的CRUD控制器
use yii\web\Controller; class StudentController extends Controller { public function actionCreate() { $model = new Student(); if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view']); } return $this->render('create', [ 'model' => $model, ]); } // ... }
在创建网页表单时,您可以引用相关角色模型的属性,因为它们属于主要模型
<?php use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $model Student */ ?> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name'); ?> <?= $form->field($model, 'address'); ?> <?= $form->field($model, 'studyGroupId')->dropDownList(ArrayHelper::map(StudyGroup::find()->all(), 'id', 'name')); ?> <?= $form->field($model, 'hasScholarship')->checkbox(); ?> <div class="form-group"> <?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?>
为了最佳集成,您还可以合并相关模型的标签和提示
class Student extends Human { // ... public function attributeLabels() { return array_merge( parent::attributeLabels(), $this->getRoleRelationModel()->attributeLabels() ); } public function attributeHints() { return array_merge( parent::attributeHints(), $this->getRoleRelationModel()->attributeHints() ); } }
注意!为了以这种方式工作,您应该在主模型中声明角色模型属性的有效性规则为'安全'。
class Student extends Human { // ... public function rules() { return [ // ... [$this->getRoleRelationModel()->attributes(), 'safe'], ]; } }
否则,您将不得不分别加载角色模型的数据
use yii\web\Controller; class StudentController extends Controller { public function actionCreate() { $model = new Student(); $post = Yii::$app->request->post(); // data loading separated, however only single save required : if ($model->load($post) && $model->getRoleRelationModel()->load($post) && $model->save()) { return $this->redirect(['view']); } return $this->render('create', [ 'model' => $model, ]); } // ... }
在创建表单时,您应该使用角色模型进行输入
<?php use yii\helpers\ArrayHelper; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $model Student */ ?> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name'); ?> <?= $form->field($model, 'address'); ?> <?= $form->field($model->getRoleRelationModel(), 'studyGroupId')->dropDownList(ArrayHelper::map(StudyGroup::find()->all(), 'id', 'name')); ?> <?= $form->field($model->getRoleRelationModel(), 'hasScholarship')->checkbox(); ?> <div class="form-group"> <?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?>