UserOrganizationsBehavior   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 453
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 64
lcom 1
cbo 15
dl 0
loc 453
ccs 0
cts 270
cp 0
rs 3.28
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 37 1
A onAfterDelete() 0 9 1
A onAfterSave() 0 22 5
A onAfterValidate() 0 8 2
A organizationQuery() 0 17 2
A getOrganizations() 0 15 3
A setOrganizations() 0 13 2
A addOrganizations() 0 21 4
A addOrganization() 0 18 4
A resolveOrganization() 0 18 5
B saveOrganizations() 0 44 9
B associateOrganization() 0 27 6
B associateOrganizations() 0 31 6
A dissociateOrganization() 0 20 5
B dissociateOrganizations() 0 29 6
A currentAssociationQuery() 0 7 2
A resetOrganizations() 0 5 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace flipbox\organizations\behaviors;
4
5
use Craft;
6
use craft\elements\User;
7
use craft\events\ModelEvent;
8
use craft\helpers\ArrayHelper;
9
use flipbox\craft\ember\helpers\QueryHelper;
10
use flipbox\organizations\elements\Organization;
11
use flipbox\organizations\Organizations as OrganizationPlugin;
12
use flipbox\organizations\queries\OrganizationQuery;
13
use flipbox\organizations\queries\UserAssociationQuery;
14
use flipbox\organizations\records\UserAssociation;
15
use flipbox\organizations\validators\OrganizationsValidator;
16
use yii\base\Behavior;
17
use yii\base\Event;
18
use yii\base\Exception;
19
use yii\helpers\Json;
20
21
/**
22
 * Class UserOrganizationsBehavior
23
 * @package flipbox\organizations\behaviors
24
 *
25
 * @property User $owner;
26
 */
27
class UserOrganizationsBehavior extends Behavior
28
{
29
    /**
30
     * @var OrganizationQuery|null
31
     */
32
    private $organizations;
33
34
    /**
35
     * @inheritdoc
36
     */
37
    public function init()
38
    {
39
        parent::init();
40
41
        // Validate organizations
42
        Event::on(
43
            User::class,
44
            User::EVENT_AFTER_VALIDATE,
45
            function (Event $e) {
46
                /** @var User $user */
47
                $user = $e->sender;
48
                $this->onAfterValidate($user);
49
            }
50
        );
51
52
        // Associate
53
        Event::on(
54
            User::class,
55
            User::EVENT_AFTER_SAVE,
56
            function (ModelEvent $e) {
57
                /** @var User $user */
58
                $user = $e->sender;
59
                $this->onAfterSave($user);
60
            }
61
        );
62
63
        // Dissociate
64
        Event::on(
65
            User::class,
66
            User::EVENT_AFTER_DELETE,
67
            function (Event $e) {
68
                /** @var User $user */
69
                $user = $e->sender;
70
                $this->onAfterDelete($user);
71
            }
72
        );
73
    }
74
75
    /**
76
     * @param User $user
77
     * @return void
78
     * @throws \Throwable
79
     */
80
    private function onAfterDelete(User $user)
81
    {
82
        /** @var UserOrganizationsBehavior $user */
83
        // Remove organizations
84
        $user->setOrganizations([]);
85
86
        // Save associations (which is really deleting them all)
87
        $user->saveOrganizations();
88
    }
89
90
    /**
91
     * @param User|self $user
92
     * @throws Exception
93
     * @throws \Exception
94
     * @throws \Throwable
95
     * @throws \craft\errors\ElementNotFoundException
96
     */
97
    private function onAfterSave(User $user)
98
    {
99
        // Check cache for explicitly set (and possibly not saved) organizations
100
        if (null !== ($organizations = $user->getOrganizations()->getCachedResult())) {
101
102
            /** @var Organization $organization */
103
            foreach ($organizations as $organization) {
104
                if (!$organization->id) {
105
                    if (!Craft::$app->getElements()->saveElement($organization)) {
106
                        $user->addError(
107
                            'organizations',
108
                            Craft::t('organizations', 'Unable to save organization.')
109
                        );
110
111
                        throw new Exception('Unable to save organization.');
112
                    }
113
                }
114
            }
115
        }
116
117
        $this->saveOrganizations();
118
    }
119
120
    /**
121
     * @param User|self $user
122
     * @return void
123
     */
124
    private function onAfterValidate(User $user)
125
    {
126
        $error = null;
127
128
        if (!(new OrganizationsValidator())->validate($user->getOrganizations(), $error)) {
129
            $user->addError('organizations', $error);
130
        }
131
    }
132
133
    /**
134
     * @param array $criteria
135
     * @return OrganizationQuery
136
     */
137
    public function organizationQuery($criteria = []): OrganizationQuery
138
    {
139
        $query = Organization::find()
140
            ->user($this->owner)
0 ignored issues
show
Bug introduced by nateiler
It seems like $this->owner can also be of type object<yii\base\Component>; however, flipbox\craft\ember\quer...rAttributeTrait::user() does only seem to accept string|array<integer,str...ft\elements\User>>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
141
            ->orderBy([
142
                'organizationOrder' => SORT_ASC
143
            ]);
144
145
        if (!empty($criteria)) {
146
            QueryHelper::configure(
147
                $query,
148
                $criteria
149
            );
150
        }
151
152
        return $query;
153
    }
154
155
    /**
156
     * Get a query with associated organizations
157
     *
158
     * @param array $criteria
159
     * @return OrganizationQuery
160
     */
161
    public function getOrganizations($criteria = []): OrganizationQuery
162
    {
163
        if (null === $this->organizations) {
164
            $this->organizations = $this->organizationQuery();
165
        }
166
167
        if (!empty($criteria)) {
168
            QueryHelper::configure(
169
                $this->organizations,
170
                $criteria
171
            );
172
        }
173
174
        return $this->organizations;
175
    }
176
177
    /**
178
     * Set an array or query of organizations to a user
179
     *
180
     * @param $organizations
181
     * @return $this
182
     */
183
    public function setOrganizations($organizations)
184
    {
185
        if ($organizations instanceof OrganizationQuery) {
186
            $this->organizations = $organizations;
187
            return $this;
188
        }
189
190
        // Reset the query
191
        $this->organizations = $this->organizationQuery();
192
        $this->organizations->setCachedResult([]);
193
        $this->addOrganizations($organizations);
194
        return $this;
195
    }
196
197
    /**
198
     * Add an array of organizations to a user.  Note: This does not save the organization associations.
199
     *
200
     * @param $organizations
201
     * @return $this
202
     */
203
    protected function addOrganizations(array $organizations)
204
    {
205
        // In case a config is directly passed
206
        if (ArrayHelper::isAssociative($organizations)) {
207
            $organizations = [$organizations];
208
        }
209
210
        foreach ($organizations as $key => $organization) {
211
            if (!$organization = $this->resolveOrganization($organization)) {
0 ignored issues
show
Bug introduced by Nate Iler
Are you sure the assignment to $organization is correct as $this->resolveOrganization($organization) (which targets flipbox\organizations\be...::resolveOrganization()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
212
                OrganizationPlugin::info(sprintf(
213
                    "Unable to resolve organization: %s",
214
                    (string)Json::encode($organization)
215
                ));
216
                continue;
217
            }
218
219
            $this->addOrganization($organization);
220
        }
221
222
        return $this;
223
    }
224
225
    /**
226
     * Add a organization to a user.  Note: This does not save the organization association.
227
     *
228
     * @param Organization $organization
229
     * @param bool $addToOrganization
230
     * @return $this
231
     */
232
    public function addOrganization(Organization $organization, bool $addToOrganization = true)
233
    {
234
        // Current associated organizations
235
        $allOrganizations = $this->getOrganizations()->all();
236
        $allOrganizations[] = $organization;
237
238
        $this->getOrganizations()->setCachedResult($allOrganizations);
239
240
        // Add user to organization as well?
241
        if ($addToOrganization && $organization->id !== null) {
242
            $user = $this->owner;
243
            if ($user instanceof User) {
244
                $organization->addUser($user);
245
            };
246
        }
247
248
        return $this;
249
    }
250
251
    /**
252
     * @param $organization
253
     * @return Organization
254
     */
255
    protected function resolveOrganization($organization)
256
    {
257
        if ($organization instanceof Organization) {
258
            return $organization;
259
        }
260
261
        if (is_array($organization) &&
262
            null !== ($id = ArrayHelper::getValue($organization, 'id'))
263
        ) {
264
            return Organization::findOne($id);
0 ignored issues
show
Bug Compatibility introduced by Nate Iler
The expression \flipbox\organizations\e...nization::findOne($id); of type craft\base\Element|null|craft\base\Element[] adds the type craft\base\Element[] to the return on line 264 which is incompatible with the return type documented by flipbox\organizations\be...or::resolveOrganization of type flipbox\organizations\elements\Organization|null.
Loading history...
265
        }
266
267
        if (null !== ($object = Organization::findOne($organization))) {
0 ignored issues
show
Bug Compatibility introduced by Nate Iler
The expression \flipbox\organizations\e...findOne($organization); of type craft\base\Element|null|craft\base\Element[] adds the type craft\base\Element[] to the return on line 268 which is incompatible with the return type documented by flipbox\organizations\be...or::resolveOrganization of type flipbox\organizations\elements\Organization|null.
Loading history...
268
            return $object;
269
        }
270
271
        return new Organization($organization);
272
    }
273
274
    /*******************************************
275
     * ASSOCIATE and/or DISASSOCIATE
276
     *******************************************/
277
278
    /**
279
     * @return bool
280
     * @throws \Throwable
281
     * @throws \yii\db\StaleObjectException
282
     */
283
    public function saveOrganizations(): bool
284
    {
285
        // No changes?
286
        if (null === ($records = $this->getOrganizations()->getCachedResult())) {
287
            return true;
288
        }
289
290
        $currentAssociations = $this->currentAssociationQuery()->all();
291
292
        $success = true;
293
        $associations = [];
294
        $order = 1;
295
        foreach ($records as $type) {
296
            if (null === ($association = ArrayHelper::remove($currentAssociations, $type->getId()))) {
297
                $association = (new UserAssociation())
298
                    ->setUser($this->owner)
299
                    ->setOrganization($type);
300
            }
301
302
            $association->organizationOrder = $order++;
303
304
            $associations[] = $association;
305
        }
306
307
        // Delete anything that has been removed
308
        foreach ($currentAssociations as $currentAssociation) {
309
            if (!$currentAssociation->delete()) {
310
                $success = false;
311
            }
312
        }
313
314
        // Save'em
315
        foreach ($associations as $association) {
316
            if (!$association->save()) {
317
                $success = false;
318
            }
319
        }
320
321
        if (!$success) {
322
            $this->owner->addError('organizations', 'Unable to save user organizations.');
323
        }
324
325
        return $success;
326
    }
327
328
    /**
329
     * @param Organization $organization
330
     * @param int|null $sortOrder
331
     * @return bool
332
     */
333
    public function associateOrganization(Organization $organization, int $sortOrder = null): bool
334
    {
335
        if (null === ($association = UserAssociation::find()
336
                ->userId($this->owner->getId() ?: false)
337
                ->organizationId($organization->getId() ?: false)
0 ignored issues
show
Security Bug introduced by nateiler
It seems like $organization->getId() ?: false can also be of type false; however, flipbox\organizations\qu...Trait::organizationId() does only seem to accept string|array<integer,str...nts\Organization>>|null, did you maybe forget to handle an error condition?
Loading history...
338
                ->one())
339
        ) {
340
            $association = new UserAssociation([
341
                'organization' => $organization,
342
                'user' => $this->owner
343
            ]);
344
        }
345
346
        if (null !== $sortOrder) {
347
            $association->organizationOrder = $sortOrder;
348
        }
349
350
        if (!$association->save()) {
351
            $this->owner->addError('organizations', 'Unable to associate organization.');
352
353
            return false;
354
        }
355
356
        $this->resetOrganizations();
357
358
        return true;
359
    }
360
361
    /**
362
     * @param OrganizationQuery $query
363
     * @return bool
364
     * @throws \Throwable
365
     */
366
    public function associateOrganizations(OrganizationQuery $query): bool
367
    {
368
        $organizations = $query->all();
369
370
        if (empty($organizations)) {
371
            return true;
372
        }
373
374
        $currentAssociations = $this->currentAssociationQuery()->all();
375
376
        $success = true;
377
        foreach ($organizations as $organization) {
378
            if (null === ($association = ArrayHelper::remove($currentAssociations, $organization->getId()))) {
379
                $association = (new UserAssociation())
380
                    ->setUser($this->owner)
381
                    ->setOrganization($organization);
382
            }
383
384
            if (!$association->save()) {
385
                $success = false;
386
            }
387
        }
388
389
        if (!$success) {
390
            $this->owner->addError('organizations', 'Unable to associate organizations.');
391
        }
392
393
        $this->resetOrganizations();
394
395
        return $success;
396
    }
397
398
    /**
399
     * @param Organization $organization
400
     * @return bool
401
     * @throws \Throwable
402
     * @throws \yii\db\StaleObjectException
403
     */
404
    public function dissociateOrganization(Organization $organization): bool
405
    {
406
        if (null === ($association = UserAssociation::find()
407
                ->userId($this->owner->getId() ?: false)
408
                ->organizationId($organization->getId() ?: false)
0 ignored issues
show
Security Bug introduced by nateiler
It seems like $organization->getId() ?: false can also be of type false; however, flipbox\organizations\qu...Trait::organizationId() does only seem to accept string|array<integer,str...nts\Organization>>|null, did you maybe forget to handle an error condition?
Loading history...
409
                ->one())
410
        ) {
411
            return true;
412
        }
413
414
        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...
415
            $this->owner->addError('organizations', 'Unable to dissociate organization.');
416
417
            return false;
418
        }
419
420
        $this->resetOrganizations();
421
422
        return true;
423
    }
424
425
    /**
426
     * @param OrganizationQuery $query
427
     * @return bool
428
     * @throws \Throwable
429
     */
430
    public function dissociateOrganizations(OrganizationQuery $query): bool
431
    {
432
        $organizations = $query->all();
433
434
        if (empty($organizations)) {
435
            return true;
436
        }
437
438
        $currentAssociations = $this->currentAssociationQuery()->all();
439
440
        $success = true;
441
        foreach ($organizations as $organization) {
442
            if (null === ($association = ArrayHelper::remove($currentAssociations, $organization->getId()))) {
443
                continue;
444
            }
445
446
            if (!$association->delete()) {
447
                $success = false;
448
            }
449
        }
450
451
        if (!$success) {
452
            $this->owner->addError('organizations', 'Unable to associate organizations.');
453
        }
454
455
        $this->resetOrganizations();
456
457
        return $success;
458
    }
459
460
    /**
461
     * @return UserAssociationQuery
462
     */
463
    protected function currentAssociationQuery(): UserAssociationQuery
464
    {
465
        return UserAssociation::find()
466
            ->userId($this->owner->getId() ?: false)
467
            ->indexBy('organizationId')
468
            ->orderBy(['organizationOrder' => SORT_ASC]);
469
    }
470
471
    /**
472
     * @return User
473
     */
474
    public function resetOrganizations()
475
    {
476
        $this->organizations = null;
477
        return $this->owner;
478
    }
479
}
480