Syncer::__construct()   A
last analyzed

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

163
        }, /** @scrutinizer ignore-type */ Arr::get($response, 'difference'));
Loading history...
164
165
        $this->mergeRemoteStats($remoteStats = Arr::get($response, 'stats'));
0 ignored issues
show
Bug introduced by
It seems like $remoteStats = Illuminat...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

165
        $this->mergeRemoteStats(/** @scrutinizer ignore-type */ $remoteStats = Arr::get($response, 'stats'));
Loading history...
166
167
        $this->outputMessage(
168
            'Remote affection:'
169
            . ' created ' . $remoteStats['created']
170
            . ', updated ' . $remoteStats['updated']
171
            . ', deleted ' . $remoteStats['deleted']
172
        );
173
174
        $this->foreigners = $this->foreigners->merge($foreigners);
175
    }
176
177
    /**
178
     * Apply changes locally from the given response.
179
     *
180
     * @return void
181
     */
182
    public function apply()
183
    {
184
        $count = $this->getForeignersCount();
185
186
        foreach ($this->foreigners as $index => $foreigner) {
187
            $index++;
188
189
            $this->outputMessage(
190
                "[$index of $count] Handling the action \"" . $foreigner->getRemoteAction() . '"'
191
                    . ' of ' . $foreigner->getName()
192
                    . ' (' . $foreigner->getEmail() . ')'
193
            );
194
195
            try {
196
                $this->handleAction(
197
                    $foreigner,
198
                    $action = $foreigner->getRemoteAction()
199
                );
200
            }
201
            catch(\Slides\Connector\Auth\Exceptions\SyncException $e) {
202
                \Illuminate\Support\Facades\Log::error(
203
                    "Cannot $action the user {$foreigner->getEmail()}: " . $e->getMessage()
204
                );
205
            }
206
        }
207
    }
208
209
    /**
210
     * Format local users for a request payload.
211
     *
212
     * @param Collection $locals
213
     *
214
     * @return array
215
     */
216
    private function formatLocals(Collection $locals)
217
    {
218
        return $locals
219
            ->map(function(Syncable $user) {
220
                return [
221
                    'id' => $user->retrieveId(),
222
                    'remoteId' => $user->retrieveRemoteId(),
223
                    'name' => $user->retrieveName(),
224
                    'email' => $user->retrieveEmail(),
225
                    'password' => $user->retrievePassword(),
226
                    'country' => $user->retrieveCountry(),
227
                    'created_at' => $user->retrieveCreatedAt()->toDateTimeString(),
228
                    'updated_at' => $user->retrieveUpdatedAt()->toDateTimeString(),
229
                    'deleted_at' => $user->retrieveDeletedAt()
230
                        ? $user->retrieveDeletedAt()->toDateTimeString()
231
                        : null
232
                ];
233
            })
234
            ->toArray();
235
    }
236
237
    /**
238
     * Create a remote user from the response.
239
     *
240
     * @param array $user
241
     *
242
     * @return User
243
     */
244
    public function createRemoteUserFromResponse(array $user)
245
    {
246
        return new RemoteUser(
247
            Arr::get($user, 'id'),
0 ignored issues
show
Bug introduced by
It seems like Illuminate\Support\Arr::get($user, 'id') can also be of type null; however, parameter $remoteId of Slides\Connector\Auth\Sync\User::__construct() does only seem to accept integer, 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

247
            /** @scrutinizer ignore-type */ Arr::get($user, 'id'),
Loading history...
248
            Arr::get($user, 'name'),
249
            Arr::get($user, 'email'),
0 ignored issues
show
Bug introduced by
It seems like Illuminate\Support\Arr::get($user, 'email') can also be of type null; however, parameter $email of Slides\Connector\Auth\Sync\User::__construct() does only seem to accept string, 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

249
            /** @scrutinizer ignore-type */ Arr::get($user, 'email'),
Loading history...
250
            Arr::get($user, 'password'),
251
            Arr::get($user, 'updated_at'),
252
            Arr::get($user, 'created_at'),
0 ignored issues
show
Bug introduced by
It seems like Illuminate\Support\Arr::get($user, 'created_at') can also be of type null; however, parameter $created of Slides\Connector\Auth\Sync\User::__construct() does only seem to accept string, 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

252
            /** @scrutinizer ignore-type */ Arr::get($user, 'created_at'),
Loading history...
253
            Arr::get($user, 'deleted_at'),
254
            Arr::get($user, 'country'),
255
            Arr::get($user, 'action')
256
        );
257
    }
258
259
    /**
260
     * Check whether a mode is passed.
261
     *
262
     * @param string $mode
263
     *
264
     * @return bool
265
     */
266
    public function hasMode(string $mode): bool
267
    {
268
        return array_key_exists($mode, $this->modes);
269
    }
270
271
    /**
272
     * Get passed modes.
273
     *
274
     * @return array
275
     */
276
    public function getModes(): array
277
    {
278
        return $this->modes;
279
    }
280
281
    /**
282
     * Check if there are difference detected by remote service.
283
     *
284
     * @return bool
285
     */
286
    public function hasDifference(): bool
287
    {
288
        return $this->foreigners->isNotEmpty();
289
    }
290
291
    /**
292
     * Get the local stats.
293
     *
294
     * @return array
295
     */
296
    public function getLocalStats(): array
297
    {
298
        return $this->localStats;
299
    }
300
301
    /**
302
     * Get the remote stats.
303
     *
304
     * @return array
305
     */
306
    public function getRemoteStats(): array
307
    {
308
        return $this->remoteStats;
309
    }
310
311
    /**
312
     * Set remote users.
313
     *
314
     * @param Collection|RemoteUser[] $foreigners
315
     */
316
    public function setForeigners(Collection $foreigners): void
317
    {
318
        $this->foreigners = $foreigners;
319
    }
320
321
    /**
322
     * Get number of foreign users.
323
     *
324
     * @return int
325
     */
326
    public function getForeignersCount(): int
327
    {
328
        return $this->foreigners->count();
329
    }
330
331
    /**
332
     * Retrieve all local users.
333
     *
334
     * @return Collection
335
     */
336
    public static function retrieveLocals(): Collection
337
    {
338
        return \Illuminate\Support\Facades\Auth::getProvider()->createModel()
339
            ->newQuery()
340
            ->get();
341
    }
342
343
    /**
344
     * Merge the remote stats.
345
     *
346
     * @param array $stats
347
     */
348
    private function mergeRemoteStats(array $stats)
349
    {
350
        $this->remoteStats['created'] += $stats['created'];
351
        $this->remoteStats['updated'] += $stats['updated'];
352
        $this->remoteStats['deleted'] += $stats['deleted'];
353
    }
354
355
    /**
356
     * Add a message to the output
357
     *
358
     * @param string $message
359
     *
360
     * @return void
361
     */
362
    private function outputMessage(string $message)
363
    {
364
        $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...
365
366
        if($this->outputCallback instanceof \Closure) {
0 ignored issues
show
introduced by
$this->outputCallback is always a sub-type of Closure.
Loading history...
367
            call_user_func($this->outputCallback, $message);
368
        }
369
    }
370
371
    /**
372
     * Set a callback which should be called on adding output message.
373
     *
374
     * @param \Closure $outputCallback
375
     *
376
     * @return void
377
     */
378
    public function setOutputCallback(\Closure $outputCallback): void
379
    {
380
        $this->outputCallback = $outputCallback;
381
    }
382
383
    /**
384
     * Increment a local stats value.
385
     *
386
     * @param string $key
387
     */
388
    protected function incrementStats(string $key)
389
    {
390
        $value = Arr::get($this->localStats, $key, 0);
391
392
        $this->localStats[$key] = ++$value;
393
    }
394
}