ClientService   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 5
dl 0
loc 352
c 0
b 0
f 0
rs 8.8

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getClientKey() 0 4 1
A getClientUsersListKey() 0 4 1
A getUserClientsListKey() 0 4 1
A getClientListKey() 0 4 1
A save() 0 9 2
B insert() 0 44 7
F update() 0 72 14
B findOne() 0 31 6
A delete() 0 41 3
A hasUser() 0 6 1
A addUser() 0 12 1
A removeUser() 0 12 1
A findAllByUserId() 0 13 3
A findAll() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like ClientService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClientService, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ClientService.php
4
 *
5
 * PHP version 5.6+
6
 *
7
 * @author Philippe Gaultier <[email protected]>
8
 * @copyright 2010-2017 Philippe Gaultier
9
 * @license http://www.sweelix.net/license license
10
 * @version 1.2.0
11
 * @link http://www.sweelix.net
12
 * @package sweelix\oauth2\server\services\redis
13
 */
14
15
namespace sweelix\oauth2\server\services\redis;
16
17
use sweelix\oauth2\server\exceptions\DuplicateIndexException;
18
use sweelix\oauth2\server\exceptions\DuplicateKeyException;
19
use sweelix\oauth2\server\interfaces\ClientModelInterface;
20
use sweelix\oauth2\server\interfaces\ClientServiceInterface;
21
use yii\db\Exception as DatabaseException;
22
use Yii;
23
24
/**
25
 * This is the client service for redis
26
 *  database structure
27
 *    * oauth2:clients:<cid> : hash (Client)
28
 *    * oauth2:clients:<cid>:users : set
29
 *    * oauth2:users:<uid>:clients : set
30
 *
31
 * @author Philippe Gaultier <[email protected]>
32
 * @copyright 2010-2017 Philippe Gaultier
33
 * @license http://www.sweelix.net/license license
34
 * @version 1.2.0
35
 * @link http://www.sweelix.net
36
 * @package sweelix\oauth2\server\services\redis
37
 * @since 1.0.0
38
 */
39
class ClientService extends BaseService implements ClientServiceInterface
40
{
41
    /**
42
     * @var string user namespace (collection for clients)
43
     */
44
    public $userNamespace = '';
45
46
    /**
47
     * @param string $cid client ID
48
     * @return string client Key
49
     * @since 1.0.0
50
     */
51
    protected function getClientKey($cid)
52
    {
53
        return $this->namespace . ':' . $cid;
54
    }
55
56
    /**
57
     * @param string $cid client ID
58
     * @return string clientUsers Key
59
     * @since 1.0.0
60
     */
61
    protected function getClientUsersListKey($cid)
62
    {
63
        return $this->namespace . ':' . $cid . ':users';
64
    }
65
66
    /**
67
     * @param string $uid user ID
68
     * @return string user clients collection Key
69
     * @since XXX
70
     */
71
    protected function getUserClientsListKey($uid)
72
    {
73
        return $this->userNamespace . ':' . $uid . ':clients';
74
    }
75
76
    /**
77
     * @return string key of all clients list
78
     */
79
    protected function getClientListKey()
80
    {
81
        return $this->namespace . ':keys';
82
    }
83
84
    /**
85
     * @inheritdoc
86
     */
87
    public function save(ClientModelInterface $client, $attributes)
88
    {
89
        if ($client->getIsNewRecord()) {
90
            $result = $this->insert($client, $attributes);
91
        } else {
92
            $result = $this->update($client, $attributes);
93
        }
94
        return $result;
95
    }
96
97
    /**
98
     * Save Client
99
     * @param ClientModelInterface $client
100
     * @param null|array $attributes attributes to save
101
     * @return bool
102
     * @throws DatabaseException
103
     * @throws DuplicateIndexException
104
     * @throws DuplicateKeyException
105
     * @since 1.0.0
106
     */
107
    protected function insert(ClientModelInterface $client, $attributes)
108
    {
109
        $result = false;
110
        if (!$client->beforeSave(true)) {
111
            return $result;
112
        }
113
        $clientKey = $this->getClientKey($client->getKey());
114
        $clientListKey = $this->getClientListKey();
115
        //check if record exists
116
        $entityStatus = (int)$this->db->executeCommand('EXISTS', [$clientKey]);
117
        if ($entityStatus === 1) {
118
            throw new DuplicateKeyException('Duplicate key "' . $clientKey . '"');
119
        }
120
121
        $values = $client->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 107 can also be of type array; however, sweelix\oauth2\server\in...e::getDirtyAttributes() does only seem to accept array<integer,string>|null, 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...
122
        $redisParameters = [$clientKey];
123
        $this->setAttributesDefinitions($client->attributesDefinition());
124
        foreach ($values as $key => $value) {
125
            if ($value !== null) {
126
                $redisParameters[] = $key;
127
                $redisParameters[] = $this->convertToDatabase($key, $value);
128
            }
129
        }
130
        //TODO: use EXEC/MULTI to avoid errors
131
        $transaction = $this->db->executeCommand('MULTI');
132
        if ($transaction === true) {
133
            try {
134
                $this->db->executeCommand('HMSET', $redisParameters);
135
                $this->db->executeCommand('SADD', [$clientListKey, $client->getKey()]);
136
                $this->db->executeCommand('EXEC');
137
            } catch (DatabaseException $e) {
138
                // @codeCoverageIgnoreStart
139
                // we have a REDIS exception, we should not discard
140
                Yii::debug('Error while inserting entity', __METHOD__);
141
                throw $e;
142
                // @codeCoverageIgnoreEnd
143
            }
144
        }
145
        $changedAttributes = array_fill_keys(array_keys($values), null);
146
        $client->setOldAttributes($values);
147
        $client->afterSave(true, $changedAttributes);
148
        $result = true;
149
        return $result;
150
    }
151
152
153
    /**
154
     * Update Client
155
     * @param ClientModelInterface $client
156
     * @param null|array $attributes attributes to save
157
     * @return bool
158
     * @throws DatabaseException
159
     * @throws DuplicateIndexException
160
     * @throws DuplicateKeyException
161
     */
162
    protected function update(ClientModelInterface $client, $attributes)
163
    {
164
        if (!$client->beforeSave(false)) {
165
            return false;
166
        }
167
168
        $values = $client->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 162 can also be of type array; however, sweelix\oauth2\server\in...e::getDirtyAttributes() does only seem to accept array<integer,string>|null, 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...
169
        $modelKey = $client->key();
170
        $clientId = isset($values[$modelKey]) ? $values[$modelKey] : $client->getKey();
171
        $clientKey = $this->getClientKey($clientId);
172
        $clientListKey = $this->getClientListKey();
173
174
        if (isset($values[$modelKey]) === true) {
175
            $newClientKey = $this->getClientKey($values[$modelKey]);
176
            $entityStatus = (int)$this->db->executeCommand('EXISTS', [$newClientKey]);
177
            if ($entityStatus === 1) {
178
                throw new DuplicateKeyException('Duplicate key "' . $newClientKey . '"');
179
            }
180
        }
181
182
        $this->db->executeCommand('MULTI');
183
        try {
184
            $reAddKeyInList = false;
185
            if (array_key_exists($modelKey, $values) === true) {
186
                $oldId = $client->getOldKey();
187
                $oldClientKey = $this->getClientKey($oldId);
188
189
                $this->db->executeCommand('RENAMENX', [$oldClientKey, $clientKey]);
190
                $this->db->executeCommand('SREM', [$clientListKey, $oldClientKey]);
191
                $reAddKeyInList = true;
192
            }
193
194
            $redisUpdateParameters = [$clientKey];
195
            $redisDeleteParameters = [$clientKey];
196
            $this->setAttributesDefinitions($client->attributesDefinition());
197
            foreach ($values as $key => $value) {
198
                if ($value === null) {
199
                    $redisDeleteParameters[] = $key;
200
                } else {
201
                    $redisUpdateParameters[] = $key;
202
                    $redisUpdateParameters[] = $this->convertToDatabase($key, $value);
203
                }
204
            }
205
            if (count($redisDeleteParameters) > 1) {
206
                $this->db->executeCommand('HDEL', $redisDeleteParameters);
207
            }
208
            if (count($redisUpdateParameters) > 1) {
209
                $this->db->executeCommand('HMSET', $redisUpdateParameters);
210
            }
211
212
            if ($reAddKeyInList === true) {
213
                $this->db->executeCommand('SADD', [$clientListKey, $clientId]);
214
            }
215
216
            $this->db->executeCommand('EXEC');
217
        } catch (DatabaseException $e) {
218
            // @codeCoverageIgnoreStart
219
            // we have a REDIS exception, we should not discard
220
            Yii::debug('Error while updating entity', __METHOD__);
221
            throw $e;
222
            // @codeCoverageIgnoreEnd
223
        }
224
225
        $changedAttributes = [];
226
        foreach ($values as $name => $value) {
227
            $oldAttributes = $client->getOldAttributes();
228
            $changedAttributes[$name] = isset($oldAttributes[$name]) ? $oldAttributes[$name] : null;
229
            $client->setOldAttribute($name, $value);
230
        }
231
        $client->afterSave(false, $changedAttributes);
232
        return true;
233
    }
234
235
    /**
236
     * @inheritdoc
237
     */
238
    public function findOne($key)
239
    {
240
        $record = null;
241
        $clientKey = $this->getClientKey($key);
242
        $clientExists = (bool)$this->db->executeCommand('EXISTS', [$clientKey]);
243
        if ($clientExists === true) {
244
            $clientData = $this->db->executeCommand('HGETALL', [$clientKey]);
245
            $record = Yii::createObject('sweelix\oauth2\server\interfaces\ClientModelInterface');
246
            /** @var ClientModelInterface $record */
247
            $properties = $record->attributesDefinition();
248
            $this->setAttributesDefinitions($properties);
249
            $attributes = [];
250
            for ($i = 0; $i < count($clientData); $i += 2) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
251
                if (isset($properties[$clientData[$i]]) === true) {
252
                    $clientData[$i + 1] = $this->convertToModel($clientData[$i], $clientData[($i + 1)]);
253
                    $record->setAttribute($clientData[$i], $clientData[$i + 1]);
254
                    $attributes[$clientData[$i]] = $clientData[$i + 1];
255
                    // @codeCoverageIgnoreStart
256
                } elseif ($record->canSetProperty($clientData[$i])) {
257
                    // TODO: find a way to test attribute population
258
                    $record->{$clientData[$i]} = $clientData[$i + 1];
259
                }
260
                // @codeCoverageIgnoreEnd
261
            }
262
            if (empty($attributes) === false) {
263
                $record->setOldAttributes($attributes);
264
            }
265
            $record->afterFind();
266
        }
267
        return $record;
268
    }
269
270
    /**
271
     * @inheritdoc
272
     */
273
    public function delete(ClientModelInterface $client)
274
    {
275
        $result = false;
276
        if ($client->beforeDelete()) {
277
            $id = $client->getOldKey();
278
            $clientKey = $this->getClientKey($id);
279
            $clientListKey = $this->getClientListKey();
280
            $clientUsersListKey = $this->getClientUsersListKey($id);
281
282
            // before cleaning the client, drop all access tokens, refresh tokens, auth codes and jtis
283
            $token = Yii::createObject('sweelix\oauth2\server\interfaces\AccessTokenModelInterface');
284
            $tokenClass = get_class($token);
285
            $tokenClass::deleteAllByClientId($id);
286
287
            $token = Yii::createObject('sweelix\oauth2\server\interfaces\RefreshTokenModelInterface');
288
            $tokenClass = get_class($token);
289
            $tokenClass::deleteAllByClientId($id);
290
291
            $token = Yii::createObject('sweelix\oauth2\server\interfaces\JtiModelInterface');
292
            $tokenClass = get_class($token);
293
            $tokenClass::deleteAllByClientId($id);
294
295
            $usersList = $this->db->executeCommand('SMEMBERS', [$clientUsersListKey]);
296
297
            $this->db->executeCommand('MULTI');
298
            // remove client from all userClient sets
299
            foreach ($usersList as $user) {
300
                $userClientKey = $this->getUserClientsListKey($user);
301
                $this->db->executeCommand('SREM', [$userClientKey, $id]);
302
            }
303
            $this->db->executeCommand('DEL', [$clientKey]);
304
            $this->db->executeCommand('DEL', [$clientUsersListKey]);
305
            $this->db->executeCommand('SREM', [$clientListKey, $id]);
306
            //TODO: check results to return correct information
307
            $queryResult = $this->db->executeCommand('EXEC');
0 ignored issues
show
Unused Code introduced by
$queryResult is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
308
            $client->setIsNewRecord(true);
309
            $client->afterDelete();
310
            $result = true;
311
        }
312
        return $result;
313
    }
314
315
    /**
316
     * @inheritdoc
317
     */
318
    public function hasUser(ClientModelInterface $client, $userId)
319
    {
320
        $key = $client->getKey();
321
        $clientUsersListKey = $this->getClientUsersListKey($key);
322
        return (bool)$this->db->executeCommand('SISMEMBER', [$clientUsersListKey, $userId]);
323
    }
324
325
    /**
326
     * @inheritdoc
327
     */
328
    public function addUser(ClientModelInterface $client, $userId)
329
    {
330
        $key = $client->getKey();
331
        $clientUsersListKey = $this->getClientUsersListKey($key);
332
        $userClientsListKey = $this->getUserClientsListKey($userId);
333
        $this->db->executeCommand('MULTI');
334
        $this->db->executeCommand('SADD', [$clientUsersListKey, $userId]);
335
        $this->db->executeCommand('SADD', [$userClientsListKey, $key]);
336
        $queryResult = $this->db->executeCommand('EXEC');
0 ignored issues
show
Unused Code introduced by
$queryResult is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
337
        //TODO: check if we should send back false or not
338
        return true;
339
    }
340
341
    /**
342
     * @inheritdoc
343
     */
344
    public function removeUser(ClientModelInterface $client, $userId)
345
    {
346
        $key = $client->getKey();
347
        $clientUsersListKey = $this->getClientUsersListKey($key);
348
        $userClientsListKey = $this->getUserClientsListKey($userId);
349
        $this->db->executeCommand('MULTI');
350
        $this->db->executeCommand('SREM', [$clientUsersListKey, $userId]);
351
        $this->db->executeCommand('SREM', [$userClientsListKey, $key]);
352
        $queryResult = $this->db->executeCommand('EXEC');
0 ignored issues
show
Unused Code introduced by
$queryResult is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
353
        //TODO: check if we should send back false or not
354
        return true;
355
    }
356
357
    /**
358
     * @inheritdoc
359
     */
360
    public function findAllByUserId($userId)
361
    {
362
        $userClientsListKey = $this->getUserClientsListKey($userId);
363
        $clientsList = $this->db->executeCommand('SMEMBERS', [$userClientsListKey]);
364
        $clients = [];
365
        foreach ($clientsList as $clientId) {
366
            $result = $this->findOne($clientId);
367
            if ($result instanceof ClientModelInterface) {
368
                $clients[] = $result;
369
            }
370
        }
371
        return $clients;
372
    }
373
374
    /**
375
     * @inheritdoc
376
     */
377
    public function findAll()
378
    {
379
        $clientListKey = $this->getClientListKey();
380
        $clientList = $this->db->executeCommand('SMEMBERS', [$clientListKey]);
381
        $clients = [];
382
        foreach ($clientList as $clientId) {
383
            $result = $this->findOne($clientId);
384
            if ($result instanceof ClientModelInterface) {
385
                $clients[] = $result;
386
            }
387
        }
388
        return $clients;
389
    }
390
}
391