Organization   F
last analyzed

Complexity

Total Complexity 153

Size/Duplication

Total Lines 1036
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 21

Test Coverage

Coverage 75.41%

Importance

Changes 2
Bugs 0 Features 2
Metric Value
wmc 153
c 2
b 0
f 2
lcom 2
cbo 21
dl 0
loc 1036
ccs 279
cts 370
cp 0.7541
rs 1.0357

60 Methods

Rating   Name   Duplication   Size   Complexity  
A getNoInitMember() 0 8 2
A getNoInitSubordinateLimit() 0 8 2
A getNoInitMemberLimit() 0 8 2
A getNoInitOrganizationSetting() 0 11 3
A getSearchModel() 0 8 3
A init() 0 17 3
A attributeLabels() 0 18 1
A tableName() 0 4 1
A find() 0 4 1
A getTypeRules() 0 8 1
A rules() 0 4 1
A getMembers() 0 6 1
A getMemberUsers() 0 9 1
A getSubordinateLimit() 0 9 2
A getMemberLimit() 0 9 2
A getSettings() 0 11 4
B setSetting() 0 24 6
A getMember() 0 4 1
C addMember() 0 54 26
A createMemberModel() 0 8 2
A createMemberModelWithUser() 0 11 1
B removeMember() 0 17 5
B removeAdministrator() 0 17 6
A onAddProfile() 0 8 2
A onAssignCreator() 0 4 1
A onRevokeCreator() 0 9 2
A onRevokeAdministrators() 0 12 2
A onRevokePermissions() 0 4 1
A isOrganization() 0 4 1
A isDepartment() 0 4 1
A hasMember() 0 4 1
A getMemberCreators() 0 4 1
A getMemberAdministrators() 0 4 1
A getCreator() 0 9 1
A getAdministrators() 0 9 1
B addCreator() 0 24 6
A addAdministrator() 0 17 4
A hasAdministrator() 0 8 2
A hasReachedSubordinateLimit() 0 8 2
A getRemainingSubordinatePlaces() 0 13 3
A hasReachedMemberLimit() 0 8 2
A getRemainingMemberPlaces() 0 13 3
A getIsExcludeOtherMembers() 0 9 2
A setIsExcludeOtherMembers() 0 4 2
A getIsDisallowMemberJoinOther() 0 9 2
A setIsDisallowMemberJoinOther() 0 4 2
A getIsOnlyAcceptCurrentOrgMember() 0 9 2
A setIsOnlyAcceptCurrentOrgMember() 0 4 2
A getIsOnlyAcceptSuperiorOrgMember() 0 12 4
A setIsOnlyAcceptSuperiorOrgMember() 0 7 4
A getJoinPassword() 0 9 2
A setJoinPassword() 0 4 1
A getJoinIpAddress() 0 9 2
A setJoinIpAddress() 0 4 1
A getJoinEntranceUrl() 0 9 2
A setJoinEntranceUrl() 0 4 1
A getExitAllowWithdrawActively() 0 9 2
A setExitAllowWithdrawActively() 0 4 2
A getTopOrganization() 0 8 2
A hasMemberInSubordinates() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like Organization often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Organization, and based on these observations, apply Extract Interface, too.

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\organization\exceptions\DisallowMemberJoinOtherException;
19
use rhosocial\organization\exceptions\ExcludeOtherMembersException;
20
use rhosocial\organization\exceptions\OnlyAcceptCurrentOrgMemberException;
21
use rhosocial\organization\exceptions\OnlyAcceptSuperiorOrgMemberException;
22
use rhosocial\organization\rbac\roles\DepartmentAdmin;
23
use rhosocial\organization\rbac\roles\DepartmentCreator;
24
use rhosocial\organization\rbac\roles\OrganizationAdmin;
25
use rhosocial\organization\rbac\roles\OrganizationCreator;
26
use rhosocial\organization\queries\MemberQuery;
27
use rhosocial\organization\queries\OrganizationQuery;
28
use rhosocial\user\User;
29
use Yii;
30
use yii\base\Event;
31
use yii\base\InvalidParamException;
32
use yii\db\IntegrityException;
33
34
/**
35
 * Organization.
36
 * This class is used to describe an organization or department, depending on the type property.
37
 * Organization or department should be created by the user, it is best not to directly implement their own such.
38
 *
39
 * In general, the organization needs to have `setUpOrganization` permission, and the user does not have this permission
40
 * by default. You need to give this permission to the user who created the organization in advance.
41
 * Department, affiliated with the organization or other department, also need the appropriate permission to set up.
42
 *
43
 * While this can work independently, we still strongly recommend that you declare the Organization class yourself and
44
 * inherit this.
45
 * Then you need to specify the Profile and Member class yourself, like following:
46
```php
47
class Organization extends \rhosocial\organization\Organization
48
{
49
    public $profileClass = Profile::class;
50
    public $memberClass = Member::class;
51
}
52
```
53
 * If you need to limit the number of subordinates, the number of members, you need to specify the appropriate class
54
 * name.
55
 * If there is no limit, you need to set it to false manually.
56
 *
57
 * @method Member createMember(array $config) Create member who is subordinate to this.
58
 * @property int $type Whether indicate this instance is an organization or a department.
59
 *
60
 * @property bool $isExcludeOtherMembers Determine whether the other organization and its subordinate departments
61
 * members could join in the current organization and its subordinate departments. (Only fit for Organization)
62
 * @property bool $isDisallowMemberJoinOther Determine whether the current organization and its subordinate
63
 * departments members could join in the other organization and its subordinate departments. (Only fit for Organization)
64
 * @property bool $isOnlyAcceptCurrentOrgMember Determine whether the current department only accept the member of
65
 * the top level organization. (Only fit for Department)
66
 * @property bool $isOnlyAcceptSuperiorOrgMember Determine whether the current department only accept the member of
67
 * the superior organization or department. (Only fit for Department)
68
 * @property string $joinPassword
69
 * @property string $joinIpAddress
70
 * @property string $joinEntranceUrl
71
 * @property bool $exitAllowWithdrawActively
72
 *
73
 * @property-read Member[] $members Get all member models of this organization/department.
74
 * @property-read User[] $memberUsers Get all members of this organization/department.
75
 * @property-read User $creator Get creator of this organization/department.
76
 * @property-read User[] $administrators Get administrators of this organization/department.
77
 * @property-read SubordinateLimit subordinateLimit
78
 * @property-read MemberLimit memberLimit
79
 * @property-read static|null $topOrganization The top level organization of current organization or departments.
80
 * @property-read Profile $profile Get profile model. Friendly to IDE.
81
 * @property-read OrganizationSetting[] $settings Get all settings.
82
 *
83
 * @version 1.0
84
 * @author vistart <[email protected]>
85
 */
86
class Organization extends User
87
{
88
    use SelfBlameableTrait;
89
90
    const TYPE_ORGANIZATION = 1;
91
    const TYPE_DEPARTMENT = 2;
92
93
    /**
94
     * @var boolean Organization does not need password and corresponding features.
95
     */
96
    public $passwordHashAttribute = false;
97
98
    /**
99
     * @var boolean Organization does not need password and corresponding features.
100
     */
101
    public $passwordResetTokenAttribute = false;
102
103
    /**
104
     * @var boolean Organization does not need password and corresponding features.
105
     */
106
    public $passwordHistoryClass = false;
107
108
    /**
109
     * @var boolean Organization does not need source.
110
     */
111
    public $sourceAttribute = false;
112
113
    /**
114
     * @var boolean Organization does not need auth key.
115
     */
116
    public $authKeyAttribute = false;
117
118
    /**
119
     * @var boolean Organization does not need access token.
120
     */
121
    public $accessTokenAttribute = false;
122
123
    /**
124
     * @var boolean Organization does not need login log.
125
     */
126
    public $loginLogClass = false;
127
128
    /**
129
     * @var string The Organization Profile Class
130
     */
131
    public $profileClass = Profile::class;
132
133
    /**
134
     * @var string The Member Class.
135
     */
136
    public $memberClass = Member::class;
137
138
    /**
139
     * @var string The Subordinate Limit Class
140
     */
141
    public $subordinateLimitClass = SubordinateLimit::class;
142
143
    /**
144
     * @var string The Member Limit Class
145
     */
146
    public $memberLimitClass = MemberLimit::class;
147
148
    /**
149
     * @var string The Organization Search Class
150
     */
151
    public $searchClass = OrganizationSearch::class;
152
153
    /**
154
     * @var string The Organization Setting Class
155
     */
156
    public $organizationSettingClass = OrganizationSetting::class;
157
158
    /**
159
     * @var Member
160
     */
161
    private $noInitMember;
162
163
    /**
164
     * @var SubordinateLimit
165
     */
166
    private $noInitSubordinateLimit;
167
168
    /**
169
     * @var MemberLimit
170
     */
171
    private $noInitMemberLimit;
172
173
    /**
174
     * @var OrganizationSetting
175
     */
176
    private $noInitOrganizationSetting;
177
178
    /**
179
     * @var User the creator of current Organization or Department.
180
     * This property is only available after registration.
181
     * Please do not access it at other times.
182
     * If you want to get creator model except registration, please
183
     * access [[$creator]] magic-property instead.
184
     */
185
    public $creatorModel;
186
187
    /**
188
     * @var array The configuration array of Organization Profile.
189
     * This property is only available after registration.
190
     * Please do not access it at other times.
191
     * If you want to get profile model except registration, please
192
     * access [[$profile]] magic-property instead.
193
     */
194
    public $profileConfig;
195
196
    const EVENT_BEFORE_ADD_MEMBER = 'eventBeforeAddMember';
197
    const EVENT_AFTER_ADD_MEMBER = 'eventAfterAddMember';
198
    const EVENT_BEFORE_REMOVE_MEMBER = 'eventBeforeRemoveMember';
199
    const EVENT_AFTER_REMOVE_MEMBER = 'eventAfterRemoveMember';
200
201
    public $cacheTagPrefix = 'tag_organization_';
202
203
    /**
204
     * @return Member
205
     */
206 51
    public function getNoInitMember()
207
    {
208 51
        if (!$this->noInitMember) {
209 51
            $class = $this->memberClass;
210 51
            $this->noInitMember = $class::buildNoInitModel();
211
        }
212 51
        return $this->noInitMember;
213
    }
214
215
    /**
216
     * @return SubordinateLimit
217
     */
218 2
    public function getNoInitSubordinateLimit()
219
    {
220 2
        if (!$this->noInitSubordinateLimit) {
221 2
            $class = $this->subordinateLimitClass;
222 2
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
223
        }
224 2
        return $this->noInitSubordinateLimit;
225
    }
226
227
    /**
228
     * @return MemberLimit
229
     */
230 1
    public function getNoInitMemberLimit()
231
    {
232 1
        if (!$this->noInitMemberLimit) {
233 1
            $class = $this->memberLimitClass;
234 1
            $this->noInitMemberLimit = $class::buildNoInitModel();
235
        }
236 1
        return $this->noInitMemberLimit;
237
    }
238
239
    /**
240
     * @return null|OrganizationSetting
241
     */
242 31
    public function getNoInitOrganizationSetting()
243
    {
244 31
        if (!$this->noInitOrganizationSetting) {
245 31
            $class = $this->organizationSettingClass;
246 31
            if (empty($class)) {
247
                return null;
248
            }
249 31
            $this->noInitOrganizationSetting = $class::buildNoInitModel();
250
        }
251 31
        return $this->noInitOrganizationSetting;
252
    }
253
254
    /**
255
     * @return null|OrganizationSearch
256
     */
257
    public function getSearchModel()
258
    {
259
        $class = $this->searchClass;
260
        if (empty($class) || !class_exists($class)) {
261
            return null;
262
        }
263
        return new $class;
264
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269 52
    public function init()
270
    {
271 52
        $this->parentAttribute = 'parent_guid';
272 52
        if (class_exists($this->memberClass)) {
273 52
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
274
        }
275 52
        if ($this->skipInit) {
276 52
            return;
277
        }
278 52
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
279 52
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
280 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
281 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
282 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
283 52
        $this->initSelfBlameableEvents();
284 52
        parent::init();
285 52
    }
286
287
    /**
288
     * @inheritdoc
289
     */
290 1
    public function attributeLabels()
291
    {
292
        return [
293 1
            $this->guidAttribute => Yii::t('user', 'GUID'),
294 1
            $this->idAttribute => Yii::t('user', 'ID'),
295 1
            $this->ipAttribute => Yii::t('user', 'IP Address'),
296 1
            $this->ipTypeAttribute => Yii::t('user', 'IP Address Type'),
297 1
            $this->parentAttribute => Yii::t('organization', 'Parent'),
298 1
            $this->createdAtAttribute => Yii::t('user', 'Creation Time'),
299 1
            $this->updatedAtAttribute => Yii::t('user', 'Last Updated Time'),
300 1
            $this->statusAttribute => Yii::t('user', 'Status'),
301 1
            'type' => Yii::t('user', 'Type'),
302 1
            'isExcludeOtherMembers' => Yii::t('organization', 'Exclude Other Members'),
303 1
            'isDisallowMemberJoinOther' => Yii::t('organization', 'Disallow Member to Join in Other Organizations'),
304 1
            'isOnlyAcceptCurrentOrgMember' => Yii::t('organization', 'Only Accept Current Organization Members'),
305 1
            'isOnlyAcceptSuperiorOrgMember' => Yii::t('organization', 'Only Accept Superior Organization Members'),
306
        ];
307
    }
308
309
    /**
310
     * @inheritdoc
311
     */
312 52
    public static function tableName()
313
    {
314 52
        return '{{%organization}}';
315
    }
316
317
    /**
318
     * Find.
319
     * Friendly to IDE.
320
     * @return OrganizationQuery
321
     */
322 52
    public static function find()
323
    {
324 52
        return parent::find();
325
    }
326
327
    /**
328
     * Get rules associated with type attribute.
329
     * @return array
330
     */
331 51
    protected function getTypeRules()
332
    {
333
        return [
334 51
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
335
            ['type', 'required'],
336 51
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
337
        ];
338
    }
339
340
    /**
341
     * @inheritdoc
342
     */
343 51
    public function rules()
344
    {
345 51
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
346
    }
347
348
    /**
349
     * Get Member Query.
350
     * @return MemberQuery
351
     */
352 50
    public function getMembers()
353
    {
354 50
        return $this->hasMany($this->memberClass, [
355 50
            $this->getNoInitMember()->createdByAttribute => $this->guidAttribute
356 50
        ])->inverseOf('organization');
357
    }
358
359
    /**
360
     * Get organization member users' query.
361
     * @return BaseUserQuery
362
     */
363 6
    public function getMemberUsers()
364
    {
365 6
        $noInit = $this->getNoInitMember();
366 6
        $class = $noInit->memberUserClass;
367 6
        $noInitUser = $class::buildNoInitModel();
368 6
        return $this->hasMany($class, [
369 6
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
370 6
        ])->via('members')->inverseOf('atOrganizations');
371
    }
372
373
    /**
374
     * Get subordinate limit query.
375
     * @return null|BaseBlameableQuery
376
     */
377 2
    public function getSubordinateLimit()
378
    {
379 2
        if (empty($this->subordinateLimitClass)) {
380
            return null;
381
        }
382 2
        return $this->hasOne($this->subordinateLimitClass, [
383 2
            $this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute
384
        ]);
385
    }
386
387
    /**
388
     * Get member limit query.
389
     * @return null|BaseBlameableQuery
390
     */
391 1
    public function getMemberLimit()
392
    {
393 1
        if (empty($this->memberLimitClass)) {
394
            return null;
395
        }
396 1
        return $this->hasOne($this->memberLimitClass, [
397 1
            $this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute
398
        ]);
399
    }
400
401
    /**
402
     * @param string|null $item If you want to get all settings, please set it null.
403
     * @return null
404
     */
405 31
    public function getSettings($item = null)
406
    {
407 31
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
408
            return null;
409
        }
410 31
        $query = $this->hasMany($this->organizationSettingClass, [$this->getNoInitOrganizationSetting()->createdByAttribute => $this->guidAttribute]);
411 31
        if (!empty($item)) {
412 31
            $query = $query->andWhere([$this->getNoInitOrganizationSetting()->idAttribute => $item]);
413
        }
414 31
        return $query;
415
    }
416
417
    /**
418
     * Set organization setting.
419
     * @param string $item
420
     * @param string $value
421
     * @param bool $unique
422
     * @return bool|null Null if organization setting not enabled.
423
     * @throws IntegrityException throw if "item-value" unique broke.
424
     */
425 31
    public function setSetting($item, $value, $unique = false)
426
    {
427 31
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
428
            return null;
429
        }
430 31
        $setting = $this->getSettings($item)->one();
431
        /* @var $setting OrganizationSetting */
432 31
        if (!$setting) {
433 31
            $setting = $this->create($this->organizationSettingClass, [
434 31
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
435
            ]);
436
        }
437 31
        $setting->value = $value;
438 31
        if ($unique) {
439
            $class = $this->organizationSettingClass;
440
            if ($class::find()->andWhere([
441
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
442
                $this->getNoInitOrganizationSetting()->contentAttribute => $value
443
            ])->exists()) {
444
                throw new IntegrityException("`$item` : `$value` existed.");
445
            }
446
        }
447 31
        return $setting->save();
448
    }
449
450
    /**
451
     * Get member with specified user.
452
     * @param User|string|integer $user
453
     * @return Member Null if `user` is not in this organization.
454
     */
455 50
    public function getMember($user)
456
    {
457 50
        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 457 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
458
    }
459
460
    /**
461
     * Add member to organization.
462
     * @param Member|User|string|integer $member Member or User model, or User ID or GUID.
463
     * If member is created, it will be re-assigned to this parameter.
464
     * @see createMemberModel
465
     * @see createMemberModelWithUser
466
     * @return boolean
467
     * @throws DisallowMemberJoinOtherException
468
     * @throws ExcludeOtherMembersException
469
     * @throws OnlyAcceptCurrentOrgMemberException
470
     * @throws OnlyAcceptSuperiorOrgMemberException
471
     */
472 50
    public function addMember(&$member)
473
    {
474 50
        if ($this->getIsNewRecord()) {
475
            return false;
476
        }
477 50
        if ($this->hasReachedMemberLimit()) {
478 1
            return false;
479
        }
480 50
        $user = null;
481 50
        if ($member instanceof Member) {
482
            if ($member->getIsNewRecord()) {
483
                return false;
484
            }
485
            $user = $member->memberUser;
486
        }
487 50
        if ($member instanceof User) {
488 50
            $user = $member;
489
        }
490 50
        if (is_string($member) || is_int($member)) {
491
            $class = Yii::$app->user->identityClass;
492
            $user = $class::find()->guidOrId($member)->one();
493
        }
494 50
        if ($this->hasMember($user)) {
495
            return false;
496
        }
497 50
        $orgs = $user->getAtOrganizations()->all();
498
        /* @var $orgs Organization[] */
499 50
        foreach ($orgs as $org) {
500 31
            if ($org->topOrganization->isDisallowMemberJoinOther && !$org->topOrganization->equals($this->topOrganization)) {
0 ignored issues
show
Bug introduced by
It seems like $this->topOrganization can be null; however, equals() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
501 1
                throw new DisallowMemberJoinOtherException(Yii::t('organization', "An organization in which the user is located does not allow its members to join other organizations."));
502
            }
503 31
            if ($this->topOrganization->isExcludeOtherMembers && !$org->topOrganization->equals($this->topOrganization)) {
504 31
                throw new ExcludeOtherMembersException(Yii::t('organization', "The organization does not allow users who have joined other organizations to join."));
505
            }
506
        }
507 50
        if ($this->isDepartment() && $this->isOnlyAcceptCurrentOrgMember && !$this->topOrganization->hasMember($user)) {
508 1
            throw new OnlyAcceptCurrentOrgMemberException(Yii::t('organization' ,'This department is only accepted by members of the organization.'));
509
        }
510 50
        if ($this->isDepartment() && !$this->parent->equals($this->topOrganization) && $this->isOnlyAcceptSuperiorOrgMember && !$this->parent->hasMember($user)) {
0 ignored issues
show
Bug introduced by
It seems like equals() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like hasMember() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
511 1
            throw new OnlyAcceptSuperiorOrgMemberException(Yii::t('organization', 'This department only accepts members of the parent organization or department.'));
512
        }
513
514 50
        $this->trigger(self::EVENT_BEFORE_ADD_MEMBER);
515 50
        $model = null;
516 50
        if ($member instanceof Member) {
517
            $model = $this->createMemberModel($member);
518 50
        } elseif (($member instanceof User) || is_string($member) || is_int($member)) {
519 50
            $model = $this->createMemberModelWithUser($member);
520
        }
521 50
        $member = $model;
522 50
        $result = ($member instanceof Member) ? $member->save() : false;
523 50
        $this->trigger(self::EVENT_AFTER_ADD_MEMBER);
524 50
        return $result;
525
    }
526
527
    /**
528
     * Create member model, and set organization with this.
529
     * @param Member $member If this parameter is not new record, it's organization
530
     * will be set with this, and return it. Otherwise, it will extract `User`
531
     * model and create new `Member` model.
532
     * @see createMemberModelWithUser
533
     * @return Member
534
     */
535
    public function createMemberModel($member)
536
    {
537
        if (!$member->getIsNewRecord()) {
538
            $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...
539
            return $member;
540
        }
541
        return $this->createMemberModelWithUser($member->memberUser);
542
    }
543
544
    /**
545
     * Create member model with user, and set organization with this.
546
     * @param User|string|integer $user
547
     * @return Member
548
     */
549 50
    public function createMemberModelWithUser($user)
550
    {
551
        $config = [
552 50
            'memberUser' => $user,
553 50
            'organization' => $this,
554 50
            'nickname' => '',
555
        ];
556 50
        $member = $this->createMember($config);
557 50
        $member->nickname = $member->memberUser->profile->nickname;
558 50
        return $member;
559
    }
560
561
    /**
562
     * Remove member.
563
     * Note: the creator cannot be removed.
564
     * @param Member|User $member
565
     * @return boolean
566
     */
567 4
    public function removeMember(&$member)
568
    {
569 4
        if ($this->getIsNewRecord()) {
570
            return false;
571
        }
572 4
        $this->trigger(self::EVENT_BEFORE_REMOVE_MEMBER);
573 4
        if ($member instanceof $this->memberClass) {
574 4
            $member = $member->{$member->memberAttribute};
575
        }
576 4
        $member = $this->getMember($member);
577 4
        if (!$member || $member->isCreator()) {
578
            return false;
579
        }
580 4
        $result = $member->delete() > 0;
581 4
        $this->trigger(self::EVENT_AFTER_REMOVE_MEMBER);
582 4
        return $result;
583
    }
584
585
    /**
586
     * Remove administrator.
587
     * @param Member|User|integer|string $member Member instance, or User instance or its GUID or ID.
588
     * @param boolean $keep Keep member after administrator being revoked.
589
     * @return boolean
590
     * @throws IntegrityException
591
     */
592
    public function removeAdministrator(&$member, $keep = true)
593
    {
594
        if ($this->getIsNewRecord()) {
595
            return false;
596
        }
597
        if ($member instanceof $this->memberClass) {
598
            $member = $member->{$member->memberAttribute};
599
        }
600
        $member = $this->getMember($member);
601
        if ($member && $member->isAdministrator()) {
602
            if ($keep) {
603
                return $member->revokeAdministrator();
604
            }
605
            return $this->removeMember($member);
606
        }
607
        return false;
608
    }
609
610
    /**
611
     * 
612
     * @param Event $event
613
     * @throws IntegrityException
614
     * @return boolean
615
     */
616 51
    public function onAddProfile($event)
617
    {
618 51
        $profile = $event->sender->createProfile($event->data);
619 51
        if (!$profile->save()) {
620
            throw new IntegrityException('Profile Save Failed.');
621
        }
622 51
        return true;
623
    }
624
625
    /**
626
     * 
627
     * @param Event $event
628
     */
629 51
    public function onAssignCreator($event)
630
    {
631 51
        return $event->sender->addCreator($event->data);
632
    }
633
634
    /**
635
     * 
636
     * @param Event $event
637
     * @return boolean
638
     */
639 20
    public function onRevokeCreator($event)
640
    {
641 20
        $sender = $event->sender;
642
        /* @var $sender static */
643 20
        $member = $sender->getMemberCreators()->one();
644
        /* @var $member Member */
645 20
        $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
646 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...
647
    }
648
649
    /**
650
     * 
651
     * @param Event $event
652
     * @return boolean
653
     */
654 20
    public function onRevokeAdministrators($event)
655
    {
656 20
        $sender = $event->sender;
657
        /* @var $sender static */
658 20
        $members = $sender->getMemberAdministrators()->all();
659
        /* @var $members Member[] */
660 20
        foreach ($members as $member)
661
        {
662 1
            $member->revokeAdministrator();
663
        }
664 20
        return true;
665
    }
666
667
    /**
668
     * 
669
     * @param Event $event
670
     */
671 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...
672
    {
673
        
674 20
    }
675
676
    /**
677
     * Check whether current instance is an organization.
678
     * @return boolean
679
     */
680 50
    public function isOrganization()
681
    {
682 50
        return $this->type == static::TYPE_ORGANIZATION;
683
    }
684
685
    /**
686
     * Check whether current instance if a department.
687
     * @return boolean
688
     */
689 50
    public function isDepartment()
690
    {
691 50
        return $this->type == static::TYPE_DEPARTMENT;
692
    }
693
694
    /**
695
     * Check whether the current organization has a member.
696
     * @param User|string|integer $user User instance, GUID or ID.
697
     * @return boolean
698
     */
699 50
    public function hasMember($user)
700
    {
701 50
        return !empty($this->getMember($user));
702
    }
703
704
    /**
705
     * Get member query which role is specified `Creator`.
706
     * @return MemberQuery
707
     */
708 24
    public function getMemberCreators()
709
    {
710 24
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
711
    }
712
713
    /**
714
     * Get member query which role is specified `Administrator`.
715
     * @return MemberQuery
716
     */
717 22
    public function getMemberAdministrators()
718
    {
719 22
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
720
    }
721
722
    /**
723
     * Get user query which role is specified `Creator`.
724
     * @return BaseUserQuery
725
     */
726 4
    public function getCreator()
727
    {
728 4
        $noInit = $this->getNoInitMember();
729 4
        $class = $noInit->memberUserClass;
730 4
        $noInitUser = $class::buildNoInitModel();
731 4
        return $this->hasOne($class, [
732 4
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
733 4
        ])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
734
    }
735
736
    /**
737
     * Get user query which role is specified `Administrator`.
738
     * @return BaseUserQuery
739
     */
740 2
    public function getAdministrators()
741
    {
742 2
        $noInit = $this->getNoInitMember();
743 2
        $class = $noInit->memberUserClass;
744 2
        $noInitUser = $class::buildNoInitModel();
745 2
        return $this->hasMany($class, [
746 2
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
747 2
        ])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
748
    }
749
750
    /**
751
     * 
752
     * @param User $user
753
     * @return boolean
754
     * @throws \Exception
755
     * @throws IntegrityException
756
     */
757 51
    protected function addCreator($user)
758
    {
759 51
        if (!$user) {
760 1
            throw new InvalidParamException('Creator Invalid.');
761
        }
762 50
        $member = $user;
763 50
        $transaction = Yii::$app->db->beginTransaction();
764
        try {
765 50
            if (!$this->addMember($member)) {
766
                throw new IntegrityException('Failed to add member.');
767
            }
768 50
            $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
769 50
            $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...
770 50
            if (!$member->save()) {
771
                throw new IntegrityException('Failed to assign creator.');
772
            }
773 50
            $transaction->commit();
774
        } catch (\Exception $ex) {
775
            $transaction->rollBack();
776
            Yii::error($ex->getMessage(), __METHOD__);
777
            throw $ex;
778
        }
779 50
        return true;
780
    }
781
782
    /**
783
     * Add administrator.
784
     * @param User|integer|string $user User instance, or its GUID or ID.
785
     * @return boolean
786
     * @throws \Exception
787
     * @throws IntegrityException
788
     */
789 17
    public function addAdministrator($user)
790
    {
791 17
        $transaction = Yii::$app->db->beginTransaction();
792
        try {
793 17
            if (!$this->hasMember($user) && !$this->addMember($user)) {
794
                throw new IntegrityException(Yii::t('organization', 'Failed to add member.'));
795
            }
796 17
            $member = $this->getMember($user);
797 17
            $member->assignAdministrator();
798 17
            $transaction->commit();
799 2
        } catch (\Exception $ex) {
800 2
            $transaction->rollBack();
801 2
            Yii::error($ex->getMessage(), __METHOD__);
802 2
            throw $ex;
803
        }
804 17
        return true;
805
    }
806
807
    /**
808
     * Check whether the current organization has administrator.
809
     * @param User|integer|string $user
810
     * @return boolean
811
     */
812 2
    public function hasAdministrator($user)
813
    {
814 2
        $member = $this->getMember($user);
815 2
        if (!$member) {
816
            return false;
817
        }
818 2
        return $member->isAdministrator();
819
    }
820
821
    /**
822
     * Check whether this organization has reached the upper limit of subordinates.
823
     * @return boolean
824
     */
825 19
    public function hasReachedSubordinateLimit()
826
    {
827 19
        $remaining = $this->getRemainingSubordinatePlaces();
828 19
        if ($remaining === false) {
829
            return false;
830
        }
831 19
        return $remaining <= 0;
832
    }
833
834
    /**
835
     * Get the remaining places of subordinates.
836
     * @return bool|int False if no limit
837
     */
838 19
    public function getRemainingSubordinatePlaces()
839
    {
840 19
        $class = $this->subordinateLimitClass;
841 19
        if (empty($class)) {
842
            return false;
843
        }
844 19
        $limit = $class::getLimit($this);
845 19
        if ($limit === false) {
846
            return false;
847
        }
848 19
        $count = (int)$this->getChildren()->count();
849 19
        return $limit - $count;
850
    }
851
852
    /**
853
     * Check whether this organization has reached the upper limit of members.
854
     * @return boolean
855
     */
856 50
    public function hasReachedMemberLimit()
857
    {
858 50
        $remaining = $this->getRemainingMemberPlaces();
859 50
        if ($remaining === false) {
860
            return false;
861
        }
862 50
        return $remaining <= 0;
863
    }
864
865
    /**
866
     * Get the remaining places of members.
867
     * @return bool|int False if no limit.
868
     */
869 50
    public function getRemainingMemberPlaces()
870
    {
871 50
        $class = $this->memberLimitClass;
872 50
        if (empty($class)) {
873
            return false;
874
        }
875 50
        $limit = $class::getLimit($this);
876 50
        if ($limit === false) {
877
            return false;
878
        }
879 50
        $count = (int)$this->getMembers()->count();
880 50
        return $limit - $count;
881
    }
882
883
    const SETTING_ITEM_EXCLUDE_OTHER_MEMBERS = 'exclude_other_members';
884
885
    /**
886
     * @return bool
887
     */
888 31
    public function getIsExcludeOtherMembers()
889
    {
890 31
        $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
891 31
        if (!$setting) {
892 31
            $this->setIsExcludeOtherMembers(false);
893 31
            $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
894
        }
895 31
        return $setting->value == '1';
896
    }
897
898
    /**
899
     * @param bool $value
900
     * @return bool
901
     */
902 31
    public function setIsExcludeOtherMembers($value = true)
903
    {
904 31
        return $this->setSetting(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS, $value ? '1' : '0');
905
    }
906
907
    const SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER = 'disallow_member_join_other';
908
909
    /**
910
     * @return bool
911
     */
912 31
    public function getIsDisallowMemberJoinOther()
913
    {
914 31
        $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
915 31
        if (!$setting) {
916 31
            $this->setIsDisallowMemberJoinOther(false);
917 31
            $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
918
        }
919 31
        return $setting->value == '1';
920
    }
921
922
    /**
923
     * @param bool $value
924
     * @return bool
925
     */
926 31
    public function setIsDisallowMemberJoinOther($value = true)
927
    {
928 31
        return $this->setSetting(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER, $value ? '1' : '0');
929
    }
930
931
    const SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER = 'only_accept_current_org_member';
932
933
    /**
934
     * @return bool
935
     */
936 18
    public function getIsOnlyAcceptCurrentOrgMember()
937
    {
938 18
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
939 18
        if (!$setting) {
940 18
            $this->setIsOnlyAcceptCurrentOrgMember(false);
941 18
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
942
        }
943 18
        return $setting->value == '1';
944
    }
945
946
    /**
947
     * @param bool $value
948
     * @return bool
949
     */
950 18
    public function setIsOnlyAcceptCurrentOrgMember($value = true)
951
    {
952 18
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER, $value ? '1' : '0');
953
    }
954
955
    const SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER = 'only_accept_superior_org_member';
956
957
    /**
958
     * @return bool
959
     */
960 10
    public function getIsOnlyAcceptSuperiorOrgMember()
961
    {
962 10
        if ($this->parent && $this->parent->equals($this->topOrganization)) {
0 ignored issues
show
Bug introduced by
It seems like equals() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
963
            return $this->getIsOnlyAcceptCurrentOrgMember();
964
        }
965 10
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
966 10
        if (!$setting) {
967 10
            $this->setIsOnlyAcceptSuperiorOrgMember(false);
968 10
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
969
        }
970 10
        return $setting->value == '1';
971
    }
972
973
    /**
974
     * @param bool $value
975
     * @return bool
976
     */
977 10
    public function setIsOnlyAcceptSuperiorOrgMember($value = true)
978
    {
979 10
        if ($this->parent && $this->parent->equals($this->topOrganization)) {
0 ignored issues
show
Bug introduced by
It seems like equals() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
980
            return $this->setIsOnlyAcceptCurrentOrgMember($value);
981
        }
982 10
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER, $value ? '1' : '0');
983
    }
984
985
    const SETTING_ITEM_JOIN_PASSWORD = 'join_password';
986
987
    /**
988
     * Get join password.
989
     * @return mixed
990
     */
991
    public function getJoinPassword()
992
    {
993
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_PASSWORD)->one();
994
        if (!$setting) {
995
            $this->setJoinPassword();
996
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_PASSWORD)->one();
997
        }
998
        return $setting->value;
999
    }
1000
1001
    /**
1002
     * Set join password.
1003
     * @param string $value
1004
     * @return bool|null
1005
     */
1006
    public function setJoinPassword($value = '')
1007
    {
1008
        return $this->setSetting(static::SETTING_ITEM_JOIN_PASSWORD, $value);
1009
    }
1010
1011
    const SETTING_ITEM_JOIN_IP_ADDRESS = 'join_ip_address';
1012
1013
    /**
1014
     * Get Join IP address
1015
     * @return mixed
1016
     */
1017
    public function getJoinIpAddress()
1018
    {
1019
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_IP_ADDRESS)->one();
1020
        if (!$setting) {
1021
            $this->setJoinIpAddress();
1022
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_IP_ADDRESS)->one();
1023
        }
1024
        return $setting->value;
1025
    }
1026
1027
    /**
1028
     * Set join IP address.
1029
     * @param $value
1030
     * @return bool|null
1031
     */
1032
    public function setJoinIpAddress($value = '')
1033
    {
1034
        return $this->setSetting(static::SETTING_ITEM_JOIN_IP_ADDRESS, $value);
1035
    }
1036
1037
    const SETTING_ITEM_JOIN_ENTRANCE_URL = 'join_entrance_url';
1038
1039
    /**
1040
     * Get join entrance URL.
1041
     * This setting should be confirmed unique.
1042
     * @return string
1043
     */
1044
    public function getJoinEntranceUrl()
1045
    {
1046
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_ENTRANCE_URL)->one();
1047
        if (!$setting) {
1048
            $this->setJoinEntranceUrl();
1049
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_ENTRANCE_URL)->one();
1050
        }
1051
        return $setting->value;
1052
    }
1053
1054
    /**
1055
     * Set join entrance URL.
1056
     * @param string $value
1057
     * @return bool|null
1058
     */
1059
    public function setJoinEntranceUrl($value = '')
1060
    {
1061
        return $this->setSetting(static::SETTING_ITEM_JOIN_ENTRANCE_URL, $value, !empty($value));
1062
    }
1063
1064
    const SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY = 'exit_allow_withdraw_actively';
1065
1066
    /**
1067
     * @return bool
1068
     */
1069
    public function getExitAllowWithdrawActively()
1070
    {
1071
        $setting = $this->getSettings(static::SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY)->one();
1072
        if (!$setting) {
1073
            $this->setExitAllowWithdrawActively();
1074
            $setting = $this->getSettings(static::SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY)->one();
1075
        }
1076
        return $setting->value == '1';
1077
    }
1078
1079
    /**
1080
     * @param bool $value
1081
     * @return bool|null
1082
     */
1083
    public function setExitAllowWithdrawActively($value = false)
1084
    {
1085
        return $this->setSetting(static::SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY, $value ? '1' : '0');
1086
    }
1087
1088
    /**
1089
     * @return $this|null|static
1090
     */
1091 31
    public function getTopOrganization()
1092
    {
1093 31
        if ($this->isOrganization()) {
1094 31
            return $this;
1095
        }
1096 18
        $chain = $this->getAncestorChain();
1097 18
        return static::findOne(end($chain));
1098
    }
1099
1100
    /**
1101
     * Check whether the subordinates have the [[$user]]
1102
     * Note, this operation may consume the quantity of database selection.
1103
     * @param User $user
1104
     * @return bool
1105
     */
1106 2
    public function hasMemberInSubordinates($user)
1107
    {
1108 2
        if ($this->getChildren()->joinWith(['memberUsers mu_alias'])
1109 2
            ->andWhere(['mu_alias.' . $user->guidAttribute => $user->getGUID()])->exists()) {
1110 1
            return true;
1111
        }
1112 2
        $children = $this->children;
1113
        /* @var $children static[] */
1114 2
        foreach ($children as $child) {
1115 2
            if ($child->hasMemberInSubordinates($user)) {
1116 2
                return true;
1117
            }
1118
        }
1119 2
        return false;
1120
    }
1121
}
1122