UserOrganizationTrait   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 88.61%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 61
lcom 1
cbo 18
dl 0
loc 448
ccs 140
cts 158
cp 0.8861
rs 2.9203
c 1
b 0
f 1

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getNoInitOrganization() 0 8 2
A getNoInitMember() 0 4 1
A getOfMembers() 0 4 1
A getOfCreators() 0 4 1
A getOfAdministrators() 0 4 1
A getAtOrganizations() 0 4 1
A getAtOrganizationsOnly() 0 4 1
A getAtDepartmentsOnly() 0 4 1
A getCreatorsAtOrganizations() 0 4 1
A getCreatorsAtOrganizationsOnly() 0 4 1
A getAdministratorsAtOrganizations() 0 4 1
A getAdministratorsAtOrganizationsOnly() 0 4 1
A setUpOrganization() 0 19 4
B setUpDepartment() 0 22 5
C setUpBaseOrganization() 0 22 7
A createOrganization() 0 4 1
A createDepartment() 0 4 1
A createBaseOrganization() 0 19 3
C revokeOrganization() 0 37 11
A isOrganizationCreator() 0 8 2
A isOrganizationAdministrator() 0 8 2
A initOrganizationEvents() 0 4 1
A onRevokeOrganizationsByCreator() 0 10 2
A hasReachedOrganizationLimit() 0 8 2
A getRemainingOrganizationPlaces() 0 13 3
A getNoInitOrganizationLimit() 0 8 2
A getOrganizationLimit() 0 7 2

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
/**
4
 *  _   __ __ _____ _____ ___  ____  _____
5
 * | | / // // ___//_  _//   ||  __||_   _|
6
 * | |/ // /(__  )  / / / /| || |     | |
7
 * |___//_//____/  /_/ /_/ |_||_|     |_|
8
 * @link https://vistart.me/
9
 * @copyright Copyright (c) 2016 - 2017 vistart
10
 * @license https://vistart.me/license/
11
 */
12
13
namespace rhosocial\organization;
14
15
use rhosocial\base\models\queries\BaseBlameableQuery;
16
use rhosocial\organization\exceptions\RevokePreventedException;
17
use rhosocial\organization\queries\MemberQuery;
18
use rhosocial\organization\queries\OrganizationQuery;
19
use rhosocial\organization\rbac\permissions\SetUpOrganization;
20
use rhosocial\organization\rbac\permissions\SetUpDepartment;
21
use rhosocial\organization\rbac\permissions\RevokeOrganization;
22
use rhosocial\organization\rbac\permissions\RevokeDepartment;
23
use rhosocial\organization\rbac\roles\DepartmentAdmin;
24
use rhosocial\organization\rbac\roles\DepartmentCreator;
25
use rhosocial\organization\rbac\roles\OrganizationAdmin;
26
use rhosocial\organization\rbac\roles\OrganizationCreator;
27
use Yii;
28
use yii\base\Event;
29
use yii\base\InvalidConfigException;
30
use yii\base\InvalidParamException;
31
32
/**
33
 * @property string $guidAttribute GUID Attribute.
34
 * @property-read Member[] $ofMembers
35
 * @property-read Member[] $ofCreators
36
 * @property-read Member[] $ofAdministrators
37
 * @property-read Organization[] $atOrganizations
38
 * @property-read Organization[] $atOrganizationsOnly
39
 * @property-read Organization[] $atDepartmentsOnly
40
 * @property-read Organization[] $creatorsAtOrganizations
41
 * @property-read Organization[] $creatorsAtOrganizationsOnly
42
 * @property-read Organization[] $administratorsAtOrganizations
43
 * @property-read Organization[] $administratorsAtOrganizationsOnly
44
 * @property-read OrganizationLimit $organizationLimit
45
 *
46
 * @version 1.0
47
 * @author vistart <[email protected]>
48
 */
49
trait UserOrganizationTrait
50
{
51
    /**
52
     * @var string The organization class.
53
     * Note: Please assign it with your own Organization class.
54
     */
55
    public $organizationClass = Organization::class;
56
57
    /**
58
     * @var string The organization limit class.
59
     * Note: Please assign it with your own OrganizationLimit class.
60
     */
61
    public $organizationLimitClass = OrganizationLimit::class;
62
    private $noInitOrganizationLimit;
63
    private $noInitOrganization;
64
    public $lastSetUpOrganization;
65
66
    /**
67
     * @return OrganizationLimit
68
     */
69 51
    public function getNoInitOrganizationLimit()
70
    {
71 51
        if (!$this->noInitOrganizationLimit) {
72 51
            $class = $this->organizationLimitClass;
73 51
            $this->noInitOrganizationLimit = $class::buildNoInitModel();
74
        }
75 51
        return $this->noInitOrganizationLimit;
76
    }
77
    /**
78
     * @return Organization
79
     */
80 51
    public function getNoInitOrganization()
81
    {
82 51
        if (!$this->noInitOrganization) {
83 51
            $class = $this->organizationClass;
84 51
            $this->noInitOrganization = $class::buildNoInitModel();
85
        }
86 51
        return $this->noInitOrganization;
87
    }
88
    /**
89
     * @return Member
90
     */
91 51
    public function getNoInitMember()
92
    {
93 51
        return $this->getNoInitOrganization()->getNoInitMember();
94
    }
95
96
    /**
97
     * Get member query.
98
     * @return MemberQuery
99
     */
100 51
    public function getOfMembers()
101
    {
102 51
        return $this->hasMany(get_class($this->getNoInitMember()), [$this->getNoInitMember()->memberAttribute => $this->guidAttribute])->inverseOf('memberUser');
0 ignored issues
show
Bug introduced by
It seems like hasMany() 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...
103
    }
104
105
    /**
106
     * Get query of member whose role is creator.
107
     * @return MemberQuery
108
     */
109 51
    public function getOfCreators()
110
    {
111 51
        return $this->getOfMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
112
    }
113
114
    /**
115
     * Get query of member whose role is administrator.
116
     * @return MemberQuery
117
     */
118 4
    public function getOfAdministrators()
119
    {
120 4
        return $this->getOfMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
121
    }
122
123
    /**
124
     * Get query of organization of which this user has been a member.
125
     * If you access this method as magic-property `atOrganizations`, you will
126
     * get all organizations the current user has joined in.
127
     * @return OrganizationQuery
128
     */
129 50
    public function getAtOrganizations()
130
    {
131 50
        return $this->hasMany($this->organizationClass, [$this->guidAttribute => $this->getNoInitMember()->createdByAttribute])->via('ofMembers');
0 ignored issues
show
Bug introduced by
It seems like hasMany() 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...
132
    }
133
134
    /**
135
     * 
136
     * @return OrganizationQuery
137
     */
138 2
    public function getAtOrganizationsOnly()
139
    {
140 2
        return $this->getAtOrganizations()->andWhere(['type' => Organization::TYPE_ORGANIZATION]);
141
    }
142
143
    /**
144
     * 
145
     * @return OrganizationQuery
146
     */
147 1
    public function getAtDepartmentsOnly()
148
    {
149 1
        return $this->getAtOrganizations()->andWhere(['type' => Organization::TYPE_DEPARTMENT]);
150
    }
151
152
    /**
153
     * 
154
     * @return OrganizationQuery
155
     */
156 51
    public function getCreatorsAtOrganizations()
157
    {
158 51
        return $this->hasMany($this->organizationClass, [$this->guidAttribute => $this->getNoInitMember()->createdByAttribute])->via('ofCreators');
0 ignored issues
show
Bug introduced by
It seems like hasMany() 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...
159
    }
160
161
    /**
162
     *
163
     * @return OrganizationQuery
164
     */
165 51
    public function getCreatorsAtOrganizationsOnly()
166
    {
167 51
        return $this->getCreatorsAtOrganizations()->andWhere(['type' => Organization::TYPE_ORGANIZATION]);
168
    }
169
170
    /**
171
     * 
172
     * @return OrganizationQuery
173
     */
174 4
    public function getAdministratorsAtOrganizations()
175
    {
176 4
        return $this->hasMany($this->organizationClass, [$this->guidAttribute => $this->getNoInitMember()->createdByAttribute])->via('ofAdministrators');
0 ignored issues
show
Bug introduced by
It seems like hasMany() 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...
177
    }
178
179
    /**
180
     * @return OrganizationQuery
181
     */
182 2
    public function getAdministratorsAtOrganizationsOnly()
183
    {
184 2
        return $this->getAdministratorsAtOrganizations()->andWhere(['type' => Organization::TYPE_ORGANIZATION]);
185
    }
186
187
    /**
188
     * Get Organization Limit Query.
189
     * @return BaseBlameableQuery
190
     */
191 51
    public function getOrganizationLimit()
192
    {
193 51
        if (empty($this->organizationLimitClass)) {
194
            return null;
195
        }
196 51
        return $this->hasOne($this->organizationLimitClass, [$this->getNoInitOrganizationLimit()->createdByAttribute => $this->guidAttribute]);
0 ignored issues
show
Bug introduced by
It seems like hasOne() 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...
197
    }
198
199
    /**
200
     * Set up organization.
201
     * @param string $name
202
     * @param string $nickname
203
     * @param integer $gravatar_type
204
     * @param string $gravatar
205
     * @param string $timezone
206
     * @param string $description
207
     * @return boolean Whether indicate the setting-up succeeded or not.
208
     * @throws InvalidParamException
209
     * @throws \Exception
210
     */
211 51
    public function setUpOrganization($name, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '')
212
    {
213 51
        $accessChecker = Yii::$app->authManager;
214 51
        if (!$accessChecker->checkAccess($this, (new SetUpOrganization)->name)) {
215 1
            throw new InvalidParamException("You do not have permission to set up organization.");
216
        }
217 51
        $transaction = Yii::$app->db->beginTransaction();
218
        try {
219 51
            $models = $this->createOrganization($name, null, $nickname, $gravatar_type, $gravatar, $timezone, $description);
220 51
            $this->setUpBaseOrganization($models);
221 50
            $transaction->commit();
222 1
        } catch (\Exception $ex) {
223 1
            $transaction->rollBack();
224 1
            Yii::error($ex->getMessage(), __METHOD__);
225 1
            throw $ex;
226
        }
227 50
        $this->lastSetUpOrganization = is_array($models) ? $models[0] : $models;
228 50
        return true;
229
    }
230
231
    /**
232
     * Set up organization.
233
     * @param string $name Department name.
234
     * @param Organization $parent Parent organization or department.
235
     * @param string $nickname
236
     * @param integer $gravatar_type
237
     * @param string $gravatar
238
     * @param string $timezone
239
     * @param string $description
240
     * @return boolean Whether indicate the setting-up succeeded or not.
241
     * @throws InvalidParamException
242
     * @throws \Exception
243
     */
244 20
    public function setUpDepartment($name, $parent, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '')
245
    {
246 20
        if (!($parent instanceof $this->organizationClass)) {
247 1
            throw new InvalidParamException('Invalid Parent Parameter.');
248
        }
249 19
        $accessChecker = Yii::$app->authManager;
250 19
        if (!$accessChecker->checkAccess($this, (new SetUpDepartment)->name, ['organization' => $parent])) {
251 2
            throw new InvalidParamException("You do not have permission to set up department.");
252
        }
253 19
        $transaction = Yii::$app->db->beginTransaction();
254
        try {
255 19
            $models = $this->createDepartment($name, $parent, $nickname, $gravatar_type, $gravatar, $timezone, $description);
256 19
            $this->setUpBaseOrganization($models);
257 18
            $transaction->commit();
258 1
        } catch (\Exception $ex) {
259 1
            $transaction->rollBack();
260 1
            Yii::error($ex->getMessage(), __METHOD__);
261 1
            throw $ex;
262
        }
263 18
        $this->lastSetUpOrganization = is_array($models) ? $models[0] : $models;
264 18
        return true;
265
    }
266
267
    /**
268
     * Set up base organization.
269
     * @param Organization $models
270
     * @return boolean
271
     * @throws InvalidConfigException
272
     * @throws \Exception
273
     */
274 51
    protected function setUpBaseOrganization($models)
275
    {
276 51
        $model = null;
277 51
        $associatedModels = [];
278 51
        if (is_array($models)) {
279 2
            if (!array_key_exists(0, $models)) {
280 2
                throw new InvalidConfigException('Invalid Organization Model.');
281
            }
282
            $model = $models[0];
283
            $associatedModels = array_key_exists('associatedModels', $models) ? $models['associatedModels'] : [];
284 50
        } elseif ($models instanceof $this->organizationClass) {
285 50
            $model = $models;
286
        }
287 50
        $result = $model->register($associatedModels);
288 50
        if ($result instanceof \Exception) {
289
            throw $result;
290
        }
291 50
        if ($result !== true) {
292
            throw new \Exception('Failed to set up.');
293
        }
294 50
        return true;
295
    }
296
297
    /**
298
     * Create organization.
299
     * @param string $name
300
     * @param Organization $parent
301
     * @param string $nickname
302
     * @param string $gravatar_type
303
     * @param string $gravatar
304
     * @param string $timezone
305
     * @param string $description
306
     * @return Organization
307
     */
308 50
    public function createOrganization($name, $parent = null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '')
309
    {
310 50
        return $this->createBaseOrganization($name, $parent, $nickname, $gravatar_type, $gravatar, $timezone, $description);
311
    }
312
313
    /**
314
     * Create department.
315
     * @param string $name
316
     * @param Organization $parent
317
     * @param string $nickname
318
     * @param string $gravatar_type
319
     * @param string $gravatar
320
     * @param string $timezone
321
     * @param string $description
322
     * @return Organization
323
     */
324 18
    public function createDepartment($name, $parent = null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '')
325
    {
326 18
        return $this->createBaseOrganization($name, $parent, $nickname, $gravatar_type, $gravatar, $timezone, $description, Organization::TYPE_DEPARTMENT);
327
    }
328
329
    /**
330
     * Create Base Organization.
331
     * @param string $name
332
     * @param Organization $parent
333
     * @param string $nickname
334
     * @param integer $gravatar_type
335
     * @param string $gravatar
336
     * @param string $timezone
337
     * @param string $description
338
     * @param integer $type
339
     * @return Organization
340
     * @throws InvalidParamException throw if setting parent failed. Possible reasons include:
341
     * - The parent is itself.
342
     * - The parent has already been its ancestor.
343
     * - The current organization has reached the limit of ancestors.
344
     */
345 50
    protected function createBaseOrganization($name, $parent = null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '', $type = Organization::TYPE_ORGANIZATION)
346
    {
347 50
        $class = $this->organizationClass;
348
        $profileConfig = [
349 50
            'name' => $name,
350 50
            'nickname' => $nickname,
351 50
            'gravatar_type' => $gravatar_type,
352 50
            'gravatar' => $gravatar,
353 50
            'timezone' => $timezone,
354 50
            'description' => $description,
355
        ];
356 50
        $organization = new $class(['type' => $type, 'creatorModel' => $this, 'profileConfig' => $profileConfig]);
357 50
        if (empty($parent)) {
358 50
            $organization->setNullParent();
359 18
        } elseif ($organization->setParent($parent) === false) {
360
            throw new InvalidParamException("Failed to set parent.");
361
        }
362 50
        return $organization;
363
    }
364
365
    /**
366
     * Revoke organization or department.
367
     * @param Organization|string|integer $organization Organization or it's ID or GUID.
368
     * @param boolean $revokeIfHasChildren True represents revoking organization if there are subordinates.
369
     * @return boolean True if revocation is successful.
370
     * @throws InvalidParamException throws if organization is invalid.
371
     * @throws \Exception
372
     * @throws RevokePreventedException throws if $revokeIfHasChildren is false, at the
373
     * same time the current organization or department has subordinates.
374
     * @throws @var:$organization@mtd:deregister
375
     */
376 14
    public function revokeOrganization($organization, $revokeIfHasChildren = true)
377
    {
378 14
        if (!($organization instanceof $this->organizationClass))
379
        {
380 2
            $class = $this->organizationClass;
381 2
            $organization = $class::find()->guidOrId($organization)->one();
382
        }
383 14
        if (!($organization instanceof $this->organizationClass)) {
384
            throw new InvalidParamException('Invalid Organization.');
385
        }
386 14
        if (!Yii::$app->authManager->checkAccess(
387
                $this,
388 14
                $organization->type == Organization::TYPE_ORGANIZATION ? (new RevokeOrganization)->name : (new RevokeDepartment)->name,
389 14
                ['organization' => $organization])) {
390 1
            throw new InvalidParamException("You do not have permission to revoke it.");
391
        }
392 13
        $transaction = Yii::$app->db->beginTransaction();
393
        try {
394 13
            if (!$revokeIfHasChildren && ((int)($organization->getChildren()->count())) > 0) {
395
                $type = $organization->isOrganization() ? "organization" : "department";
396
                throw new RevokePreventedException("The $type has children. Revoking prevented.");
397
            }
398 13
            $result = $organization->deregister();
399 13
            if ($result instanceof \Exception){
400
                throw $result;
401
            }
402 13
            if ($result !== true) {
403
                throw new InvalidParamException("Failed to revoke.");
404
            }
405 13
            $transaction->commit();
406
        } catch (\Exception $ex) {
407
            $transaction->rollBack();
408
            Yii::error($ex->getMessage(), __METHOD__);
409
            throw $ex;
410
        }
411 13
        return true;
412
    }
413
414
    /**
415
     * Check whether current user is organization or department creator.
416
     * @param Organization $organization
417
     * @return boolean True if current is organization or department creator.
418
     */
419 29
    public function isOrganizationCreator($organization)
420
    {
421 29
        $member = $organization->getMember($this);
422 29
        if (!$member) {
423 2
            return false;
424
        }
425 29
        return $member->isCreator();
426
    }
427
428
    /**
429
     * Check whether current user is organization or department administrator.
430
     * @param Organization $organization
431
     * @return boolean True if current is organization or department administrator.
432
     */
433 14
    public function isOrganizationAdministrator($organization)
434
    {
435 14
        $member = $organization->getMember($this);
436 14
        if (!$member) {
437 3
            return false;
438
        }
439 14
        return $member->isAdministrator();
440
    }
441
442
    /**
443
     * Attach events associated with organization.
444
     */
445 52
    public function initOrganizationEvents()
446
    {
447 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, "onRevokeOrganizationsByCreator"]);
0 ignored issues
show
Bug introduced by
It seems like on() 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...
448 52
    }
449
450
    /**
451
     * Revoke Organization Event.
452
     * It should be triggered when deleting (not deregistering).
453
     * @param Event $event
454
     */
455 22
    public function onRevokeOrganizationsByCreator($event)
456
    {
457 22
        $sender = $event->sender;
458
        /* @var $sender static */
459 22
        $organizations = $this->creatorsAtOrganizations;
460 22
        foreach ($organizations as $org)
461
        {
462 4
            $sender->revokeOrganization($org);
463
        }
464 22
    }
465
466
    /**
467
     * Check whether the current user has reached the upper limit of organizations.
468
     * @return boolean the upper limit of organizations which current could be set up.
469
     */
470 51
    public function hasReachedOrganizationLimit()
471
    {
472 51
        $remaining = $this->getRemainingOrganizationPlaces();
473 51
        if ($remaining === false) {
474
            return false;
475
        }
476 51
        return $remaining <= 0;
477
    }
478
479
    /**
480
     * Get the remaining places of organizations.
481
     * @return bool|int False if no limit.
482
     */
483 51
    public function getRemainingOrganizationPlaces()
484
    {
485 51
        $class = $this->organizationLimitClass;
486 51
        if (empty($class)) {
487
            return false;
488
        }
489 51
        $limit = $class::getLimit($this);
490 51
        if ($limit === false) {
491
            return false;
492
        }
493 51
        $count = (int)$this->getCreatorsAtOrganizationsOnly()->count();
494 51
        return $limit - $count;
495
    }
496
}
497