Completed
Push — master ( ca3e5a...8325fc )
by vistart
04:16
created

getNoInitOrganizationLimit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 9.4285
ccs 0
cts 5
cp 0
cc 2
eloc 5
nc 2
nop 0
crap 6
1
<?php
2
3
/**
4
 *  _   __ __ _____ _____ ___  ____  _____
5
 * | | / // // ___//_  _//   ||  __||_   _|
6
 * | |/ // /(__  )  / / / /| || |     | |
7
 * |___//_//____/  /_/ /_/ |_||_|     |_|
8
 * @link https://vistart.me/
9
 * @copyright Copyright (c) 2016 - 2017 vistart
10
 * @license https://vistart.me/license/
11
 */
12
13
namespace rhosocial\organization;
14
15
use rhosocial\base\models\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 36
    public function getNoInitMember()
95
    {
96 36
        if (!$this->noInitMember) {
97 36
            $class = $this->memberClass;
98 36
            $this->noInitMember = $class::buildNoInitModel();
99
        }
100 36
        return $this->noInitMember;
101
    }
102
103
    /**
104
     * Get member query.
105
     * @return MemberQuery
106
     */
107 36
    public function getOfMembers()
108
    {
109 36
        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 36
    public function getOfCreators()
117
    {
118 36
        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 36
    public function getCreatorsAtOrganizations()
164
    {
165 36
        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 2
    public function getAdministratorsAtOrganizations()
173
    {
174 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...
175
    }
176
177
    /**
178
     * Get Organization Limit Query.
179
     * @return BaseBlameableQuery
180
     */
181
    public function getOrganizationLimit()
182
    {
183
        if (empty($this->organizationLimitClass)) {
184
            return null;
185
        }
186
        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...
187
    }
188
189
    /**
190
     * Set up organization.
191
     * @param string $name
192
     * @param string $nickname
193
     * @param integer $gravatar_type
194
     * @param string $gravatar
195
     * @param string $timezone
196
     * @param string $description
197
     * @return boolean Whether indicate the setting-up succeeded or not.
198
     * @throws InvalidParamException
199
     * @throws \Exception
200
     */
201 36
    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...
202
    {
203 36
        $accessChecker = Yii::$app->authManager;
204 36
        if (!$accessChecker->checkAccess($this, (new SetUpOrganization)->name)) {
205
            throw new InvalidParamException("You do not have permission to set up organization.");
206
        }
207 36
        $transaction = Yii::$app->db->beginTransaction();
208
        try {
209 36
            $models = $this->createOrganization($name, null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '');
210 36
            $this->setUpBaseOrganization($models);
211 35
            $transaction->commit();
212 1
        } catch (\Exception $ex) {
213 1
            $transaction->rollBack();
214 1
            Yii::error($ex->getMessage(), __METHOD__);
215 1
            throw $ex;
216
        }
217 35
        $this->lastSetUpOrganization = is_array($models) ? $models[0] : $models;
218 35
        return true;
219
    }
220
221
    /**
222
     * Set up organization.
223
     * @param string $name Department name.
224
     * @param Organization $parent Parent organization or department.
225
     * @param string $nickname
226
     * @param integer $gravatar_type
227
     * @param string $gravatar
228
     * @param string $timezone
229
     * @param string $description
230
     * @return boolean Whether indicate the setting-up succeeded or not.
231
     * @throws InvalidParamException
232
     * @throws \Exception
233
     */
234 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...
235
    {
236 8
        if (!($parent instanceof $this->organizationClass)) {
237 1
            throw new InvalidParamException('Invalid Parent Parameter.');
238
        }
239 7
        $accessChecker = Yii::$app->authManager;
240 7
        if (!$accessChecker->checkAccess($this, (new SetUpDepartment)->name, ['organization' => $parent])) {
241 1
            throw new InvalidParamException("You do not have permission to set up department.");
242
        }
243 7
        $transaction = Yii::$app->db->beginTransaction();
244
        try {
245 7
            $models = $this->createDepartment($name, $parent, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '');
246 7
            $this->setUpBaseOrganization($models);
247 6
            $transaction->commit();
248 1
        } catch (\Exception $ex) {
249 1
            $transaction->rollBack();
250 1
            Yii::error($ex->getMessage(), __METHOD__);
251 1
            throw $ex;
252
        }
253 6
        $this->lastSetUpOrganization = is_array($models) ? $models[0] : $models;
254 6
        return true;
255
    }
256
257
    /**
258
     * Set up base organization.
259
     * @param Organization $models
260
     * @return boolean
261
     * @throws InvalidConfigException
262
     * @throws \Exception
263
     */
264 36
    protected function setUpBaseOrganization($models)
265
    {
266 36
        $model = null;
267 36
        $associatedModels = [];
268 36
        if (is_array($models)) {
269 2
            if (!array_key_exists(0, $models)) {
270 2
                throw new InvalidConfigException('Invalid Organization Model.');
271
            }
272
            $model = $models[0];
273
            $associatedModels = array_key_exists('associatedModels', $models) ? $models['associatedModels'] : [];
274 35
        } elseif ($models instanceof $this->organizationClass) {
275 35
            $model = $models;
276
        }
277 35
        $result = $model->register($associatedModels);
278 35
        if ($result instanceof \Exception) {
279
            throw $result;
280
        }
281 35
        if ($result !== true) {
282
            throw new \Exception('Failed to set up.');
283
        }
284 35
        return true;
285
    }
286
287
    /**
288
     * Create organization.
289
     * @param string $name
290
     * @param Organization $parent
291
     * @param string $nickname
292
     * @param string $gravatar_type
293
     * @param string $gravatar
294
     * @param string $timezone
295
     * @param string $description
296
     * @return Organization
297
     */
298 35
    public function createOrganization($name, $parent = null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '')
299
    {
300 35
        return $this->createBaseOrganization($name, $parent, $nickname, $gravatar_type, $gravatar, $timezone, $description);
301
    }
302
303
    /**
304
     * Create department.
305
     * @param string $name
306
     * @param Organization $parent
307
     * @param string $nickname
308
     * @param string $gravatar_type
309
     * @param string $gravatar
310
     * @param string $timezone
311
     * @param string $description
312
     * @return Organization
313
     */
314 6
    public function createDepartment($name, $parent = null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '')
315
    {
316 6
        return $this->createBaseOrganization($name, $parent, $nickname, $gravatar_type, $gravatar, $timezone, $description, Organization::TYPE_DEPARTMENT);
317
    }
318
319
    /**
320
     * Create Base Organization.
321
     * @param string $name
322
     * @param Organization $parent
323
     * @param string $nickname
324
     * @param integer $gravatar_type
325
     * @param string $gravatar
326
     * @param string $timezone
327
     * @param string $description
328
     * @param integer $type
329
     * @return Organization
330
     * @throws InvalidParamException throw if setting parent failed. Possible reasons include:
331
     * - The parent is itself.
332
     * - The parent has already been its ancestor.
333
     * - The current organization has reached the limit of ancestors.
334
     */
335 35
    protected function createBaseOrganization($name, $parent = null, $nickname = '', $gravatar_type = 0, $gravatar = '', $timezone = 'UTC', $description = '', $type = Organization::TYPE_ORGANIZATION)
336
    {
337 35
        $class = $this->organizationClass;
338
        $profileConfig = [
339 35
            'name' => $name,
340 35
            'nickname' => $nickname,
341 35
            'gravatar_type' => $gravatar_type,
342 35
            'gravatar' => $gravatar,
343 35
            'timezone' => $timezone,
344 35
            'description' => $description,
345
        ];
346 35
        $organization = new $class(['type' => $type, 'creatorModel' => $this, 'profileConfig' => $profileConfig]);
347 35
        if (empty($parent)) {
348 35
            $organization->setNullParent();
349 6
        } elseif ($organization->setParent($parent) === false) {
350
            throw new InvalidParamException("Failed to set parent.");
351
        }
352 35
        return $organization;
353
    }
354
355
    /**
356
     * Revoke organization or department.
357
     * @param Organization|string|integer $organization Organization or it's ID or GUID.
358
     * @param boolean $revokeIfHasChildren True represents revoking organization if there are subordinates.
359
     * @return boolean True if revocation is successful.
360
     * @throws InvalidParamException throws if organization is invalid.
361
     * @throws \Exception
362
     * @throws RevokePreventedException throws if $revokeIfHasChildren is false, at the
363
     * same time the current organization or department has subordinates.
364
     * @throws @var:$organization@mtd:deregister
365
     */
366 10
    public function revokeOrganization($organization, $revokeIfHasChildren = true)
367
    {
368 10
        if (!($organization instanceof $this->organizationClass))
369
        {
370 2
            $class = $this->organizationClass;
371 2
            if (is_numeric($organization)) {
372 1
                $organization = $class::find()->id($organization)->one();
373 1
            } elseif (is_string($organization)) {
374 1
                $organization = $class::find()->guid($organization)->one();
375
            }
376
        }
377 10
        if (!($organization instanceof $this->organizationClass)) {
378
            throw new InvalidParamException('Invalid Organization.');
379
        }
380 10
        if (!Yii::$app->authManager->checkAccess(
381
                $this,
382 10
                $organization->type == Organization::TYPE_ORGANIZATION ? (new RevokeOrganization)->name : (new RevokeDepartment)->name,
383 10
                ['organization' => $organization])) {
384 1
            throw new InvalidParamException("You do not have permission to revoke it.");
385
        }
386 9
        $transaction = Yii::$app->db->beginTransaction();
387
        try {
388 9
            if (!$revokeIfHasChildren && ((int)($organization->getChildren()->count())) > 0) {
389
                $type = $organization->isOrganization() ? "organization" : "department";
390
                throw new RevokePreventedException("The $type has children. Revoking prevented.");
391
            }
392 9
            $result = $organization->deregister();
393 9
            if ($result instanceof \Exception){
394
                throw $result;
395
            }
396 9
            if ($result !== true) {
397
                throw new InvalidParamException("Failed to revoke.");
398
            }
399 9
            $transaction->commit();
400
        } catch (\Exception $ex) {
401
            $transaction->rollBack();
402
            Yii::error($ex->getMessage(), __METHOD__);
403
            throw $ex;
404
        }
405 9
        return true;
406
    }
407
408
    /**
409
     * Check whether current user is organization or department creator.
410
     * @param Organization $organization
411
     * @return boolean True if current is organization or department creator.
412
     */
413 15
    public function isOrganizationCreator($organization)
414
    {
415 15
        $member = $organization->getMember($this);
416 15
        if (!$member) {
417 2
            return false;
418
        }
419 15
        return $member->isCreator();
420
    }
421
422
    /**
423
     * Check whether current user is organization or department administrator.
424
     * @param Organization $organization
425
     * @return boolean True if current is organization or department administrator.
426
     */
427 5
    public function isOrganizationAdministrator($organization)
428
    {
429 5
        $member = $organization->getMember($this);
430 5
        if (!$member) {
431 3
            return false;
432
        }
433 5
        return $member->isAdministrator();
434
    }
435
436
    /**
437
     * Attach events associated with organization.
438
     */
439 37
    public function initOrganizationEvents()
440
    {
441 37
        $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...
442 37
    }
443
444
    /**
445
     * Revoke Organization Event.
446
     * It should be triggered when deleting (not deregistering).
447
     * @param Event $event
448
     */
449 16
    public function onRevokeOrganizationsByCreator($event)
450
    {
451 16
        $sender = $event->sender;
452
        /* @var $sender static */
453 16
        $organizations = $this->creatorsAtOrganizations;
454 16
        foreach ($organizations as $org)
455
        {
456 3
            $sender->revokeOrganization($org);
457
        }
458 16
    }
459
460
    /**
461
     * Check whether the current user has reached the upper limit of organizations.
462
     * @return boolean the upper limit of organizations which current could be set up.
463
     */
464 36
    public function hasReachedOrganizationLimit()
465
    {
466 36
        if (empty($this->organizationLimitClass)) {
467
            return false;
468
        }
469 36
        $class = $this->organizationLimitClass;
470 36
        $limit = $class::getLimit($this);
471 36
        if ($limit === false) {
472
            return false;
473
        }
474 36
        $count = (int)$this->getCreatorsAtOrganizations()->andWhere(['type' => Organization::TYPE_ORGANIZATION])->count();
475 36
        return $count >= $limit;
476
    }
477
}
478