Passed
Push — master ( f14a0a...1212a9 )
by vistart
04:51
created

onRevokeOrganizationsByCreator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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