├── 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 | [](https://packagist.org/packages/yii2tech/ar-role)
14 | [](https://packagist.org/packages/yii2tech/ar-role)
15 | [](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 | = $form->field($model, 'name'); ?>
401 | = $form->field($model, 'address'); ?>
402 |
403 | = $form->field($model, 'studyGroupId')->dropDownList(ArrayHelper::map(StudyGroup::find()->all(), 'id', 'name')); ?>
404 | = $form->field($model, 'hasScholarship')->checkbox(); ?>
405 |
406 |
407 | = Html::submitButton('Save', ['class' => '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 | = $form->field($model, 'name'); ?>
496 | = $form->field($model, 'address'); ?>
497 |
498 | = $form->field($model->getRoleRelationModel(), 'studyGroupId')->dropDownList(ArrayHelper::map(StudyGroup::find()->all(), 'id', 'name')); ?>
499 | = $form->field($model->getRoleRelationModel(), 'hasScholarship')->checkbox(); ?>
500 |
501 |
502 | = Html::submitButton('Save', ['class' => '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 | }
--------------------------------------------------------------------------------