Passed
Push — master ( 21ec97...663af3 )
by vistart
20:03
created

Organization::createMemberModelWithUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
ccs 7
cts 7
cp 1
cc 1
eloc 8
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
 *  _   __ __ _____ _____ ___  ____  _____
5
 * | | / // // ___//_  _//   ||  __||_   _|
6
 * | |/ // /(__  )  / / / /| || |     | |
7
 * |___//_//____/  /_/ /_/ |_||_|     |_|
8
 * @link https://vistart.me/
9
 * @copyright Copyright (c) 2016 - 2017 vistart
10
 * @license https://vistart.me/license/
11
 */
12
13
namespace rhosocial\organization;
14
15
use rhosocial\base\models\traits\SelfBlameableTrait;
16
use rhosocial\base\models\queries\BaseBlameableQuery;
17
use rhosocial\base\models\queries\BaseUserQuery;
18
use rhosocial\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
    /**
203
     * @return Member
204
     */
205 51
    public function getNoInitMember()
206
    {
207 51
        if (!$this->noInitMember) {
208 51
            $class = $this->memberClass;
209 51
            $this->noInitMember = $class::buildNoInitModel();
210
        }
211 51
        return $this->noInitMember;
212
    }
213
214
    /**
215
     * @return SubordinateLimit
216
     */
217 2
    public function getNoInitSubordinateLimit()
218
    {
219 2
        if (!$this->noInitSubordinateLimit) {
220 2
            $class = $this->subordinateLimitClass;
221 2
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
222
        }
223 2
        return $this->noInitSubordinateLimit;
224
    }
225
226
    /**
227
     * @return MemberLimit
228
     */
229 1
    public function getNoInitMemberLimit()
230
    {
231 1
        if (!$this->noInitMemberLimit) {
232 1
            $class = $this->memberLimitClass;
233 1
            $this->noInitMemberLimit = $class::buildNoInitModel();
234
        }
235 1
        return $this->noInitMemberLimit;
236
    }
237
238
    /**
239
     * @return null|OrganizationSetting
240
     */
241 31
    public function getNoInitOrganizationSetting()
242
    {
243 31
        if (!$this->noInitOrganizationSetting) {
244 31
            $class = $this->organizationSettingClass;
245 31
            if (empty($class)) {
246
                return null;
247
            }
248 31
            $this->noInitOrganizationSetting = $class::buildNoInitModel();
249
        }
250 31
        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
    /**
266
     * @inheritdoc
267
     */
268 52
    public function init()
269
    {
270 52
        $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
        }
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 52
        $this->initSelfBlameableEvents();
283 52
        parent::init();
284 52
    }
285
286
    /**
287
     * @inheritdoc
288
     */
289 1
    public function attributeLabels()
290
    {
291
        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 1
            'isDisallowMemberJoinOther' => Yii::t('organization', 'Disallow Member to Join in Other Organizations'),
303 1
            'isOnlyAcceptCurrentOrgMember' => Yii::t('organization', 'Only Accept Current Organization Members'),
304 1
            'isOnlyAcceptSuperiorOrgMember' => Yii::t('organization', 'Only Accept Superior Organization Members'),
305
        ];
306
    }
307
308
    /**
309
     * @inheritdoc
310
     */
311 52
    public static function tableName()
312
    {
313 52
        return '{{%organization}}';
314
    }
315
316
    /**
317
     * Find.
318
     * Friendly to IDE.
319
     * @return OrganizationQuery
320
     */
321 52
    public static function find()
322
    {
323 52
        return parent::find();
324
    }
325
326
    /**
327
     * Get rules associated with type attribute.
328
     * @return array
329
     */
330 51
    protected function getTypeRules()
331
    {
332
        return [
333 51
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
334
            ['type', 'required'],
335 51
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
336
        ];
337
    }
338
339
    /**
340
     * @inheritdoc
341
     */
342 51
    public function rules()
343
    {
344 51
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
345
    }
346
347
    /**
348
     * Get Member Query.
349
     * @return MemberQuery
350
     */
351 50
    public function getMembers()
352
    {
353 50
        return $this->hasMany($this->memberClass, [
354 50
            $this->getNoInitMember()->createdByAttribute => $this->guidAttribute
355 50
        ])->inverseOf('organization');
356
    }
357
358
    /**
359
     * Get organization member users' query.
360
     * @return BaseUserQuery
361
     */
362 6
    public function getMemberUsers()
363
    {
364 6
        $noInit = $this->getNoInitMember();
365 6
        $class = $noInit->memberUserClass;
366 6
        $noInitUser = $class::buildNoInitModel();
367 6
        return $this->hasMany($class, [
368 6
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
369 6
        ])->via('members')->inverseOf('atOrganizations');
370
    }
371
372
    /**
373
     * Get subordinate limit query.
374
     * @return null|BaseBlameableQuery
375
     */
376 2
    public function getSubordinateLimit()
377
    {
378 2
        if (empty($this->subordinateLimitClass)) {
379
            return null;
380
        }
381 2
        return $this->hasOne($this->subordinateLimitClass, [
382 2
            $this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute
383
        ]);
384
    }
385
386
    /**
387
     * Get member limit query.
388
     * @return null|BaseBlameableQuery
389
     */
390 1
    public function getMemberLimit()
391
    {
392 1
        if (empty($this->memberLimitClass)) {
393
            return null;
394
        }
395 1
        return $this->hasOne($this->memberLimitClass, [
396 1
            $this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute
397
        ]);
398
    }
399
400
    /**
401
     * @param string|null $item If you want to get all settings, please set it null.
402
     * @return null
403
     */
404 31
    public function getSettings($item = null)
405
    {
406 31
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
407
            return null;
408
        }
409 31
        $query = $this->hasMany($this->organizationSettingClass, [$this->getNoInitOrganizationSetting()->createdByAttribute => $this->guidAttribute]);
410 31
        if (!empty($item)) {
411 31
            $query = $query->andWhere([$this->getNoInitOrganizationSetting()->idAttribute => $item]);
412
        }
413 31
        return $query;
414
    }
415
416
    /**
417
     * Set organization setting.
418
     * @param string $item
419
     * @param string $value
420
     * @param bool $unique
421
     * @return bool|null Null if organization setting not enabled.
422
     * @throws IntegrityException throw if "item-value" unique broke.
423
     */
424 31
    public function setSetting($item, $value, $unique = false)
425
    {
426 31
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
427
            return null;
428
        }
429 31
        $setting = $this->getSettings($item)->one();
430
        /* @var $setting OrganizationSetting */
431 31
        if (!$setting) {
432 31
            $setting = $this->create($this->organizationSettingClass, [
433 31
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
434
            ]);
435
        }
436 31
        $setting->value = $value;
437 31
        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 31
        return $setting->save();
447
    }
448
449
    /**
450
     * Get member with specified user.
451
     * @param User|string|integer $user
452
     * @return Member Null if `user` is not in this organization.
453
     */
454 50
    public function getMember($user)
455
    {
456 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 456 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
457
    }
458
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
     * @see createMemberModelWithUser
465
     * @return boolean
466
     * @throws DisallowMemberJoinOtherException
467
     * @throws ExcludeOtherMembersException
468
     * @throws OnlyAcceptCurrentOrgMemberException
469
     * @throws OnlyAcceptSuperiorOrgMemberException
470
     */
471 50
    public function addMember(&$member)
472
    {
473 50
        if ($this->getIsNewRecord()) {
474
            return false;
475
        }
476 50
        if ($this->hasReachedMemberLimit()) {
477 1
            return false;
478
        }
479 50
        $user = null;
480 50
        if ($member instanceof Member) {
481
            if ($member->getIsNewRecord()) {
482
                return false;
483
            }
484
            $user = $member->memberUser;
485
        }
486 50
        if ($member instanceof User) {
487 50
            $user = $member;
488
        }
489 50
        if (is_string($member) || is_int($member)) {
490
            $class = Yii::$app->user->identityClass;
491
            $user = $class::find()->guidOrId($member)->one();
492
        }
493 50
        if ($this->hasMember($user)) {
494
            return false;
495
        }
496 50
        $orgs = $user->getAtOrganizations()->all();
497
        /* @var $orgs Organization[] */
498 50
        foreach ($orgs as $org) {
499 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...
500 1
                throw new DisallowMemberJoinOtherException(Yii::t('organization', "An organization in which the user is located does not allow its members to join other organizations."));
501
            }
502 31
            if ($this->topOrganization->isExcludeOtherMembers && !$org->topOrganization->equals($this->topOrganization)) {
503 31
                throw new ExcludeOtherMembersException(Yii::t('organization', "The organization does not allow users who have joined other organizations to join."));
504
            }
505
        }
506 50
        if ($this->isDepartment() && $this->isOnlyAcceptCurrentOrgMember && !$this->topOrganization->hasMember($user)) {
507 1
            throw new OnlyAcceptCurrentOrgMemberException(Yii::t('organization' ,'This department is only accepted by members of the organization.'));
508
        }
509 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...
510 1
            throw new OnlyAcceptSuperiorOrgMemberException(Yii::t('organization', 'This department only accepts members of the parent organization or department.'));
511
        }
512
513 50
        $this->trigger(self::EVENT_BEFORE_ADD_MEMBER);
514 50
        $model = null;
515 50
        if ($member instanceof Member) {
516
            $model = $this->createMemberModel($member);
517 50
        } elseif (($member instanceof User) || is_string($member) || is_int($member)) {
518 50
            $model = $this->createMemberModelWithUser($member);
519
        }
520 50
        $member = $model;
521 50
        $result = ($member instanceof Member) ? $member->save() : false;
522 50
        $this->trigger(self::EVENT_AFTER_ADD_MEMBER);
523 50
        return $result;
524
    }
525
526
    /**
527
     * Create member model, and set organization with this.
528
     * @param Member $member If this parameter is not new record, it's organization
529
     * will be set with this, and return it. Otherwise, it will extract `User`
530
     * model and create new `Member` model.
531
     * @see createMemberModelWithUser
532
     * @return Member
533
     */
534
    public function createMemberModel($member)
535
    {
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
     * Create member model with user, and set organization with this.
545
     * @param User|string|integer $user
546
     * @return Member
547
     */
548 50
    public function createMemberModelWithUser($user)
549
    {
550
        $config = [
551 50
            'memberUser' => $user,
552 50
            'organization' => $this,
553 50
            'nickname' => '',
554
        ];
555 50
        $member = $this->createMember($config);
556 50
        $member->nickname = $member->memberUser->profile->nickname;
557 50
        return $member;
558
    }
559
560
    /**
561
     * Remove member.
562
     * Note: the creator cannot be removed.
563
     * @param Member|User $member
564
     * @return boolean
565
     */
566 4
    public function removeMember(&$member)
567
    {
568 4
        if ($this->getIsNewRecord()) {
569
            return false;
570
        }
571 4
        $this->trigger(self::EVENT_BEFORE_REMOVE_MEMBER);
572 4
        if ($member instanceof $this->memberClass) {
573 4
            $member = $member->{$member->memberAttribute};
574
        }
575 4
        $member = $this->getMember($member);
576 4
        if (!$member || $member->isCreator()) {
577
            return false;
578
        }
579 4
        $result = $member->delete() > 0;
580 4
        $this->trigger(self::EVENT_AFTER_REMOVE_MEMBER);
581 4
        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
        if ($this->getIsNewRecord()) {
594
            return false;
595
        }
596
        if ($member instanceof $this->memberClass) {
597
            $member = $member->{$member->memberAttribute};
598
        }
599
        $member = $this->getMember($member);
600
        if ($member && $member->isAdministrator()) {
601
            if ($keep) {
602
                return $member->revokeAdministrator();
603
            }
604
            return $this->removeMember($member);
605
        }
606
        return false;
607
    }
608
609
    /**
610
     * 
611
     * @param Event $event
612
     * @throws IntegrityException
613
     * @return boolean
614
     */
615 51
    public function onAddProfile($event)
616
    {
617 51
        $profile = $event->sender->createProfile($event->data);
618 51
        if (!$profile->save()) {
619
            throw new IntegrityException('Profile Save Failed.');
620
        }
621 51
        return true;
622
    }
623
624
    /**
625
     * 
626
     * @param Event $event
627
     */
628 51
    public function onAssignCreator($event)
629
    {
630 51
        return $event->sender->addCreator($event->data);
631
    }
632
633
    /**
634
     * 
635
     * @param Event $event
636
     * @return boolean
637
     */
638 20
    public function onRevokeCreator($event)
639
    {
640 20
        $sender = $event->sender;
641
        /* @var $sender static */
642 20
        $member = $sender->getMemberCreators()->one();
643
        /* @var $member Member */
644 20
        $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
645 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...
646
    }
647
648
    /**
649
     * 
650
     * @param Event $event
651
     * @return boolean
652
     */
653 20
    public function onRevokeAdministrators($event)
654
    {
655 20
        $sender = $event->sender;
656
        /* @var $sender static */
657 20
        $members = $sender->getMemberAdministrators()->all();
658
        /* @var $members Member[] */
659 20
        foreach ($members as $member)
660
        {
661 1
            $member->revokeAdministrator();
662
        }
663 20
        return true;
664
    }
665
666
    /**
667
     * 
668
     * @param Event $event
669
     */
670 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...
671
    {
672
        
673 20
    }
674
675
    /**
676
     * Check whether current instance is an organization.
677
     * @return boolean
678
     */
679 50
    public function isOrganization()
680
    {
681 50
        return $this->type == static::TYPE_ORGANIZATION;
682
    }
683
684
    /**
685
     * Check whether current instance if a department.
686
     * @return boolean
687
     */
688 50
    public function isDepartment()
689
    {
690 50
        return $this->type == static::TYPE_DEPARTMENT;
691
    }
692
693
    /**
694
     * Check whether the current organization has a member.
695
     * @param User|string|integer $user User instance, GUID or ID.
696
     * @return boolean
697
     */
698 50
    public function hasMember($user)
699
    {
700 50
        return !empty($this->getMember($user));
701
    }
702
703
    /**
704
     * Get member query which role is specified `Creator`.
705
     * @return MemberQuery
706
     */
707 24
    public function getMemberCreators()
708
    {
709 24
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
710
    }
711
712
    /**
713
     * Get member query which role is specified `Administrator`.
714
     * @return MemberQuery
715
     */
716 22
    public function getMemberAdministrators()
717
    {
718 22
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
719
    }
720
721
    /**
722
     * Get user query which role is specified `Creator`.
723
     * @return BaseUserQuery
724
     */
725 4
    public function getCreator()
726
    {
727 4
        $noInit = $this->getNoInitMember();
728 4
        $class = $noInit->memberUserClass;
729 4
        $noInitUser = $class::buildNoInitModel();
730 4
        return $this->hasOne($class, [
731 4
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
732 4
        ])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
733
    }
734
735
    /**
736
     * Get user query which role is specified `Administrator`.
737
     * @return BaseUserQuery
738
     */
739 2
    public function getAdministrators()
740
    {
741 2
        $noInit = $this->getNoInitMember();
742 2
        $class = $noInit->memberUserClass;
743 2
        $noInitUser = $class::buildNoInitModel();
744 2
        return $this->hasMany($class, [
745 2
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
746 2
        ])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
747
    }
748
749
    /**
750
     * 
751
     * @param User $user
752
     * @return boolean
753
     * @throws \Exception
754
     * @throws IntegrityException
755
     */
756 51
    protected function addCreator($user)
757
    {
758 51
        if (!$user) {
759 1
            throw new InvalidParamException('Creator Invalid.');
760
        }
761 50
        $member = $user;
762 50
        $transaction = Yii::$app->db->beginTransaction();
763
        try {
764 50
            if (!$this->addMember($member)) {
765
                throw new IntegrityException('Failed to add member.');
766
            }
767 50
            $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
768 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...
769 50
            if (!$member->save()) {
770
                throw new IntegrityException('Failed to assign creator.');
771
            }
772 50
            $transaction->commit();
773
        } catch (\Exception $ex) {
774
            $transaction->rollBack();
775
            Yii::error($ex->getMessage(), __METHOD__);
776
            throw $ex;
777
        }
778 50
        return true;
779
    }
780
781
    /**
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 17
    public function addAdministrator($user)
789
    {
790 17
        $transaction = Yii::$app->db->beginTransaction();
791
        try {
792 17
            if (!$this->hasMember($user) && !$this->addMember($user)) {
793
                throw new IntegrityException(Yii::t('organization', 'Failed to add member.'));
794
            }
795 17
            $member = $this->getMember($user);
796 17
            $member->assignAdministrator();
797 17
            $transaction->commit();
798 2
        } catch (\Exception $ex) {
799 2
            $transaction->rollBack();
800 2
            Yii::error($ex->getMessage(), __METHOD__);
801 2
            throw $ex;
802
        }
803 17
        return true;
804
    }
805
806
    /**
807
     * Check whether the current organization has administrator.
808
     * @param User|integer|string $user
809
     * @return boolean
810
     */
811 2
    public function hasAdministrator($user)
812
    {
813 2
        $member = $this->getMember($user);
814 2
        if (!$member) {
815
            return false;
816
        }
817 2
        return $member->isAdministrator();
818
    }
819
820
    /**
821
     * Check whether this organization has reached the upper limit of subordinates.
822
     * @return boolean
823
     */
824 19
    public function hasReachedSubordinateLimit()
825
    {
826 19
        $remaining = $this->getRemainingSubordinatePlaces();
827 19
        if ($remaining === false) {
828
            return false;
829
        }
830 19
        return $remaining <= 0;
831
    }
832
833
    /**
834
     * Get the remaining places of subordinates.
835
     * @return bool|int False if no limit
836
     */
837 19
    public function getRemainingSubordinatePlaces()
838
    {
839 19
        $class = $this->subordinateLimitClass;
840 19
        if (empty($class)) {
841
            return false;
842
        }
843 19
        $limit = $class::getLimit($this);
844 19
        if ($limit === false) {
845
            return false;
846
        }
847 19
        $count = (int)$this->getChildren()->count();
848 19
        return $limit - $count;
849
    }
850
851
    /**
852
     * Check whether this organization has reached the upper limit of members.
853
     * @return boolean
854
     */
855 50
    public function hasReachedMemberLimit()
856
    {
857 50
        $remaining = $this->getRemainingMemberPlaces();
858 50
        if ($remaining === false) {
859
            return false;
860
        }
861 50
        return $remaining <= 0;
862
    }
863
864
    /**
865
     * Get the remaining places of members.
866
     * @return bool|int False if no limit.
867
     */
868 50
    public function getRemainingMemberPlaces()
869
    {
870 50
        $class = $this->memberLimitClass;
871 50
        if (empty($class)) {
872
            return false;
873
        }
874 50
        $limit = $class::getLimit($this);
875 50
        if ($limit === false) {
876
            return false;
877
        }
878 50
        $count = (int)$this->getMembers()->count();
879 50
        return $limit - $count;
880
    }
881
882
    const SETTING_ITEM_EXCLUDE_OTHER_MEMBERS = 'exclude_other_members';
883
884
    /**
885
     * @return bool
886
     */
887 31
    public function getIsExcludeOtherMembers()
888
    {
889 31
        $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
890 31
        if (!$setting) {
891 31
            $this->setIsExcludeOtherMembers(false);
892 31
            $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
893
        }
894 31
        return $setting->value == '1';
895
    }
896
897
    /**
898
     * @param bool $value
899
     * @return bool
900
     */
901 31
    public function setIsExcludeOtherMembers($value = true)
902
    {
903 31
        return $this->setSetting(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS, $value ? '1' : '0');
904
    }
905
906
    const SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER = 'disallow_member_join_other';
907
908
    /**
909
     * @return bool
910
     */
911 31
    public function getIsDisallowMemberJoinOther()
912
    {
913 31
        $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
914 31
        if (!$setting) {
915 31
            $this->setIsDisallowMemberJoinOther(false);
916 31
            $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
917
        }
918 31
        return $setting->value == '1';
919
    }
920
921
    /**
922
     * @param bool $value
923
     * @return bool
924
     */
925 31
    public function setIsDisallowMemberJoinOther($value = true)
926
    {
927 31
        return $this->setSetting(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER, $value ? '1' : '0');
928
    }
929
930
    const SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER = 'only_accept_current_org_member';
931
932
    /**
933
     * @return bool
934
     */
935 18
    public function getIsOnlyAcceptCurrentOrgMember()
936
    {
937 18
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
938 18
        if (!$setting) {
939 18
            $this->setIsOnlyAcceptCurrentOrgMember(false);
940 18
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
941
        }
942 18
        return $setting->value == '1';
943
    }
944
945
    /**
946
     * @param bool $value
947
     * @return bool
948
     */
949 18
    public function setIsOnlyAcceptCurrentOrgMember($value = true)
950
    {
951 18
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER, $value ? '1' : '0');
952
    }
953
954
    const SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER = 'only_accept_superior_org_member';
955
956
    /**
957
     * @return bool
958
     */
959 10
    public function getIsOnlyAcceptSuperiorOrgMember()
960
    {
961 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...
962
            return $this->getIsOnlyAcceptCurrentOrgMember();
963
        }
964 10
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
965 10
        if (!$setting) {
966 10
            $this->setIsOnlyAcceptSuperiorOrgMember(false);
967 10
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
968
        }
969 10
        return $setting->value == '1';
970
    }
971
972
    /**
973
     * @param bool $value
974
     * @return bool
975
     */
976 10
    public function setIsOnlyAcceptSuperiorOrgMember($value = true)
977
    {
978 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...
979
            return $this->setIsOnlyAcceptCurrentOrgMember($value);
980
        }
981 10
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER, $value ? '1' : '0');
982
    }
983
984
    const SETTING_ITEM_JOIN_PASSWORD = 'join_password';
985
986
    /**
987
     * 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->setJoinPassword();
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->setJoinIpAddress();
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->setJoinEntranceUrl();
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 31
    public function getTopOrganization()
1067
    {
1068 31
        if ($this->isOrganization()) {
1069 31
            return $this;
1070
        }
1071 18
        $chain = $this->getAncestorChain();
1072 18
        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 2
    public function hasMemberInSubordinates($user)
1082
    {
1083 2
        if ($this->getChildren()->joinWith(['memberUsers mu_alias'])
1084 2
            ->andWhere(['mu_alias.' . $user->guidAttribute => $user->getGUID()])->exists()) {
1085 1
            return true;
1086
        }
1087 2
        $children = $this->children;
1088
        /* @var $children static[] */
1089 2
        foreach ($children as $child) {
1090 2
            if ($child->hasMemberInSubordinates($user)) {
1091 2
                return true;
1092
            }
1093
        }
1094 2
        return false;
1095
    }
1096
}
1097