Completed
Push — master ( 9eb8f3...27c704 )
by vistart
05:33
created

Organization::hasMember()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
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
    public $searchClass = OrganizationSearch::class;
96
    /**
97
     * @var Member
98
     */
99
    private $noInitMember;
100
    /**
101
     * @var SubordinateLimit
102
     */
103
    private $noInitSubordinateLimit;
104
    /**
105
     * @var MemberLimit
106
     */
107
    private $noInitMemberLimit;
108
    public $creatorModel;
109
    public $profileConfig;
110
111
    const EVENT_BEFORE_ADD_MEMBER = 'eventBeforeAddMember';
112
    const EVENT_AFTER_ADD_MEMBER = 'eventAfterAddMember';
113
    const EVENT_BEFORE_REMOVE_MEMBER = 'eventBeforeRemoveMember';
114
    const EVENT_AFTER_REMOVE_MEMBER = 'eventAfterRemoveMember';
115
116
    /**
117
     * @return Member
118
     */
119 42
    public function getNoInitMember()
120
    {
121 42
        if (!$this->noInitMember) {
122 42
            $class = $this->memberClass;
123 42
            $this->noInitMember = $class::buildNoInitModel();
124
        }
125 42
        return $this->noInitMember;
126
    }
127
128
    /**
129
     * @return SubordinateLimit
130
     */
131 2
    public function getNoInitSubordinateLimit()
132
    {
133 2
        if (!$this->noInitSubordinateLimit) {
134 2
            $class = $this->subordinateLimitClass;
135 2
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
136
        }
137 2
        return $this->noInitSubordinateLimit;
138
    }
139
140
    /**
141
     * @return MemberLimit
142
     */
143 1
    public function getNoInitMemberLimit()
144
    {
145 1
        if (!$this->noInitMemberLimit) {
146 1
            $class = $this->memberLimitClass;
147 1
            $this->noInitMemberLimit = $class::buildNoInitModel();
148
        }
149 1
        return $this->noInitMemberLimit;
150
    }
151
152
    /**
153
     * @return null|OrganizationSearch
154
     */
155
    public function getSearchModel()
156
    {
157
        $class = $this->searchClass;
158
        if (empty($class) || !class_exists($class)) {
159
            return null;
160
        }
161
        return new $class;
162
    }
163
164 43
    public function init()
165
    {
166 43
        $this->parentAttribute = 'parent_guid';
167 43
        if (class_exists($this->memberClass)) {
168 43
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
169
        }
170 43
        if ($this->skipInit) {
171 43
            return;
172
        }
173 43
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
174 43
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
175 43
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
176 43
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
177 43
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
178 43
        $this->initSelfBlameableEvents();
179 43
        parent::init();
180 43
    }
181
182
    /**
183
     * @inheritdoc
184
     */
185 1
    public function attributeLabels()
186
    {
187
        return [
188 1
            'guid' => Yii::t('user', 'GUID'),
189 1
            'id' => Yii::t('user', 'ID'),
190 1
            'ip' => Yii::t('user', 'IP Address'),
191 1
            'ip_type' => Yii::t('user', 'IP Address Type'),
192 1
            'parent' => Yii::t('organization', 'Parent'),
193 1
            'created_at' => Yii::t('user', 'Creation Time'),
194 1
            'updated_at' => Yii::t('user', 'Last Updated Time'),
195 1
            'status' => Yii::t('user', 'Status'),
196 1
            'type' => Yii::t('user', 'Type'),
197
        ];
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203 43
    public static function tableName()
204
    {
205 43
        return '{{%organization}}';
206
    }
207
208
    /**
209
     * Find.
210
     * Friendly to IDE.
211
     * @return OrganizationQuery
212
     */
213 43
    public static function find()
214
    {
215 43
        return parent::find();
216
    }
217
218 42
    protected function getTypeRules()
219
    {
220
        return [
221 42
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
222
            ['type', 'required'],
223 42
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
224
        ];
225
    }
226
227 42
    public function rules()
228
    {
229 42
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
230
    }
231
232
    /**
233
     * Get Member Query.
234
     * @return MemberQuery
235
     */
236 41
    public function getMembers()
237
    {
238 41
        return $this->hasMany($this->memberClass, [$this->getNoInitMember()->createdByAttribute => $this->guidAttribute])->inverseOf('organization');
239
    }
240
241
    /**
242
     * Get organization member users' query.
243
     * @return BaseUserQuery
244
     */
245 4
    public function getMemberUsers()
246
    {
247 4
        $noInit = $this->getNoInitMember();
248 4
        $class = $noInit->memberUserClass;
249 4
        $noInitUser = $class::buildNoInitModel();
250 4
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('members')->inverseOf('atOrganizations');
251
    }
252
253
    /**
254
     * Get subordinate limit query.
255
     * @return null|BaseBlameableQuery
256
     */
257 2
    public function getSubordinateLimit()
258
    {
259 2
        if (empty($this->subordinateLimitClass)) {
260
            return null;
261
        }
262 2
        return $this->hasOne($this->subordinateLimitClass, [$this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute]);
263
    }
264
265
    /**
266
     * Get member limit query.
267
     * @return null|BaseBlameableQuery
268
     */
269 1
    public function getMemberLimit()
270
    {
271 1
        if (empty($this->memberLimitClass)) {
272
            return null;
273
        }
274 1
        return $this->hasOne($this->memberLimitClass, [$this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute]);
275
    }
276
277
    /**
278
     * Get member with specified user.
279
     * @param User|string|integer $user
280
     * @return Member Null if `user` is not in this organization.
281
     */
282 41
    public function getMember($user)
283
    {
284 41
        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 284 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
285
    }
286
287
    /**
288
     * Add member to organization.
289
     * @param Member|User|string|integer $member Member or User model, or User ID or GUID.
290
     * If member is created, it will be re-assigned to this parameter.
291
     * @see createMemberModel
292
     * @see createMemberModelWithUser
293
     * @return boolean
294
     */
295 41
    public function addMember(&$member)
296
    {
297 41
        if ($this->getIsNewRecord()) {
298
            return false;
299
        }
300 41
        if ($this->hasReachedMemberLimit()) {
301 1
            return false;
302
        }
303 41
        $this->trigger(self::EVENT_BEFORE_ADD_MEMBER);
304 41
        $model = null;
305 41
        if ($member instanceof Member) {
306
            if (!$member->getIsNewRecord()) {
307
                return false;
308
            }
309
            $model = $this->createMemberModel($member);
310
        }
311 41
        if (($member instanceof User) || is_string($member) || is_int($member)) {
312 41
            if ($this->hasMember($member)) {
313
                return false;
314
            }
315 41
            $model = $this->createMemberModelWithUser($member);
316
        }
317 41
        $member = $model;
318 41
        $result = ($member instanceof Member) ? $member->save() : false;
319 41
        $this->trigger(self::EVENT_AFTER_ADD_MEMBER);
320 41
        return $result;
321
    }
322
323
    /**
324
     * Create member model, and set organization with this.
325
     * @param Member $member If this parameter is not new record, it's organization
326
     * will be set with this, and return it. Otherwise, it will extract `User`
327
     * model and create new `Member` model.
328
     * @see createMemberModelWithUser
329
     * @return Member
330
     */
331
    public function createMemberModel($member)
332
    {
333
        if (!$member->getIsNewRecord()) {
334
            $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...
335
            return $member;
336
        }
337
        return $this->createMemberModelWithUser($member->memberUser);
338
    }
339
340
    /**
341
     * Create member model with user, and set organization with this.
342
     * @param User|string|integer $user
343
     * @return Member
344
     */
345 41
    public function createMemberModelWithUser($user)
346
    {
347
        $config = [
348 41
            'memberUser' => $user,
349 41
            'organization' => $this,
350 41
            'nickname' => '',
351
        ];
352 41
        $member = $this->createMember($config);
353 41
        $member->nickname = $member->memberUser->profile->nickname;
354 41
        return $member;
355
    }
356
357
    /**
358
     * Remove member.
359
     * Note: the creator cannot be removed.
360
     * @param Member|User $member
361
     * @return boolean
362
     */
363 1
    public function removeMember(&$member)
364
    {
365 1
        if ($this->getIsNewRecord()) {
366
            return false;
367
        }
368 1
        $this->trigger(self::EVENT_BEFORE_REMOVE_MEMBER);
369 1
        if ($member instanceof $this->memberClass) {
370 1
            $member = $member->{$member->memberAttribute};
371
        }
372 1
        $member = $this->getMember($member);
373 1
        if (!$member || $member->isCreator()) {
374
            return false;
375
        }
376 1
        $result = $member->delete() > 0;
377 1
        $this->trigger(self::EVENT_AFTER_REMOVE_MEMBER);
378 1
        return $result;
379
    }
380
381
    /**
382
     * Remove administrator.
383
     * @param Member|User|integer|string $member Member instance, or User instance or its GUID or ID.
384
     * @param boolean $keep Keep member after administrator being revoked.
385
     * @return boolean
386
     * @throws IntegrityException
387
     */
388
    public function removeAdministrator(&$member, $keep = true)
389
    {
390
        if ($this->getIsNewRecord()) {
391
            return false;
392
        }
393
        if ($member instanceof $this->memberClass) {
394
            $member = $member->{$member->memberAttribute};
395
        }
396
        $member = $this->getMember($member);
397
        if ($member && $member->isAdministrator()) {
398
            if ($keep) {
399
                return $member->revokeAdministrator();
400
            }
401
            return $this->removeMember($member);
402
        }
403
        return false;
404
    }
405
406
    /**
407
     * 
408
     * @param Event $event
409
     * @throws IntegrityException
410
     * @return boolean
411
     */
412 42
    public function onAddProfile($event)
413
    {
414 42
        $profile = $event->sender->createProfile($event->data);
415 42
        if (!$profile->save()) {
416
            throw new IntegrityException('Profile Save Failed.');
417
        }
418 42
        return true;
419
    }
420
421
    /**
422
     * 
423
     * @param Event $event
424
     */
425 42
    public function onAssignCreator($event)
426
    {
427 42
        return $event->sender->addCreator($event->data);
428
    }
429
430
    /**
431
     * 
432
     * @param Event $event
433
     * @return boolean
434
     */
435 20
    public function onRevokeCreator($event)
436
    {
437 20
        $sender = $event->sender;
438
        /* @var $sender static */
439 20
        $member = $sender->getMemberCreators()->one();
440
        /* @var $member Member */
441 20
        $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
442 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...
443
    }
444
445
    /**
446
     * 
447
     * @param Event $event
448
     * @return boolean
449
     */
450 20
    public function onRevokeAdministrators($event)
451
    {
452 20
        $sender = $event->sender;
453
        /* @var $sender static */
454 20
        $members = $sender->getMemberAdministrators()->all();
455
        /* @var $members Member[] */
456 20
        foreach ($members as $member)
457
        {
458 1
            $member->revokeAdministrator();
459
        }
460 20
        return true;
461
    }
462
463
    /**
464
     * 
465
     * @param Event $event
466
     */
467 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...
468
    {
469
        
470 20
    }
471
472
    /**
473
     * Check whether current instance is an organization.
474
     * @return boolean
475
     */
476 2
    public function isOrganization()
477
    {
478 2
        return $this->type == static::TYPE_ORGANIZATION;
479
    }
480
481
    /**
482
     * Check whether current instance if a department.
483
     * @return boolean
484
     */
485 2
    public function isDepartment()
486
    {
487 2
        return $this->type == static::TYPE_DEPARTMENT;
488
    }
489
490
    /**
491
     * Check whether the current organization has a member.
492
     * @param User|string|integer $user User instance, GUID or ID.
493
     * @return boolean
494
     */
495 41
    public function hasMember($user)
496
    {
497 41
        return !is_null($this->getMember($user));
498
    }
499
500
    /**
501
     * Get member query which role is specified `Creator`.
502
     * @return MemberQuery
503
     */
504 23
    public function getMemberCreators()
505
    {
506 23
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
507
    }
508
509
    /**
510
     * Get member query which role is specified `Administrator`.
511
     * @return MemberQuery
512
     */
513 22
    public function getMemberAdministrators()
514
    {
515 22
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
516
    }
517
518
    /**
519
     * Get user query which role is specified `Creator`.
520
     * @return BaseUserQuery
521
     */
522 3
    public function getCreator()
523
    {
524 3
        $noInit = $this->getNoInitMember();
525 3
        $class = $noInit->memberUserClass;
526 3
        $noInitUser = $class::buildNoInitModel();
527 3
        return $this->hasOne($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
528
    }
529
530
    /**
531
     * Get user query which role is specified `Administrator`.
532
     * @return BaseUserQuery
533
     */
534 2
    public function getAdministrators()
535
    {
536 2
        $noInit = $this->getNoInitMember();
537 2
        $class = $noInit->memberUserClass;
538 2
        $noInitUser = $class::buildNoInitModel();
539 2
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
540
    }
541
542
    /**
543
     * 
544
     * @param User $user
545
     * @return boolean
546
     * @throws \Exception
547
     * @throws IntegrityException
548
     */
549 42
    protected function addCreator($user)
550
    {
551 42
        if (!$user) {
552 1
            throw new InvalidParamException('Creator Invalid.');
553
        }
554 41
        $member = $user;
555 41
        $transaction = Yii::$app->db->beginTransaction();
556
        try {
557 41
            if (!$this->addMember($member)) {
558
                throw new IntegrityException('Failed to add member.');
559
            }
560 41
            $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
561 41
            $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...
562 41
            if (!$member->save()) {
563
                throw new IntegrityException('Failed to assign creator.');
564
            }
565 41
            $transaction->commit();
566
        } catch (\Exception $ex) {
567
            $transaction->rollBack();
568
            Yii::error($ex->getMessage(), __METHOD__);
569
            throw $ex;
570
        }
571 41
        return true;
572
    }
573
574
    /**
575
     * Add administrator.
576
     * @param User|integer|string $user User instance, or its GUID or ID.
577
     * @return boolean
578
     * @throws \Exception
579
     * @throws IntegrityException
580
     */
581 8
    public function addAdministrator($user)
582
    {
583 8
        $transaction = Yii::$app->db->beginTransaction();
584
        try {
585 8
            if (!$this->hasMember($user) && !$this->addMember($user)) {
586
                throw new IntegrityException(Yii::t('organization', 'Failed to add member.'));
587
            }
588 8
            $member = $this->getMember($user);
589 8
            $member->assignAdministrator();
590 8
            $transaction->commit();
591
        } catch (\Exception $ex) {
592
            $transaction->rollBack();
593
            Yii::error($ex->getMessage(), __METHOD__);
594
            throw $ex;
595
        }
596 8
        return true;
597
    }
598
599
    /**
600
     * Check whether the current organization has administrator.
601
     * @param User|integer|string $user
602
     * @return boolean
603
     */
604 2
    public function hasAdministrator($user)
605
    {
606 2
        $member = $this->getMember($user);
607 2
        if (!$member) {
608
            return false;
609
        }
610 2
        return $member->isAdministrator();
611
    }
612
613
    /**
614
     * Check whether this organization has reached the upper limit of subordinates.
615
     * @return boolean
616
     */
617 9
    public function hasReachedSubordinateLimit()
618
    {
619 9
        $class = $this->subordinateLimitClass;
620 9
        if (empty($class)) {
621
            return false;
622
        }
623 9
        $limit = $class::getLimit($this);
624 9
        if ($limit === false) {
625
            return false;
626
        }
627 9
        $count = (int)$this->getChildren()->count();
628 9
        return $count >= $limit;
629
    }
630
631
    /**
632
     * Check whether this organization has reached the upper limit of members.
633
     * @return boolean
634
     */
635 41
    public function hasReachedMemberLimit()
636
    {
637 41
        $class = $this->memberLimitClass;
638 41
        if (empty($class)) {
639
            return false;
640
        }
641 41
        $limit = $class::getLimit($this);
642 41
        if ($limit === false) {
643
            return false;
644
        }
645 41
        $count = (int)$this->getMembers()->count();
646 41
        return $count >= $limit;
647
    }
648
}
649