Completed
Push — master ( 9ed145...8e4a93 )
by vistart
04:04
created

Organization::find()   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 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
269
     * @see createMemberModel
270
     * @see createMemberModelWithUser
271
     * @return boolean
272
     */
273 36
    public function addMember(&$member)
274
    {
275 36
        if ($this->getIsNewRecord()) {
276
            return false;
277
        }
278 36
        $model = null;
279 36
        if ($member instanceof Member) {
280
            if (!$member->getIsNewRecord()) {
281
                return false;
282
            }
283
            $model = $this->createMemberModel($member);
284
        }
285 36
        if (($member instanceof User) || is_string($member) || is_int($member)) {
286 36
            if ($this->hasMember($member)) {
287
                return false;
288
            }
289 36
            $model = $this->createMemberModelWithUser($member);
290
        }
291 36
        $member = $model;
292 36
        return ($member instanceof Member) ? $member->save() : false;
293
    }
294
295
    /**
296
     * Create member model, and set organization with this.
297
     * @param Member $member If this parameter is not new record, it's organization
298
     * will be set with this, and return it. Otherwise, it will extract `User`
299
     * model and create new `Member` model.
300
     * @see createMemberModelWithUser
301
     * @return Member
302
     */
303
    public function createMemberModel($member)
304
    {
305
        if (!$member->getIsNewRecord()) {
306
            $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...
307
            return $member;
308
        }
309
        return $this->createMemberModelWithUser($member->memberUser);
310
    }
311
312
    /**
313
     * Create member model with user, and set organization with this.
314
     * @param User|string|integer $user
315
     * @return Member
316
     */
317 36
    public function createMemberModelWithUser($user)
318
    {
319
        $config = [
320 36
            'memberUser' => $user,
321 36
            'organization' => $this,
322 36
            'nickname' => '',
323
        ];
324 36
        $member = $this->createMember($config);
325 36
        $member->nickname = $member->memberUser->profile->nickname;
326 36
        return $member;
327
    }
328
329
    /**
330
     * Remove member.
331
     * Note: the creator cannot be removed.
332
     * @param Member|User $member
333
     * @return boolean
334
     */
335 1
    public function removeMember(&$member)
336
    {
337 1
        if ($this->getIsNewRecord()) {
338
            return false;
339
        }
340 1
        if ($member instanceof $this->memberClass) {
341 1
            $member = $member->{$member->memberAttribute};
342
        }
343 1
        $member = $this->getMember($member);
344 1
        if (!$member || $member->isCreator()) {
345
            return false;
346
        }
347 1
        return $member->delete() > 0;
348
    }
349
350
    /**
351
     * Remove administrator.
352
     * @param Member|User $member
353
     * @param boolean $keepMember Keep member after administrator being revoked.
354
     * @return boolean
355
     * @throws IntegrityException
356
     */
357
    public function removeAdministrator(&$member, $keepMember = true)
358
    {
359
        if ($this->getIsNewRecord()) {
360
            return false;
361
        }
362
        if ($member instanceof $this->memberClass) {
363
            $member = $member->{$member->memberAttribute};
364
        }
365
        $member = $this->getMember($member);
366
        if ($member && $member->isAdministrator()) {
367
            if ($keepMember) {
368
                return $member->revokeAdministrator();
369
            }
370
            return $this->removeMember($member);
371
        }
372
        return false;
373
    }
374
375
    /**
376
     * 
377
     * @param Event $event
378
     */
379 37
    public function onAddProfile($event)
380
    {
381 37
        $profile = $event->sender->createProfile($event->data);
382 37
        if (!$profile->save()) {
383
            throw new IntegrityException('Profile Save Failed.');
384
        }
385 37
        return true;
386
    }
387
388
    /**
389
     * 
390
     * @param Event $event
391
     */
392 37
    public function onAssignCreator($event)
393
    {
394 37
        return $event->sender->addCreator($event->data);
395
    }
396
397
    /**
398
     * 
399
     * @param Event $event
400
     * @return boolean
401
     */
402 17
    public function onRevokeCreator($event)
403
    {
404 17
        $sender = $event->sender;
405
        /* @var $sender static */
406 17
        $member = $sender->getMemberCreators()->one();
407
        /* @var $member Member */
408 17
        $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
409 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...
410
    }
411
412
    /**
413
     * 
414
     * @param Event $event
415
     * @return boolean
416
     */
417 17
    public function onRevokeAdministrators($event)
418
    {
419 17
        $sender = $event->sender;
420
        /* @var $sender static */
421 17
        $members = $sender->getMemberAdministrators()->all();
422
        /* @var $members Member[] */
423 17
        foreach ($members as $member)
424
        {
425 1
            $member->revokeAdministrator();
426
        }
427 17
        return true;
428
    }
429
430
    /**
431
     * 
432
     * @param Event $event
433
     */
434 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...
435
    {
436
        
437 17
    }
438
439
    /**
440
     * Check whether current instance is an organization.
441
     * @return boolean
442
     */
443 2
    public function isOrganization()
444
    {
445 2
        return $this->type == static::TYPE_ORGANIZATION;
446
    }
447
448
    /**
449
     * Check whether current instance if a department.
450
     * @return boolean
451
     */
452 2
    public function isDepartment()
453
    {
454 2
        return $this->type == static::TYPE_DEPARTMENT;
455
    }
456
457
    /**
458
     * Check whether the current organization has a member.
459
     * @param User|string|integer $user User instance, GUID or ID.
460
     * @return boolean
461
     */
462 36
    public function hasMember($user)
463
    {
464 36
        return !is_null($this->getMember($user));
465
    }
466
467
    /**
468
     * Get member query which role is specified `Creator`.
469
     * @return MemberQuery
470
     */
471 18
    public function getMemberCreators()
472
    {
473 18
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
474
    }
475
476
    /**
477
     * Get member query which role is specified `Administrator`.
478
     * @return MemberQuery
479
     */
480 18
    public function getMemberAdministrators()
481
    {
482 18
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
483
    }
484
485
    /**
486
     * Get user query which role is specified `Creator`.
487
     * @return BaseUserQuery
488
     */
489 1
    public function getCreator()
490
    {
491 1
        $noInit = $this->getNoInitMember();
492 1
        $class = $noInit->memberUserClass;
493 1
        $noInitUser = $class::buildNoInitModel();
494 1
        return $this->hasOne($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
495
    }
496
497
    /**
498
     * Get user query which role is specified `Administrator`.
499
     * @return BaseUserQuery
500
     */
501 1
    public function getAdministrators()
502
    {
503 1
        $noInit = $this->getNoInitMember();
504 1
        $class = $noInit->memberUserClass;
505 1
        $noInitUser = $class::buildNoInitModel();
506 1
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
507
    }
508
509
    /**
510
     * 
511
     * @param User $user
512
     * @return boolean
513
     * @throws \Exception
514
     * @throws IntegrityException
515
     */
516 37
    protected function addCreator($user)
517
    {
518 37
        if (!$user) {
519 1
            throw new InvalidParamException('Creator Invalid.');
520
        }
521 36
        $member = $user;
522 36
        $transaction = Yii::$app->db->beginTransaction();
523
        try {
524 36
            if (!$this->addMember($member)) {
525
                throw new IntegrityException('Failed to add member.');
526
            }
527 36
            $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
528 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...
529 36
            if (!$member->save()) {
530
                throw new IntegrityException('Failed to assign creator.');
531
            }
532 36
            $transaction->commit();
533
        } catch (\Exception $ex) {
534
            $transaction->rollBack();
535
            Yii::error($ex->getMessage(), __METHOD__);
536
            throw $ex;
537
        }
538 36
        return true;
539
    }
540
541
    /**
542
     * 
543
     * @param User $user
544
     * @return boolean
545
     * @throws \Exception
546
     * @throws IntegrityException
547
     */
548 7
    public function addAdministrator($user)
549
    {
550 7
        $transaction = Yii::$app->db->beginTransaction();
551
        try {
552 7
            if (!$this->hasMember($user) && !$this->addMember($user)) {
553
                throw new IntegrityException('Failed to add member.');
554
            }
555 7
            $member = $this->getMember($user);
556 7
            $member->assignAdministrator();
557 7
            $transaction->commit();
558
        } catch (\Exception $ex) {
559
            $transaction->rollBack();
560
            Yii::error($ex->getMessage(), __METHOD__);
561
            throw $ex;
562
        }
563 7
        return true;
564
    }
565
566
    /**
567
     * 
568
     * @param type $user
569
     * @return boolean
570
     */
571 2
    public function hasAdministrator($user)
572
    {
573 2
        $member = $this->getMember($user);
574 2
        if (!$member) {
575
            return false;
576
        }
577 2
        return $member->isAdministrator();
578
    }
579
580
    /**
581
     * Check whether this organization has reached the upper limit of subordinates.
582
     * @return boolean
583
     */
584
    public function hasReachedSubordinateLimit()
585
    {
586
        $class = $this->subordinateLimitClass;
587
        if (empty($class)) {
588
            return false;
589
        }
590
        $limit = $class::getLimit($this);
591
        if ($limit === false) {
592
            return false;
593
        }
594
        $count = (int)$this->getChildren()->count();
595
        return $count >= $limit->limit;
596
    }
597
598
    /**
599
     * Check whether this organization has reached the upper limit of members.
600
     * @return boolean
601
     */
602
    public function hasReachedMemberLimit()
603
    {
604
        $class = $this->memberLimitClass;
605
        if (empty($class)) {
606
            return false;
607
        }
608
        $limit = $class::getLimit($this);
609
        if ($limit === false) {
610
            return false;
611
        }
612
        $count = (int)$this->getMembers()->count();
613
        return $count >= $limit->limit;
614
    }
615
}
616