Completed
Push — master ( e9ec3f...21ec97 )
by vistart
39:50
created

Organization::setSetting()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 3
cts 3
cp 1
rs 8.5125
c 0
b 0
f 0
cc 6
eloc 16
nc 7
nop 3
crap 6
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
 *
72
 * @property-read Member[] $members Get all member models of this organization/department.
73
 * @property-read User[] $memberUsers Get all members of this organization/department.
74
 * @property-read User $creator Get creator of this organization/department.
75
 * @property-read User[] $administrators Get administrators of this organization/department.
76
 * @property-read SubordinateLimit subordinateLimit
77
 * @property-read MemberLimit memberLimit
78
 * @property-read static|null $topOrganization The top level organization of current organization or departments.
79
 * @property-read Profile $profile Get profile model. Friendly to IDE.
80
 * @property-read OrganizationSetting[] $settings Get all settings.
81
 *
82
 * @version 1.0
83
 * @author vistart <[email protected]>
84
 */
85
class Organization extends User
86
{
87
    use SelfBlameableTrait;
88
89
    const TYPE_ORGANIZATION = 1;
90
    const TYPE_DEPARTMENT = 2;
91
92
    /**
93
     * @var boolean Organization does not need password and corresponding features.
94
     */
95
    public $passwordHashAttribute = false;
96
97
    /**
98
     * @var boolean Organization does not need password and corresponding features.
99
     */
100
    public $passwordResetTokenAttribute = false;
101
102
    /**
103
     * @var boolean Organization does not need password and corresponding features.
104
     */
105
    public $passwordHistoryClass = false;
106
107
    /**
108
     * @var boolean Organization does not need source.
109
     */
110
    public $sourceAttribute = false;
111
112
    /**
113
     * @var boolean Organization does not need auth key.
114
     */
115
    public $authKeyAttribute = false;
116
117
    /**
118
     * @var boolean Organization does not need access token.
119
     */
120
    public $accessTokenAttribute = false;
121
122
    /**
123
     * @var boolean Organization does not need login log.
124
     */
125
    public $loginLogClass = false;
126
127
    /**
128
     * @var string The Organization Profile Class
129
     */
130
    public $profileClass = Profile::class;
131
132
    /**
133
     * @var string The Member Class.
134
     */
135
    public $memberClass = Member::class;
136
137
    /**
138
     * @var string The Subordinate Limit Class
139
     */
140
    public $subordinateLimitClass = SubordinateLimit::class;
141
142
    /**
143
     * @var string The Member Limit Class
144
     */
145
    public $memberLimitClass = MemberLimit::class;
146
147
    /**
148
     * @var string The Organization Search Class
149
     */
150
    public $searchClass = OrganizationSearch::class;
151
152
    /**
153
     * @var string The Organization Setting Class
154
     */
155
    public $organizationSettingClass = OrganizationSetting::class;
156
157
    /**
158
     * @var Member
159
     */
160
    private $noInitMember;
161
162
    /**
163
     * @var SubordinateLimit
164
     */
165
    private $noInitSubordinateLimit;
166
167
    /**
168
     * @var MemberLimit
169
     */
170
    private $noInitMemberLimit;
171
172
    /**
173
     * @var OrganizationSetting
174
     */
175
    private $noInitOrganizationSetting;
176
177
    /**
178
     * @var User the creator of current Organization or Department.
179
     * This property is only available after registration.
180
     * Please do not access it at other times.
181
     * If you want to get creator model except registration, please
182
     * access [[$creator]] magic-property instead.
183
     */
184
    public $creatorModel;
185
186
    /**
187
     * @var array The configuration array of Organization Profile.
188
     * This property is only available after registration.
189
     * Please do not access it at other times.
190
     * If you want to get profile model except registration, please
191
     * access [[$profile]] magic-property instead.
192
     */
193
    public $profileConfig;
194
195
    const EVENT_BEFORE_ADD_MEMBER = 'eventBeforeAddMember';
196
    const EVENT_AFTER_ADD_MEMBER = 'eventAfterAddMember';
197
    const EVENT_BEFORE_REMOVE_MEMBER = 'eventBeforeRemoveMember';
198
    const EVENT_AFTER_REMOVE_MEMBER = 'eventAfterRemoveMember';
199
200
    public $cacheTagPrefix = 'tag_organization_';
201
202 51
    /**
203
     * @return Member
204 51
     */
205 51
    public function getNoInitMember()
206 51
    {
207
        if (!$this->noInitMember) {
208 51
            $class = $this->memberClass;
209
            $this->noInitMember = $class::buildNoInitModel();
210
        }
211
        return $this->noInitMember;
212
    }
213
214 2
    /**
215
     * @return SubordinateLimit
216 2
     */
217 2
    public function getNoInitSubordinateLimit()
218 2
    {
219
        if (!$this->noInitSubordinateLimit) {
220 2
            $class = $this->subordinateLimitClass;
221
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
222
        }
223
        return $this->noInitSubordinateLimit;
224
    }
225
226 1
    /**
227
     * @return MemberLimit
228 1
     */
229 1
    public function getNoInitMemberLimit()
230 1
    {
231
        if (!$this->noInitMemberLimit) {
232 1
            $class = $this->memberLimitClass;
233
            $this->noInitMemberLimit = $class::buildNoInitModel();
234
        }
235
        return $this->noInitMemberLimit;
236
    }
237
238 31
    /**
239
     * @return null|OrganizationSetting
240 31
     */
241 31
    public function getNoInitOrganizationSetting()
242 31
    {
243
        if (!$this->noInitOrganizationSetting) {
244
            $class = $this->organizationSettingClass;
245 31
            if (empty($class)) {
246
                return null;
247 31
            }
248
            $this->noInitOrganizationSetting = $class::buildNoInitModel();
249
        }
250
        return $this->noInitOrganizationSetting;
251
    }
252
253
    /**
254
     * @return null|OrganizationSearch
255
     */
256
    public function getSearchModel()
257
    {
258
        $class = $this->searchClass;
259
        if (empty($class) || !class_exists($class)) {
260
            return null;
261
        }
262
        return new $class;
263
    }
264
265 52
    /**
266
     * @inheritdoc
267 52
     */
268 52
    public function init()
269 52
    {
270
        $this->parentAttribute = 'parent_guid';
271 52
        if (class_exists($this->memberClass)) {
272 52
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
273
        }
274 52
        if ($this->skipInit) {
275 52
            return;
276 52
        }
277 52
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
278 52
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
279 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
280 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
281 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
282
        $this->initSelfBlameableEvents();
283
        parent::init();
284
    }
285
286 1
    /**
287
     * @inheritdoc
288
     */
289 1
    public function attributeLabels()
290 1
    {
291 1
        return [
292 1
            $this->guidAttribute => Yii::t('user', 'GUID'),
293 1
            $this->idAttribute => Yii::t('user', 'ID'),
294 1
            $this->ipAttribute => Yii::t('user', 'IP Address'),
295 1
            $this->ipTypeAttribute => Yii::t('user', 'IP Address Type'),
296 1
            $this->parentAttribute => Yii::t('organization', 'Parent'),
297 1
            $this->createdAtAttribute => Yii::t('user', 'Creation Time'),
298 1
            $this->updatedAtAttribute => Yii::t('user', 'Last Updated Time'),
299 1
            $this->statusAttribute => Yii::t('user', 'Status'),
300 1
            'type' => Yii::t('user', 'Type'),
301 1
            'isExcludeOtherMembers' => Yii::t('organization', 'Exclude Other Members'),
302
            'isDisallowMemberJoinOther' => Yii::t('organization', 'Disallow Member to Join in Other Organizations'),
303
            'isOnlyAcceptCurrentOrgMember' => Yii::t('organization', 'Only Accept Current Organization Members'),
304
            'isOnlyAcceptSuperiorOrgMember' => Yii::t('organization', 'Only Accept Superior Organization Members'),
305
        ];
306
    }
307
308 52
    /**
309
     * @inheritdoc
310 52
     */
311
    public static function tableName()
312
    {
313
        return '{{%organization}}';
314
    }
315
316
    /**
317
     * Find.
318 52
     * Friendly to IDE.
319
     * @return OrganizationQuery
320 52
     */
321
    public static function find()
322
    {
323 51
        return parent::find();
324
    }
325
326 51
    /**
327
     * Get rules associated with type attribute.
328 51
     * @return array
329
     */
330
    protected function getTypeRules()
331
    {
332 51
        return [
333
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
334 51
            ['type', 'required'],
335
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
336
        ];
337
    }
338
339
    /**
340
     * @inheritdoc
341 50
     */
342
    public function rules()
343 50
    {
344 50
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
345 50
    }
346
347
    /**
348
     * Get Member Query.
349
     * @return MemberQuery
350
     */
351
    public function getMembers()
352 6
    {
353
        return $this->hasMany($this->memberClass, [
354 6
            $this->getNoInitMember()->createdByAttribute => $this->guidAttribute
355 6
        ])->inverseOf('organization');
356 6
    }
357 6
358 6
    /**
359 6
     * Get organization member users' query.
360
     * @return BaseUserQuery
361
     */
362
    public function getMemberUsers()
363
    {
364
        $noInit = $this->getNoInitMember();
365
        $class = $noInit->memberUserClass;
366 2
        $noInitUser = $class::buildNoInitModel();
367
        return $this->hasMany($class, [
368 2
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
369
        ])->via('members')->inverseOf('atOrganizations');
370
    }
371 2
372 2
    /**
373
     * Get subordinate limit query.
374
     * @return null|BaseBlameableQuery
375
     */
376
    public function getSubordinateLimit()
377
    {
378
        if (empty($this->subordinateLimitClass)) {
379
            return null;
380 1
        }
381
        return $this->hasOne($this->subordinateLimitClass, [
382 1
            $this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute
383
        ]);
384
    }
385 1
386 1
    /**
387
     * Get member limit query.
388
     * @return null|BaseBlameableQuery
389
     */
390
    public function getMemberLimit()
391
    {
392
        if (empty($this->memberLimitClass)) {
393
            return null;
394 31
        }
395
        return $this->hasOne($this->memberLimitClass, [
396 31
            $this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute
397
        ]);
398
    }
399 31
400 31
    /**
401 31
     * @param string|null $item If you want to get all settings, please set it null.
402
     * @return null
403 31
     */
404
    public function getSettings($item = null)
405
    {
406
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
407
            return null;
408
        }
409
        $query = $this->hasMany($this->organizationSettingClass, [$this->getNoInitOrganizationSetting()->createdByAttribute => $this->guidAttribute]);
410
        if (!empty($item)) {
411 31
            $query = $query->andWhere([$this->getNoInitOrganizationSetting()->idAttribute => $item]);
412
        }
413 31
        return $query;
414
    }
415
416 31
    /**
417
     * Set organization setting.
418 31
     * @param string $item
419 31
     * @param string $value
420 31
     * @param bool $unique
421
     * @return bool|null Null if organization setting not enabled.
422
     * @throws IntegrityException throw if "item-value" unique broke.
423 31
     */
424 31
    public function setSetting($item, $value, $unique = false)
425
    {
426
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
427
            return null;
428
        }
429
        $setting = $this->getSettings($item)->one();
430
        /* @var $setting OrganizationSetting */
431
        if (!$setting) {
432 50
            $setting = $this->create($this->organizationSettingClass, [
433
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
434 50
            ]);
435
        }
436
        $setting->value = $value;
437
        if ($unique) {
438
            $class = $this->organizationSettingClass;
439
            if ($class::find()->andWhere([
440
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
441
                $this->getNoInitOrganizationSetting()->contentAttribute => $value
442
            ])->exists()) {
443
                throw new IntegrityException("`$item` : `$value` existed.");
444
            }
445
        }
446
        return $setting->save();
447
    }
448
449 50
    /**
450
     * Get member with specified user.
451 50
     * @param User|string|integer $user
452
     * @return Member Null if `user` is not in this organization.
453
     */
454 50
    public function getMember($user)
455 1
    {
456
        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 456 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
457 50
    }
458 50
459
    /**
460
     * Add member to organization.
461
     * @param Member|User|string|integer $member Member or User model, or User ID or GUID.
462
     * If member is created, it will be re-assigned to this parameter.
463
     * @see createMemberModel
464 50
     * @see createMemberModelWithUser
465 50
     * @return boolean
466
     * @throws DisallowMemberJoinOtherException
467 50
     * @throws ExcludeOtherMembersException
468
     * @throws OnlyAcceptCurrentOrgMemberException
469
     * @throws OnlyAcceptSuperiorOrgMemberException
470
     */
471 50
    public function addMember(&$member)
472
    {
473
        if ($this->getIsNewRecord()) {
474 50
            return false;
475
        }
476 50
        if ($this->hasReachedMemberLimit()) {
477 31
            return false;
478 1
        }
479
        $user = null;
480 31
        if ($member instanceof Member) {
481 31
            if ($member->getIsNewRecord()) {
482
                return false;
483
            }
484 50
            $user = $member->memberUser;
485 1
        }
486
        if ($member instanceof User) {
487 50
            $user = $member;
488 1
        }
489
        if (is_string($member) || is_int($member)) {
490
            $class = Yii::$app->user->identityClass;
491 50
            $user = $class::find()->guidOrId($member)->one();
492 50
        }
493 50
        if ($this->hasMember($user)) {
494
            return false;
495 50
        }
496 50
        $orgs = $user->getAtOrganizations()->all();
497
        /* @var $orgs Organization[] */
498 50
        foreach ($orgs as $org) {
499 50
            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...
500 50
                throw new DisallowMemberJoinOtherException(Yii::t('organization', "An organization in which the user is located does not allow its members to join other organizations."));
501 50
            }
502
            if ($this->topOrganization->isExcludeOtherMembers && !$org->topOrganization->equals($this->topOrganization)) {
503
                throw new ExcludeOtherMembersException(Yii::t('organization', "The organization does not allow users who have joined other organizations to join."));
504
            }
505
        }
506
        if ($this->isDepartment() && $this->isOnlyAcceptCurrentOrgMember && !$this->topOrganization->hasMember($user)) {
507
            throw new OnlyAcceptCurrentOrgMemberException(Yii::t('organization' ,'This department is only accepted by members of the organization.'));
508
        }
509
        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...
510
            throw new OnlyAcceptSuperiorOrgMemberException(Yii::t('organization', 'This department only accepts members of the parent organization or department.'));
511
        }
512
513
        $this->trigger(self::EVENT_BEFORE_ADD_MEMBER);
514
        $model = null;
515
        if ($member instanceof Member) {
516
            $model = $this->createMemberModel($member);
517
        } elseif (($member instanceof User) || is_string($member) || is_int($member)) {
518
            $model = $this->createMemberModelWithUser($member);
519
        }
520
        $member = $model;
521
        $result = ($member instanceof Member) ? $member->save() : false;
522
        $this->trigger(self::EVENT_AFTER_ADD_MEMBER);
523
        return $result;
524
    }
525
526 50
    /**
527
     * Create member model, and set organization with this.
528
     * @param Member $member If this parameter is not new record, it's organization
529 50
     * will be set with this, and return it. Otherwise, it will extract `User`
530 50
     * model and create new `Member` model.
531 50
     * @see createMemberModelWithUser
532
     * @return Member
533 50
     */
534 50
    public function createMemberModel($member)
535 50
    {
536
        if (!$member->getIsNewRecord()) {
537
            $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...
538
            return $member;
539
        }
540
        return $this->createMemberModelWithUser($member->memberUser);
541
    }
542
543
    /**
544 4
     * Create member model with user, and set organization with this.
545
     * @param User|string|integer $user
546 4
     * @return Member
547
     */
548
    public function createMemberModelWithUser($user)
549 4
    {
550 4
        $config = [
551 4
            'memberUser' => $user,
552
            'organization' => $this,
553 4
            'nickname' => '',
554 4
        ];
555
        $member = $this->createMember($config);
556
        $member->nickname = $member->memberUser->profile->nickname;
557 4
        return $member;
558 4
    }
559 4
560
    /**
561
     * Remove member.
562
     * Note: the creator cannot be removed.
563
     * @param Member|User $member
564
     * @return boolean
565
     */
566
    public function removeMember(&$member)
567
    {
568
        if ($this->getIsNewRecord()) {
569
            return false;
570
        }
571
        $this->trigger(self::EVENT_BEFORE_REMOVE_MEMBER);
572
        if ($member instanceof $this->memberClass) {
573
            $member = $member->{$member->memberAttribute};
574
        }
575
        $member = $this->getMember($member);
576
        if (!$member || $member->isCreator()) {
577
            return false;
578
        }
579
        $result = $member->delete() > 0;
580
        $this->trigger(self::EVENT_AFTER_REMOVE_MEMBER);
581
        return $result;
582
    }
583
584
    /**
585
     * Remove administrator.
586
     * @param Member|User|integer|string $member Member instance, or User instance or its GUID or ID.
587
     * @param boolean $keep Keep member after administrator being revoked.
588
     * @return boolean
589
     * @throws IntegrityException
590
     */
591
    public function removeAdministrator(&$member, $keep = true)
592
    {
593 51
        if ($this->getIsNewRecord()) {
594
            return false;
595 51
        }
596 51
        if ($member instanceof $this->memberClass) {
597
            $member = $member->{$member->memberAttribute};
598
        }
599 51
        $member = $this->getMember($member);
600
        if ($member && $member->isAdministrator()) {
601
            if ($keep) {
602
                return $member->revokeAdministrator();
603
            }
604
            return $this->removeMember($member);
605
        }
606 51
        return false;
607
    }
608 51
609
    /**
610
     * 
611
     * @param Event $event
612
     * @throws IntegrityException
613
     * @return boolean
614
     */
615
    public function onAddProfile($event)
616 20
    {
617
        $profile = $event->sender->createProfile($event->data);
618 20
        if (!$profile->save()) {
619
            throw new IntegrityException('Profile Save Failed.');
620 20
        }
621
        return true;
622 20
    }
623 20
624
    /**
625
     * 
626
     * @param Event $event
627
     */
628
    public function onAssignCreator($event)
629
    {
630
        return $event->sender->addCreator($event->data);
631 20
    }
632
633 20
    /**
634
     * 
635 20
     * @param Event $event
636
     * @return boolean
637 20
     */
638
    public function onRevokeCreator($event)
639 1
    {
640
        $sender = $event->sender;
641 20
        /* @var $sender static */
642
        $member = $sender->getMemberCreators()->one();
643
        /* @var $member Member */
644
        $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
645
        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...
646
    }
647
648 20
    /**
649
     * 
650
     * @param Event $event
651 20
     * @return boolean
652
     */
653
    public function onRevokeAdministrators($event)
654
    {
655
        $sender = $event->sender;
656
        /* @var $sender static */
657 50
        $members = $sender->getMemberAdministrators()->all();
658
        /* @var $members Member[] */
659 50
        foreach ($members as $member)
660
        {
661
            $member->revokeAdministrator();
662
        }
663
        return true;
664
    }
665
666 50
    /**
667
     * 
668 50
     * @param Event $event
669
     */
670
    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...
671
    {
672
        
673
    }
674
675
    /**
676 50
     * Check whether current instance is an organization.
677
     * @return boolean
678 50
     */
679
    public function isOrganization()
680
    {
681
        return $this->type == static::TYPE_ORGANIZATION;
682
    }
683
684
    /**
685 24
     * Check whether current instance if a department.
686
     * @return boolean
687 24
     */
688
    public function isDepartment()
689
    {
690
        return $this->type == static::TYPE_DEPARTMENT;
691
    }
692
693
    /**
694 22
     * Check whether the current organization has a member.
695
     * @param User|string|integer $user User instance, GUID or ID.
696 22
     * @return boolean
697
     */
698
    public function hasMember($user)
699
    {
700
        return !empty($this->getMember($user));
701
    }
702
703 4
    /**
704
     * Get member query which role is specified `Creator`.
705 4
     * @return MemberQuery
706 4
     */
707 4
    public function getMemberCreators()
708 4
    {
709 4
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
710 4
    }
711
712
    /**
713
     * Get member query which role is specified `Administrator`.
714
     * @return MemberQuery
715
     */
716
    public function getMemberAdministrators()
717 2
    {
718
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
719 2
    }
720 2
721 2
    /**
722 2
     * Get user query which role is specified `Creator`.
723 2
     * @return BaseUserQuery
724 2
     */
725
    public function getCreator()
726
    {
727
        $noInit = $this->getNoInitMember();
728
        $class = $noInit->memberUserClass;
729
        $noInitUser = $class::buildNoInitModel();
730
        return $this->hasOne($class, [
731
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
732
        ])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
733
    }
734 51
735
    /**
736 51
     * Get user query which role is specified `Administrator`.
737 1
     * @return BaseUserQuery
738
     */
739 50
    public function getAdministrators()
740 50
    {
741
        $noInit = $this->getNoInitMember();
742 50
        $class = $noInit->memberUserClass;
743
        $noInitUser = $class::buildNoInitModel();
744
        return $this->hasMany($class, [
745 50
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
746 50
        ])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
747 50
    }
748
749
    /**
750 50
     * 
751
     * @param User $user
752
     * @return boolean
753
     * @throws \Exception
754
     * @throws IntegrityException
755
     */
756 50
    protected function addCreator($user)
757
    {
758
        if (!$user) {
759
            throw new InvalidParamException('Creator Invalid.');
760
        }
761
        $member = $user;
762
        $transaction = Yii::$app->db->beginTransaction();
763
        try {
764
            if (!$this->addMember($member)) {
765
                throw new IntegrityException('Failed to add member.');
766 17
            }
767
            $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
768 17
            $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...
769
            if (!$member->save()) {
770 17
                throw new IntegrityException('Failed to assign creator.');
771
            }
772
            $transaction->commit();
773 17
        } catch (\Exception $ex) {
774 17
            $transaction->rollBack();
775 17
            Yii::error($ex->getMessage(), __METHOD__);
776 2
            throw $ex;
777 2
        }
778 2
        return true;
779 2
    }
780
781 17
    /**
782
     * Add administrator.
783
     * @param User|integer|string $user User instance, or its GUID or ID.
784
     * @return boolean
785
     * @throws \Exception
786
     * @throws IntegrityException
787
     */
788
    public function addAdministrator($user)
789 2
    {
790
        $transaction = Yii::$app->db->beginTransaction();
791 2
        try {
792 2
            if (!$this->hasMember($user) && !$this->addMember($user)) {
793
                throw new IntegrityException(Yii::t('organization', 'Failed to add member.'));
794
            }
795 2
            $member = $this->getMember($user);
796
            $member->assignAdministrator();
797
            $transaction->commit();
798
        } catch (\Exception $ex) {
799
            $transaction->rollBack();
800
            Yii::error($ex->getMessage(), __METHOD__);
801
            throw $ex;
802 19
        }
803
        return true;
804 19
    }
805 19
806
    /**
807
     * Check whether the current organization has administrator.
808 19
     * @param User|integer|string $user
809
     * @return boolean
810
     */
811
    public function hasAdministrator($user)
812
    {
813
        $member = $this->getMember($user);
814
        if (!$member) {
815 19
            return false;
816
        }
817 19
        return $member->isAdministrator();
818 19
    }
819
820
    /**
821 19
     * Check whether this organization has reached the upper limit of subordinates.
822 19
     * @return boolean
823
     */
824
    public function hasReachedSubordinateLimit()
825 19
    {
826 19
        $remaining = $this->getRemainingSubordinatePlaces();
827
        if ($remaining === false) {
828
            return false;
829
        }
830
        return $remaining <= 0;
831
    }
832
833 50
    /**
834
     * Get the remaining places of subordinates.
835 50
     * @return bool|int False if no limit
836 50
     */
837
    public function getRemainingSubordinatePlaces()
838
    {
839 50
        $class = $this->subordinateLimitClass;
840
        if (empty($class)) {
841
            return false;
842
        }
843
        $limit = $class::getLimit($this);
844
        if ($limit === false) {
845
            return false;
846 50
        }
847
        $count = (int)$this->getChildren()->count();
848 50
        return $limit - $count;
849 50
    }
850
851
    /**
852 50
     * Check whether this organization has reached the upper limit of members.
853 50
     * @return boolean
854
     */
855
    public function hasReachedMemberLimit()
856 50
    {
857 50
        $remaining = $this->getRemainingMemberPlaces();
858
        if ($remaining === false) {
859
            return false;
860
        }
861
        return $remaining <= 0;
862
    }
863
864
    /**
865 31
     * Get the remaining places of members.
866
     * @return bool|int False if no limit.
867 31
     */
868 31
    public function getRemainingMemberPlaces()
869 31
    {
870 31
        $class = $this->memberLimitClass;
871
        if (empty($class)) {
872 31
            return false;
873
        }
874
        $limit = $class::getLimit($this);
875
        if ($limit === false) {
876
            return false;
877
        }
878
        $count = (int)$this->getMembers()->count();
879 2
        return $limit - $count;
880
    }
881 2
882
    const SETTING_ITEM_EXCLUDE_OTHER_MEMBERS = 'exclude_other_members';
883
884
    /**
885
     * @return bool
886
     */
887
    public function getIsExcludeOtherMembers()
888
    {
889 31
        $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
890
        if (!$setting) {
891 31
            $this->setSetting(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS, '0');
892 31
            $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
893 31
        }
894 31
        return $setting->value == '1';
895
    }
896 31
897
    /**
898
     * @param bool $value
899
     * @return bool
900
     */
901
    public function setIsExcludeOtherMembers($value = true)
902
    {
903 2
        return $this->setSetting(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS, $value ? '1' : '0');
904
    }
905 2
906
    const SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER = 'disallow_member_join_other';
907
908
    /**
909
     * @return bool
910
     */
911
    public function getIsDisallowMemberJoinOther()
912
    {
913 18
        $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
914
        if (!$setting) {
915 18
            $this->setSetting(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER, '0');
916 18
            $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
917 18
        }
918 18
        return $setting->value == '1';
919
    }
920 18
921
    /**
922
     * @param bool $value
923
     * @return bool
924
     */
925
    public function setIsDisallowMemberJoinOther($value = true)
926
    {
927 2
        return $this->setSetting(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER, $value ? '1' : '0');
928
    }
929 2
930
    const SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER = 'only_accept_current_org_member';
931
932
    /**
933
     * @return bool
934
     */
935
    public function getIsOnlyAcceptCurrentOrgMember()
936
    {
937 18
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
938
        if (!$setting) {
939 18
            $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER, '0');
940 18
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
941 18
        }
942 18
        return $setting->value == '1';
943
    }
944 18
945
    /**
946
     * @param bool $value
947
     * @return bool
948
     */
949
    public function setIsOnlyAcceptCurrentOrgMember($value = true)
950
    {
951 2
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER, $value ? '1' : '0');
952
    }
953 2
954
    const SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER = 'only_accept_superior_org_member';
955
956
    /**
957
     * @return bool
958
     */
959 31
    public function getIsOnlyAcceptSuperiorOrgMember()
960
    {
961 31
        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...
962 31
            return $this->getIsOnlyAcceptCurrentOrgMember();
963
        }
964 18
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
965 18
        if (!$setting) {
966
            $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER, '0');
967
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
968
        }
969
        return $setting->value == '1';
970
    }
971
972
    /**
973
     * @param bool $value
974 2
     * @return bool
975
     */
976 2
    public function setIsOnlyAcceptSuperiorOrgMember($value = true)
977 2
    {
978 1
        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...
979
            return $this->setIsOnlyAcceptCurrentOrgMember($value);
980 2
        }
981
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER, $value ? '1' : '0');
982 2
    }
983 2
984 2
    const SETTING_ITEM_JOIN_PASSWORD = 'join_password';
985
986
    /**
987 2
     * Get join password.
988
     * @return mixed
989
     */
990
    public function getJoinPassword()
991
    {
992
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_PASSWORD)->one();
993
        if (!$setting) {
994
            $this->setSetting(static::SETTING_ITEM_JOIN_PASSWORD, '');
995
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_PASSWORD)->one();
996
        }
997
        return $setting->value;
998
    }
999
1000
    /**
1001
     * Set join password.
1002
     * @param string $value
1003
     * @return bool|null
1004
     */
1005
    public function setJoinPassword($value = '')
1006
    {
1007
        return $this->setSetting(static::SETTING_ITEM_JOIN_PASSWORD, $value);
1008
    }
1009
1010
    const SETTING_ITEM_JOIN_IP_ADDRESS = 'join_ip_address';
1011
1012
    /**
1013
     * Get Join IP address
1014
     * @return mixed
1015
     */
1016
    public function getJoinIpAddress()
1017
    {
1018
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_IP_ADDRESS)->one();
1019
        if (!$setting) {
1020
            $this->setSetting(static::SETTING_ITEM_JOIN_IP_ADDRESS, '');
1021
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_IP_ADDRESS)->one();
1022
        }
1023
        return $setting->value;
1024
    }
1025
1026
    /**
1027
     * Set join IP address.
1028
     * @param $value
1029
     * @return bool|null
1030
     */
1031
    public function setJoinIpAddress($value = '')
1032
    {
1033
        return $this->setSetting(static::SETTING_ITEM_JOIN_IP_ADDRESS, $value);
1034
    }
1035
1036
    const SETTING_ITEM_JOIN_ENTRANCE_URL = 'join_entrance_url';
1037
1038
    /**
1039
     * Get join entrance URL.
1040
     * This setting should be confirmed unique.
1041
     * @return string
1042
     */
1043
    public function getJoinEntranceUrl()
1044
    {
1045
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_ENTRANCE_URL)->one();
1046
        if (!$setting) {
1047
            $this->setSetting(static::SETTING_ITEM_JOIN_ENTRANCE_URL, '');
1048
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_ENTRANCE_URL)->one();
1049
        }
1050
        return $setting->value;
1051
    }
1052
1053
    /**
1054
     * Set join entrance URL.
1055
     * @param string $value
1056
     * @return bool|null
1057
     */
1058
    public function setJoinEntranceUrl($value = '')
1059
    {
1060
        return $this->setSetting(static::SETTING_ITEM_JOIN_ENTRANCE_URL, $value, !empty($value));
1061
    }
1062
1063
    /**
1064
     * @return $this|null|static
1065
     */
1066
    public function getTopOrganization()
1067
    {
1068
        if ($this->isOrganization()) {
1069
            return $this;
1070
        }
1071
        $chain = $this->getAncestorChain();
1072
        return static::findOne(end($chain));
1073
    }
1074
1075
    /**
1076
     * Check whether the subordinates have the [[$user]]
1077
     * Note, this operation may consume the quantity of database selection.
1078
     * @param User $user
1079
     * @return bool
1080
     */
1081
    public function hasMemberInSubordinates($user)
1082
    {
1083
        if ($this->getChildren()->joinWith(['memberUsers mu_alias'])
1084
            ->andWhere(['mu_alias.' . $user->guidAttribute => $user->getGUID()])->exists()) {
1085
            return true;
1086
        }
1087
        $children = $this->children;
1088
        /* @var $children static[] */
1089
        foreach ($children as $child) {
1090
            if ($child->hasMemberInSubordinates($user)) {
1091
                return true;
1092
            }
1093
        }
1094
        return false;
1095
    }
1096
}
1097