Organization::defineSortOptions()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 10
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 0
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\elements;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\db\Query;
14
use craft\elements\actions\Edit as EditAction;
15
use craft\elements\db\ElementQueryInterface;
16
use craft\elements\db\UserQuery;
17
use craft\elements\User;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, flipbox\organization\elements\User.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
18
use craft\helpers\ArrayHelper;
19
use craft\helpers\StringHelper;
20
use craft\helpers\UrlHelper as UrlHelper;
21
use craft\validators\DateTimeValidator;
22
use DateTime;
23
use flipbox\organization\elements\actions\ChangeOrganizationStatus as StatusAction;
24
use flipbox\organization\elements\actions\DeleteOrganization as DeleteAction;
25
use flipbox\organization\elements\db\Organization as OrganizationQuery;
26
use flipbox\organization\helpers\Type as TypeHelper;
27
use flipbox\organization\helpers\User as UserHelper;
28
use flipbox\organization\models\Type as TypeModel;
29
use flipbox\organization\Organization as OrganizationPlugin;
30
use flipbox\organization\records\Organization as OrganizationRecord;
31
use flipbox\organization\records\User as OrganizationUsersRecord;
32
use flipbox\organization\validators\Owner;
33
use flipbox\spark\helpers\ElementHelper;
34
use flipbox\spark\helpers\QueryHelper;
35
use yii\base\ErrorException as Exception;
36
37
/**
38
 * @author Flipbox Factory <[email protected]>
39
 * @since 1.0.0
40
 */
41
class Organization extends Element
42
{
43
44
    /**
45
     * @var string
46
     */
47
    private $status;
48
49
    /**
50
     * @var DateTime|null
51
     */
52
    public $dateJoined;
53
54
    /**
55
     * @var int
56
     */
57
    public $ownerId;
58
59
    /**
60
     * @var User
61
     */
62
    private $owner;
63
64
    /**
65
     * @var TypeModel[]
66
     */
67
    private $types;
68
69
    /**
70
     * @var TypeModel
71
     */
72
    private $activeType;
73
74
    /**
75
     * @var TypeModel
76
     */
77
    private $primaryType;
78
79
    /**
80
     * @var UserQuery
81
     */
82
    private $users;
83
84
    /**
85
     * @var UserQuery
86
     */
87
    private $members;
88
89
    /**
90
     * @inheritdoc
91
     */
92
    public static function displayName(): string
93
    {
94
        return Craft::t('organization', 'Organization');
95
    }
96
97
    /**
98
     * @inheritdoc
99
     */
100
    public static function hasTitles(): bool
101
    {
102
        return true;
103
    }
104
105
    /**
106
     * @inheritdoc
107
     */
108
    public static function hasContent(): bool
109
    {
110
        return true;
111
    }
112
113
    /**
114
     * @return array
115
     */
116
    public function attributes()
117
    {
118
        return array_merge(
119
            parent::attributes(),
120
            [
121
                'types',
122
                'activeType',
123
                'primaryType',
124
                'users',
125
            ]
126
        );
127
    }
128
129
    /**
130
     * @inheritdoc
131
     */
132
    public function rules()
133
    {
134
135
        return array_merge(
136
            parent::rules(),
137
            [
138
                [
139
                    [
140
                        'ownerId'
141
                    ],
142
                    'number',
143
                    'integerOnly' => true
144
145
                ],
146
                [
147
                    [
148
                        'ownerId'
149
                    ],
150
                    Owner::class
151
                ],
152
                [
153
                    [
154
                        'dateJoined'
155
                    ],
156
                    DateTimeValidator::class
157
                ],
158
                [
159
                    [
160
                        'activeType',
161
                        'primaryType',
162
                        'ownerId',
163
                        'owner',
164
                        'types',
165
                        'users',
166
                        'members',
167
                        'status'
168
                    ],
169
                    'safe',
170
                    'on' => [
171
                        ElementHelper::SCENARIO_DEFAULT
172
                    ]
173
174
                ],
175
            ]
176
        );
177
    }
178
179
    /**
180
     * Returns the names of any attributes that should be converted to DateTime objects from [[populate()]].
181
     *
182
     * @return string[]
183
     */
184
    public function datetimeAttributes(): array
185
    {
186
187
        return array_merge(
188
            parent::datetimeAttributes(),
189
            [
190
                'dateJoined'
191
            ]
192
        );
193
    }
194
195
196
    /************************************************************
197
     * FIELD LAYOUT
198
     ************************************************************/
199
200
    /**
201
     * @inheritdoc
202
     */
203
    public function getFieldLayout(int $siteId = null)
204
    {
205
206
        /** @var TypeModel $type */
207
        if (!$type = $this->getActiveType()) {
208
            return OrganizationPlugin::getInstance()->getOrganization()->getDefaultFieldLayout();
209
        }
210
211
        return $type->getFieldLayout($siteId);
212
    }
213
214
215
    /************************************************************
216
     * FIND / GET
217
     ************************************************************/
218
219
    /**
220
     * @inheritdoc
221
     *
222
     * @return OrganizationQuery
223
     */
224
    public static function find(): ElementQueryInterface
225
    {
226
        return new OrganizationQuery(get_called_class());
227
    }
228
229
230
    /************************************************************
231
     * STATUS
232
     ************************************************************/
233
234
    /**
235
     * Returns whether this element type can have statuses.
236
     *
237
     * @return boolean
238
     */
239
    public static function hasStatuses(): bool
240
    {
241
        return true;
242
    }
243
244
    /**
245
     * @inheritdoc
246
     */
247
    public static function statuses(): array
248
    {
249
        return OrganizationPlugin::getInstance()->getOrganization()->getStatuses();
250
    }
251
252
    /**
253
     * @param $status
254
     * @return $this
255
     */
256
    public function setStatus($status)
257
    {
258
259
        $this->status = null;
260
261
        // A custom organization status
262
        if (OrganizationPlugin::getInstance()->getOrganization()->isCustomStatus($status)) {
263
            $this->archived = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $archived was declared of type boolean, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
264
            $this->enabled = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $enabled was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
265
            $this->enabledForSite = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $enabledForSite was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
266
267
            $this->status = $status;
268
        } else {
269
            switch ($status) {
270
                case Element::STATUS_ENABLED:
271
                    $this->enabled = 1;
272
                    $this->enabledForSite = 1;
273
                    break;
274
275
                case Element::STATUS_DISABLED:
276
                    $this->enabled = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $enabled was declared of type boolean, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
277
                    break;
278
279
                case Element::STATUS_ARCHIVED:
280
                    $this->archived = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $archived was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
281
                    break;
282
            }
283
        }
284
285
        return $this;
286
    }
287
288
    /**
289
     * @inheritdoc
290
     */
291
    public function getStatus()
292
    {
293
294
        if (null === $this->status) {
295
            return parent::getStatus();
296
        }
297
298
        return $this->status;
299
    }
300
301
    /************************************************************
302
     * ACTIVE TYPE
303
     ************************************************************/
304
305
    /**
306
     * @param TypeModel|null $type
307
     * @return $this
308
     */
309
    public function setActiveType(TypeModel $type = null)
310
    {
311
312
        if ($type) {
313
            $this->addType($type);
314
        }
315
316
        $this->activeType = (null === $type) ? false : $type;
317
        return $this;
318
    }
319
320
    /**
321
     * @return TypeModel|null
322
     */
323
    public function getActiveType()
324
    {
325
326
        if (null === $this->activeType) {
327
            // Default to the primary type
328
            if (!$activeType = $this->getPrimaryType()) {
329
                // Set false vs null to indicate population has taken place
330
                $activeType = false;
331
            }
332
333
            $this->activeType = $activeType;
0 ignored issues
show
Documentation Bug introduced by
It seems like $activeType can also be of type false. However, the property $activeType is declared as type object<flipbox\organization\models\Type>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
334
        }
335
336
        return (false === $this->activeType) ? null : $this->activeType;
337
    }
338
339
340
    /************************************************************
341
     * PRIMARY TYPE
342
     ************************************************************/
343
344
    /**
345
     * Populate the primary type
346
     */
347
    protected function ensurePrimaryType()
348
    {
349
350
        if (null === $this->primaryType) {
351
            if (!$primaryType = OrganizationPlugin::getInstance()->getType()->findPrimaryByOrganization($this)) {
352
                // Set false vs null to indicate population has taken place
353
                $primaryType = false;
354
            }
355
356
            // Set cache
357
            $this->primaryType = $primaryType;
358
        }
359
    }
360
361
    /**
362
     * Identify whether a primary type is set
363
     *
364
     * @return bool
365
     */
366
    public function hasPrimaryType()
367
    {
368
369
        $this->ensurePrimaryType();
370
371
        return $this->primaryType instanceof TypeModel;
372
    }
373
374
    /**
375
     * Identify whether the type is primary
376
     *
377
     * @param $type
378
     * @return bool
379
     */
380
    public function isPrimaryType(TypeModel $type)
381
    {
382
383
        if ($primaryType = $this->getPrimaryType()) {
384
            return $primaryType->id === $type->id;
385
        }
386
387
        return false;
388
    }
389
390
    /**
391
     * @param TypeModel $type
392
     * @return $this
393
     */
394
    public function setPrimaryType(TypeModel $type)
395
    {
396
397
        $this->primaryType = $type;
398
399
        // Remove active type cache
400
        if (false === $this->activeType) {
401
            $this->activeType = null;
402
        }
403
404
        return $this;
405
    }
406
407
    /**
408
     * Get the primary type
409
     *
410
     * @return TypeModel|null
411
     */
412
    public function getPrimaryType()
413
    {
414
415
        if (!$this->hasPrimaryType()) {
416
            return null;
417
        }
418
419
        return $this->primaryType;
420
    }
421
422
    /************************************************************
423
     * TYPES
424
     ************************************************************/
425
426
    /**
427
     * Associate a type to the element
428
     *
429
     * @param TypeModel $type
430
     * @return $this
431
     */
432
    public function addType(TypeModel $type)
433
    {
434
435
        $this->ensureTypes();
436
437
        // Already set?
438
        if (!array_key_exists($type->id, $this->types)) {
439
            $this->types[$type->id] = $type;
440
        }
441
442
        return $this;
443
    }
444
445
    /**
446
     * Set the types associated to the element
447
     *
448
     * @param null $types
449
     * @return $this
450
     */
451
    public function setTypes($types = null)
452
    {
453
454
        $this->types = [];
455
456
        // In case a type config is directly passed
457
        if (!is_array($types) || ArrayHelper::isAssociative($types)) {
458
            $types = [$types];
459
        }
460
461
        foreach ($types as $type) {
462
            if ($type = TypeHelper::resolve($type)) {
463
                $this->addType($type);
464
            }
465
        }
466
467
        return $this;
468
    }
469
470
    /**
471
     * Get all associated types associated to the element
472
     *
473
     * @return TypeModel[]
474
     */
475
    public function getTypes(): array
476
    {
477
478
        $this->ensureTypes();
479
480
        return $this->types;
481
    }
482
483
    /**
484
     * Ensure all types are associated to the element
485
     *
486
     * @return $this
487
     */
488
    private function ensureTypes()
489
    {
490
491
        if (null === $this->types) {
492
            $this->types = ArrayHelper::index(
493
                OrganizationPlugin::getInstance()->getType()->findAllByOrganization($this),
494
                'id'
495
            );
496
        }
497
498
        return $this;
499
    }
500
501
    /**
502
     * Get an associated type by identifier (id/handle)
503
     *
504
     * @param $identifier
505
     * @return null|TypeModel
506
     */
507
    public function getType($identifier)
508
    {
509
510
        // Determine index type
511
        $indexBy = (is_numeric($identifier)) ? 'id' : 'handle';
512
513
        // Find all types
514
        $allTypes = ArrayHelper::index(
515
            $this->getTypes(),
516
            $indexBy
517
        );
518
519
        return array_key_exists($identifier, $allTypes) ? $allTypes[$identifier] : null;
520
    }
521
522
    /**
523
     * Identify whether a type is associated to the element
524
     *
525
     * @param TypeModel|null $type
526
     * @return bool
527
     */
528
    public function hasType(TypeModel $type = null): bool
529
    {
530
531
        // Check if any type is set
532
        if (null === $type) {
533
            return !empty($this->getTypes());
534
        }
535
536
        return null !== $this->getType($type->id);
537
    }
538
539
    /**
540
     * @param TypeModel|null $type
541
     * @return bool
542
     * @deprecated
543
     */
544
    public function getHasType(TypeModel $type = null): bool
545
    {
546
547
        Craft::$app->getDeprecator()->log(
548
            __METHOD__,
549
            'Use "hasType()" method'
550
        );
551
552
        return $this->hasType($type);
553
    }
554
555
556
    /************************************************************
557
     * MEMBERS
558
     ************************************************************/
559
    /**
560
     * Get an array of users associated to an organization
561
     *
562
     * @param array $criteria
563
     * @return UserQuery
564
     */
565
    public function getMembers($criteria = [])
566
    {
567
568
        if (null === $this->members) {
569
            $this->members = OrganizationPlugin::getInstance()->getOrganization()->getMemberQuery($this);
570
        }
571
572
        if (!empty($criteria)) {
573
            QueryHelper::configure(
574
                $this->members,
575
                $criteria
576
            );
577
        }
578
579
        return $this->members;
580
    }
581
582
    /**
583
     * Associate users to an organization
584
     *
585
     * @param $members
586
     * @return $this
587
     */
588
    protected function setMembers($members)
589
    {
590
591
        // Reset the query
592
        $this->members = OrganizationPlugin::getInstance()->getOrganization()->getMemberQuery($this);
593
594
        // Remove all users
595
        $this->members->setCachedResult([]);
596
597
        $this->addMembers($members);
598
599
        return $this;
600
    }
601
602
    /**
603
     * Associate an array of users to an organization
604
     *
605
     * @param $members
606
     * @return $this
607
     */
608
    protected function addMembers(array $members)
609
    {
610
611
        // In case a type config is directly passed
612
        if (!is_array($members) || ArrayHelper::isAssociative($members)) {
613
            $members = [$members];
614
        }
615
616
        foreach ($members as $key => $user) {
617
            // Ensure we have a model
618
            if (!$user instanceof User) {
619
                $user = UserHelper::resolve($user);
620
            }
621
622
            $this->addMember($user);
0 ignored issues
show
Bug introduced by
It seems like $user can be null; however, addMember() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
623
        }
624
625
        return $this;
626
    }
627
628
    /**
629
     * Associate a user to an organization
630
     *
631
     * @param User $user
632
     * @param bool $addAsUser
633
     * @return $this
634
     */
635
    protected function addMember(User $user, bool $addAsUser = true)
636
    {
637
638
        $currentUsers = $this->getMembers()->all();
639
640
        $userElementsByEmail = ArrayHelper::index(
641
            $currentUsers,
642
            'email'
643
        );
644
645
        // Does the user already exist?
646
        if (!array_key_exists($user->email, $userElementsByEmail)) {
647
            $currentUsers[] = $user;
648
            $this->getMembers()->setCachedResult($currentUsers);
649
        }
650
651
        // Add as a 'user' as well?
652
        if ($addAsUser) {
653
            $this->addUser($user, false);
654
        }
655
656
        return $this;
657
    }
658
659
660
    /************************************************************
661
     * USERS
662
     ************************************************************/
663
664
    /**
665
     * Get an array of users associated to an organization
666
     *
667
     * @param array $criteria
668
     * @return UserQuery
669
     */
670
    public function getUsers($criteria = [])
671
    {
672
673
        if (null === $this->users) {
674
            $this->users = OrganizationPlugin::getInstance()->getOrganization()->getUserQuery($this);
675
        }
676
677
        if (!empty($criteria)) {
678
            QueryHelper::configure(
679
                $this->users,
680
                $criteria
681
            );
682
        }
683
684
        return $this->users;
685
    }
686
687
    /**
688
     * @param User $user
689
     * @param array $criteria
690
     * @return bool
691
     */
692
    public function isUser(User $user, $criteria = [])
693
    {
694
695
        Craft::$app->getDeprecator()->log(
696
            __METHOD__,
697
            'Moved into service.  Organization::isUser()'
698
        );
699
700
        return OrganizationPlugin::getInstance()->getOrganization()->isUser($user, $this, $criteria);
701
    }
702
703
    /**
704
     * Associate users to an organization
705
     *
706
     * @param $users
707
     * @return $this
708
     */
709
    public function setUsers($users)
710
    {
711
712
        // Reset the query
713
        $this->users = OrganizationPlugin::getInstance()->getOrganization()->getUserQuery($this);
714
715
        // Remove all users
716
        $this->users->setCachedResult([]);
717
718
        $this->addUsers($users);
719
720
        return $this;
721
    }
722
723
    /**
724
     * Associate an array of users to an organization
725
     *
726
     * @param $users
727
     * @return $this
728
     */
729
    public function addUsers(array $users)
730
    {
731
732
        // In case a type config is directly passed
733
        if (!is_array($users) || ArrayHelper::isAssociative($users)) {
734
            $users = [$users];
735
        }
736
737
        foreach ($users as $key => $user) {
738
            // Ensure we have a model
739
            if (!$user instanceof User) {
740
                $user = UserHelper::resolve($user);
741
            }
742
743
            $this->addUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user can be null; however, addUser() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
744
        }
745
746
        return $this;
747
    }
748
749
    /**
750
     * Associate a user to an organization
751
     *
752
     * @param User $user
753
     * @param bool $addAsMember
754
     * @return $this
755
     */
756
    public function addUser(User $user, bool $addAsMember = true)
757
    {
758
759
        $currentUsers = $this->getUsers()->all();
760
761
        $userElementsByEmail = ArrayHelper::index(
762
            $currentUsers,
763
            'email'
764
        );
765
766
        // Does the user already exist?
767
        if (!array_key_exists($user->email, $userElementsByEmail)) {
768
            $currentUsers[] = $user;
769
            $this->getUsers()->setCachedResult($currentUsers);
770
        }
771
772
        if ($addAsMember) {
773
            $this->addMember($user, false);
774
        }
775
776
        return $this;
777
    }
778
779
    /**
780
     * Dissociate a user from an organization
781
     *
782
     * @param array $users
783
     * @return $this
784
     */
785
    public function removeUsers(array $users)
786
    {
787
788
        // In case a type config is directly passed
789
        if (!is_array($users) || ArrayHelper::isAssociative($users)) {
790
            $users = [$users];
791
        }
792
793
        foreach ($users as $key => $user) {
794
            // Ensure we have a model
795
            if (!$user instanceof User) {
796
                $user = UserHelper::resolve($user);
797
            }
798
799
            $this->removeUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user can be null; however, removeUser() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
800
        }
801
802
        return $this;
803
    }
804
805
    /**
806
     * Dissociate a user from an organization
807
     *
808
     * @param User $user
809
     * @return $this
810
     */
811
    public function removeUser(User $user)
812
    {
813
814
        $userElementsByEmail = ArrayHelper::index(
815
            $this->getUsers()->all(),
816
            'email'
817
        );
818
819
        // Does the user already exist?
820
        if (array_key_exists($user->email, $userElementsByEmail)) {
821
            unset($userElementsByEmail[$user->email]);
822
823
            $this->getUsers()->setCachedResult(
824
                array_values($userElementsByEmail)
825
            );
826
        }
827
828
        return $this;
829
    }
830
831
    /**
832
     * Associate an array of types from request input
833
     *
834
     * @param string $identifier
835
     * @return $this
836
     */
837
    public function setTypesFromRequest(string $identifier = 'types')
838
    {
839
840
        // Set users array
841
        $this->setTypes(
842
            Craft::$app->getRequest()->getBodyParam($identifier, [])
843
        );
844
845
        return $this;
846
    }
847
848
    /**
849
     * Associate an array of users from request input
850
     *
851
     * @param string $identifier
852
     * @return $this
853
     */
854
    public function setUsersFromRequest(string $identifier = 'users')
855
    {
856
857
        if ($users = Craft::$app->getRequest()->getBodyParam($identifier, [])) {
858
            // Set users array
859
            $this->setUsers($users);
860
        }
861
862
        return $this;
863
    }
864
865
866
    /************************************************************
867
     * ELEMENT ADMIN
868
     ************************************************************/
869
870
    /**
871
     * @inheritdoc
872
     */
873
    public function getCpEditUrl()
874
    {
875
        return UrlHelper::cpUrl('organization/' . $this->id);
876
    }
877
878
    /**
879
     * @inheritdoc
880
     */
881
    protected static function defineSources(string $context = null): array
882
    {
883
884
        switch ($context) {
885
            case 'user':
886
                return self::defineUserSources();
887
888
            case 'owner':
889
                return self::defineOwnerSources();
890
891
            default:
892
                return self::defineTypeSources();
893
        }
894
    }
895
896
    /**
897
     * @return array
898
     */
899
    private static function defineDefaultSources(): array
900
    {
901
902
        return [
903
            [
904
                'key' => '*',
905
                'label' => Craft::t('app', 'All organizations'),
906
                'criteria' => ['status' => null],
907
                'hasThumbs' => true
908
            ]
909
        ];
910
    }
911
912
    /**
913
     * @return array
914
     */
915
    private static function defineTypeSources(): array
916
    {
917
918
        $sources = static::defineDefaultSources();
0 ignored issues
show
Bug introduced by
Since defineDefaultSources() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of defineDefaultSources() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
919
920
        // Array of all organization types
921
        $organizationTypes = OrganizationPlugin::getInstance()->getType()->findAll();
922
923
        $sources[] = ['heading' => Craft::t('organization', 'Types')];
924
925
        /** @var TypeModel $organizationType */
926
        foreach ($organizationTypes as $organizationType) {
927
            $sources[] = [
928
                'key' => 'type:' . $organizationType->id,
929
                'label' => $organizationType->name,
930
                'criteria' => ['status' => null, 'typeId' => $organizationType->id],
931
                'hasThumbs' => true
932
            ];
933
        }
934
935
        return $sources;
936
    }
937
938
    /**
939
     * @return array
940
     */
941
    private static function defineUserSources(): array
942
    {
943
944
        $sources = static::defineDefaultSources();
0 ignored issues
show
Bug introduced by
Since defineDefaultSources() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of defineDefaultSources() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
945
946
        // Array of all organization types
947
        $organizationUsers = OrganizationPlugin::getInstance()->getUser()->getQuery();
948
949
        $sources[] = ['heading' => Craft::t('organization', 'Users')];
950
951
        /** @var User $organizationUser */
952
        foreach ($organizationUsers as $organizationUser) {
953
            $sources[] = [
954
                'key' => 'user:' . $organizationUser->id,
955
                'label' => $organizationUser->getFullName(),
956
                'criteria' => [
957
                    'status' => null,
958
                    'users' => [$organizationUser->id]
959
                ],
960
                'hasThumbs' => true
961
            ];
962
        }
963
964
        return $sources;
965
    }
966
967
    /**
968
     * @return array
969
     */
970
    private static function defineOwnerSources(): array
971
    {
972
973
        $sources = static::defineDefaultSources();
0 ignored issues
show
Bug introduced by
Since defineDefaultSources() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of defineDefaultSources() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
974
975
        // Array of all organization types
976
        $organizationOwners = OrganizationPlugin::getInstance()->getUser()->getOwnerQuery();
977
978
        $sources[] = ['heading' => Craft::t('organization', 'Users')];
979
980
        /** @var User $organizationOwner */
981
        foreach ($organizationOwners as $organizationOwner) {
982
            $sources[] = [
983
                'key' => 'owner:' . $organizationOwner->id,
984
                'label' => $organizationOwner->getFullName(),
985
                'criteria' => [
986
                    'status' => null,
987
                    'ownerId' => $organizationOwner->id
988
                ],
989
                'hasThumbs' => true
990
            ];
991
        }
992
993
        return $sources;
994
    }
995
996
    /**
997
     * @inheritdoc
998
     */
999
    protected static function defineActions(string $source = null): array
1000
    {
1001
1002
        $actions = [];
1003
1004
        // Edit
1005
        $actions[] = Craft::$app->getElements()->createAction([
1006
            'type' => EditAction::class,
1007
            'label' => Craft::t('app', 'Edit organization'),
1008
        ]);
1009
1010
        if (Craft::$app->getUser()->checkPermission('administrateOrganizations')) {
1011
            // Change status
1012
            $actions[] = StatusAction::class;
1013
        }
1014
1015
        if (Craft::$app->getUser()->checkPermission('deleteOrganizations')) {
1016
            // Delete
1017
            $actions[] = DeleteAction::class;
1018
        }
1019
1020
        return $actions;
1021
    }
1022
1023
    /**
1024
     * @inheritdoc
1025
     */
1026
    protected static function defineSearchableAttributes(): array
1027
    {
1028
        return [
1029
            'id',
1030
            'status'
1031
        ];
1032
    }
1033
1034
    /**
1035
     * @inheritdoc
1036
     */
1037
    protected static function defineSortOptions(): array
1038
    {
1039
1040
        return [
1041
            'title' => Craft::t('organization', 'Name'),
1042
            'ownerId' => Craft::t('organization', 'Owner'),
1043
            'dateJoined' => Craft::t('organization', 'Join Date'),
1044
            'status' => Craft::t('organization', 'Status'),
1045
            'type' => Craft::t('organization', 'Type')
1046
        ];
1047
    }
1048
1049
    /**
1050
     * @inheritdoc
1051
     */
1052
    public static function eagerLoadingMap(array $sourceElements, string $handle)
1053
    {
1054
1055
        switch ($handle) {
1056
            case 'owner':
1057
                return self::eagerLoadingOwnerMap($sourceElements);
1058
1059
            case 'users':
1060
                return self::eagerLoadingUsersMap($sourceElements);
1061
1062
            case 'members':
1063
                return ArrayHelper::merge(
1064
                    self::eagerLoadingUsersMap($sourceElements),
1065
                    self::eagerLoadingOwnerMap($sourceElements)
1066
                );
1067
        }
1068
1069
        return parent::eagerLoadingMap($sourceElements, $handle);
1070
    }
1071
1072
    /**
1073
     * @param array $sourceElements
1074
     * @return array
1075
     */
1076
    private static function eagerLoadingOwnerMap(array $sourceElements)
1077
    {
1078
1079
        // Get the source element IDs
1080
        $sourceElementIds = ArrayHelper::getColumn($sourceElements, 'id');
1081
1082
        $map = (new Query())
1083
            ->select(['id as source', 'ownerId as target'])
1084
            ->from(OrganizationRecord::tableName())
1085
            ->where(['id' => $sourceElementIds])
1086
            ->all();
1087
1088
        return [
1089
            'elementType' => User::class,
1090
            'map' => $map
1091
        ];
1092
    }
1093
1094
    /**
1095
     * @param array $sourceElements
1096
     * @return array
1097
     */
1098
    private static function eagerLoadingUsersMap(array $sourceElements)
1099
    {
1100
1101
        // Get the source element IDs
1102
        $sourceElementIds = ArrayHelper::getColumn($sourceElements, 'id');
1103
1104
        $map = (new Query())
1105
            ->select(['organizationId as source', 'userId as target'])
1106
            ->from(OrganizationUsersRecord::tableName())
1107
            ->where(['organizationId' => $sourceElementIds])
1108
            ->all();
1109
1110
        return [
1111
            'elementType' => User::class,
1112
            'map' => $map
1113
        ];
1114
    }
1115
1116
1117
    /**
1118
     * @inheritdoc
1119
     */
1120
    public function setEagerLoadedElements(string $handle, array $elements)
1121
    {
1122
1123
        switch ($handle) {
1124
            case 'owner':
1125
                $owner = $elements[0] ?? null;
1126
                $this->setOwner($owner);
1127
                break;
1128
1129
            case 'users':
1130
                $users = $elements ?? [];
1131
                $this->setUsers($users);
1132
                break;
1133
1134
            case 'members':
1135
                $users = $elements ?? [];
1136
                $this->setMembers($users);
1137
                break;
1138
1139
            default:
1140
                parent::setEagerLoadedElements($handle, $elements);
1141
        }
1142
    }
1143
1144
    /**
1145
     * @inheritdoc
1146
     */
1147
    public static function defineTableAttributes(): array
1148
    {
1149
        return [
1150
            'id' => ['label' => Craft::t('app', 'ID')],
1151
            'uri' => ['label' => Craft::t('app', 'URI')],
1152
            'title' => ['label' => Craft::t('organization', 'Name')],
1153
            'owner' => ['label' => Craft::t('organization', 'Owner')],
1154
            'status' => ['label' => Craft::t('organization', 'Status')],
1155
            'types' => ['label' => Craft::t('organization', 'Type(s)')],
1156
            'dateJoined' => ['label' => Craft::t('organization', 'Join Date')],
1157
            'dateCreated' => ['label' => Craft::t('app', 'Date Created')],
1158
            'dateUpdated' => ['label' => Craft::t('app', 'Date Updated')],
1159
        ];
1160
    }
1161
1162
1163
1164
    // Indexes, etc.
1165
    // -------------------------------------------------------------------------
1166
1167
    /**
1168
     * @inheritdoc
1169
     */
1170
    public function tableAttributeHtml(string $attribute): string
1171
    {
1172
1173
        switch ($attribute) {
1174
            case 'status':
1175
                $value = $this->getStatus();
1176
                $availableStatuses = self::statuses();
1177
                if (array_key_exists($value, $availableStatuses)) {
1178
                    return '<span class="status ' . $value . '"></span> ' . ucfirst($availableStatuses[$value]);
1179
                }
1180
                return $value . $availableStatuses;
1181
1182
            case 'owner':
1183
                if ($this->hasOwner()) {
1184
                    return '<span class="status ' . $this->getOwner()->getStatus() . '"></span>' .
1185
                        $this->getOwner()->getFullName();
1186
                }
1187
1188
                return '';
1189
1190
            case 'types':
1191
                // Get all configured types
1192
                $types = $this->getTypes();
1193
1194
                foreach ($types as $type) {
1195
                    $typeHtmlParts[] = '<a href="' .
0 ignored issues
show
Coding Style Comprehensibility introduced by
$typeHtmlParts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $typeHtmlParts = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1196
                        UrlHelper::cpUrl('/organization/' . $this->id . '/' . $type->handle) .
1197
                        '">' .
1198
                        $type->name .
1199
                        '</a>';
1200
                }
1201
1202
                return !empty($typeHtmlParts) ? StringHelper::toString($typeHtmlParts, ', ') : '';
1203
        }
1204
1205
        return parent::tableAttributeHtml($attribute);
1206
    }
1207
1208
1209
    /**
1210
     * @inheritdoc
1211
     */
1212
    protected function route()
1213
    {
1214
1215
        // Make sure that the organization is actually live
1216
        if (in_array($this->getStatus(), [Element::STATUS_DISABLED, Element::STATUS_ARCHIVED], true)) {
1217
            return null;
1218
        }
1219
1220
        // todo - match on other organization types or than primary?
1221
1222
        // Use primary type as the element route
1223
        if (!$primaryType = $this->getPrimaryType()) {
1224
            return null;
1225
        }
1226
1227
        $primaryTypeSettings = $primaryType->getSite();
1228
1229
        if (!$primaryTypeSettings->hasUrls) {
1230
            return null;
1231
        }
1232
1233
        return [
1234
            'templates/render', [
1235
                'template' => $primaryTypeSettings->template,
1236
                'variables' => [
1237
                    'organization' => $this,
1238
                ]
1239
            ]
1240
        ];
1241
    }
1242
1243
    // Events
1244
    // -------------------------------------------------------------------------
1245
1246
    /**
1247
     * @inheritdoc
1248
     * @throws Exception
1249
     */
1250
    public function beforeSave(bool $isNew): bool
1251
    {
1252
1253
        OrganizationPlugin::getInstance()->getOrganization()->beforeSave($this, $isNew);
1254
1255
        return parent::beforeSave($isNew);
1256
    }
1257
1258
    /**
1259
     * @inheritdoc
1260
     * @throws Exception
1261
     */
1262
    public function afterSave(bool $isNew)
1263
    {
1264
1265
        OrganizationPlugin::getInstance()->getOrganization()->afterSave($this, $isNew);
1266
1267
        parent::afterSave($isNew);
1268
    }
1269
1270
    /**
1271
     * Identify whether element has an owner
1272
     *
1273
     * @return bool
1274
     */
1275
    public function hasOwner()
1276
    {
1277
        // Get owner if it already isn't set
1278
        if (is_null($this->owner)) {
1279
            $this->getOwner();
1280
        }
1281
1282
        return $this->owner instanceof User;
1283
    }
1284
1285
    /**
1286
     * @return bool|User|null
1287
     */
1288
    public function getOwner()
1289
    {
1290
1291
        // Check cache
1292
        if (is_null($this->owner)) {
1293
            // Check property
1294
            if (!empty($this->ownerId)) {
1295
                // Find element
1296
                if ($ownerElement = Craft::$app->getUsers()->getUserById($this->ownerId)) {
1297
                    // Set
1298
                    $this->setOwner($ownerElement);
1299
                } else {
1300
                    // Clear property (it's invalid)
1301
                    $this->ownerId = null;
1302
1303
                    // Prevent subsequent look-ups
1304
                    $this->owner = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<craft\elements\User> of property $owner.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1305
                }
1306
            } else {
1307
                // Prevent subsequent look-ups
1308
                $this->owner = false;
1309
            }
1310
        } else {
1311
            // Cache changed?
1312
            if ($this->ownerId && (($this->owner === false) || ($this->ownerId !== $this->owner->getId()))) {
1313
                // Clear cache
1314
                $this->owner = null;
1315
1316
                // Again
1317
                return $this->getOwner();
1318
            }
1319
        }
1320
1321
        return $this->hasOwner() ? $this->owner : null;
1322
    }
1323
1324
    /**
1325
     * Associate an owner to the element
1326
     *
1327
     * @param $owner
1328
     * @return $this
1329
     */
1330
    public function setOwner($owner)
1331
    {
1332
1333
        // Clear cache
1334
        $this->owner = null;
1335
1336
        // Find element
1337
        if (!$owner = $this->findUserElement($owner)) {
1338
            // Clear property / cache
1339
            $this->ownerId = $this->owner = null;
1340
        } else {
1341
            // Set property
1342
            $this->ownerId = $owner->getId();
1343
1344
            // Set cache
1345
            $this->owner = $owner;
1346
        }
1347
1348
        return $this;
1349
    }
1350
1351
    /**
1352
     * @param string $user
1353
     * @return bool
1354
     */
1355
    public function getIsOwner($user = 'CURRENT_USER')
1356
    {
1357
1358
        if ('CURRENT_USER' === $user) {
1359
            // Current user
1360
            $element = Craft::$app->getUser()->getIdentity();
1361
        } else {
1362
            // Find element
1363
            $element = $this->findUserElement($user);
1364
        }
1365
1366
        return ($element && $element->getId() == $this->ownerId);
1367
    }
1368
1369
    /**
1370
     * @param string|User $user
1371
     * @return bool
1372
     */
1373
    public function isOwner($user = 'CURRENT_USER')
1374
    {
1375
        return $this->getIsOwner($user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by parameter $user on line 1373 can also be of type object<craft\elements\User>; however, flipbox\organization\ele...anization::getIsOwner() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1376
    }
1377
1378
    /**
1379
     * @param $user
1380
     * @return User|null
1381
     */
1382
    private function findUserElement($user)
1383
    {
1384
1385
        // Element
1386
        if ($user instanceof User) {
1387
            return $user;
1388
1389
            // Id
1390
        } elseif (is_numeric($user)) {
1391
            return Craft::$app->getUsers()->getUserById($user);
1392
1393
            // Username / Email
1394
        } elseif (!is_null($user)) {
1395
            return Craft::$app->getUsers()->getUserByUsernameOrEmail($user);
1396
        }
1397
1398
        return null;
1399
    }
1400
}
1401