Completed
Push — master ( fae420...efd6c7 )
by vistart
13:31
created

Organization::addMember()   C

Complexity

Conditions 10
Paths 13

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 13.0517

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
ccs 11
cts 16
cp 0.6875
rs 5.2164
cc 10
eloc 16
nc 13
nop 1
crap 13.0517

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 *  _   __ __ _____ _____ ___  ____  _____
5
 * | | / // // ___//_  _//   ||  __||_   _|
6
 * | |/ // /(__  )  / / / /| || |     | |
7
 * |___//_//____/  /_/ /_/ |_||_|     |_|
8
 * @link https://vistart.me/
9
 * @copyright Copyright (c) 2016 - 2017 vistart
10
 * @license https://vistart.me/license/
11
 */
12
13
namespace rhosocial\organization;
14
15
use rhosocial\base\models\traits\SelfBlameableTrait;
16
use rhosocial\base\models\queries\BaseBlameableQuery;
17
use rhosocial\base\models\queries\BaseUserQuery;
18
use rhosocial\user\User;
19
use rhosocial\organization\rbac\roles\DepartmentAdmin;
20
use rhosocial\organization\rbac\roles\DepartmentCreator;
21
use rhosocial\organization\rbac\roles\OrganizationAdmin;
22
use rhosocial\organization\rbac\roles\OrganizationCreator;
23
use rhosocial\organization\queries\MemberQuery;
24
use rhosocial\organization\queries\OrganizationQuery;
25
use Yii;
26
use yii\base\Event;
27
use yii\base\InvalidParamException;
28
use yii\db\IntegrityException;
29
30
/**
31
 * Base Organization.
32
 * This class is an abstract class that can not be instantiated directly.
33
 * You can use [[Organization]] or [[Department]] instead.
34
 *
35
 * @method Member createMember(array $config) Create member who is subordinate to this.
36
 * @property integer $type Whether indicate this instance is an organization or a department.
37
 *
38
 * @property-read User[] $memberUsers Get all members of this organization/department.
39
 * @property-read User $creator Get creator of this organization/department.
40
 * @property-read User[] $administrators Get administrators of this organization/department.
41
 * @property-read SubordinateLimit subordinateLimit
42
 * @property-read MemberLimit memberLimit
43
 *
44
 * @version 1.0
45
 * @author vistart <[email protected]>
46
 */
47
class Organization extends User
48
{
49
    use SelfBlameableTrait;
50
51
    const TYPE_ORGANIZATION = 1;
52
    const TYPE_DEPARTMENT = 2;
53
54
    /**
55
     * @var boolean Organization does not need password and corresponding features.
56
     */
57
    public $passwordHashAttribute = false;
58
59
    /**
60
     * @var boolean Organization does not need password and corresponding features.
61
     */
62
    public $passwordResetTokenAttribute = false;
63
64
    /**
65
     * @var boolean Organization does not need password and corresponding features.
66
     */
67
    public $passwordHistoryClass = false;
68
69
    /**
70
     * @var boolean Organization does not need source.
71
     */
72
    public $sourceAttribute = false;
73
74
    /**
75
     * @var boolean Organization does not need auth key.
76
     */
77
    public $authKeyAttribute = false;
78
79
    /**
80
     * @var boolean Organization does not need access token.
81
     */
82
    public $accessTokenAttribute = false;
83
84
    /**
85
     *
86
     * @var boolean Organization does not need login log.
87
     */
88
    public $loginLogClass = false;
89
90
    public $profileClass = Profile::class;
91
92
    public $memberClass = Member::class;
93
    public $subordinateLimitClass = SubordinateLimit::class;
94
    public $memberLimitClass = MemberLimit::class;
95
    /**
96
     * @var Member
97
     */
98
    private $noInitMember;
99
    /**
100
     * @var SubordinateLimit
101
     */
102
    private $noInitSubordinateLimit;
103
    /**
104
     * @var MemberLimit
105
     */
106
    private $noInitMemberLimit;
107
    public $creatorModel;
108
    public $profileConfig;
109
    /**
110
     * @return Member
111
     */
112 39
    protected function getNoInitMember()
113
    {
114 39
        if (!$this->noInitMember) {
115 39
            $class = $this->memberClass;
116 39
            $this->noInitMember = $class::buildNoInitModel();
117
        }
118 39
        return $this->noInitMember;
119
    }
120
121
    /**
122
     * @return SubordinateLimit
123
     */
124 2
    protected function getNoInitSubordinateLimit()
125
    {
126 2
        if (!$this->noInitSubordinateLimit) {
127 2
            $class = $this->subordinateLimitClass;
128 2
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
129
        }
130 2
        return $this->noInitSubordinateLimit;
131
    }
132
133
    /**
134
     * @return MemberLimit
135
     */
136 1
    protected function getNoInitMemberLimit()
137
    {
138 1
        if (!$this->noInitMemberLimit) {
139 1
            $class = $this->memberLimitClass;
140 1
            $this->noInitMemberLimit = $class::buildNoInitModel();
141
        }
142 1
        return $this->noInitMemberLimit;
143
    }
144
145 41
    public function init()
146
    {
147 41
        $this->parentAttribute = 'parent_guid';
148 41
        if (class_exists($this->memberClass)) {
149 41
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
150
        }
151 41
        if ($this->skipInit) {
152 41
            return;
153
        }
154 41
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
155 41
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
156 41
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
157 41
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
158 41
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
159 41
        $this->initSelfBlameableEvents();
160 41
        parent::init();
161 41
    }
162
163
    /**
164
     * @inheritdoc
165
     */
166 1
    public function attributeLabels()
167
    {
168
        return [
169 1
            'guid' => Yii::t('user', 'GUID'),
170 1
            'id' => Yii::t('user', 'ID'),
171 1
            'ip' => Yii::t('user', 'IP Address'),
172 1
            'ip_type' => Yii::t('user', 'IP Address Type'),
173 1
            'parent' => Yii::t('organization', 'Parent'),
174 1
            'created_at' => Yii::t('user', 'Creation Time'),
175 1
            'updated_at' => Yii::t('user', 'Last Updated Time'),
176 1
            'status' => Yii::t('user', 'Status'),
177 1
            'type' => Yii::t('user', 'Type'),
178
        ];
179
    }
180
181
    /**
182
     * @inheritdoc
183
     */
184 41
    public static function tableName()
185
    {
186 41
        return '{{%organization}}';
187
    }
188
189
    /**
190
     * Find.
191
     * Friendly to IDE.
192
     * @return OrganizationQuery
193
     */
194 41
    public static function find()
195
    {
196 41
        return parent::find();
197
    }
198
199 40
    protected function getTypeRules()
200
    {
201
        return [
202 40
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
203
            ['type', 'required'],
204 40
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
205
        ];
206
    }
207
208 40
    public function rules()
209
    {
210 40
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
211
    }
212
213
    /**
214
     * Get Member Query.
215
     * @return MemberQuery
216
     */
217 39
    public function getMembers()
218
    {
219 39
        return $this->hasMany($this->memberClass, [$this->getNoInitMember()->createdByAttribute => $this->guidAttribute])->inverseOf('organization');
220
    }
221
222
    /**
223
     * Get organization member users' query.
224
     * @return BaseUserQuery
225
     */
226 4
    public function getMemberUsers()
227
    {
228 4
        $noInit = $this->getNoInitMember();
229 4
        $class = $noInit->memberUserClass;
230 4
        $noInitUser = $class::buildNoInitModel();
231 4
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('members')->inverseOf('atOrganizations');
232
    }
233
234
    /**
235
     * Get subordinate limit query.
236
     * @return null|BaseBlameableQuery
237
     */
238 2
    public function getSubordinateLimit()
239
    {
240 2
        if (empty($this->subordinateLimitClass)) {
241
            return null;
242
        }
243 2
        return $this->hasOne($this->subordinateLimitClass, [$this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute]);
244
    }
245
246
    /**
247
     * Get member limit query.
248
     * @return null|BaseBlameableQuery
249
     */
250 1
    public function getMemberLimit()
251
    {
252 1
        if (empty($this->memberLimitClass)) {
253
            return null;
254
        }
255 1
        return $this->hasOne($this->memberLimitClass, [$this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute]);
256
    }
257
258
    /**
259
     * Get member with specified user.
260
     * @param User|string|integer $user
261
     * @return Member Null if `user` is not in this organization.
262
     */
263 39
    public function getMember($user)
264
    {
265 39
        return $this->getMembers()->user($user)->one();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getMembers()->user($user)->one(); of type yii\db\ActiveRecord|array|null adds the type array to the return on line 265 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
266
    }
267
268
    /**
269
     * Add member to organization.
270
     * @param Member|User|string|integer $member Member or User model, or User ID or GUID.
271
     * If member is created, it will be re-assigned to this parameter.
272
     * @see createMemberModel
273
     * @see createMemberModelWithUser
274
     * @return boolean
275
     */
276 39
    public function addMember(&$member)
277
    {
278 39
        if ($this->getIsNewRecord()) {
279
            return false;
280
        }
281 39
        if ($this->hasReachedMemberLimit()) {
282 1
            return false;
283
        }
284 39
        $model = null;
285 39
        if ($member instanceof Member) {
286
            if (!$member->getIsNewRecord()) {
287
                return false;
288
            }
289
            $model = $this->createMemberModel($member);
290
        }
291 39
        if (($member instanceof User) || is_string($member) || is_int($member)) {
292 39
            if ($this->hasMember($member)) {
293
                return false;
294
            }
295 39
            $model = $this->createMemberModelWithUser($member);
296
        }
297 39
        $member = $model;
298 39
        return ($member instanceof Member) ? $member->save() : false;
299
    }
300
301
    /**
302
     * Create member model, and set organization with this.
303
     * @param Member $member If this parameter is not new record, it's organization
304
     * will be set with this, and return it. Otherwise, it will extract `User`
305
     * model and create new `Member` model.
306
     * @see createMemberModelWithUser
307
     * @return Member
308
     */
309
    public function createMemberModel($member)
310
    {
311
        if (!$member->getIsNewRecord()) {
312
            $member->setOrganization($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<rhosocial\organization\Organization>, but the function expects a object<rhosocial\organization\BaseOrganization>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
313
            return $member;
314
        }
315
        return $this->createMemberModelWithUser($member->memberUser);
316
    }
317
318
    /**
319
     * Create member model with user, and set organization with this.
320
     * @param User|string|integer $user
321
     * @return Member
322
     */
323 39
    public function createMemberModelWithUser($user)
324
    {
325
        $config = [
326 39
            'memberUser' => $user,
327 39
            'organization' => $this,
328 39
            'nickname' => '',
329
        ];
330 39
        $member = $this->createMember($config);
331 39
        $member->nickname = $member->memberUser->profile->nickname;
332 39
        return $member;
333
    }
334
335
    /**
336
     * Remove member.
337
     * Note: the creator cannot be removed.
338
     * @param Member|User $member
339
     * @return boolean
340
     */
341 1
    public function removeMember(&$member)
342
    {
343 1
        if ($this->getIsNewRecord()) {
344
            return false;
345
        }
346 1
        if ($member instanceof $this->memberClass) {
347 1
            $member = $member->{$member->memberAttribute};
348
        }
349 1
        $member = $this->getMember($member);
350 1
        if (!$member || $member->isCreator()) {
351
            return false;
352
        }
353 1
        return $member->delete() > 0;
354
    }
355
356
    /**
357
     * Remove administrator.
358
     * @param Member|User $member
359
     * @param boolean $keepMember Keep member after administrator being revoked.
360
     * @return boolean
361
     * @throws IntegrityException
362
     */
363
    public function removeAdministrator(&$member, $keepMember = true)
364
    {
365
        if ($this->getIsNewRecord()) {
366
            return false;
367
        }
368
        if ($member instanceof $this->memberClass) {
369
            $member = $member->{$member->memberAttribute};
370
        }
371
        $member = $this->getMember($member);
372
        if ($member && $member->isAdministrator()) {
373
            if ($keepMember) {
374
                return $member->revokeAdministrator();
375
            }
376
            return $this->removeMember($member);
377
        }
378
        return false;
379
    }
380
381
    /**
382
     * 
383
     * @param Event $event
384
     */
385 40
    public function onAddProfile($event)
386
    {
387 40
        $profile = $event->sender->createProfile($event->data);
388 40
        if (!$profile->save()) {
389
            throw new IntegrityException('Profile Save Failed.');
390
        }
391 40
        return true;
392
    }
393
394
    /**
395
     * 
396
     * @param Event $event
397
     */
398 40
    public function onAssignCreator($event)
399
    {
400 40
        return $event->sender->addCreator($event->data);
401
    }
402
403
    /**
404
     * 
405
     * @param Event $event
406
     * @return boolean
407
     */
408 20
    public function onRevokeCreator($event)
409
    {
410 20
        $sender = $event->sender;
411
        /* @var $sender static */
412 20
        $member = $sender->getMemberCreators()->one();
413
        /* @var $member Member */
414 20
        $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
415 20
        return $member->revokeRole($role);
0 ignored issues
show
Documentation introduced by
$role is of type string, but the function expects a object<rhosocial\user\rbac\Role>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
416
    }
417
418
    /**
419
     * 
420
     * @param Event $event
421
     * @return boolean
422
     */
423 20
    public function onRevokeAdministrators($event)
424
    {
425 20
        $sender = $event->sender;
426
        /* @var $sender static */
427 20
        $members = $sender->getMemberAdministrators()->all();
428
        /* @var $members Member[] */
429 20
        foreach ($members as $member)
430
        {
431 1
            $member->revokeAdministrator();
432
        }
433 20
        return true;
434
    }
435
436
    /**
437
     * 
438
     * @param Event $event
439
     */
440 20
    public function onRevokePermissions($event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
441
    {
442
        
443 20
    }
444
445
    /**
446
     * Check whether current instance is an organization.
447
     * @return boolean
448
     */
449 2
    public function isOrganization()
450
    {
451 2
        return $this->type == static::TYPE_ORGANIZATION;
452
    }
453
454
    /**
455
     * Check whether current instance if a department.
456
     * @return boolean
457
     */
458 2
    public function isDepartment()
459
    {
460 2
        return $this->type == static::TYPE_DEPARTMENT;
461
    }
462
463
    /**
464
     * Check whether the current organization has a member.
465
     * @param User|string|integer $user User instance, GUID or ID.
466
     * @return boolean
467
     */
468 39
    public function hasMember($user)
469
    {
470 39
        return !is_null($this->getMember($user));
471
    }
472
473
    /**
474
     * Get member query which role is specified `Creator`.
475
     * @return MemberQuery
476
     */
477 21
    public function getMemberCreators()
478
    {
479 21
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
480
    }
481
482
    /**
483
     * Get member query which role is specified `Administrator`.
484
     * @return MemberQuery
485
     */
486 21
    public function getMemberAdministrators()
487
    {
488 21
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
489
    }
490
491
    /**
492
     * Get user query which role is specified `Creator`.
493
     * @return BaseUserQuery
494
     */
495 1
    public function getCreator()
496
    {
497 1
        $noInit = $this->getNoInitMember();
498 1
        $class = $noInit->memberUserClass;
499 1
        $noInitUser = $class::buildNoInitModel();
500 1
        return $this->hasOne($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
501
    }
502
503
    /**
504
     * Get user query which role is specified `Administrator`.
505
     * @return BaseUserQuery
506
     */
507 1
    public function getAdministrators()
508
    {
509 1
        $noInit = $this->getNoInitMember();
510 1
        $class = $noInit->memberUserClass;
511 1
        $noInitUser = $class::buildNoInitModel();
512 1
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
513
    }
514
515
    /**
516
     * 
517
     * @param User $user
518
     * @return boolean
519
     * @throws \Exception
520
     * @throws IntegrityException
521
     */
522 40
    protected function addCreator($user)
523
    {
524 40
        if (!$user) {
525 1
            throw new InvalidParamException('Creator Invalid.');
526
        }
527 39
        $member = $user;
528 39
        $transaction = Yii::$app->db->beginTransaction();
529
        try {
530 39
            if (!$this->addMember($member)) {
531
                throw new IntegrityException('Failed to add member.');
532
            }
533 39
            $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
534 39
            $member->assignRole($role);
0 ignored issues
show
Documentation introduced by
$role is of type string, but the function expects a object<rhosocial\user\rbac\Role>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
The method assignRole does only exist in rhosocial\organization\Member, but not in rhosocial\user\User.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
535 39
            if (!$member->save()) {
536
                throw new IntegrityException('Failed to assign creator.');
537
            }
538 39
            $transaction->commit();
539
        } catch (\Exception $ex) {
540
            $transaction->rollBack();
541
            Yii::error($ex->getMessage(), __METHOD__);
542
            throw $ex;
543
        }
544 39
        return true;
545
    }
546
547
    /**
548
     * 
549
     * @param User $user
550
     * @return boolean
551
     * @throws \Exception
552
     * @throws IntegrityException
553
     */
554 7
    public function addAdministrator($user)
555
    {
556 7
        $transaction = Yii::$app->db->beginTransaction();
557
        try {
558 7
            if (!$this->hasMember($user) && !$this->addMember($user)) {
559
                throw new IntegrityException('Failed to add member.');
560
            }
561 7
            $member = $this->getMember($user);
562 7
            $member->assignAdministrator();
563 7
            $transaction->commit();
564
        } catch (\Exception $ex) {
565
            $transaction->rollBack();
566
            Yii::error($ex->getMessage(), __METHOD__);
567
            throw $ex;
568
        }
569 7
        return true;
570
    }
571
572
    /**
573
     * 
574
     * @param type $user
575
     * @return boolean
576
     */
577 2
    public function hasAdministrator($user)
578
    {
579 2
        $member = $this->getMember($user);
580 2
        if (!$member) {
581
            return false;
582
        }
583 2
        return $member->isAdministrator();
584
    }
585
586
    /**
587
     * Check whether this organization has reached the upper limit of subordinates.
588
     * @return boolean
589
     */
590 9
    public function hasReachedSubordinateLimit()
591
    {
592 9
        $class = $this->subordinateLimitClass;
593 9
        if (empty($class)) {
594
            return false;
595
        }
596 9
        $limit = $class::getLimit($this);
597 9
        if ($limit === false) {
598
            return false;
599
        }
600 9
        $count = (int)$this->getChildren()->count();
601 9
        return $count >= $limit;
602
    }
603
604
    /**
605
     * Check whether this organization has reached the upper limit of members.
606
     * @return boolean
607
     */
608 39
    public function hasReachedMemberLimit()
609
    {
610 39
        $class = $this->memberLimitClass;
611 39
        if (empty($class)) {
612
            return false;
613
        }
614 39
        $limit = $class::getLimit($this);
615 39
        if ($limit === false) {
616
            return false;
617
        }
618 39
        $count = (int)$this->getMembers()->count();
619 39
        return $count >= $limit;
620
    }
621
}
622