Completed
Push — master ( 94fceb...32340d )
by vistart
07:04
created

Organization::isDepartment()   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 0
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 Yii;
25
use yii\base\Event;
26
use yii\base\InvalidParamException;
27
use yii\db\IntegrityException;
28
29
/**
30
 * Base Organization.
31
 * This class is an abstract class that can not be instantiated directly.
32
 * You can use [[Organization]] or [[Department]] instead.
33
 *
34
 * @method Member createMember(array $config) Create member who is subordinate to this.
35
 * @property integer $type Whether indicate this instance is an organization or a department.
36
 *
37
 * @property-read User[] $memberUsers Get all members of this organization/department.
38
 * @property-read User $creator Get creator of this organization/department.
39
 * @property-read User[] $administrators Get administrators of this organization/department.
40
 *
41
 * @version 1.0
42
 * @author vistart <[email protected]>
43
 */
44
class Organization extends User
45
{
46
    use SelfBlameableTrait;
47
48
    const TYPE_ORGANIZATION = 1;
49
    const TYPE_DEPARTMENT = 2;
50
51
    /**
52
     * @var boolean Organization does not need password and corresponding features.
53
     */
54
    public $passwordHashAttribute = false;
55
56
    /**
57
     * @var boolean Organization does not need password and corresponding features.
58
     */
59
    public $passwordResetTokenAttribute = false;
60
61
    /**
62
     * @var boolean Organization does not need password and corresponding features.
63
     */
64
    public $passwordHistoryClass = false;
65
66
    /**
67
     * @var boolean Organization does not need source.
68
     */
69
    public $sourceAttribute = false;
70
71
    /**
72
     * @var boolean Organization does not need auth key.
73
     */
74
    public $authKeyAttribute = false;
75
76
    /**
77
     * @var boolean Organization does not need access token.
78
     */
79
    public $accessTokenAttribute = false;
80
81
    /**
82
     *
83
     * @var boolean Organization does not need login log.
84
     */
85
    public $loginLogClass = false;
86
87
    public $profileClass = Profile::class;
88
89
    public $memberClass = Member::class;
90
    public $subordinateLimitClass = SubordinateLimit::class;
91
    public $memberLimitClass = MemberLimit::class;
92
    /**
93
     * @var Member
94
     */
95
    private $noInitMember;
96
    /**
97
     * @var SubordinateLimit
98
     */
99
    private $noInitSubordinateLimit;
100
    /**
101
     * @var MemberLimit
102
     */
103
    private $noInitMemberLimit;
104
    public $creatorModel;
105
    public $profileConfig;
106
    /**
107
     * @return Member
108
     */
109 36
    protected function getNoInitMember()
110
    {
111 36
        if (!$this->noInitMember) {
112 36
            $class = $this->memberClass;
113 36
            $this->noInitMember = $class::buildNoInitModel();
114 36
        }
115 36
        return $this->noInitMember;
116
    }
117
118
    /**
119
     * @return SubordinateLimit
120
     */
121
    protected function getNoInitSubordinateLimit()
122
    {
123
        if (!$this->noInitSubordinateLimit) {
124
            $class = $this->subordinateLimitClass;
125
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
126
        }
127
        return $this->noInitSubordinateLimit;
128
    }
129
130
    /**
131
     * @return MemberLimit
132
     */
133
    protected function getNoInitMemberLimit()
134
    {
135
        if (!$this->noInitMemberLimit) {
136
            $class = $this->memberLimitClass;
137
            $this->noInitMemberLimit = $class::buildNoInitModel();
138
        }
139
        return $this->noInitMemberLimit;
140
    }
141
142 38
    public function init()
143
    {
144 38
        $this->parentAttribute = 'parent_guid';
145 38
        if (class_exists($this->memberClass)) {
146 38
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
147 38
        }
148 38
        if ($this->skipInit) {
149 38
            return;
150
        }
151 38
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
152 38
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
153 38
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
154 38
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
155 38
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
156 38
        $this->initSelfBlameableEvents();
157 38
        parent::init();
158 38
    }
159
160
    /**
161
     * @inheritdoc
162
     */
163 36
    public function attributeLabels()
164
    {
165
        return [
166 36
            'guid' => Yii::t('user', 'GUID'),
167 1
            'id' => Yii::t('user', 'ID'),
168 36
            'ip' => Yii::t('user', 'IP Address'),
169 1
            'ip_type' => Yii::t('user', 'IP Address Type'),
170 1
            'parent' => Yii::t('organization', 'Parent'),
171 1
            'created_at' => Yii::t('user', 'Creation Time'),
172 1
            'updated_at' => Yii::t('user', 'Last Updated Time'),
173 1
            'status' => Yii::t('user', 'Status'),
174 1
            'type' => Yii::t('user', 'Type'),
175 1
        ];
176
    }
177
178
    /**
179
     * @inheritdoc
180
     */
181 38
    public static function tableName()
182
    {
183 38
        return '{{%organization}}';
184 7
    }
185
186 37
    protected function getTypeRules()
187
    {
188
        return [
189 37
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
190 37
            ['type', 'required'],
191 37
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
192 37
        ];
193
    }
194
195 37
    public function rules()
196
    {
197 37
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
198
    }
199
200
    /**
201
     * Get Member Query.
202
     * @return MemberQuery
203
     */
204 36
    public function getMembers()
205
    {
206 36
        return $this->hasMany($this->memberClass, [$this->getNoInitMember()->createdByAttribute => $this->guidAttribute])->inverseOf('organization');
207
    }
208
209
    /**
210
     * Get organization member users' query.
211
     * @return BaseUserQuery
212
     */
213 4
    public function getMemberUsers()
214
    {
215 4
        $noInit = $this->getNoInitMember();
216 4
        $class = $noInit->memberUserClass;
217 4
        $noInitUser = $class::buildNoInitModel();
218 4
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('members')->inverseOf('atOrganizations');
219
    }
220
221
    /**
222
     * Get subordinate limit query.
223
     * @return null|BaseBlameableQuery
224
     */
225 16
    public function getSubordinateLimit()
226
    {
227
        if (empty($this->subordinateLimitClass)) {
228
            return null;
229
        }
230
        return $this->hasOne($this->subordinateLimitClass, [$this->guidAttribute => $this->getNoInitSubordinateLimit()->createdByAttribute]);
231 16
    }
232
233
    /**
234
     * Get member limit query.
235
     * @return null|BaseBlameableQuery
236
     */
237 16
    public function getMemberLimit()
238 16
    {
239
        if (empty($this->memberLimitClass)) {
240 16
            return null;
241
        }
242
        return $this->hasOne($this->memberLimitClass, [$this->guidAttribute => $this->getNoInitMemberLimit()->createdByAttribute]);
243
    }
244
245
    /**
246
     * Get member with specified user.
247
     * @param User|string|integer $user
248
     * @return Member Null if `user` is not in this organization.
249
     */
250 36
    public function getMember($user)
251
    {
252 36
        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 252 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
253
    }
254
255
    /**
256
     * Add member to organization.
257
     * @param Member|User|string|integer $member
258
     * @see createMemberModel
259
     * @see createMemberModelWithUser
260
     * @return boolean
261
     */
262 36
    public function addMember(&$member)
263
    {
264 36
        if ($this->getIsNewRecord()) {
265
            return false;
266
        }
267 36
        $model = null;
268 36
        if ($member instanceof Member) {
269
            if (!$member->getIsNewRecord()) {
270
                return false;
271
            }
272
            $model = $this->createMemberModel($member);
273
        }
274 36
        if (($member instanceof User) || is_string($member) || is_int($member)) {
275 36
            if ($this->hasMember($member)) {
276
                return false;
277
            }
278 36
            $model = $this->createMemberModelWithUser($member);
279 36
        }
280 36
        $member = $model;
281 36
        return ($member instanceof Member) ? $member->save() : false;
282
    }
283
284
    /**
285
     * Create member model, and set organization with this.
286
     * @param Member $member If this parameter is not new record, it's organization
287
     * will be set with this, and return it. Otherwise, it will extract `User`
288
     * model and create new `Member` model.
289
     * @see createMemberModelWithUser
290
     * @return Member
291
     */
292
    public function createMemberModel($member)
293
    {
294
        if (!$member->getIsNewRecord()) {
295
            $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...
296
            return $member;
297
        }
298
        return $this->createMemberModelWithUser($member->memberUser);
299
    }
300
301
    /**
302
     * Create member model with user, and set organization with this.
303
     * @param User|string|integer $user
304
     * @return Member
305
     */
306 36
    public function createMemberModelWithUser($user)
307
    {
308
        $config = [
309 36
            'memberUser' => $user,
310 36
            'organization' => $this,
311 36
            'nickname' => '',
312 36
        ];
313 36
        $member = $this->createMember($config);
314 36
        $member->nickname = $member->memberUser->profile->nickname;
315 36
        return $member;
316
    }
317
318
    /**
319
     * Remove member.
320
     * @param Member|User $member
321
     * @return boolean
322
     */
323 1
    public function removeMember(&$member)
324
    {
325 1
        if ($this->getIsNewRecord()) {
326
            return false;
327
        }
328 1
        if ($member instanceof $this->memberClass) {
329 1
            $member = $member->{$member->memberAttribute};
330 1
        }
331 1
        $member = $this->getMember($member);
332 1
        return $member && $member->delete() > 0;
333
    }
334
335
    /**
336
     * Remove administrator.
337
     * @param Member|User $member
338
     * @param boolean $keepMember Keep member after administrator being revoked.
339
     * @return boolean
340
     * @throws IntegrityException
341
     */
342
    public function removeAdministrator(&$member, $keepMember = true)
343
    {
344
        if ($this->getIsNewRecord()) {
345
            return false;
346
        }
347
        if ($member instanceof $this->memberClass) {
348
            $member = $member->{$member->memberAttribute};
349
        }
350
        $member = $this->getMember($member);
351
        if ($member && $member->isAdministrator()) {
352
            if ($keepMember) {
353
                return $member->revokeAdministrator();
354
            }
355
            return $this->removeMember($member);
356
        }
357
        return false;
358
    }
359
360
    /**
361
     * 
362
     * @param Event $event
363
     */
364 37
    public function onAddProfile($event)
365
    {
366 37
        $profile = $event->sender->createProfile($event->data);
367 37
        if (!$profile->save()) {
368
            throw new IntegrityException('Profile Save Failed.');
369
        }
370 37
        return true;
371
    }
372
373
    /**
374
     * 
375
     * @param Event $event
376
     */
377 37
    public function onAssignCreator($event)
378
    {
379 37
        return $event->sender->addCreator($event->data);
380
    }
381
382
    /**
383
     * 
384
     * @param Event $event
385
     * @return boolean
386
     */
387 16
    public function onRevokeCreator($event)
388
    {
389 16
        $sender = $event->sender;
390
        /* @var $sender static */
391 16
        $member = $sender->getMemberCreators()->one();
392
        /* @var $member Member */
393 16
        $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
394 16
        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...
395
    }
396
397
    /**
398
     * 
399
     * @param Event $event
400
     * @return boolean
401
     */
402 16
    public function onRevokeAdministrators($event)
403
    {
404 16
        $sender = $event->sender;
405
        /* @var $sender static */
406 16
        $members = $sender->getMemberAdministrators()->all();
407
        /* @var $members Member[] */
408 16
        foreach ($members as $member)
409
        {
410
            $member->revokeAdministrator();
411 16
        }
412 16
        return true;
413
    }
414
415
    /**
416
     * 
417
     * @param Event $event
418
     */
419 16
    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...
420
    {
421
        
422 16
    }
423
424
    /**
425
     * Check whether current instance is an organization.
426
     * @return boolean
427
     */
428 2
    public function isOrganization()
429
    {
430 2
        return $this->type == static::TYPE_ORGANIZATION;
431
    }
432
433
    /**
434
     * Check whether current instance if a department.
435
     * @return boolean
436
     */
437 2
    public function isDepartment()
438
    {
439 2
        return $this->type == static::TYPE_DEPARTMENT;
440
    }
441
442
    /**
443
     * Check whether the current organization has a member.
444
     * @param User|string|integer $user User instance, GUID or ID.
445
     * @return boolean
446
     */
447 36
    public function hasMember($user)
448
    {
449 36
        return !is_null($this->getMember($user));
450
    }
451
452
    /**
453
     * Get member query which role is specified `Creator`.
454
     * @return MemberQuery
455
     */
456 17
    public function getMemberCreators()
457
    {
458 17
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
459
    }
460
461
    /**
462
     * Get member query which role is specified `Administrator`.
463
     * @return MemberQuery
464
     */
465 17
    public function getMemberAdministrators()
466
    {
467 17
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
468
    }
469
470
    /**
471
     * Get user query which role is specified `Creator`.
472
     * @return BaseUserQuery
473
     */
474 1
    public function getCreator()
475
    {
476 1
        $noInit = $this->getNoInitMember();
477 1
        $class = $noInit->memberUserClass;
478 1
        $noInitUser = $class::buildNoInitModel();
479 1
        return $this->hasOne($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
480
    }
481
482
    /**
483
     * Get user query which role is specified `Administrator`.
484
     * @return BaseUserQuery
485
     */
486 1
    public function getAdministrators()
487
    {
488 1
        $noInit = $this->getNoInitMember();
489 1
        $class = $noInit->memberUserClass;
490 1
        $noInitUser = $class::buildNoInitModel();
491 1
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
492
    }
493
494
    /**
495
     * 
496
     * @param User $user
497
     * @return boolean
498
     * @throws \Exception
499
     * @throws IntegrityException
500
     */
501 37
    protected function addCreator($user)
502
    {
503 37
        if (!$user) {
504 1
            throw new InvalidParamException('Creator Invalid.');
505
        }
506 36
        $member = $user;
507 36
        $transaction = Yii::$app->db->beginTransaction();
508
        try {
509 36
            if (!$this->addMember($member)) {
510
                throw new IntegrityException('Failed to add member.');
511
            }
512 36
            $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
513 36
            $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...
514 36
            if (!$member->save()) {
515
                throw new IntegrityException('Failed to assign creator.');
516
            }
517 36
            $transaction->commit();
518 36
        } catch (\Exception $ex) {
519
            $transaction->rollBack();
520
            Yii::error($ex->getMessage(), __METHOD__);
521
            throw $ex;
522
        }
523 36
        return true;
524
    }
525
526
    /**
527
     * 
528
     * @param User $user
529
     * @return boolean
530
     * @throws \Exception
531
     * @throws IntegrityException
532
     */
533 7
    public function addAdministrator($user)
534
    {
535 7
        $transaction = Yii::$app->db->beginTransaction();
536
        try {
537 7
            if (!$this->hasMember($user) && !$this->addMember($user)) {
538
                throw new IntegrityException('Failed to add member.');
539
            }
540 7
            $member = $this->getMember($user);
541 7
            $member->assignAdministrator();
542 7
            $transaction->commit();
543 7
        } catch (\Exception $ex) {
544
            $transaction->rollBack();
545
            Yii::error($ex->getMessage(), __METHOD__);
546
            throw $ex;
547
        }
548 7
        return true;
549
    }
550
551
    /**
552
     * 
553
     * @param type $user
554
     * @return boolean
555
     */
556 2
    public function hasAdministrator($user)
557
    {
558 2
        $member = $this->getMember($user);
559 2
        if (!$member) {
560
            return false;
561
        }
562 2
        return $member->isAdministrator();
563
    }
564
565
    /**
566
     * Check whether this organization has reached the upper limit of subordinates.
567
     * @return boolean
568
     */
569
    public function hasReachedSubordinateLimit()
570
    {
571
        $class = $this->subordinateLimitClass;
572
        if (empty($class)) {
573
            return false;
574
        }
575
        $limit = $class::getLimit($this);
576
        if ($limit === false) {
577
            return false;
578
        }
579
        $count = (int)$this->getChildren()->count();
580
        return $count >= $limit->limit;
581
    }
582
583
    /**
584
     * Check whether this organization has reached the upper limit of members.
585
     * @return boolean
586
     */
587
    public function hasReachedMemberLimit()
588
    {
589
        $class = $this->memberLimitClass;
590
        if (empty($class)) {
591
            return false;
592
        }
593
        $limit = $class::getLimit($this);
594
        if ($limit === false) {
595
            return false;
596
        }
597
        $count = (int)$this->getMembers()->count();
598
        return $count >= $limit->limit;
599
    }
600
}
601