Completed
Push — master ( 88044c...e67d17 )
by vistart
05:48
created

Organization::attributeLabels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

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