├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src └── RoleBehavior.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii 2 ActiveRecord Role Inheritance extension Change Log 2 | ======================================================== 3 | 4 | 1.0.3, April 9, 2018 5 | -------------------- 6 | 7 | - Bug #12: Fixed role relation is lost in case using Yii 2.0.14 (klimov-paul) 8 | 9 | 10 | 1.0.2, February 14, 2017 11 | ------------------------ 12 | 13 | - Bug #7: Fixed support for composite key at role relation (bethrezen, klimov-paul) 14 | - Enh #8: `RoleBehavior::$roleAttributes` values now applied before model validation as well (klimov-paul) 15 | 16 | 17 | 1.0.1, July 6, 2016 18 | ------------------- 19 | 20 | - Enh #6: Saving on 'slave' role inheritance now skips saving of the role model, if it has not been touched (klimov-paul) 21 | 22 | 23 | 1.0.0, December 29, 2015 24 | ------------------------ 25 | 26 | - Initial release. 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Yii framework is free software. It is released under the terms of 2 | the following BSD License. 3 | 4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Yii2tech nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

ActiveRecord Role Inheritance Extension for Yii2

6 |
7 |

8 | 9 | This extension provides support for ActiveRecord relation role (table inheritance) composition. 10 | 11 | For license information check the [LICENSE](LICENSE.md)-file. 12 | 13 | [![Latest Stable Version](https://poser.pugx.org/yii2tech/ar-role/v/stable.png)](https://packagist.org/packages/yii2tech/ar-role) 14 | [![Total Downloads](https://poser.pugx.org/yii2tech/ar-role/downloads.png)](https://packagist.org/packages/yii2tech/ar-role) 15 | [![Build Status](https://travis-ci.org/yii2tech/ar-role.svg?branch=master)](https://travis-ci.org/yii2tech/ar-role) 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 22 | 23 | Either run 24 | 25 | ``` 26 | php composer.phar require --prefer-dist yii2tech/ar-role 27 | ``` 28 | 29 | or add 30 | 31 | ```json 32 | "yii2tech/ar-role": "*" 33 | ``` 34 | 35 | to the require section of your composer.json. 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | This extension provides support for ActiveRecord relation role composition, which is also known as table inheritance. 42 | 43 | For example: assume we have a database for the University. There are students studying in the University and there are 44 | instructors teaching the students. Student has a study group and scholarship information, while instructor has a rank 45 | and salary. However, both student and instructor have name, address, phone number and so on. Thus we can split 46 | their data in the three different tables: 47 | 48 | - 'Human' - stores common data 49 | - 'Student' - stores student special data and reference to the 'Human' record 50 | - 'Instructor' - stores instructor special data and reference to the 'Human' record 51 | 52 | DDL for such solution may look like following: 53 | 54 | ```sql 55 | CREATE TABLE `Human` 56 | ( 57 | `id` integer NOT NULL AUTO_INCREMENT, 58 | `role` varchar(20) NOT NULL, 59 | `name` varchar(64) NOT NULL, 60 | `address` varchar(64) NOT NULL, 61 | `phone` varchar(20) NOT NULL, 62 | PRIMARY KEY (`id`) 63 | ) ENGINE InnoDB; 64 | 65 | CREATE TABLE `Student` 66 | ( 67 | `humanId` integer NOT NULL, 68 | `studyGroupId` integer NOT NULL, 69 | `hasScholarship` integer(1) NOT NULL, 70 | PRIMARY KEY (`humanId`) 71 | FOREIGN KEY (`humanId`) REFERENCES `Human` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 72 | ) ENGINE InnoDB; 73 | 74 | CREATE TABLE `Instructor` 75 | ( 76 | `humanId` integer NOT NULL, 77 | `rankId` integer NOT NULL, 78 | `salary` integer NOT NULL, 79 | PRIMARY KEY (`humanId`) 80 | FOREIGN KEY (`humanId`) REFERENCES `Human` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 81 | ) ENGINE InnoDB; 82 | ``` 83 | 84 | This extension introduces [[\yii2tech\ar\role\RoleBehavior]] ActiveRecord behavior, which allows role relation based 85 | ActiveRecord inheritance. 86 | In oder to make it work, first of all, you should create an ActiveRecord class for the base table, in our example it 87 | will be 'Human': 88 | 89 | ```php 90 | class Human extends \yii\db\ActiveRecord 91 | { 92 | public static function tableName() 93 | { 94 | return 'Human'; 95 | } 96 | } 97 | ``` 98 | 99 | Then you will be able to compose ActiveRecord classes, which implements role-based inheritance using [[\yii2tech\ar\role\RoleBehavior]]. 100 | There are 2 different ways for such classes composition: 101 | - Master role inheritance 102 | - Slave role inheritance 103 | 104 | 105 | ## Master role inheritance 106 | 107 | This approach assumes role ActiveRecord class be descendant of the base role class: 108 | 109 | ```php 110 | class Student extends Human // extending `Human` - not `ActiveRecord`! 111 | { 112 | public function behaviors() 113 | { 114 | return [ 115 | 'roleBehavior' => [ 116 | 'class' => RoleBehavior::className(), // Attach role behavior 117 | 'roleRelation' => 'studentRole', // specify name of the relation to the slave table 118 | 'roleAttributes' => [ 119 | 'roleId' => Human::ROLE_STUDENT // mark 'Human' record as 'student' 120 | ], 121 | ], 122 | ]; 123 | } 124 | 125 | public function getStudentRole() 126 | { 127 | // Here `StudentRole` is and ActiveRecord, which uses 'Student' table : 128 | return $this->hasOne(StudentRole::className(), ['humanId' => 'id']); 129 | } 130 | } 131 | ``` 132 | 133 | The main benefit of this approach is that role class directly inherits all methods, validation and other logic from 134 | the base one. However, you'll need to declare an extra ActiveRecord class, which corresponds the role table. 135 | Yet another problem is that you'll need to separate 'Student' records from 'Instructor' ones for the search process. 136 | Without following code, it will return all 'Human' records, both 'Student' and 'Instructor': 137 | 138 | ```php 139 | $students = Student::find()->all(); 140 | ``` 141 | 142 | The solution for this could be introduction of special column 'role' in the 'Human' table and usage of the default 143 | scope: 144 | 145 | ```php 146 | class Student extends Human 147 | { 148 | // ... 149 | 150 | public static function find() 151 | { 152 | return parent::find()->where(['role' => 'student']); 153 | } 154 | } 155 | ``` 156 | 157 | This approach should be chosen in case most functionality depends on the 'Human' attributes. 158 | 159 | 160 | ## Slave role inheritance 161 | 162 | This approach assumes role ActiveRecord does not extends the base one, but relates to it: 163 | 164 | ```php 165 | class Instructor extends \yii\db\ActiveRecord // do not extending `Human`! 166 | { 167 | public function behaviors() 168 | { 169 | return [ 170 | 'roleBehavior' => [ 171 | 'class' => RoleBehavior::className(), // Attach role behavior 172 | 'roleRelation' => 'human', // specify name of the relation to the master table 173 | 'isOwnerSlave' => true, // indicate that owner is a role slave - not master 174 | 'roleAttributes' => [ 175 | 'roleId' => Human::ROLE_STUDENT // will be applied to the 'Human' record 176 | ], 177 | ], 178 | ]; 179 | } 180 | 181 | public function getHuman() 182 | { 183 | return $this->hasOne(Human::className(), ['id' => 'humanId']); 184 | } 185 | } 186 | ``` 187 | 188 | This approach does not require extra ActiveRecord class for functioning and it does not need default scope specification. 189 | It does not directly inherit logic declared in the base ActiveRecord, however any custom method declared in the related 190 | class will be available via magic method `__call()` mechanism. Thus if class `Human` has method `sayHello()`, you are 191 | able to invoke it through `Instructor` instance. 192 | 193 | This approach should be chosen in case most functionality depends on the 'Instructor' attributes. 194 | 195 | 196 | ## Accessing role attributes 197 | 198 | After being attached [[\yii2tech\ar\role\RoleBehavior]] provides access to the properties of the model bound by relation, 199 | which is specified via [[\yii2tech\ar\role\RoleBehavior::roleRelation]], as they were the main one: 200 | 201 | ```php 202 | $model = Student::findOne(1); 203 | echo $model->studyGroupId; // equals to $model->studentRole->studyGroupId 204 | 205 | $model = Instructor::findOne(2); 206 | echo $model->name; // equals to $model->human->name 207 | ``` 208 | 209 | If the related model does not exist, for example, in case of new record, it will be automatically instantiated: 210 | 211 | ```php 212 | $model = new Student(); 213 | $model->studyGroupId = 12; 214 | 215 | $model = new Instructor(); 216 | $model->name = 'John Doe'; 217 | ``` 218 | 219 | 220 | ## Accessing role methods 221 | 222 | Any non-static method declared in the model related via [[\yii2tech\ar\role\RoleBehavior::roleRelation]] can be accessed 223 | from the owner model: 224 | 225 | ```php 226 | class Human extends \yii\db\ActiveRecord 227 | { 228 | // ... 229 | 230 | public function sayHello($name) 231 | { 232 | return 'Hello, ' . $name; 233 | } 234 | } 235 | 236 | class Instructor extends \yii\db\ActiveRecord 237 | { 238 | public function behaviors() 239 | { 240 | return [ 241 | 'roleBehavior' => [ 242 | 'class' => RoleBehavior::className(), // Attach role behavior 243 | // ... 244 | ], 245 | ]; 246 | } 247 | } 248 | 249 | $model = new Instructor(); 250 | echo $model->sayHello('John'); // outputs: 'Hello, John' 251 | ``` 252 | 253 | This feature allows to inherit logic from the base role model in case of using 'slave' behavior setup approach. 254 | However, this works both for the 'master' and 'slave' role approaches. 255 | 256 | 257 | ## Validation 258 | 259 | Each time the main model is validated the related role model will be validated as well and its errors will be attached 260 | to the main model: 261 | 262 | ```php 263 | $model = new Student(); 264 | $model->studyGroupId = 'invalid value'; 265 | var_dump($model->validate()); // outputs "false" 266 | var_dump($model->hasErrors('studyGroupId')); // outputs "true" 267 | ``` 268 | 269 | You may as well specify validation rules for the related model attributes as they belong to the main model: 270 | 271 | ```php 272 | class Student extends Human 273 | { 274 | // ... 275 | 276 | public function rules() 277 | { 278 | return [ 279 | // ... 280 | ['studyGroupId', 'integer'], 281 | ['hasScholarship', 'boolean'], 282 | ]; 283 | } 284 | } 285 | ``` 286 | 287 | 288 | ## Saving role data 289 | 290 | When main model is saved the related role model will be saved as well: 291 | 292 | ```php 293 | $model = new Student(); 294 | $model->name = 'John Doe'; 295 | $model->address = 'Wall Street, 12'; 296 | $model->studyGroupId = 14; 297 | $model->save(); // insert one record to the 'Human' table and one record - to the 'Student' table 298 | ``` 299 | 300 | When main model is deleted related role model will be delete as well: 301 | 302 | ```php 303 | $student = Student::findOne(17); 304 | $student->delete(); // Deletes one record from 'Human' table and one record from 'Student' table 305 | ``` 306 | 307 | 308 | ## Querying role records 309 | 310 | [[\yii2tech\ar\role\RoleBehavior]] works through relations. Thus, in order to make role attributes feature work, 311 | it will perform an extra query to retrieve the role slave or master model, which may produce performance impact 312 | in case you are working with several models. In order to reduce number of queries you may use `with()` on the 313 | role relation: 314 | 315 | ```php 316 | $students = Student::find()->with('studentRole')->all(); // only 2 queries will be performed 317 | foreach ($students as $student) { 318 | echo $student->studyGroupId . '
'; 319 | } 320 | 321 | $instructors = Instructor::find()->with('human')->all(); // only 2 queries will be performed 322 | foreach ($instructors as $instructor) { 323 | echo $instructor->name . '
'; 324 | } 325 | ``` 326 | 327 | You may apply 'with' for the role relation as default scope for the ActiveRecord query: 328 | 329 | ```php 330 | class Instructor extends ActiveRecord 331 | { 332 | // ... 333 | 334 | public static function find() 335 | { 336 | return parent::find()->with('human'); 337 | } 338 | } 339 | ``` 340 | 341 | > Tip: you may name slave table primary key same as master one: use 'id' instead of 'humanId' for it. 342 | In this case conditions based on primary key will be always the same. However, this trick may cause extra 343 | troubles in case you are using joins for role relations at some point. 344 | 345 | If you need to specify search condition based on fields from both entities and you are using relational database, 346 | you can use `joinWith()` method: 347 | 348 | ```php 349 | $students = Student::find() 350 | ->innerJoinWith('studentRole') 351 | ->andWhere(['name' => 'John']) // condition for 'Human' table 352 | ->andWhere(['hasScholarship' => true]) // condition for 'Student' table 353 | ->all(); 354 | ``` 355 | 356 | > Tip: using `joinWith()` will still require an extra SQL query to retrieve relational data. 357 | You can use [yii2tech/ar-eagerjoin](https://github.com/yii2tech/ar-eagerjoin) extension to remove this extra query. 358 | 359 | 360 | ## Creating role setup web interface 361 | 362 | Figuratively speaking, [[\yii2tech\ar\role\RoleBehavior]] merges 2 ActiveRecords into a single one. 363 | This means you don't need anything special, while creating web interface for their editing. 364 | You may use standard CRUD controller: 365 | 366 | ```php 367 | use yii\web\Controller; 368 | 369 | class StudentController extends Controller 370 | { 371 | public function actionCreate() 372 | { 373 | $model = new Student(); 374 | 375 | if ($model->load(Yii::$app->request->post()) && $model->save()) { 376 | return $this->redirect(['view']); 377 | } 378 | 379 | return $this->render('create', [ 380 | 'model' => $model, 381 | ]); 382 | } 383 | 384 | // ... 385 | } 386 | ``` 387 | 388 | While creating a web form you may use attributes from related role model as they belong to the main one: 389 | 390 | ```php 391 | 398 | 399 | 400 | field($model, 'name'); ?> 401 | field($model, 'address'); ?> 402 | 403 | field($model, 'studyGroupId')->dropDownList(ArrayHelper::map(StudyGroup::find()->all(), 'id', 'name')); ?> 404 | field($model, 'hasScholarship')->checkbox(); ?> 405 | 406 |
407 | 'btn btn-primary']) ?> 408 |
409 | 410 | 411 | ``` 412 | 413 | For the best integration you may as well merge labels and hints of the related model: 414 | 415 | ```php 416 | class Student extends Human 417 | { 418 | // ... 419 | 420 | public function attributeLabels() 421 | { 422 | return array_merge( 423 | parent::attributeLabels(), 424 | $this->getRoleRelationModel()->attributeLabels() 425 | ); 426 | } 427 | 428 | public function attributeHints() 429 | { 430 | return array_merge( 431 | parent::attributeHints(), 432 | $this->getRoleRelationModel()->attributeHints() 433 | ); 434 | } 435 | } 436 | ``` 437 | 438 | **Heads up!** In order to work in this simple way you should declare validation rules for the role model attributes 439 | being 'safe' in the main one: 440 | 441 | ```php 442 | class Student extends Human 443 | { 444 | // ... 445 | 446 | public function rules() 447 | { 448 | return [ 449 | // ... 450 | [$this->getRoleRelationModel()->attributes(), 'safe'], 451 | ]; 452 | } 453 | } 454 | ``` 455 | 456 | Otherwise you'll have to load data for the role model separately: 457 | 458 | ```php 459 | use yii\web\Controller; 460 | 461 | class StudentController extends Controller 462 | { 463 | public function actionCreate() 464 | { 465 | $model = new Student(); 466 | 467 | $post = Yii::$app->request->post(); 468 | 469 | // data loading separated, however only single save required : 470 | if ($model->load($post) && $model->getRoleRelationModel()->load($post) && $model->save()) { 471 | return $this->redirect(['view']); 472 | } 473 | 474 | return $this->render('create', [ 475 | 'model' => $model, 476 | ]); 477 | } 478 | 479 | // ... 480 | } 481 | ``` 482 | 483 | You should use the role model for its inputs while creating form as well: 484 | 485 | ```php 486 | 493 | 494 | 495 | field($model, 'name'); ?> 496 | field($model, 'address'); ?> 497 | 498 | field($model->getRoleRelationModel(), 'studyGroupId')->dropDownList(ArrayHelper::map(StudyGroup::find()->all(), 'id', 'name')); ?> 499 | field($model->getRoleRelationModel(), 'hasScholarship')->checkbox(); ?> 500 | 501 |
502 | 'btn btn-primary']) ?> 503 |
504 | 505 | 506 | ``` 507 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii2tech/ar-role", 3 | "description": "Provides support for ActiveRecord relation role (table inheritance) composition in Yii2", 4 | "keywords": ["yii2", "active", "record", "role", "relation", "table", "inheritance"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "support": { 8 | "issues": "https://github.com/yii2tech/ar-role/issues", 9 | "forum": "http://www.yiiframework.com/forum/", 10 | "wiki": "https://github.com/yii2tech/ar-role/wiki", 11 | "source": "https://github.com/yii2tech/ar-role" 12 | }, 13 | "authors": [ 14 | { 15 | "name": "Paul Klimov", 16 | "email": "klimov.paul@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "yiisoft/yii2": "~2.0.14" 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "composer", 25 | "url": "https://asset-packagist.org" 26 | } 27 | ], 28 | "autoload": { 29 | "psr-4": {"yii2tech\\ar\\role\\": "src"} 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "1.0.x-dev" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/RoleBehavior.php: -------------------------------------------------------------------------------- 1 | [ 28 | * 'class' => RoleBehavior::className(), // Attach role behavior 29 | * 'roleRelation' => 'studentRole', // specify name of the relation to the slave table 30 | * 'roleAttributes' => [ 31 | * 'roleId' => Human::ROLE_STUDENT 32 | * ], 33 | * ], 34 | * ]; 35 | * } 36 | * 37 | * public function getStudentRole() 38 | * { 39 | * // Here `StudentRole` is and ActiveRecord, which uses 'Student' table : 40 | * return $this->hasOne(StudentRole::className(), ['humanId' => 'id']); 41 | * } 42 | * } 43 | * ``` 44 | * 45 | * Slave role inheritance: 46 | * 47 | * ```php 48 | * class Instructor extends \yii\db\ActiveRecord // do not extending `Human`! 49 | * { 50 | * public function behaviors() 51 | * { 52 | * return [ 53 | * 'roleBehavior' => [ 54 | * 'class' => RoleBehavior::className(), // Attach role behavior 55 | * 'roleRelation' => 'human', // specify name of the relation to the master table 56 | * 'isOwnerSlave' => true, // indicate that owner is a role slave - not master 57 | * 'roleAttributes' => [ 58 | * 'roleId' => Human::ROLE_INSTRUCTOR 59 | * ], 60 | * ], 61 | * ]; 62 | * } 63 | * 64 | * public function getHuman() 65 | * { 66 | * return $this->hasOne(Human::className(), ['id' => 'humanId']); 67 | * } 68 | * } 69 | * ``` 70 | * 71 | * @property BaseActiveRecord $owner 72 | * @property BaseActiveRecord $roleRelationModel 73 | * 74 | * @author Paul Klimov 75 | * @since 1.0 76 | */ 77 | class RoleBehavior extends Behavior 78 | { 79 | /** 80 | * @var string name of relation, which corresponds to role entity. 81 | */ 82 | public $roleRelation; 83 | /** 84 | * @var array|null attribute values, which should be applied to the role main entity separating its records, 85 | * which belong to different roles. 86 | * For example: 87 | * 88 | * ```php 89 | * [ 90 | * 'roleId' => Human::ROLE_STUDENT 91 | * ] 92 | * ``` 93 | * 94 | * If [[isOwnerSlave]] is 'false', these attributes will be applied to the owner record, if 'true' - to the 95 | * related one. 96 | */ 97 | public $roleAttributes; 98 | /** 99 | * @var bool whether owner ActiveRecord should be considered as a slave in role relation. 100 | * If enabled primary key sequence should be generated by related model and then passed to the owner. 101 | */ 102 | public $isOwnerSlave = false; 103 | 104 | /** 105 | * @var BaseActiveRecord|null backup of value of the record related via [[roleRelation]] relation 106 | * at the beginning of [[beforeSave()]]. It is needed to bypass [[BaseActiveRecord::resetDependentRelations()]]. 107 | * @since 1.0.3 108 | */ 109 | private $roleRelationModelBackup; 110 | 111 | 112 | /** 113 | * Returns the record related via [[roleRelation]] relation. 114 | * If no related record exists - new one will be instantiated. 115 | * @return BaseActiveRecord role related model. 116 | */ 117 | public function getRoleRelationModel() 118 | { 119 | $model = $this->owner->{$this->roleRelation}; 120 | if (is_object($model)) { 121 | return $model; 122 | } 123 | 124 | $relation = $this->owner->getRelation($this->roleRelation); 125 | $class = $relation->modelClass; 126 | $model = new $class(); 127 | $this->owner->populateRelation($this->roleRelation, $model); 128 | 129 | return $model; 130 | } 131 | 132 | // Property Access Extension: 133 | 134 | /** 135 | * PHP getter magic method. 136 | * This method is overridden so that variation attributes can be accessed like properties. 137 | * 138 | * @param string $name property name 139 | * @throws UnknownPropertyException if the property is not defined 140 | * @return mixed property value 141 | */ 142 | public function __get($name) 143 | { 144 | try { 145 | return parent::__get($name); 146 | } catch (UnknownPropertyException $exception) { 147 | $model = $this->getRoleRelationModel(); 148 | if ($model->hasAttribute($name) || $model->canGetProperty($name)) { 149 | return $model->$name; 150 | } 151 | throw $exception; 152 | } 153 | } 154 | 155 | /** 156 | * PHP setter magic method. 157 | * This method is overridden so that role model attributes can be accessed like properties. 158 | * @param string $name property name 159 | * @param mixed $value property value 160 | * @throws UnknownPropertyException if the property is not defined 161 | */ 162 | public function __set($name, $value) 163 | { 164 | try { 165 | parent::__set($name, $value); 166 | } catch (UnknownPropertyException $exception) { 167 | $model = $this->getRoleRelationModel(); 168 | if ($model->hasAttribute($name) || $model->canSetProperty($name)) { 169 | $model->$name = $value; 170 | } else { 171 | throw $exception; 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function canGetProperty($name, $checkVars = true) 180 | { 181 | if (parent::canGetProperty($name, $checkVars)) { 182 | return true; 183 | } 184 | $model = $this->getRoleRelationModel(); 185 | return $model->hasAttribute($name) || $model->canGetProperty($name, $checkVars); 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | */ 191 | public function canSetProperty($name, $checkVars = true) 192 | { 193 | if (parent::canSetProperty($name, $checkVars)) { 194 | return true; 195 | } 196 | $model = $this->getRoleRelationModel(); 197 | return $model->hasAttribute($name) || $model->canSetProperty($name, $checkVars); 198 | } 199 | 200 | // Method Access Extension: 201 | 202 | /** 203 | * {@inheritdoc} 204 | */ 205 | public function __call($name, $params) 206 | { 207 | $model = $this->getRoleRelationModel(); 208 | if ($model->hasMethod($name)) { 209 | return call_user_func_array([$model, $name], $params); 210 | } 211 | 212 | return parent::__call($name, $params); 213 | } 214 | 215 | /** 216 | * {@inheritdoc} 217 | */ 218 | public function hasMethod($name) 219 | { 220 | if (parent::hasMethod($name)) { 221 | return true; 222 | } 223 | $model = $this->getRoleRelationModel(); 224 | return $model->hasMethod($name); 225 | } 226 | 227 | // Events : 228 | 229 | /** 230 | * {@inheritdoc} 231 | */ 232 | public function events() 233 | { 234 | return [ 235 | Model::EVENT_BEFORE_VALIDATE => 'beforeValidate', 236 | Model::EVENT_AFTER_VALIDATE => 'afterValidate', 237 | BaseActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave', 238 | BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave', 239 | BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave', 240 | BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave', 241 | BaseActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', 242 | BaseActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', 243 | ]; 244 | } 245 | 246 | /** 247 | * Handles owner 'beforeValidate' event, ensuring role attributes are populated, 248 | * ensuring the correct validation. 249 | * @param \yii\base\ModelEvent $event event instance. 250 | * @since 1.0.2 251 | */ 252 | public function beforeValidate($event) 253 | { 254 | if (empty($this->roleAttributes)) { 255 | return; 256 | } 257 | 258 | if ($this->isOwnerSlave) { 259 | $model = $this->getRoleRelationModel(); 260 | foreach ($this->roleAttributes as $name => $value) { 261 | $model->$name = $value; 262 | } 263 | return; 264 | } 265 | 266 | foreach ($this->roleAttributes as $name => $value) { 267 | $this->owner->$name = $value; 268 | } 269 | } 270 | 271 | /** 272 | * Handles owner 'afterValidate' event, ensuring role model is validated as well 273 | * in case it have been fetched. 274 | * @param \yii\base\Event $event event instance. 275 | */ 276 | public function afterValidate($event) 277 | { 278 | if (!$this->owner->isRelationPopulated($this->roleRelation)) { 279 | return; 280 | } 281 | $model = $this->getRoleRelationModel(); 282 | if (!$model->validate()) { 283 | $this->owner->addErrors($model->getErrors()); 284 | } 285 | } 286 | 287 | /** 288 | * Handles owner 'beforeInsert' and 'beforeUpdate' events, ensuring role model is saved. 289 | * @param \yii\base\ModelEvent $event event instance. 290 | */ 291 | public function beforeSave($event) 292 | { 293 | // Backup to bypass [[BaseActiveRecord::resetDependentRelations()]] : 294 | if ($this->owner->isRelationPopulated($this->roleRelation)) { 295 | $this->roleRelationModelBackup = $this->owner->{$this->roleRelation}; 296 | } else { 297 | $this->roleRelationModelBackup = null; 298 | } 299 | 300 | // Master : 301 | if (!$this->isOwnerSlave) { 302 | if (!empty($this->roleAttributes)) { 303 | foreach ($this->roleAttributes as $name => $value) { 304 | $this->owner->$name = $value; 305 | } 306 | } 307 | return; 308 | } 309 | 310 | // Slave : 311 | $relation = $this->owner->getRelation($this->roleRelation); 312 | 313 | if ($this->roleRelationModelBackup === null) { 314 | $ownerLinkPopulated = true; 315 | foreach ($relation->link as $to => $from) { 316 | if ($this->owner->{$from} === null) { 317 | $ownerLinkPopulated = false; 318 | break; 319 | } 320 | } 321 | if ($ownerLinkPopulated) { 322 | return; 323 | } 324 | } 325 | 326 | $model = $this->getRoleRelationModel(); 327 | 328 | if (!empty($this->roleAttributes)) { 329 | foreach ($this->roleAttributes as $name => $value) { 330 | $model->$name = $value; 331 | } 332 | } 333 | 334 | $model->save(false); 335 | 336 | foreach ($relation->link as $to => $from) { 337 | $this->owner->{$from} = $model->{$to}; 338 | } 339 | 340 | $this->roleRelationModelBackup = $model; 341 | $this->owner->populateRelation($this->roleRelation, $model); 342 | } 343 | 344 | /** 345 | * Handles owner 'afterInsert' and 'afterUpdate' events, ensuring role model is saved 346 | * in case it has been fetched before. 347 | * @param \yii\base\ModelEvent $event event instance. 348 | */ 349 | public function afterSave($event) 350 | { 351 | // Restore backup : 352 | if ($this->roleRelationModelBackup !== null) { 353 | if (!$this->owner->isRelationPopulated($this->roleRelation)) { 354 | $this->owner->populateRelation($this->roleRelation, $this->roleRelationModelBackup); 355 | } 356 | $this->roleRelationModelBackup = null; 357 | } 358 | 359 | // Slave : 360 | if ($this->isOwnerSlave) { 361 | return; 362 | } 363 | 364 | // Master : 365 | if (!$this->owner->isRelationPopulated($this->roleRelation)) { 366 | return; 367 | } 368 | 369 | $model = $this->getRoleRelationModel(); 370 | 371 | $relation = $this->owner->getRelation($this->roleRelation); 372 | foreach ($relation->link as $to => $from) { 373 | $model->{$to} = $this->owner->{$from}; 374 | } 375 | 376 | $model->save(false); 377 | } 378 | 379 | /** 380 | * Handles owner 'beforeDelete' events, ensuring role model is deleted as well. 381 | * @param \yii\base\ModelEvent $event event instance. 382 | */ 383 | public function beforeDelete($event) 384 | { 385 | if ($this->isOwnerSlave) { 386 | return; 387 | } 388 | $this->deleteRoleRelationModel(); 389 | } 390 | 391 | /** 392 | * Handles owner 'beforeDelete' events, ensuring role model is deleted as well. 393 | * @param \yii\base\ModelEvent $event event instance. 394 | */ 395 | public function afterDelete($event) 396 | { 397 | if (!$this->isOwnerSlave) { 398 | return; 399 | } 400 | $this->deleteRoleRelationModel(); 401 | } 402 | 403 | /** 404 | * Deletes related role model. 405 | */ 406 | protected function deleteRoleRelationModel() 407 | { 408 | $model = $this->owner->{$this->roleRelation}; 409 | if (is_object($model)) { 410 | $model->delete(); 411 | } 412 | } 413 | } --------------------------------------------------------------------------------