Completed
Push — master ( efd6c7...e843b5 )
by vistart
04:27
created

Organization::getSearchModel()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 9.4285
cc 3
eloc 5
nc 2
nop 0
crap 12
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
     * @return Member
112
     */
113 39
    protected function getNoInitMember()
114
    {
115 39
        if (!$this->noInitMember) {
116 39
            $class = $this->memberClass;
117 39
            $this->noInitMember = $class::buildNoInitModel();
118
        }
119 39
        return $this->noInitMember;
120
    }
121
122
    /**
123
     * @return SubordinateLimit
124
     */
125 2
    protected function getNoInitSubordinateLimit()
126
    {
127 2
        if (!$this->noInitSubordinateLimit) {
128 2
            $class = $this->subordinateLimitClass;
129 2
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
130
        }
131 2
        return $this->noInitSubordinateLimit;
132
    }
133
134
    /**
135
     * @return MemberLimit
136
     */
137 1
    protected function getNoInitMemberLimit()
138
    {
139 1
        if (!$this->noInitMemberLimit) {
140 1
            $class = $this->memberLimitClass;
141 1
            $this->noInitMemberLimit = $class::buildNoInitModel();
142
        }
143 1
        return $this->noInitMemberLimit;
144
    }
145
146
    /**
147
     * @return null|OrganizationSearch
148
     */
149
    public function getSearchModel()
150
    {
151
        $class = $this->searchClass;
152
        if (empty($class) || !class_exists($class)) {
153
            return null;
154
        }
155
        return new $class;
156
    }
157
158 41
    public function init()
159
    {
160 41
        $this->parentAttribute = 'parent_guid';
161 41
        if (class_exists($this->memberClass)) {
162 41
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
163
        }
164 41
        if ($this->skipInit) {
165 41
            return;
166
        }
167 41
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
168 41
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
169 41
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
170 41
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
171 41
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
172 41
        $this->initSelfBlameableEvents();
173 41
        parent::init();
174 41
    }
175
176
    /**
177
     * @inheritdoc
178
     */
179 1
    public function attributeLabels()
180
    {
181
        return [
182 1
            'guid' => Yii::t('user', 'GUID'),
183 1
            'id' => Yii::t('user', 'ID'),
184 1
            'ip' => Yii::t('user', 'IP Address'),
185 1
            'ip_type' => Yii::t('user', 'IP Address Type'),
186 1
            'parent' => Yii::t('organization', 'Parent'),
187 1
            'created_at' => Yii::t('user', 'Creation Time'),
188 1
            'updated_at' => Yii::t('user', 'Last Updated Time'),
189 1
            'status' => Yii::t('user', 'Status'),
190 1
            'type' => Yii::t('user', 'Type'),
191
        ];
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197 41
    public static function tableName()
198
    {
199 41
        return '{{%organization}}';
200
    }
201
202
    /**
203
     * Find.
204
     * Friendly to IDE.
205
     * @return OrganizationQuery
206
     */
207 41
    public static function find()
208
    {
209 41
        return parent::find();
210
    }
211
212 40
    protected function getTypeRules()
213
    {
214
        return [
215 40
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
216
            ['type', 'required'],
217 40
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
218
        ];
219
    }
220
221 40
    public function rules()
222
    {
223 40
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
224
    }
225
226
    /**
227
     * Get Member Query.
228
     * @return MemberQuery
229
     */
230 39
    public function getMembers()
231
    {
232 39
        return $this->hasMany($this->memberClass, [$this->getNoInitMember()->createdByAttribute => $this->guidAttribute])->inverseOf('organization');
233
    }
234
235
    /**
236
     * Get organization member users' query.
237
     * @return BaseUserQuery
238
     */
239 4
    public function getMemberUsers()
240
    {
241 4
        $noInit = $this->getNoInitMember();
242 4
        $class = $noInit->memberUserClass;
243 4
        $noInitUser = $class::buildNoInitModel();
244 4
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('members')->inverseOf('atOrganizations');
245
    }
246
247
    /**
248
     * Get subordinate limit query.
249
     * @return null|BaseBlameableQuery
250
     */
251 2
    public function getSubordinateLimit()
252
    {
253 2
        if (empty($this->subordinateLimitClass)) {
254
            return null;
255
        }
256 2
        return $this->hasOne($this->subordinateLimitClass, [$this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute]);
257
    }
258
259
    /**
260
     * Get member limit query.
261
     * @return null|BaseBlameableQuery
262
     */
263 1
    public function getMemberLimit()
264
    {
265 1
        if (empty($this->memberLimitClass)) {
266
            return null;
267
        }
268 1
        return $this->hasOne($this->memberLimitClass, [$this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute]);
269
    }
270
271
    /**
272
     * Get member with specified user.
273
     * @param User|string|integer $user
274
     * @return Member Null if `user` is not in this organization.
275
     */
276 39
    public function getMember($user)
277
    {
278 39
        return $this->getMembers()->user($user)->one();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getMembers()->user($user)->one(); of type yii\db\ActiveRecord|array|null adds the type array to the return on line 278 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
279
    }
280
281
    /**
282
     * Add member to organization.
283
     * @param Member|User|string|integer $member Member or User model, or User ID or GUID.
284
     * If member is created, it will be re-assigned to this parameter.
285
     * @see createMemberModel
286
     * @see createMemberModelWithUser
287
     * @return boolean
288
     */
289 39
    public function addMember(&$member)
290
    {
291 39
        if ($this->getIsNewRecord()) {
292
            return false;
293
        }
294 39
        if ($this->hasReachedMemberLimit()) {
295 1
            return false;
296
        }
297 39
        $model = null;
298 39
        if ($member instanceof Member) {
299
            if (!$member->getIsNewRecord()) {
300
                return false;
301
            }
302
            $model = $this->createMemberModel($member);
303
        }
304 39
        if (($member instanceof User) || is_string($member) || is_int($member)) {
305 39
            if ($this->hasMember($member)) {
306
                return false;
307
            }
308 39
            $model = $this->createMemberModelWithUser($member);
309
        }
310 39
        $member = $model;
311 39
        return ($member instanceof Member) ? $member->save() : false;
312
    }
313
314
    /**
315
     * Create member model, and set organization with this.
316
     * @param Member $member If this parameter is not new record, it's organization
317
     * will be set with this, and return it. Otherwise, it will extract `User`
318
     * model and create new `Member` model.
319
     * @see createMemberModelWithUser
320
     * @return Member
321
     */
322
    public function createMemberModel($member)
323
    {
324
        if (!$member->getIsNewRecord()) {
325
            $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...
326
            return $member;
327
        }
328
        return $this->createMemberModelWithUser($member->memberUser);
329
    }
330
331
    /**
332
     * Create member model with user, and set organization with this.
333
     * @param User|string|integer $user
334
     * @return Member
335
     */
336 39
    public function createMemberModelWithUser($user)
337
    {
338
        $config = [
339 39
            'memberUser' => $user,
340 39
            'organization' => $this,
341 39
            'nickname' => '',
342
        ];
343 39
        $member = $this->createMember($config);
344 39
        $member->nickname = $member->memberUser->profile->nickname;
345 39
        return $member;
346
    }
347
348
    /**
349
     * Remove member.
350
     * Note: the creator cannot be removed.
351
     * @param Member|User $member
352
     * @return boolean
353
     */
354 1
    public function removeMember(&$member)
355
    {
356 1
        if ($this->getIsNewRecord()) {
357
            return false;
358
        }
359 1
        if ($member instanceof $this->memberClass) {
360 1
            $member = $member->{$member->memberAttribute};
361
        }
362 1
        $member = $this->getMember($member);
363 1
        if (!$member || $member->isCreator()) {
364
            return false;
365
        }
366 1
        return $member->delete() > 0;
367
    }
368
369
    /**
370
     * Remove administrator.
371
     * @param Member|User $member
372
     * @param boolean $keepMember Keep member after administrator being revoked.
373
     * @return boolean
374
     * @throws IntegrityException
375
     */
376
    public function removeAdministrator(&$member, $keepMember = true)
377
    {
378
        if ($this->getIsNewRecord()) {
379
            return false;
380
        }
381
        if ($member instanceof $this->memberClass) {
382
            $member = $member->{$member->memberAttribute};
383
        }
384
        $member = $this->getMember($member);
385
        if ($member && $member->isAdministrator()) {
386
            if ($keepMember) {
387
                return $member->revokeAdministrator();
388
            }
389
            return $this->removeMember($member);
390
        }
391
        return false;
392
    }
393
394
    /**
395
     * 
396
     * @param Event $event
397
     */
398 40
    public function onAddProfile($event)
399
    {
400 40
        $profile = $event->sender->createProfile($event->data);
401 40
        if (!$profile->save()) {
402
            throw new IntegrityException('Profile Save Failed.');
403
        }
404 40
        return true;
405
    }
406
407
    /**
408
     * 
409
     * @param Event $event
410
     */
411 40
    public function onAssignCreator($event)
412
    {
413 40
        return $event->sender->addCreator($event->data);
414
    }
415
416
    /**
417
     * 
418
     * @param Event $event
419
     * @return boolean
420
     */
421 20
    public function onRevokeCreator($event)
422
    {
423 20
        $sender = $event->sender;
424
        /* @var $sender static */
425 20
        $member = $sender->getMemberCreators()->one();
426
        /* @var $member Member */
427 20
        $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
428 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...
429
    }
430
431
    /**
432
     * 
433
     * @param Event $event
434
     * @return boolean
435
     */
436 20
    public function onRevokeAdministrators($event)
437
    {
438 20
        $sender = $event->sender;
439
        /* @var $sender static */
440 20
        $members = $sender->getMemberAdministrators()->all();
441
        /* @var $members Member[] */
442 20
        foreach ($members as $member)
443
        {
444 1
            $member->revokeAdministrator();
445
        }
446 20
        return true;
447
    }
448
449
    /**
450
     * 
451
     * @param Event $event
452
     */
453 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...
454
    {
455
        
456 20
    }
457
458
    /**
459
     * Check whether current instance is an organization.
460
     * @return boolean
461
     */
462 2
    public function isOrganization()
463
    {
464 2
        return $this->type == static::TYPE_ORGANIZATION;
465
    }
466
467
    /**
468
     * Check whether current instance if a department.
469
     * @return boolean
470
     */
471 2
    public function isDepartment()
472
    {
473 2
        return $this->type == static::TYPE_DEPARTMENT;
474
    }
475
476
    /**
477
     * Check whether the current organization has a member.
478
     * @param User|string|integer $user User instance, GUID or ID.
479
     * @return boolean
480
     */
481 39
    public function hasMember($user)
482
    {
483 39
        return !is_null($this->getMember($user));
484
    }
485
486
    /**
487
     * Get member query which role is specified `Creator`.
488
     * @return MemberQuery
489
     */
490 21
    public function getMemberCreators()
491
    {
492 21
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
493
    }
494
495
    /**
496
     * Get member query which role is specified `Administrator`.
497
     * @return MemberQuery
498
     */
499 21
    public function getMemberAdministrators()
500
    {
501 21
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
502
    }
503
504
    /**
505
     * Get user query which role is specified `Creator`.
506
     * @return BaseUserQuery
507
     */
508 1
    public function getCreator()
509
    {
510 1
        $noInit = $this->getNoInitMember();
511 1
        $class = $noInit->memberUserClass;
512 1
        $noInitUser = $class::buildNoInitModel();
513 1
        return $this->hasOne($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
514
    }
515
516
    /**
517
     * Get user query which role is specified `Administrator`.
518
     * @return BaseUserQuery
519
     */
520 1
    public function getAdministrators()
521
    {
522 1
        $noInit = $this->getNoInitMember();
523 1
        $class = $noInit->memberUserClass;
524 1
        $noInitUser = $class::buildNoInitModel();
525 1
        return $this->hasMany($class, [$noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
526
    }
527
528
    /**
529
     * 
530
     * @param User $user
531
     * @return boolean
532
     * @throws \Exception
533
     * @throws IntegrityException
534
     */
535 40
    protected function addCreator($user)
536
    {
537 40
        if (!$user) {
538 1
            throw new InvalidParamException('Creator Invalid.');
539
        }
540 39
        $member = $user;
541 39
        $transaction = Yii::$app->db->beginTransaction();
542
        try {
543 39
            if (!$this->addMember($member)) {
544
                throw new IntegrityException('Failed to add member.');
545
            }
546 39
            $role = $this->type == static::TYPE_ORGANIZATION ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
547 39
            $member->assignRole($role);
0 ignored issues
show
Documentation introduced by
$role is of type string, but the function expects a object<rhosocial\user\rbac\Role>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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