Completed
Push — develop ( a87a8b...4077f9 )
by Nate
05:20
created

UserRelationship::sync()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 0
cts 13
cp 0
rs 9.7333
c 0
b 0
f 0
cc 2
nc 2
nop 2
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\relationships;
10
11
use craft\elements\db\UserQuery;
12
use craft\elements\User;
13
use craft\helpers\ArrayHelper;
14
use flipbox\organizations\elements\Organization;
15
use flipbox\organizations\queries\UserAssociationQuery;
16
use flipbox\organizations\records\UserAssociation;
17
use Tightenco\Collect\Support\Collection;
18
19
/**
20
 * Manages Users associated to Organizations
21
 *
22
 * @author Flipbox Factory <[email protected]>
23
 * @since 2.0.0
24
 *
25
 * @method UserAssociation findOrCreate($object)
26
 * @method UserAssociation findOne($object = null)
27
 * @method UserAssociation findOrFail($object)
28
 */
29
class UserRelationship implements RelationshipInterface
30
{
31
    use RelationshipTrait;
32
33
    /**
34
     * @var Organization
35
     */
36
    private $organization;
37
38
    /**
39
     * @param Organization $organization
40
     */
41
    public function __construct(Organization $organization)
42
    {
43
        $this->organization = $organization;
44
    }
45
46
47
    /************************************************************
48
     * COLLECTION
49
     ************************************************************/
50
51
    /**
52
     * Get a collection of associated users
53
     *
54
     * @return User[]|Collection
55
     */
56
    public function getCollection(): Collection
57
    {
58
        if (null === $this->relations) {
59
            return new Collection(
60
                $this->elementQuery()
61
                    ->all()
62
            );
63
        }
64
65
        return $this->getRelationships()
66
            ->sortBy('userOrder')
67
            ->pluck('user');
68
    }
69
70
    /**
71
     * @return Collection
72
     */
73
    protected function existingRelationships(): Collection
74
    {
75
        $relationships = $this->associationQuery()
76
            ->with('types')
77
            ->all();
78
79
        // 'eager' load where we'll pre-populate all of the associations
80
        $elements = $this->elementQuery()
81
            ->id(array_keys($relationships))
82
            ->indexBy('id')
83
            ->all();
84
85
        return $this->createRelations($relationships)
86
            ->transform(function (UserAssociation $association) use ($elements) {
87
                if (isset($elements[$association->getUserId()])) {
88
                    $association->setUser($elements[$association->getUserId()]);
89
                    $association->setOrganization($this->organization);
90
                }
91
                return $association;
92
            });
93
    }
94
95
    /************************************************************
96
     * QUERY
97
     ************************************************************/
98
99
    /**
100
     * @return UserQuery
101
     */
102
    private function elementQuery(): UserQuery
103
    {
104
        return User::find()
105
            ->organizationId($this->organization->getId() ?: false)
106
            ->anyStatus()
107
            ->limit(null);
108
    }
109
110
    /**
111
     * @return UserAssociationQuery
112
     */
113
    private function associationQuery(): UserAssociationQuery
114
    {
115
        /** @noinspection PhpUndefinedMethodInspection */
116
        return UserAssociation::find()
117
            ->setOrganizationId($this->organization->getId() ?: false)
0 ignored issues
show
Security Bug introduced by
It seems like $this->organization->getId() ?: false can also be of type false; however, flipbox\organizations\qu...it::setOrganizationId() does only seem to accept string|array<integer,str...nts\Organization>>|null, did you maybe forget to handle an error condition?
Loading history...
118
            ->orderBy([
119
                'organizationOrder' => SORT_ASC
120
            ])
121
            ->limit(null);
122
    }
123
124
    /************************************************************
125
     * CREATE
126
     ************************************************************/
127
128
    /**
129
     * @param $object
130
     * @return UserAssociation
131
     */
132
    protected function create($object): UserAssociation
133
    {
134
        if ($object instanceof UserAssociation) {
135
            return $object;
136
        }
137
138
        return (new UserAssociation())
139
            ->setOrganization($this->organization)
140
            ->setUser($this->resolveObject($object));
141
    }
142
143
144
    /*******************************************
145
     * DELTA
146
     *******************************************/
147
148
    /**
149
     * @inheritDoc
150
     */
151
    protected function delta(): array
152
    {
153
        $existingAssociations = $this->associationQuery()
154
            ->indexBy('userId')
155
            ->all();
156
157
        $associations = [];
158
        $order = 1;
159
160
        /** @var UserAssociation $newAssociation */
161
        foreach ($this->getRelationships() as $newAssociation) {
162
            $association = ArrayHelper::remove(
163
                $existingAssociations,
164
                $newAssociation->getUserId()
165
            );
166
            
167
            $newAssociation->userOrder = $order++;
168
169
            /** @var UserAssociation $association */
170
            $association = $association ?: $newAssociation;
171
172
            // Has anything changed?
173
            if(!$association->getIsNewRecord() && !$this->hasChanged($newAssociation, $association)) {
174
                continue;
175
            }
176
177
            $associations[] = $this->sync($association, $newAssociation);
178
        }
179
180
        return [$associations, $existingAssociations];
181
    }
182
183
    /**
184
     * @param UserAssociation $new
185
     * @param UserAssociation $existing
186
     * @return bool
187
     */
188
    private function hasChanged(UserAssociation $new, UserAssociation $existing): bool
189
    {
190
        return $new->userOrder != $existing->userOrder ||
191
            $new->organizationOrder != $existing->organizationOrder ||
192
            $new->state != $existing->state ||
193
            $new->getTypes()->isMutated();
194
    }
195
196
    /**
197
     * @param UserAssociation $from
198
     * @param UserAssociation $to
199
     *
200
     * @return UserAssociation
201
     */
202
    private function sync(UserAssociation $to, UserAssociation $from): UserAssociation
203
    {
204
        $to->userOrder = $from->userOrder;
205
        $to->organizationOrder = $from->organizationOrder;
206
        $to->state = $from->state;
207
208
        if ($from->getTypes()->isMutated()) {
209
            $to->getTypes()->clear()->add(
210
                $from->getTypes()->getCollection()
211
            );
212
        }
213
214
        $to->ignoreSortOrder();
0 ignored issues
show
Documentation Bug introduced by
The method ignoreSortOrder does not exist on object<flipbox\organizat...ecords\UserAssociation>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
215
216
        return $to;
217
    }
218
219
220
    /*******************************************
221
     * COLLECTION UTILS
222
     *******************************************/
223
224
    /**
225
     * @inheritDoc
226
     */
227
    protected function insertCollection(Collection $collection, UserAssociation $association)
228
    {
229
        if ($association->userOrder > 0) {
230
            $collection->splice($association->userOrder - 1, 0, [$association]);
231
            return;
232
        }
233
234
        $collection->push($association);
235
    }
236
237
    /**
238
     * @inheritDoc
239
     */
240
    protected function updateCollection(Collection $collection, UserAssociation $association)
241
    {
242
        if (null !== ($key = $this->findKey($association))) {
243
            $collection->offsetUnset($key);
244
        }
245
246
        $this->insertCollection($collection, $association);
247
    }
248
249
250
    /*******************************************
251
     * UTILS
252
     *******************************************/
253
254
    /**
255
     * @param UserAssociation|User|int|array|null $object
256
     * @return int|null
257
     */
258
    protected function findKey($object = null)
259
    {
260
        if ($object instanceof UserAssociation) {
261
            if (!$object->getUser()) {
262
                var_dump("NOT FOUND");
0 ignored issues
show
Security Debugging Code introduced by
var_dump('NOT FOUND'); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
263
                return null;
264
            }
265
266
            return $this->findRelationshipKey($object->getUser()->email);
267
        }
268
269
        if (null === ($element = $this->resolveObject($object))) {
270
            return null;
271
        }
272
273
        return $this->findRelationshipKey($element->email);
274
    }
275
276
    /**
277
     * @param $identifier
278
     * @return int|string|null
279
     */
280
    private function findRelationshipKey($identifier)
281
    {
282
        /** @var UserAssociation $association */
283
        foreach ($this->getRelationships()->all() as $key => $association) {
284
            if (null !== $association->getUser() && $association->getUser()->email == $identifier) {
285
                return $key;
286
            }
287
        }
288
289
        return null;
290
    }
291
292
    /**
293
     * @param UserAssociation|User|int|array|null $user
294
     * @return User|null
295
     */
296
    protected function resolveObjectInternal($user)
297
    {
298
        if ($user instanceof UserAssociation) {
299
            return $user->getUser();
300
        }
301
302
        if ($user instanceof User) {
303
            return $user;
304
        }
305
306
        if (is_numeric($user)) {
307
            return \Craft::$app->getUsers()->getUserById($user);
308
        }
309
310
        if (is_string($user)) {
311
            return \Craft::$app->getUsers()->getUserByUsernameOrEmail($user);
312
        }
313
314
        return User::findOne($user);
0 ignored issues
show
Bug Compatibility introduced by
The expression \craft\elements\User::findOne($user); of type craft\base\Element|null|craft\base\Element[] adds the type craft\base\Element[] to the return on line 314 which is incompatible with the return type documented by flipbox\organizations\re...::resolveObjectInternal of type craft\elements\User|null.
Loading history...
315
    }
316
}
317