Completed
Push — master ( d7e260...fc7aea )
by Artem
10:19
created

Syncer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 3
dl 0
loc 7
ccs 0
cts 6
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Slides\Connector\Auth\Sync;
4
5
use Illuminate\Support\Collection;
6
use Slides\Connector\Auth\Sync\Syncable as LocalUser;
7
use Slides\Connector\Auth\Sync\User as RemoteUser;
8
use Slides\Connector\Auth\Client;
9
use Slides\Connector\Auth\AuthService;
10
11
/**
12
 * Class Syncer
13
 *
14
 * @package Slides\Connector\Auth\Sync
15
 */
16
class Syncer
17
{
18
    use HandlesActions,
19
        ExportsUsers,
20
        ImportsUsers;
21
22
    /**
23
     * Number of users which can be sent per request
24
     */
25
    const USERS_PER_REQUEST = 5000;
26
27
    /**
28
     * Synchronization modes.
29
     *
30
     * `passwords` — allows updating passwords locally and remotely.
31
     * `users` — allows syncing specific users only.
32
     */
33
    const MODE_PASSWORDS = 'passwords';
34
    const MODE_USERS = 'users';
35
36
    /**
37
     * The authentication service.
38
     *
39
     * @var AuthService
40
     */
41
    protected $authService;
42
43
    /**
44
     * Authentication Service client.
45
     *
46
     * @var Client
47
     */
48
    protected $client;
49
50
    /**
51
     * The local users for syncing remotely.
52
     *
53
     * @var LocalUser[]|Collection
54
     */
55
    protected $locals;
56
57
    /**
58
     * The remote users fetched.
59
     *
60
     * @var RemoteUser[]|Collection
61
     */
62
    protected $foreigners;
63
64
    /**
65
     * The sync modes.
66
     *
67
     * @var array
68
     */
69
    protected $modes;
70
71
    /**
72
     * The remote statistics.
73
     *
74
     * @var array
75
     */
76
    protected $remoteStats = [
77
        'created' => 0,
78
        'updated' => 0,
79
        'deleted' => 0
80
    ];
81
82
    /**
83
     * The local statistics.
84
     *
85
     * @var array
86
     */
87
    protected $localStats = [
88
        'created' => 0,
89
        'updated' => 0,
90
        'deleted' => 0
91
    ];
92
93
    /**
94
     * Output messages.
95
     *
96
     * @var array
97
     */
98
    protected $output = [];
99
100
    /**
101
     * The callback called on adding a message to the output.
102
     *
103
     * @var \Closure
104
     */
105
    protected $outputCallback;
106
107
    /**
108
     * Syncer constructor.
109
     *
110
     * @param LocalUser[]|Collection|null $locals
111
     * @param array $modes
112
     * @param Client|null $client
113
     */
114
    public function __construct(Collection $locals = null, array $modes = [], Client $client = null)
115
    {
116
        $this->locals = $locals ?? collect();
117
        $this->foreigners = collect();
118
        $this->modes = $modes;
119
        $this->client = $client ?? new Client();
120
        $this->authService = app('authService');
0 ignored issues
show
Documentation Bug introduced by
It seems like app('authService') can also be of type Illuminate\Foundation\Application. However, the property $authService is declared as type Slides\Connector\Auth\AuthService. 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...
121
    }
122
123
    /**
124
     * Synchronize local users remotely and apply changes.
125
     *
126
     * @return void
127
     */
128
    public function sync()
129
    {
130
        $iterator = new UserGroupsIterator($this->locals, static::USERS_PER_REQUEST);
131
132
        $this->outputMessage('Total requests: ' . $iterator->requestsCount());
133
134
        /** @var LocalUser[]|Collection $users */
135
        foreach ($iterator as $users) {
136
            $this->outputMessage('Sending a request with bunch of ' . $users->count() . ' users');
137
138
            $response = $this->client->request('sync', [
139
                'users' => $this->formatLocals($users),
140
                'modes' => $this->modes
141
            ]);
142
143
            $this->outputMessage('Parsing a response...');
144
145
            $this->parseResponse($response);
146
        }
147
148
        $this->outputMessage("Applying {$this->foreigners->count()} remote changes locally");
149
150
        $this->apply();
151
    }
152
153
    /**
154
     * Parse a response.
155
     *
156
     * @param array $response
157
     */
158
    protected function parseResponse(array $response)
159
    {
160
        $foreigners = array_map(function (array $user) {
161
            return $this->createRemoteUserFromResponse($user);
162
        }, array_get($response, 'difference'));
163
164
        $this->mergeRemoteStats($remoteStats = array_get($response, 'stats'));
0 ignored issues
show
Bug introduced by
It seems like $remoteStats = array_get($response, 'stats') can also be of type null; however, parameter $stats of Slides\Connector\Auth\Sy...cer::mergeRemoteStats() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
        $this->mergeRemoteStats(/** @scrutinizer ignore-type */ $remoteStats = array_get($response, 'stats'));
Loading history...
165
166
        $this->outputMessage(
167
            'Remote affection:'
168
            . ' created ' . $remoteStats['created']
169
            . ', updated ' . $remoteStats['updated']
170
            . ', deleted ' . $remoteStats['deleted']
171
        );
172
173
        $this->foreigners = $this->foreigners->merge($foreigners);
174
    }
175
176
    /**
177
     * Apply changes locally from the given response.
178
     *
179
     * @return void
180
     */
181
    public function apply()
182
    {
183
        $count = $this->getForeignersCount();
184
185
        foreach ($this->foreigners as $index => $foreigner) {
186
            $index++;
187
188
            $this->outputMessage(
189
                "[$index of $count] Handling the action \"" . $foreigner->getRemoteAction() . '"'
190
                    . ' of ' . $foreigner->getName()
191
                    . ' (' . $foreigner->getEmail() . ')'
192
            );
193
194
            try {
195
                $this->handleAction(
196
                    $foreigner,
197
                    $action = $foreigner->getRemoteAction()
198
                );
199
            }
200
            catch(\Slides\Connector\Auth\Exceptions\SyncException $e) {
201
                \Illuminate\Support\Facades\Log::error(
202
                    "Cannot $action the user {$foreigner->getEmail()}: " . $e->getMessage()
203
                );
204
            }
205
        }
206
    }
207
208
    /**
209
     * Format local users for a request payload.
210
     *
211
     * @param Collection $locals
212
     *
213
     * @return array
214
     */
215
    private function formatLocals(Collection $locals)
216
    {
217
        return $locals
218
            ->map(function(Syncable $user) {
219
                return [
220
                    'id' => $user->retrieveId(),
221
                    'remoteId' => $user->retrieveRemoteId(),
222
                    'name' => $user->retrieveName(),
223
                    'email' => $user->retrieveEmail(),
224
                    'password' => $user->retrievePassword(),
225
                    'country' => $user->retrieveCountry(),
226
                    'created_at' => $user->retrieveCreatedAt()->toDateTimeString(),
227
                    'updated_at' => $user->retrieveUpdatedAt()->toDateTimeString()
228
                ];
229
            })
230
            ->toArray();
231
    }
232
233
    /**
234
     * Create a remote user from the response.
235
     *
236
     * @param array $user
237
     *
238
     * @return User
239
     */
240
    public function createRemoteUserFromResponse(array $user)
241
    {
242
        return new RemoteUser(
243
            array_get($user, 'id'),
244
            array_get($user, 'name'),
245
            array_get($user, 'email'),
246
            array_get($user, 'password'),
247
            array_get($user, 'updated_at'),
248
            array_get($user, 'created_at'),
249
            array_get($user, 'country'),
250
            array_get($user, 'action')
251
        );
252
    }
253
254
    /**
255
     * Check whether a mode is passed.
256
     *
257
     * @param string $mode
258
     *
259
     * @return bool
260
     */
261
    public function hasMode(string $mode): bool
262
    {
263
        return array_key_exists($mode, $this->modes);
264
    }
265
266
    /**
267
     * Get passed modes.
268
     *
269
     * @return array
270
     */
271
    public function getModes(): array
272
    {
273
        return $this->modes;
274
    }
275
276
    /**
277
     * Check if there are difference detected by remote service.
278
     *
279
     * @return bool
280
     */
281
    public function hasDifference(): bool
282
    {
283
        return $this->foreigners->isNotEmpty();
284
    }
285
286
    /**
287
     * Get the local stats.
288
     *
289
     * @return array
290
     */
291
    public function getLocalStats(): array
292
    {
293
        return $this->localStats;
294
    }
295
296
    /**
297
     * Get the remote stats.
298
     *
299
     * @return array
300
     */
301
    public function getRemoteStats(): array
302
    {
303
        return $this->remoteStats;
304
    }
305
306
    /**
307
     * Set remote users.
308
     *
309
     * @param Collection|RemoteUser[] $foreigners
310
     */
311
    public function setForeigners(Collection $foreigners): void
312
    {
313
        $this->foreigners = $foreigners;
314
    }
315
316
    /**
317
     * Get number of foreign users.
318
     *
319
     * @return int
320
     */
321
    public function getForeignersCount(): int
322
    {
323
        return $this->foreigners->count();
324
    }
325
326
    /**
327
     * Retrieve all local users.
328
     *
329
     * @return Collection
330
     */
331
    public static function retrieveLocals(): Collection
332
    {
333
        return \Illuminate\Support\Facades\Auth::getProvider()->createModel()
334
            ->newQuery()
335
            ->get();
336
    }
337
338
    /**
339
     * Merge the remote stats.
340
     *
341
     * @param array $stats
342
     */
343
    private function mergeRemoteStats(array $stats)
344
    {
345
        $this->remoteStats['created'] += $stats['created'];
346
        $this->remoteStats['updated'] += $stats['updated'];
347
        $this->remoteStats['deleted'] += $stats['deleted'];
348
    }
349
350
    /**
351
     * Add a message to the output
352
     *
353
     * @param string $message
354
     *
355
     * @return void
356
     */
357
    private function outputMessage(string $message)
358
    {
359
        $output[] = $message;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$output was never initialized. Although not strictly required by PHP, it is generally a good practice to add $output = array(); before regardless.
Loading history...
360
361
        if($this->outputCallback instanceof \Closure) {
0 ignored issues
show
introduced by
$this->outputCallback is always a sub-type of Closure. If $this->outputCallback can have other possible types, add them to src/Sync/Syncer.php:103.
Loading history...
362
            call_user_func($this->outputCallback, $message);
363
        }
364
    }
365
366
    /**
367
     * Set a callback which should be called on adding output message.
368
     *
369
     * @param \Closure $outputCallback
370
     *
371
     * @return void
372
     */
373
    public function setOutputCallback(\Closure $outputCallback): void
374
    {
375
        $this->outputCallback = $outputCallback;
376
    }
377
378
    /**
379
     * Increment a local stats value.
380
     *
381
     * @param string $key
382
     */
383
    protected function incrementStats(string $key)
384
    {
385
        $value = array_get($this->localStats, $key, 0);
386
387
        $this->localStats[$key] = ++$value;
388
    }
389
}