Completed
Push — master ( e381cf...9deca2 )
by Nate
04:12 queued 02:07
created

TypesAttributeTrait::userTypeQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 12
cp 0
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
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\helpers\ArrayHelper;
13
use flipbox\craft\ember\helpers\QueryHelper;
14
use flipbox\organizations\queries\OrganizationTypeQuery;
15
use flipbox\organizations\records\OrganizationType;
16
use flipbox\organizations\records\OrganizationType as TypeModel;
17
use flipbox\organizations\records\OrganizationTypeAssociation;
18
19
/**
20
 * @author Flipbox Factory <[email protected]>
21
 * @since 1.0.0
22
 */
23
trait TypesAttributeTrait
24
{
25
    /**
26
     * @var OrganizationTypeQuery
27
     */
28
    private $types;
29
30
    /**
31
     * @var TypeModel|false
32
     */
33
    private $activeType;
34
35
    /************************************************************
36
     * REQUEST
37
     ************************************************************/
38
39
    /**
40
     * AssociateUserToOrganization an array of types from request input
41
     *
42
     * @param string $identifier
43
     * @return $this
44
     */
45
    public function setTypesFromRequest(string $identifier = 'types')
46
    {
47
        if (null !== ($types = Craft::$app->getRequest()->getBodyParam($identifier))) {
48
            $this->setTypes((array) $types);
49
        }
50
51
        return $this;
52
    }
53
54
    /************************************************************
55
     * ACTIVE TYPE
56
     ************************************************************/
57
58
    /**
59
     * @param TypeModel|null $type
60
     * @return $this
61
     */
62
    public function setActiveType(TypeModel $type = null)
63
    {
64
        if ($type) {
65
            $this->addType($type);
66
        }
67
68
        $this->activeType = (null === $type) ? false : $type;
69
        return $this;
70
    }
71
72
    /**
73
     * @return TypeModel|null
74
     */
75
    public function getActiveType()
76
    {
77
        if (null === $this->activeType) {
78
            if (!$activeType = $this->getPrimaryType()) {
79
                $activeType = false;
80
            }
81
82
            $this->activeType = $activeType;
83
        }
84
85
        return (false === $this->activeType) ? null : $this->activeType;
86
    }
87
88
    /************************************************************
89
     * TYPES QUERY
90
     ************************************************************/
91
92
    /**
93
     * @param array $criteria
94
     * @return OrganizationTypeQuery
95
     */
96
    public function userTypeQuery($criteria = []): OrganizationTypeQuery
97
    {
98
        /** @noinspection PhpUndefinedMethodInspection */
99
        $query = OrganizationType::find()
100
            ->organization($this);
101
102
        if (!empty($criteria)) {
103
            QueryHelper::configure(
104
                $query,
105
                $criteria
106
            );
107
        }
108
109
        return $query;
110
    }
111
    
112
    /************************************************************
113
     * TYPES
114
     ************************************************************/
115
116
    /**
117
     * Get an array of types associated to an organization
118
     *
119
     * @param array $criteria
120
     * @return OrganizationTypeQuery
121
     */
122
    public function getTypes($criteria = [])
123
    {
124
        if (null === $this->types) {
125
            $this->types = $this->userTypeQuery();
126
        }
127
128
        if (!empty($criteria)) {
129
            QueryHelper::configure(
130
                $this->types,
131
                $criteria
132
            );
133
        }
134
135
        return $this->types;
136
    }
137
138
    /**
139
     * AssociateUserToOrganization types to an organization
140
     *
141
     * @param $types
142
     * @return $this
143
     */
144
    public function setTypes($types)
145
    {
146
        if ($types instanceof OrganizationTypeQuery) {
147
            $this->types = $types;
148
            return $this;
149
        }
150
151
        // Reset the query
152
        $this->types = $this->userTypeQuery();
153
154
        // Remove all types
155
        $this->types->setCachedResult([]);
156
157
        if (!empty($types)) {
158
            if (!is_array($types)) {
159
                $types = [$types];
160
            }
161
162
            $this->addTypes($types);
163
        }
164
165
        return $this;
166
    }
167
168
    /**
169
     * AssociateUserToOrganization an array of types to an organization
170
     *
171
     * @param $types
172
     * @return $this
173
     */
174
    public function addTypes(array $types)
175
    {
176
        // In case a config is directly passed
177
        if (ArrayHelper::isAssociative($types)) {
178
            $types = [$types];
179
        }
180
181
        foreach ($types as $key => $type) {
182
            // Ensure we have a model
183
            if (!$type instanceof OrganizationType) {
184
                $type = $this->resolveType($type);
185
            }
186
187
            $this->addType($type);
188
        }
189
190
        return $this;
191
    }
192
193
    /**
194
     * AssociateUserToOrganization a type to an organization
195
     *
196
     * @param OrganizationType $type
197
     * @return $this
198
     */
199
    public function addType(OrganizationType $type)
200
    {
201
        $currentTypes = $this->getTypes()->all();
202
203
        $indexedTypes = ArrayHelper::index(
204
            $currentTypes,
205
            'handle'
206
        );
207
208
        if (!array_key_exists($type->handle, $indexedTypes)) {
209
            $currentTypes[] = $type;
210
            $this->getTypes()->setCachedResult($currentTypes);
211
        }
212
213
        return $this;
214
    }
215
216
    /**
217
     * DissociateUserFromOrganization a type from an organization
218
     *
219
     * @param array $types
220
     * @return $this
221
     */
222
    public function removeTypes(array $types)
223
    {
224
        // In case a config is directly passed
225
        if (ArrayHelper::isAssociative($types)) {
226
            $types = [$types];
227
        }
228
229
        foreach ($types as $key => $type) {
230
            if (!$type instanceof OrganizationType) {
231
                $type = $this->resolveType($type);
232
            }
233
234
            $this->removeType($type);
235
        }
236
237
        return $this;
238
    }
239
240
    /**
241
     * @param mixed $type
242
     * @return OrganizationType
243
     */
244
    protected function resolveType($type): OrganizationType
245
    {
246
        if (null !== ($type = OrganizationType::findOne($type))) {
247
            return $type;
248
        }
249
250
        if (!is_array($type)) {
251
            $type = ArrayHelper::toArray($type, [], false);
252
        }
253
254
        return new OrganizationType($type);
255
    }
256
257
    /**
258
     * DissociateUserFromOrganization a type from an organization
259
     *
260
     * @param OrganizationType $type
261
     * @return $this
262
     */
263
    public function removeType(OrganizationType $type)
264
    {
265
        $indexedTypes = ArrayHelper::index(
266
            $this->getTypes()->all(),
267
            'handle'
268
        );
269
270
        // Does the type already exist?
271
        if (array_key_exists($type->handle, $indexedTypes)) {
272
            unset($indexedTypes[$type->handle]);
273
274
            $this->getTypes()->setCachedResult(
275
                array_values($indexedTypes)
276
            );
277
        }
278
279
        return $this;
280
    }
281
282
    /**
283
     * Reset types
284
     *
285
     * @return $this
286
     */
287
    public function resetTypes()
288
    {
289
        $this->types = null;
290
        return $this;
291
    }
292
293
    /**
294
     * Get an associated type by identifier (id/handle)
295
     *
296
     * @param $identifier
297
     * @return null|TypeModel
298
     */
299
    public function getType($identifier)
300
    {
301
        // Determine index type
302
        $indexBy = (is_numeric($identifier)) ? 'id' : 'handle';
303
304
        // Find all types
305
        $allTypes = ArrayHelper::index(
306
            $this->getTypes()->all(),
307
            $indexBy
308
        );
309
310
        return array_key_exists($identifier, $allTypes) ? $allTypes[$identifier] : null;
311
    }
312
313
    /**
314
     * Identify whether a type is associated to the element
315
     *
316
     * @param TypeModel|null $type
317
     * @return bool
318
     */
319
    public function hasType(TypeModel $type = null): bool
320
    {
321
        if (null === $type) {
322
            return !empty($this->getTypes());
323
        }
324
325
        return null !== $this->getType($type->id);
326
    }
327
328
329
    /************************************************************
330
     * PRIMARY TYPE
331
     ************************************************************/
332
333
    /**
334
     * Identify whether a primary type is set
335
     *
336
     * @return bool
337
     */
338
    public function hasPrimaryType()
339
    {
340
        return $this->getTypes()->one() instanceof TypeModel;
341
    }
342
343
    /**
344
     * Identify whether the type is primary
345
     *
346
     * @param $type
347
     * @return bool
348
     */
349
    public function isPrimaryType(TypeModel $type)
350
    {
351
        if ($primaryType = $this->getPrimaryType()) {
352
            return $primaryType->id === $type->id;
353
        }
354
355
        return false;
356
    }
357
358
    /**
359
     * @param TypeModel $type
360
     * @return $this
361
     */
362
    public function setPrimaryType(TypeModel $type = null)
363
    {
364
        if (null === $type) {
365
            return $this;
366
        }
367
368
        return $this->setTypes(
369
            array_merge(
370
                [
371
                    $type
372
                ],
373
                $this->getTypes()->all()
374
            )
375
        );
376
    }
377
378
    /**
379
     * Get the primary type
380
     *
381
     * @return TypeModel|null
382
     */
383
    public function getPrimaryType()
384
    {
385
        if (!$this->hasPrimaryType()) {
386
            return null;
387
        }
388
389
        return $this->getTypes()->one();
390
    }
391
392
    /*******************************************
393
     * ASSOCIATE and/or DISASSOCIATE
394
     *******************************************/
395
396
    /**
397
     * @return bool
398
     * @throws \Throwable
399
     * @throws \flipbox\craft\ember\exceptions\RecordNotFoundException
400
     * @throws \yii\db\StaleObjectException
401
     */
402
    public function saveTypes(): bool
403
    {
404
        // No changes?
405
        if (null === ($types = $this->getTypes()->getCachedResult())) {
406
            return true;
407
        }
408
409
        $currentAssociations = OrganizationTypeAssociation::find()
410
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by
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...
411
            ->indexBy('typeId')
412
            ->all();
413
414
        $success = true;
415
        $associations = [];
416
        $order = 1;
417
        foreach ($types as $type) {
418
            if (null === ($association = ArrayHelper::remove($currentAssociations, $type->getId()))) {
419
                $association = (new OrganizationTypeAssociation())
420
                    ->setType($type)
421
                    ->setOrganization($this);
422
            }
423
424
            $association->sortOrder = $order++;
425
426
            $associations[] = $association;
427
        }
428
429
        // Delete those removed
430
        foreach ($currentAssociations as $currentAssociation) {
431
            if (!$currentAssociation->delete()) {
432
                $success = false;
433
            }
434
        }
435
436
        foreach ($associations as $association) {
437
            if (!$association->save()) {
438
                $success = false;
439
            }
440
        }
441
442
        if (!$success) {
443
            $this->addError('types', 'Unable to associate types.');
0 ignored issues
show
Bug introduced by
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...
444
        }
445
446
        return $success;
447
    }
448
449
    /**
450
     * @param TypeModel $type
451
     * @param int|null $sortOrder
452
     * @return bool
453
     */
454
    public function associateType(OrganizationType $type, int $sortOrder = null): bool
455
    {
456
        if (null === ($association = OrganizationTypeAssociation::find()
457
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by
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...
458
            ->typeId($type->getId() ?: false)
459
            ->one())
460
        ) {
461
            $association = new OrganizationTypeAssociation([
462
                'organization' => $this,
463
                'type' => $type
464
            ]);
465
        }
466
467
        if (null !== $sortOrder) {
468
            $association->sortOrder = $sortOrder;
469
        }
470
471
        if (!$association->save()) {
472
            $this->addError('organizations', 'Unable to associate type.');
0 ignored issues
show
Bug introduced by
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...
473
474
            return false;
475
        }
476
477
        $this->resetTypes();
478
479
        return true;
480
    }
481
482
    /**
483
     * @param OrganizationTypeQuery $query
484
     * @return bool
485
     * @throws \Throwable
486
     */
487
    public function associateTypes(OrganizationTypeQuery $query): bool
488
    {
489
        $types = $query->all();
490
491
        if (empty($types)) {
492
            return true;
493
        }
494
495
        $currentAssociations = OrganizationTypeAssociation::find()
496
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by
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...
497
            ->indexBy('typeId')
498
            ->all();
499
500
        $success = true;
501
        foreach ($types as $type) {
502
            if (null === ($association = ArrayHelper::remove($currentAssociations, $type->getId()))) {
503
                $association = (new OrganizationTypeAssociation())
504
                    ->setType($type)
505
                    ->setOrganization($this);
506
            }
507
508
            if (!$association->save()) {
509
                $success = false;
510
            }
511
        }
512
513
        if (!$success) {
514
            $this->addError('organizations', 'Unable to associate types.');
0 ignored issues
show
Bug introduced by
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...
515
        }
516
517
        $this->resetTypes();
518
519
        return $success;
520
    }
521
522
    /**
523
     * @param TypeModel $type
524
     * @return bool
525
     * @throws \Throwable
526
     * @throws \yii\db\StaleObjectException
527
     */
528
    public function dissociateType(OrganizationType $type): bool
529
    {
530
        if (null === ($association = OrganizationTypeAssociation::find()
531
                ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by
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...
532
                ->typeId($type->getId() ?: false)
533
                ->one())
534
        ) {
535
            return true;
536
        }
537
538
        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...
539
            $this->addError('organizations', 'Unable to dissociate type.');
0 ignored issues
show
Bug introduced by
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...
540
541
            return false;
542
        }
543
544
        $this->resetTypes();
545
546
        return true;
547
    }
548
549
    /**
550
     * @param OrganizationTypeQuery $query
551
     * @return bool
552
     */
553
    public function dissociateTypes(OrganizationTypeQuery $query): bool
554
    {
555
        $types = $query->all();
556
557
        if (empty($types)) {
558
            return true;
559
        }
560
561
        $currentAssociations = OrganizationTypeAssociation::find()
562
            ->organizationId($this->getId() ?: false)
0 ignored issues
show
Bug introduced by
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...
563
            ->indexBy('typeId')
564
            ->all();
565
566
        $success = true;
567
        foreach ($types as $type) {
568
            if (null === ($association = ArrayHelper::remove($currentAssociations, $type->getId()))) {
569
                continue;
570
            }
571
572
            if (!$association->delete()) {
573
                $success = false;
574
            }
575
        }
576
577
        if (!$success) {
578
            $this->addError('organizations', 'Unable to dissociate types.');
0 ignored issues
show
Bug introduced by
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...
579
        }
580
581
        $this->resetTypes();
582
583
        return $success;
584
    }
585
}
586