User::getUserQuery()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
crap 2
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\organization\services;
10
11
use Craft;
12
use craft\db\Query;
13
use craft\elements\User as UserElement;
14
use craft\helpers\ArrayHelper;
15
use flipbox\organization\elements\db\User as UserQuery;
16
use flipbox\organization\elements\Organization as OrganizationElement;
17
use flipbox\organization\events\ManageOrganizationUser;
18
use flipbox\organization\helpers\Query as QueryHelper;
19
use flipbox\organization\Organization as OrganizationPlugin;
20
use flipbox\organization\records\User as OrganizationUserRecord;
21
use flipbox\spark\helpers\RecordHelper;
22
use yii\base\Component;
23
use yii\base\ErrorException as Exception;
24
25
/**
26
 * @author Flipbox Factory <[email protected]>
27
 * @since 1.0.0
28
 */
29
class User extends Component
30
{
31
32
    /**
33
     * @event ManageOrganizationUserEvent The event that is triggered before a
34
     * user is associated to an organization.
35
     *
36
     * You may set [[ManageOrganizationUserEvent::isValid]] to `false` to prevent the
37
     * user from being associated to the organization.
38
     */
39
    const EVENT_BEFORE_ASSOCIATE = 'beforeAssociate';
40
41
    /**
42
     * @event ManageOrganizationUserEvent The event that is triggered after a
43
     * user is associated to an organization.
44
     *
45
     * * You may set [[ManageOrganizationUserEvent::isValid]] to `false` to prevent the
46
     * user from being associated to the organization.
47
     */
48
    const EVENT_AFTER_ASSOCIATE = 'afterAssociate';
49
50
    /**
51
     * @event ManageOrganizationUserEvent The event that is triggered before a
52
     * user is remove from an organization.
53
     *
54
     * You may set [[ManageOrganizationUserEvent::isValid]] to `false` to prevent the
55
     * user from being removed from the organization.
56
     */
57
    const EVENT_BEFORE_DISSOCIATE = 'beforeDissociate';
58
59
    /**
60
     * @event ManageOrganizationUserEvent The event that is triggered after a
61
     * user is remove from an organization.
62
     *
63
     * * You may set [[ManageOrganizationUserEvent::isValid]] to `false` to prevent the
64
     * user from being removed from the organization.
65
     */
66
    const EVENT_AFTER_DISSOCIATE = 'afterDissociate';
67
68
69
    /**
70
     * @inheritdoc
71
     */
72
    public static function elementClass(): string
73
    {
74
        return UserElement::class;
75
    }
76
77
    /*******************************************
78
     * QUERY
79
     *******************************************/
80
81
    /**
82
     * Get query
83
     *
84
     * @param $criteria
85
     * @return UserQuery
86
     */
87
    public function getQuery($criteria = [])
88
    {
89
90
        /** @var UserQuery $query */
91
        $query = new UserQuery(UserElement::class);
92
93
        // Force array
94
        if (!is_array($criteria)) {
95
            $criteria = ArrayHelper::toArray($criteria, [], false);
96
        }
97
98
        // Configure it
99
        QueryHelper::configure(
100
            $query,
101
            $criteria
102
        );
103
104
        return $query;
105
    }
106
107
    /**
108
     * @param array $ownerCriteria
109
     * @param array $criteria
110
     * @return UserQuery
111
     */
112
    public function getOwnerQuery($ownerCriteria = [], $criteria = [])
113
    {
114
115
        $query = $this->getQuery($criteria)
116
            ->organization(['owner' => $ownerCriteria]);
117
118
        return $query;
119
    }
120
121
    /**
122
     * @param array $userCriteria
123
     * @param array $criteria
124
     * @return UserQuery
125
     */
126
    public function getUserQuery($userCriteria = [], $criteria = [])
127
    {
128
129
        $query = $this->getQuery($criteria)
130
            ->organization(['user' => $userCriteria]);
131
132
        return $query;
133
    }
134
135
    /**
136
     * @param array $memberCriteria
137
     * @param array $criteria
138
     * @return UserQuery
139
     */
140
    public function getMemberQuery($memberCriteria = [], $criteria = [])
141
    {
142
143
        $query = $this->getQuery($criteria)
144
            ->organization(['member' => $memberCriteria]);
145
146
        return $query;
147
    }
148
149
    /*******************************************
150
     * UTILITY
151
     *******************************************/
152
153
    /**
154
     * @param UserElement $user
155
     * @param array $criteria
156
     * @return bool
157
     */
158
    public function isUser(UserElement $user, $criteria = [])
159
    {
160
161
        // Gotta have an Id to be a user
162
        if (!$user->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user->id of type integer|null 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...
163
            return false;
164
        }
165
166
        return $this->getUserQuery($criteria, ['id' => $user->id])
167
                ->count() > 0;
168
    }
169
170
    /**
171
     * @param UserElement $user
172
     * @param array $criteria
173
     * @return bool
174
     */
175
    public function isOwner(UserElement $user, $criteria = [])
176
    {
177
178
        // Gotta have an Id to be an owner
179
        if (!$user->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user->id of type integer|null 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...
180
            return false;
181
        }
182
183
        return $this->getOwnerQuery($criteria, ['id' => $user->id])
184
                ->count() > 0;
185
    }
186
187
    /**
188
     * @param UserElement $user
189
     * @param array $criteria
190
     * @return bool
191
     */
192
    public function isMember(UserElement $user, $criteria = [])
193
    {
194
195
        // Gotta have an Id to be a member
196
        if (!$user->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user->id of type integer|null 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...
197
            return false;
198
        }
199
200
        return $this->getMemberQuery($criteria, ['id' => $user->id])
201
                ->count() > 0;
202
    }
203
204
    /**
205
     * @param UserElement $userElement
206
     * @param OrganizationElement $organizationElement
207
     * @param int|null $siteId
208
     * @param int|null $sortOrder The order which the user should be positioned.
209
     * If the value is zero (0) we position them last.
210
     */
211
    protected function applySortOrder(
212
        UserElement $userElement,
213
        OrganizationElement $organizationElement,
214
        int $siteId = null,
215
        int $sortOrder = null
216
    ) {
217
218
        // No order
219
        if (null === $sortOrder) {
220
            return;
221
        }
222
223
        /** @var array $currentOrder */
224
        $currentOrder = $this->getCurrentSortOrder($organizationElement, $siteId);
225
226
        // The target record to position
227
        if (!$target = ArrayHelper::remove($currentOrder, $userElement->id)) {
228
            return;
229
        }
230
231
        $currentSortOrder = ArrayHelper::getValue($target, 'sortOrder');
232
233
        // Sort order already correct?
234
        if ($sortOrder === $currentSortOrder) {
235
            return;
236
        }
237
238
        /** @var int $items */
239
        $items = count($currentOrder);
240
241
        // Last
242
        if (0 === $sortOrder || $sortOrder > $items) {
243
            // Set to last
244
            Craft::$app->getDb()->createCommand()->update(
245
                OrganizationUserRecord::tableName(),
246
                [
247
                    'sortOrder' => ($items + 1)
248
                ],
249
                $target
250
            )->execute();
251
252
            return;
253
        }
254
255
        // First
256
        if (1 === $sortOrder) {
257
            $newOrder = [$userElement->id => $target] + $currentOrder;
258
        } else {
259
            $offset = $sortOrder - 1;
260
261
            // Split at sortOrder / offset
262
            $preOrder = array_slice($currentOrder, 0, $offset, true);
263
            $postOrder = array_slice($currentOrder, $offset, null, true);
264
265
            // Merge them all back together
266
            $newOrder = $preOrder + [$userElement->id => $target] + $postOrder;
267
        }
268
269
        $ct = 1;
270
        foreach ($newOrder as $userId => $condition) {
271
            // Update
272
            Craft::$app->getDb()->createCommand()->update(
273
                OrganizationUserRecord::tableName(),
274
                [
275
                    'sortOrder' => $ct++
276
                ],
277
                $condition
278
            )->execute();
279
        }
280
    }
281
282
    /************************************************************
283
     * ASSOCIATE
284
     ************************************************************/
285
286
    /**
287
     * Associate a user to an organization
288
     *
289
     * @param UserElement $userElement
290
     * @param OrganizationElement $organizationElement
291
     * @param int|null $siteId
292
     * @param int|null $sortOrder
293
     * @return bool
294
     * @throws Exception
295
     * @throws \yii\db\Exception
296
     */
297
    public function associate(
298
        UserElement $userElement,
299
        OrganizationElement $organizationElement,
300
        int $siteId = null,
301
        int $sortOrder = null
302
    ) {
303
304
        // Already associated
305
        if ($this->associationExists($userElement, $organizationElement, $siteId)) {
306
            $this->applySortOrder($userElement, $organizationElement, $siteId, $sortOrder);
307
            return true;
308
        }
309
310
        // The event
311
        $event = new ManageOrganizationUser([
312
            'user' => $userElement,
313
            'organization' => $organizationElement
314
        ]);
315
316
        // Trigger event
317
        $this->trigger(
318
            static::EVENT_BEFORE_ASSOCIATE,
319
            $event
320
        );
321
322
        // Green light?
323
        if (!$event->isValid) {
324
            return false;
325
        }
326
327
        // Restrictions
328
        if (OrganizationPlugin::getInstance()->getSettings()->hasAssociationRestriction()) {
329
            if (OrganizationPlugin::getInstance()->getSettings()->memberAssociationRestriction()) {
330
                $criteria = ['member' => $userElement->id];
331
            } else {
332
                $criteria = ['user' => $userElement->id];
333
            }
334
335
            // Ignore the current organization
336
            $query = OrganizationPlugin::getInstance()->getOrganization()->getQuery(
337
                array_merge(
338
                    [
339
                    'id' => 'not ' . $organizationElement->id,
340
                    'status' => null
341
                    ],
342
                    $criteria
343
                )
344
            );
345
346
            if ($query->count()) {
347
                return false;
348
            }
349
        }
350
351
        // Db transaction
352
        $transaction = RecordHelper::beginTransaction();
353
354
        try {
355
            // New record
356
            $organizationUserRecord = new OrganizationUserRecord();
357
358
            // Transfer element attribute(s) to record
359
            $organizationUserRecord->userId = $userElement->id;
360
            $organizationUserRecord->organizationId = $organizationElement->id;
361
            $organizationUserRecord->siteId = $siteId;
362
            $organizationUserRecord->sortOrder = $sortOrder;
363
364
            // Save record
365
            if (!$organizationUserRecord->save()) {
366
                // Roll back on failures
367
                $transaction->rollBack();
368
369
                return false;
370
            }
371
372
            // Trigger event
373
            $this->trigger(
374
                static::EVENT_AFTER_ASSOCIATE,
375
                $event
376
            );
377
378
            // Green light?
379
            if (!$event->isValid) {
380
                // Roll back on failures
381
                $transaction->rollBack();
382
383
                return false;
384
            }
385
386
            // Apply the sort order
387
            $this->applySortOrder($userElement, $organizationElement, $siteId, $sortOrder);
388
        } catch (Exception $e) {
389
            // Roll back on failures
390
            $transaction->rollBack();
391
392
            throw $e;
393
        }
394
395
        // Commit db transaction
396
        $transaction->commit();
397
398
        return true;
399
    }
400
401
402
    /************************************************************
403
     * DISSOCIATE
404
     ************************************************************/
405
406
    /**
407
     * Dissociate a user to the organization
408
     *
409
     * @param $userElement
410
     * @param $organizationElement
411
     * @return bool
412
     * @throws Exception
413
     * @throws \yii\db\Exception
414
     */
415
    public function dissociate(UserElement $userElement, OrganizationElement $organizationElement)
416
    {
417
418
        // Already not associated
419
        if (!$this->associationExists($userElement, $organizationElement)) {
420
            return true;
421
        }
422
423
        // The event
424
        $event = new ManageOrganizationUser([
425
            'user' => $userElement,
426
            'organization' => $organizationElement
427
        ]);
428
429
        // Trigger event
430
        $this->trigger(
431
            static::EVENT_BEFORE_DISSOCIATE,
432
            $event
433
        );
434
435
        // Green light?
436
        if (!$event->isValid) {
437
            return false;
438
        }
439
440
        // Db transaction
441
        $transaction = Craft::$app->getDb()->beginTransaction();
442
443
        try {
444
            // Delete
445
            Craft::$app->getDb()->createCommand()->delete(
446
                OrganizationUserRecord::tableName(),
447
                [
448
                    'userId' => $userElement->id,
449
                    'organizationId' => $organizationElement->id
450
                ]
451
            )->execute();
452
453
            // Trigger event
454
            $this->trigger(
455
                static::EVENT_AFTER_DISSOCIATE,
456
                $event
457
            );
458
459
            // Green light?
460
            if (!$event->isValid) {
461
                // Roll back on failures
462
                $transaction->rollBack();
463
464
                return false;
465
            }
466
        } catch (Exception $e) {
467
            // Roll back on failures
468
            $transaction->rollBack();
469
470
            throw $e;
471
        }
472
473
        // Commit db transaction
474
        $transaction->commit();
475
476
        return true;
477
    }
478
479
    /**
480
     * @param OrganizationElement $organizationElement
481
     * @param int|null $siteId
482
     * @return array
483
     */
484
    private function getCurrentSortOrder(OrganizationElement $organizationElement, int $siteId = null): array
485
    {
486
487
        return (new Query())
488
            ->select(['id', 'userId', 'sortOrder'])
489
            ->from([OrganizationUserRecord::tableName()])
490
            ->andWhere(
491
                [
492
                    'organizationId' => $organizationElement->id,
493
                    'siteId' => $siteId
494
                ]
495
            )
496
            ->indexBy('userId')
497
            ->orderBy([
498
                'sortOrder' => SORT_ASC
499
            ])
500
            ->limit(null)
501
            ->all();
502
    }
503
504
    /*******************************************
505
     * RECORD CHECKING
506
     *******************************************/
507
508
    /**
509
     * @param UserElement $userElement
510
     * @param OrganizationElement $organizationElement
511
     * @param int|null $siteId
512
     * @return bool
513
     */
514
    private function associationExists(
515
        UserElement $userElement,
516
        OrganizationElement $organizationElement,
517
        int $siteId = null
518
    ) {
519
        return null !== OrganizationUserRecord::findOne([
520
                'organizationId' => $organizationElement->id,
521
                'userId' => $userElement->id,
522
                'siteId' => $siteId
523
            ]);
524
    }
525
}
526