UsersAttributeTrait   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 67
lcom 1
cbo 9
dl 0
loc 465
ccs 0
cts 270
cp 0
rs 3.04
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A userQuery() 0 19 2
A getUsers() 0 15 3
A eagerLoadingUsersMap() 0 16 1
A setUsersFromRequest() 0 8 2
A setUsers() 0 23 4
A addUsers() 0 18 4
B resolveUser() 0 23 7
A addUser() 0 18 2
A removeUsers() 0 18 4
A removeUser() 0 18 2
A resetUsers() 0 5 1
B saveUsers() 0 48 10
B associateUser() 0 27 6
B associateUsers() 0 34 7
A dissociateUser() 0 20 5
B dissociateUsers() 0 33 7

How to fix   Complexity   

Complex Class

Complex classes like UsersAttributeTrait 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 UsersAttributeTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/organization/license
6
 * @link       https://www.flipboxfactory.com/software/organization/
7
 */
8
9
namespace flipbox\organizations\elements;
10
11
use Craft;
12
use craft\db\Query;
13
use craft\elements\db\UserQuery;
14
use craft\elements\User;
15
use craft\helpers\ArrayHelper;
16
use flipbox\craft\ember\helpers\QueryHelper;
17
use flipbox\organizations\records\UserAssociation;
18
19
/**
20
 * @author Flipbox Factory <[email protected]>
21
 * @since 1.0.0
22
 */
23
trait UsersAttributeTrait
24
{
25
    /**
26
     * @var UserQuery
27
     */
28
    private $users;
29
30
    /**
31
     * @param array $sourceElements
32
     * @return array
33
     */
34
    private static function eagerLoadingUsersMap(array $sourceElements)
35
    {
36
        // Get the source element IDs
37
        $sourceElementIds = ArrayHelper::getColumn($sourceElements, 'id');
38
39
        $map = (new Query())
40
            ->select(['organizationId as source', 'userId as target'])
41
            ->from(UserAssociation::tableName())
42
            ->where(['organizationId' => $sourceElementIds])
43
            ->all();
44
45
        return [
46
            'elementType' => User::class,
47
            'map' => $map
48
        ];
49
    }
50
51
    /************************************************************
52
     * REQUEST
53
     ************************************************************/
54
55
    /**
56
     * AssociateUserToOrganization an array of users from request input
57
     *
58
     * @param string $identifier
59
     * @return $this
60
     */
61
    public function setUsersFromRequest(string $identifier = 'users')
62
    {
63
        if (null !== ($users = Craft::$app->getRequest()->getBodyParam($identifier))) {
64
            $this->setUsers((array) $users);
65
        }
66
67
        return $this;
68
    }
69
70
    
71
    /************************************************************
72
     * USERS QUERY
73
     ************************************************************/
74
75
    /**
76
     * @param array $criteria
77
     * @return UserQuery
78
     */
79
    public function userQuery($criteria = []): UserQuery
80
    {
81
        /** @noinspection PhpUndefinedMethodInspection */
82
        $query = User::find()
83
            ->organization($this)
84
            ->orderBy([
85
                'userOrder' => SORT_ASC,
86
                'username' => SORT_ASC,
87
            ]);
88
89
        if (!empty($criteria)) {
90
            QueryHelper::configure(
91
                $query,
92
                $criteria
93
            );
94
        }
95
96
        return $query;
97
    }
98
99
    /**
100
     * Get an array of users associated to an organization
101
     *
102
     * @param array $criteria
103
     * @return UserQuery
104
     */
105
    public function getUsers($criteria = [])
106
    {
107
        if (null === $this->users) {
108
            $this->users = $this->userQuery();
109
        }
110
111
        if (!empty($criteria)) {
112
            QueryHelper::configure(
113
                $this->users,
114
                $criteria
115
            );
116
        }
117
118
        return $this->users;
119
    }
120
121
    /**
122
     * AssociateUserToOrganization users to an organization
123
     *
124
     * @param $users
125
     * @return $this
126
     */
127
    public function setUsers($users)
128
    {
129
        if ($users instanceof UserQuery) {
130
            $this->users = $users;
131
            return $this;
132
        }
133
134
        // Reset the query
135
        $this->users = $this->userQuery();
136
137
        // Remove all users
138
        $this->users->setCachedResult([]);
139
140
        if (!empty($users)) {
141
            if (!is_array($users)) {
142
                $users = [$users];
143
            }
144
145
            $this->addUsers($users);
146
        }
147
148
        return $this;
149
    }
150
151
    /**
152
     * AssociateUserToOrganization an array of users to an organization
153
     *
154
     * @param $users
155
     * @return $this
156
     */
157
    public function addUsers(array $users)
158
    {
159
        // In case a config is directly passed
160
        if (ArrayHelper::isAssociative($users)) {
161
            $users = [$users];
162
        }
163
164
        foreach ($users as $key => $user) {
165
            // Ensure we have a model
166
            if (!$user instanceof User) {
167
                $user = $this->resolveUser($user);
168
            }
169
170
            $this->addUser($user);
171
        }
172
173
        return $this;
174
    }
175
176
    /**
177
     * @param $user
178
     * @return User
179
     */
180
    protected function resolveUser($user)
181
    {
182
        if (is_array($user) &&
183
            null !== ($id = ArrayHelper::getValue($user, 'id'))
184
        ) {
185
            $user = ['id' => $id];
186
        }
187
188
        $object = null;
189
        if (is_array($user)) {
190
            $object = User::findOne($user);
191
        } elseif (is_numeric($user)) {
192
            $object = Craft::$app->getUsers()->getUserById($user);
193
        } elseif (is_string($user)) {
194
            $object = Craft::$app->getUsers()->getUserByUsernameOrEmail($user);
195
        }
196
197
        if (null !== $object) {
198
            return $object;
199
        }
200
201
        return new User($user);
202
    }
203
204
    /**
205
     * AssociateUserToOrganization a user to an organization
206
     *
207
     * @param User $user
208
     * @return $this
209
     */
210
    public function addUser(User $user)
211
    {
212
213
        $currentUsers = $this->getUsers()->all();
214
215
        $userElementsByEmail = ArrayHelper::index(
216
            $currentUsers,
217
            'email'
218
        );
219
220
        // Does the user already exist?
221
        if (!array_key_exists($user->email, $userElementsByEmail)) {
222
            $currentUsers[] = $user;
223
            $this->getUsers()->setCachedResult($currentUsers);
224
        }
225
226
        return $this;
227
    }
228
229
    /**
230
     * DissociateUserFromOrganization a user from an organization
231
     *
232
     * @param array $users
233
     * @return $this
234
     */
235
    public function removeUsers(array $users)
236
    {
237
        // In case a config is directly passed
238
        if (ArrayHelper::isAssociative($users)) {
239
            $users = [$users];
240
        }
241
242
        foreach ($users as $key => $user) {
243
            // Ensure we have a model
244
            if (!$user instanceof User) {
245
                $user = $this->resolveUser($user);
246
            }
247
248
            $this->removeUser($user);
249
        }
250
251
        return $this;
252
    }
253
254
    /**
255
     * DissociateUserFromOrganization a user from an organization
256
     *
257
     * @param User $user
258
     * @return $this
259
     */
260
    public function removeUser(User $user)
261
    {
262
        $userElementsByEmail = ArrayHelper::index(
263
            $this->getUsers()->all(),
264
            'email'
265
        );
266
267
        // Does the user already exist?
268
        if (array_key_exists($user->email, $userElementsByEmail)) {
269
            unset($userElementsByEmail[$user->email]);
270
271
            $this->getUsers()->setCachedResult(
272
                array_values($userElementsByEmail)
273
            );
274
        }
275
276
        return $this;
277
    }
278
279
    /**
280
     * Reset users
281
     *
282
     * @return $this
283
     */
284
    public function resetUsers()
285
    {
286
        $this->users = null;
287
        return $this;
288
    }
289
290
291
    /*******************************************
292
     * ASSOCIATE and/or DISASSOCIATE
293
     *******************************************/
294
295
    /**
296
     * @return bool
297
     * @throws \Throwable
298
     * @throws \yii\db\StaleObjectException
299
     */
300
    public function saveUsers()
301
    {
302
303
        // No changes?
304
        if (null === ($users = $this->getUsers()->getCachedResult())) {
305
            return true;
306
        }
307
308
        $currentAssociations = UserAssociation::find()
309
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by nateiler
It seems like getId() 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...
310
            ->indexBy('userId')
311
            ->orderBy(['userOrder' => SORT_ASC])
312
            ->all();
313
314
        $success = true;
315
        $associations = [];
316
        $order = 1;
317
        foreach ($users as $user) {
318
            if (null === ($association = ArrayHelper::remove($currentAssociations, $user->getId()))) {
319
                $association = (new UserAssociation())
320
                    ->setUser($user)
321
                    ->setOrganization($this);
322
            }
323
324
            $association->userOrder = $order++;
325
326
            $associations[] = $association;
327
        }
328
329
        // Delete those removed
330
        foreach ($currentAssociations as $currentAssociation) {
331
            if (!$currentAssociation->delete()) {
332
                $success = false;
333
            }
334
        }
335
336
        foreach ($associations as $association) {
337
            if (!$association->save()) {
338
                $success = false;
339
            }
340
        }
341
342
        if (!$success) {
343
            $this->addError('users', 'Unable to associate users.');
0 ignored issues
show
Bug introduced by nateiler
It seems like addError() 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...
344
        }
345
346
        return $success;
347
    }
348
349
    /**
350
     * @param User $user
351
     * @param int|null $sortOrder
352
     * @return bool
353
     */
354
    public function associateUser(User $user, int $sortOrder = null): bool
355
    {
356
        if (null === ($association = UserAssociation::find()
357
                ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by nateiler
It seems like getId() 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...
358
                ->userId($user->getId() ?: false)
0 ignored issues
show
Security Bug introduced by nateiler
It seems like $user->getId() ?: false can also be of type false; however, flipbox\craft\ember\quer...ttributeTrait::userId() does only seem to accept string|array<integer,str...ft\elements\User>>|null, did you maybe forget to handle an error condition?
Loading history...
359
                ->one())
360
        ) {
361
            $association = new UserAssociation([
362
                'organization' => $this,
363
                'user' => $user
364
            ]);
365
        }
366
367
        if (null !== $sortOrder) {
368
            $association->userOrder = $sortOrder;
369
        }
370
371
        if (!$association->save()) {
372
            $this->addError('organizations', 'Unable to associate user.');
0 ignored issues
show
Bug introduced by nateiler
It seems like addError() 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...
373
374
            return false;
375
        }
376
377
        $this->resetUsers();
378
379
        return true;
380
    }
381
382
    /**
383
     * @param UserQuery $query
384
     * @return bool
385
     * @throws \Throwable
386
     */
387
    public function associateUsers(UserQuery $query)
388
    {
389
        $users = $query->all();
390
391
        if (empty($users)) {
392
            return true;
393
        }
394
395
        $success = true;
396
        $currentAssociations = UserAssociation::find()
397
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by nateiler
It seems like getId() 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...
398
            ->indexBy('userId')
399
            ->all();
400
401
        foreach ($users as $user) {
402
            if (null === ($association = ArrayHelper::remove($currentAssociations, $user->getId()))) {
403
                $association = (new UserAssociation())
404
                    ->setUser($user)
405
                    ->setOrganization($this);
406
            }
407
408
            if (!$association->save()) {
409
                $success = false;
410
            }
411
        }
412
413
        if (!$success) {
414
            $this->addError('users', 'Unable to associate users.');
0 ignored issues
show
Bug introduced by nateiler
It seems like addError() 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...
415
        }
416
417
        $this->resetUsers();
418
419
        return $success;
420
    }
421
422
    /**
423
     * @param User $user
424
     * @return bool
425
     * @throws \Throwable
426
     * @throws \yii\db\StaleObjectException
427
     */
428
    public function dissociateUser(User $user): bool
429
    {
430
        if (null === ($association = UserAssociation::find()
431
                ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by nateiler
It seems like getId() 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...
432
                ->userId($user->getId() ?: false)
0 ignored issues
show
Security Bug introduced by nateiler
It seems like $user->getId() ?: false can also be of type false; however, flipbox\craft\ember\quer...ttributeTrait::userId() does only seem to accept string|array<integer,str...ft\elements\User>>|null, did you maybe forget to handle an error condition?
Loading history...
433
                ->one())
434
        ) {
435
            return true;
436
        }
437
438
        if (!$association->delete()) {
0 ignored issues
show
Bug Best Practice introduced by nateiler
The expression $association->delete() of type false|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
439
            $this->addError('organizations', 'Unable to dissociate user.');
0 ignored issues
show
Bug introduced by nateiler
It seems like addError() 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...
440
441
            return false;
442
        }
443
444
        $this->resetUsers();
445
446
        return true;
447
    }
448
449
    /**
450
     * @param UserQuery $query
451
     * @return bool
452
     * @throws \Throwable
453
     */
454
    public function dissociateUsers(UserQuery $query)
455
    {
456
        $users = $query->all();
457
458
        if (empty($users)) {
459
            return true;
460
        }
461
462
        $currentAssociations = UserAssociation::find()
463
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by nateiler
It seems like getId() 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...
464
            ->indexBy('userId')
465
            ->all();
466
467
        $success = true;
468
469
        foreach ($users as $user) {
470
            if (null === ($association = ArrayHelper::remove($currentAssociations, $user->getId()))) {
471
                continue;
472
            }
473
474
            if (!$association->delete()) {
475
                $success = false;
476
            }
477
        }
478
479
        if (!$success) {
480
            $this->addError('users', 'Unable to associate users.');
0 ignored issues
show
Bug introduced by nateiler
It seems like addError() 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...
481
        }
482
483
        $this->resetUsers();
484
485
        return $success;
486
    }
487
}
488