Completed
Push — develop ( aae9cf...b0912f )
by Nate
03:53
created

dissociateOrganization()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 0
cts 16
cp 0
rs 9.2888
c 0
b 0
f 0
cc 5
nc 3
nop 1
crap 30
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
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
                'title' => SORT_ASC
144
            ]);
145
146
        if (!empty($criteria)) {
147
            QueryHelper::configure(
148
                $query,
149
                $criteria
150
            );
151
        }
152
153
        return $query;
154
    }
155
156
    /**
157
     * Get a query with associated organizations
158
     *
159
     * @param array $criteria
160
     * @return OrganizationQuery
161
     */
162
    public function getOrganizations($criteria = []): OrganizationQuery
163
    {
164
        if (null === $this->organizations) {
165
            $this->organizations = $this->organizationQuery();
166
        }
167
168
        if (!empty($criteria)) {
169
            QueryHelper::configure(
170
                $this->organizations,
171
                $criteria
172
            );
173
        }
174
175
        return $this->organizations;
176
    }
177
178
    /**
179
     * Set an array or query of organizations to a user
180
     *
181
     * @param $organizations
182
     * @return $this
183
     */
184
    public function setOrganizations($organizations)
185
    {
186
        if ($organizations instanceof OrganizationQuery) {
187
            $this->organizations = $organizations;
188
            return $this;
189
        }
190
191
        // Reset the query
192
        $this->organizations = $this->organizationQuery();
193
        $this->organizations->setCachedResult([]);
194
        $this->addOrganizations($organizations);
195
        return $this;
196
    }
197
198
    /**
199
     * Add an array of organizations to a user.  Note: This does not save the organization associations.
200
     *
201
     * @param $organizations
202
     * @return $this
203
     */
204
    protected function addOrganizations(array $organizations)
205
    {
206
        // In case a config is directly passed
207
        if (ArrayHelper::isAssociative($organizations)) {
208
            $organizations = [$organizations];
209
        }
210
211
        foreach ($organizations as $key => $organization) {
212
            if (!$organization = $this->resolveOrganization($organization)) {
0 ignored issues
show
Bug introduced by
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...
213
                OrganizationPlugin::info(sprintf(
214
                    "Unable to resolve organization: %s",
215
                    (string)Json::encode($organization)
216
                ));
217
                continue;
218
            }
219
220
            $this->addOrganization($organization);
221
        }
222
223
        return $this;
224
    }
225
226
    /**
227
     * Add a organization to a user.  Note: This does not save the organization association.
228
     *
229
     * @param Organization $organization
230
     * @param bool $addToOrganization
231
     * @return $this
232
     */
233
    public function addOrganization(Organization $organization, bool $addToOrganization = true)
234
    {
235
        // Current associated organizations
236
        $allOrganizations = $this->getOrganizations()->all();
237
        $allOrganizations[] = $organization;
238
239
        $this->getOrganizations()->setCachedResult($allOrganizations);
240
241
        // Add user to organization as well?
242
        if ($addToOrganization && $organization->id !== null) {
243
            $user = $this->owner;
244
            if ($user instanceof User) {
245
                $organization->addUser($user);
246
            };
247
        }
248
249
        return $this;
250
    }
251
252
    /**
253
     * @param $organization
254
     * @return Organization
255
     */
256
    protected function resolveOrganization($organization)
257
    {
258
        if ($organization instanceof Organization) {
259
            return $organization;
260
        }
261
262
        if (is_array($organization) &&
263
            null !== ($id = ArrayHelper::getValue($organization, 'id'))
264
        ) {
265
            return Organization::findOne($id);
0 ignored issues
show
Bug Compatibility introduced by
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 265 which is incompatible with the return type documented by flipbox\organizations\be...or::resolveOrganization of type flipbox\organizations\elements\Organization|null.
Loading history...
266
        }
267
268
        if (null !== ($object = Organization::findOne($organization))) {
0 ignored issues
show
Bug Compatibility introduced by
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 269 which is incompatible with the return type documented by flipbox\organizations\be...or::resolveOrganization of type flipbox\organizations\elements\Organization|null.
Loading history...
269
            return $object;
270
        }
271
272
        return new Organization($organization);
273
    }
274
275
    /*******************************************
276
     * ASSOCIATE and/or DISASSOCIATE
277
     *******************************************/
278
279
    /**
280
     * @return bool
281
     * @throws \Throwable
282
     * @throws \yii\db\StaleObjectException
283
     */
284
    public function saveOrganizations(): bool
285
    {
286
        // No changes?
287
        if (null === ($records = $this->getOrganizations()->getCachedResult())) {
288
            return true;
289
        }
290
291
        $currentAssociations = $this->currentAssociationQuery()->all();
292
293
        $success = true;
294
        $associations = [];
295
        $order = 1;
296
        foreach ($records as $type) {
297
            if (null === ($association = ArrayHelper::remove($currentAssociations, $type->getId()))) {
298
                $association = (new UserAssociation())
299
                    ->setUser($this->owner)
300
                    ->setOrganization($type);
301
            }
302
303
            $association->organizationOrder = $order++;
304
305
            $associations[] = $association;
306
        }
307
308
        // Delete anything that has been removed
309
        foreach ($currentAssociations as $currentAssociation) {
310
            if (!$currentAssociation->delete()) {
311
                $success = false;
312
            }
313
        }
314
315
        // Save'em
316
        foreach ($associations as $association) {
317
            if (!$association->save()) {
318
                $success = false;
319
            }
320
        }
321
322
        if (!$success) {
323
            $this->owner->addError('organizations', 'Unable to save user organizations.');
324
        }
325
326
        return $success;
327
    }
328
329
    /**
330
     * @param Organization $organization
331
     * @param int|null $sortOrder
332
     * @return bool
333
     */
334
    public function associateOrganization(Organization $organization, int $sortOrder = null): bool
335
    {
336
        if (null === ($association = UserAssociation::find()
337
                ->userId($this->owner->getId() ?: false)
338
                ->organizationId($organization->getId() ?: false)
0 ignored issues
show
Security Bug introduced by
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...
339
                ->one())
340
        ) {
341
            $association = new UserAssociation([
342
                'organization' => $organization,
343
                'user' => $this->owner
344
            ]);
345
        }
346
347
        if (null !== $sortOrder) {
348
            $association->organizationOrder = $sortOrder;
349
        }
350
351
        if (!$association->save()) {
352
            $this->owner->addError('organizations', 'Unable to associate organization.');
353
354
            return false;
355
        }
356
357
        $this->resetOrganizations();
358
359
        return true;
360
    }
361
362
    /**
363
     * @param OrganizationQuery $query
364
     * @return bool
365
     * @throws \Throwable
366
     */
367
    public function associateOrganizations(OrganizationQuery $query): bool
368
    {
369
        $organizations = $query->all();
370
371
        if (empty($organizations)) {
372
            return true;
373
        }
374
375
        $currentAssociations = $this->currentAssociationQuery()->all();
376
377
        $success = true;
378
        foreach ($organizations as $organization) {
379
            if (null === ($association = ArrayHelper::remove($currentAssociations, $organization->getId()))) {
380
                $association = (new UserAssociation())
381
                    ->setUser($this->owner)
382
                    ->setOrganization($organization);
383
            }
384
385
            if (!$association->save()) {
386
                $success = false;
387
            }
388
        }
389
390
        if (!$success) {
391
            $this->owner->addError('organizations', 'Unable to associate organizations.');
392
        }
393
394
        $this->resetOrganizations();
395
396
        return $success;
397
    }
398
399
    /**
400
     * @param Organization $organization
401
     * @return bool
402
     * @throws \Throwable
403
     * @throws \yii\db\StaleObjectException
404
     */
405
    public function dissociateOrganization(Organization $organization): bool
406
    {
407
        if (null === ($association = UserAssociation::find()
408
                ->userId($this->owner->getId() ?: false)
409
                ->organizationId($organization->getId() ?: false)
0 ignored issues
show
Security Bug introduced by
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...
410
                ->one())
411
        ) {
412
            return true;
413
        }
414
415
        if (!$association->delete()) {
0 ignored issues
show
Bug Best Practice introduced by
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...
416
            $this->owner->addError('organizations', 'Unable to dissociate organization.');
417
418
            return false;
419
        }
420
421
        $this->resetOrganizations();
422
423
        return true;
424
    }
425
426
    /**
427
     * @param OrganizationQuery $query
428
     * @return bool
429
     * @throws \Throwable
430
     */
431
    public function dissociateOrganizations(OrganizationQuery $query): bool
432
    {
433
        $organizations = $query->all();
434
435
        if (empty($organizations)) {
436
            return true;
437
        }
438
439
        $currentAssociations = $this->currentAssociationQuery()->all();
440
441
        $success = true;
442
        foreach ($organizations as $organization) {
443
            if (null === ($association = ArrayHelper::remove($currentAssociations, $organization->getId()))) {
444
                continue;
445
            }
446
447
            if (!$association->delete()) {
448
                $success = false;
449
            }
450
        }
451
452
        if (!$success) {
453
            $this->owner->addError('organizations', 'Unable to associate organizations.');
454
        }
455
456
        $this->resetOrganizations();
457
458
        return $success;
459
    }
460
461
    /**
462
     * @return UserAssociationQuery
463
     */
464
    protected function currentAssociationQuery(): UserAssociationQuery
465
    {
466
        return UserAssociation::find()
467
            ->userId($this->owner->getId() ?: false)
468
            ->indexBy('organizationId')
469
            ->orderBy(['organizationOrder' => SORT_ASC]);
470
    }
471
472
    /**
473
     * @return User
474
     */
475
    public function resetOrganizations()
476
    {
477
        $this->organizations = null;
478
        return $this->owner;
479
    }
480
}
481