UserService   C
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 433
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 18

Importance

Changes 0
Metric Value
wmc 40
lcom 4
cbo 18
dl 0
loc 433
rs 6.2762
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 1
A getOnlineUsersIdsInRoom() 0 9 1
A getOnlineHumansInRoom() 0 8 2
A getOnlineUsersIds() 0 4 1
A getSessionsByUserIds() 0 4 1
A giveItems() 0 20 4
B prepareItemsArray() 0 17 5
B dropItems() 0 36 5
A logDroppedItems() 0 15 2
B takeItems() 0 33 4
A logObtainedItems() 0 16 2
A logGivenItems() 0 18 2
A pickAvatar() 0 19 2
A transliterate() 0 16 1
A getAlphabet() 0 16 1
A getStartRoom() 0 10 2
A giveStarterItems() 0 13 1
A addWaitstate() 0 7 1
A getEntityManager() 0 4 1
A dropWaitState() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like UserService 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 UserService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Rottenwood\KingdomBundle\Service;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\Common\Collections\Criteria;
7
use Doctrine\ORM\EntityManager;
8
use Monolog\Logger;
9
use Rottenwood\KingdomBundle\Entity\Human;
10
use Rottenwood\KingdomBundle\Entity\Infrastructure\InventoryItemRepository;
11
use Rottenwood\KingdomBundle\Entity\Infrastructure\Item;
12
use Rottenwood\KingdomBundle\Entity\Infrastructure\ItemRepository;
13
use Rottenwood\KingdomBundle\Entity\Infrastructure\RoomRepository;
14
use Rottenwood\KingdomBundle\Entity\InventoryItem;
15
use Rottenwood\KingdomBundle\Entity\Room;
16
use Rottenwood\KingdomBundle\Entity\Infrastructure\User;
17
use Rottenwood\KingdomBundle\Entity\Infrastructure\HumanRepository;
18
use Rottenwood\KingdomBundle\Exception\ItemNotFound;
19
use Rottenwood\KingdomBundle\Exception\NotEnoughItems;
20
use Rottenwood\KingdomBundle\Exception\RoomNotFound;
21
use Rottenwood\KingdomBundle\Redis\RedisClientInterface;
22
use Predis\Client as RedisClient;
23
use Symfony\Component\Finder\Finder;
24
use Symfony\Component\Finder\SplFileInfo;
25
use Symfony\Component\HttpKernel\KernelInterface;
26
27
class UserService
28
{
29
30
    /** @var KernelInterface */
31
    private $kernel;
32
    /** @var RedisClient */
33
    private $redis;
34
    /** @var HumanRepository */
35
    private $humanRepository;
36
    /** @var InventoryItemRepository */
37
    private $inventoryItemRepository;
38
    /** @var Logger */
39
    private $logger;
40
    /** @var RoomRepository */
41
    private $roomRepository;
42
    /** @var ItemRepository */
43
    private $itemRepository;
44
45
    /**
46
     * @param KernelInterface         $kernel
47
     * @param RedisClient             $redis
48
     * @param Logger                  $logger
49
     * @param HumanRepository         $humanRepository
50
     * @param InventoryItemRepository $inventoryItemRepository
51
     * @param RoomRepository          $roomRepository
52
     * @param ItemRepository          $itemRepository
53
     */
54
    public function __construct(
55
        KernelInterface $kernel,
56
        RedisClient $redis,
57
        Logger $logger,
58
        HumanRepository $humanRepository,
59
        InventoryItemRepository $inventoryItemRepository,
60
        RoomRepository $roomRepository,
61
        ItemRepository $itemRepository
62
    ) {
63
        $this->redis = $redis;
64
        $this->logger = $logger;
65
        $this->humanRepository = $humanRepository;
66
        $this->inventoryItemRepository = $inventoryItemRepository;
67
        $this->kernel = $kernel;
68
        $this->roomRepository = $roomRepository;
69
        $this->itemRepository = $itemRepository;
70
    }
71
72
    /**
73
     * Запрос ID всех онлайн игроков в комнате
74
     * @param Room      $room
75
     * @param int|int[] $excludePlayerIds
76
     * @return int[]
77
     */
78
    public function getOnlineUsersIdsInRoom(Room $room, $excludePlayerIds = []): array
79
    {
80
        return array_map(
81
            function (User $user) {
82
                return $user->getId();
83
            },
84
            $this->getOnlineHumansInRoom($room, $excludePlayerIds)
85
        );
86
    }
87
88
    /**
89
     * Запрос всех онлайн игроков в комнате
90
     * @param Room      $room
91
     * @param int|array $excludePlayerIds
92
     * @return Human[]
93
     */
94
    public function getOnlineHumansInRoom(Room $room, $excludePlayerIds = []): array
95
    {
96
        if (!is_array($excludePlayerIds)) {
97
            $excludePlayerIds = [$excludePlayerIds];
98
        }
99
100
        return $this->humanRepository->findOnlineByRoom($room, $this->getOnlineUsersIds(), $excludePlayerIds);
101
    }
102
103
    /**
104
     * Запрос id всех игроков онлайн из redis
105
     * @return int[]
106
     */
107
    public function getOnlineUsersIds(): array
108
    {
109
        return $this->redis->smembers(RedisClientInterface::ONLINE_LIST);
110
    }
111
112
    /**
113
     * @param array $userIds
114
     * @return array
115
     */
116
    public function getSessionsByUserIds(array $userIds): array
117
    {
118
        return array_values($this->redis->hmget(RedisClientInterface::ID_SESSION_HASH, $userIds));
119
    }
120
121
    /**
122
     * Передать один или несколько предметов другому персонажу
123
     * @param User        $userFrom
124
     * @param User        $userTo
125
     * @param Item|Item[] $items
126
     * @param int         $quantityToGive Сколько предметов передать
127
     * @return bool
128
     * @throws \Exception
129
     */
130
    public function giveItems(User $userFrom, User $userTo, $items, int $quantityToGive = 1): bool
131
    {
132
        $items = $this->prepareItemsArray($items);
133
134
        try {
135
            $this->dropItems($userFrom, $items, $quantityToGive);
136
        } catch (\Exception $exception) {
137
            if ($exception instanceof ItemNotFound || $exception instanceof NotEnoughItems) {
138
                return false;
139
            } else {
140
                throw $exception;
141
            }
142
        }
143
144
        $this->takeItems($userTo, $items, $quantityToGive);
145
146
        $this->logGivenItems($userFrom, $userTo, $items, $quantityToGive);
147
148
        return true;
149
    }
150
151
    /**
152
     * Подготовка массива предметов
153
     * @param Item|Item[] $items
154
     * @return Item[]
155
     * @throws NotEnoughItems
156
     */
157
    private function prepareItemsArray($items): array
158
    {
159
        $preparedItems = [];
160
        if (is_array($items) && current($items) instanceof Item) {
161
            $preparedItems = $items;
162
        } elseif ($items instanceof Item) {
163
            $preparedItems[] = $items;
164
        } else {
165
            throw new \RuntimeException('$items must be Item or array of Items entity');
166
        }
167
168
        if (empty($preparedItems)) {
169
            throw new NotEnoughItems('Не передано ни одного предмета для действия');
170
        }
171
172
        return $preparedItems;
173
    }
174
175
    /**
176
     * Выбросить один или несколько предметов
177
     * @param User        $user
178
     * @param Item|Item[] $items
179
     * @param int         $quantityToDrop Сколько предметов выбросить
180
     * @return bool
181
     * @throws ItemNotFound
182
     * @throws NotEnoughItems
183
     */
184
    public function dropItems(User $user, $items, int $quantityToDrop): bool
185
    {
186
        $items = $this->prepareItemsArray($items);
187
188
        $inventoryItems = $this->inventoryItemRepository->findByUser($user);
189
        $inventoryItemCollection = new ArrayCollection($inventoryItems);
190
191
        foreach ($items as $item) {
192
            $criteria = Criteria::create();
193
            $criteria->where(Criteria::expr()->eq('item', $item));
194
195
            $collectedInventoryItem = $inventoryItemCollection->matching($criteria);
196
197
            if ($collectedInventoryItem->isEmpty()) {
198
                throw new ItemNotFound;
199
            }
200
201
            $inventoryItem = $collectedInventoryItem->first();
202
            $itemQuantity = $inventoryItem->getQuantity();
203
            $itemQuantityAfterDrop = $itemQuantity - $quantityToDrop;
204
205
            if ($itemQuantityAfterDrop == 0) {
206
                $this->inventoryItemRepository->remove($inventoryItem);
207
            } elseif ($itemQuantityAfterDrop > 0) {
208
                $inventoryItem->setQuantity($itemQuantityAfterDrop);
209
            } else {
210
                throw new NotEnoughItems;
211
            }
212
        }
213
214
        $this->inventoryItemRepository->flush();
215
216
        $this->logDroppedItems($user, $items, $quantityToDrop);
217
218
        return true;
219
    }
220
221
    /**
222
     * @param User   $user
223
     * @param Item[] $items
224
     * @param int    $quantityToDrop
225
     */
226
    private function logDroppedItems($user, array $items, int $quantityToDrop)
227
    {
228
        foreach ($items as $item) {
229
            $this->logger->info(
230
                sprintf(
231
                    '[%d]%s выбросил предмет: [%d]%s x %d шт.',
232
                    $user->getId(),
233
                    $user->getName(),
234
                    $item->getId(),
235
                    $item->getName(),
236
                    $quantityToDrop
237
                )
238
            );
239
        }
240
    }
241
242
    /**
243
     * Взять один или несколько предметов
244
     * @param User        $user
245
     * @param Item|Item[] $items
246
     * @param int         $quantityToTake Сколько предметов взять
247
     */
248
    public function takeItems(User $user, $items, int $quantityToTake = 1)
249
    {
250
        $itemsToTake = $this->prepareItemsArray($items);
251
252
        $inventoryItems = $this->inventoryItemRepository->findByUser($user);
253
        $inventoryItemCollection = new ArrayCollection($inventoryItems);
254
255
        foreach ($itemsToTake as $itemToTake) {
256
            $criteria = Criteria::create();
257
            $criteria->where(Criteria::expr()->eq('item', $itemToTake));
258
259
            $collectedInventoryItem = $inventoryItemCollection->matching($criteria);
260
261
            if ($collectedInventoryItem->count() === 1) {
262
                $inventoryItem = $collectedInventoryItem->first();
263
                $quantity = $inventoryItem->getQuantity() + $quantityToTake;
264
                $inventoryItem->setQuantity($quantity);
265
            } elseif ($collectedInventoryItem->count() === 0) {
266
                $inventoryItem = new InventoryItem($user, $itemToTake, $quantityToTake);
267
                $this->inventoryItemRepository->persist($inventoryItem);
268
            } else {
269
                throw new \RuntimeException('Найдено более одного предмета');
270
            }
271
        }
272
273
        $this->inventoryItemRepository->flush();
274
275
        $this->logObtainedItems(
276
            $user,
277
            $itemsToTake,
278
            $quantityToTake
279
        );
280
    }
281
282
    /**
283
     * @param User   $user
284
     * @param Item[] $itemsToTake
285
     * @param int    $quantityToTake
286
     */
287
    private function logObtainedItems(User $user, array $itemsToTake, int $quantityToTake)
288
    {
289
        /** @var Item $item */
290
        foreach ($itemsToTake as $item) {
291
            $this->logger->info(
292
                sprintf(
293
                    '[%d]%s взял предмет: [%d]%s x %d шт.',
294
                    $user->getId(),
295
                    $user->getName(),
296
                    $item->getId(),
297
                    $item->getName(),
298
                    $quantityToTake
299
                )
300
            );
301
        }
302
    }
303
304
    /**
305
     * @param User   $userFrom
306
     * @param User   $userTo
307
     * @param Item[] $items
308
     * @param int    $quantityToGive
309
     */
310
    private function logGivenItems(User $userFrom, User $userTo, array $items, int $quantityToGive)
311
    {
312
        /** @var Item $item */
313
        foreach ($items as $item) {
314
            $this->logger->info(
315
                sprintf(
316
                    '[%d]%s передал [%d]%s предмет: [%d]%s x %d шт.',
317
                    $userFrom->getId(),
318
                    $userFrom->getName(),
319
                    $userTo->getId(),
320
                    $userTo->getName(),
321
                    $item->getId(),
322
                    $item->getName(),
323
                    $quantityToGive
324
                )
325
            );
326
        }
327
    }
328
329
    /**
330
     * Установка рэндомного аватара
331
     * @return string
332
     */
333
    public function pickAvatar(): string
334
    {
335
        $finder = new Finder();
336
337
        $prefix = 'male';
338
        $avatarPath = $this->kernel->getRootDir() . '/../web/img/avatars/' . $prefix;
339
340
        $files = $finder->files()->in($avatarPath);
341
342
        $avatars = [];
343
        /** @var SplFileInfo $file */
344
        foreach ($files as $file) {
345
            $avatars[] = $file->getBasename('.jpg');
346
        }
347
348
        $avatar = $prefix . '/' . $avatars[array_rand($avatars)];
349
350
        return $avatar;
351
    }
352
353
    /**
354
     * Транслитерация и конвертация строки, удаление цифр
355
     * @param string $string
356
     * @return string
357
     */
358
    public function transliterate(string $string): string
359
    {
360
        $englishLetters = implode('', array_keys($this->getAlphabet()));
361
        $cyrillicLetters = 'абвгдеёжзиклмнопрстуфхцчшщьыъэюяАБВГДЕЁЖЗИКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ';
362
        $pattern = '[^' . preg_quote($englishLetters . $cyrillicLetters, '/') . ']';
363
364
        $stringWithoutSpecialChars = mb_ereg_replace($pattern, '', $string);
365
366
        $cyrillicString = mb_convert_case(
367
            strtr($stringWithoutSpecialChars, $this->getAlphabet()),
368
            MB_CASE_TITLE,
369
            'UTF-8'
370
        );
371
372
        return $cyrillicString;
373
    }
374
375
    /**
376
     * Массив соответствия русских букв латинским
377
     * @return string[]
378
     */
379
    private function getAlphabet(): array
380
    {
381
        return [
382
            'a' => 'а', 'b' => 'б', 'c' => 'ц', 'd' => 'д', 'e' => 'е',
383
            'f' => 'ф', 'g' => 'г', 'h' => 'х', 'i' => 'ай', 'j' => 'дж',
384
            'k' => 'к', 'l' => 'л', 'm' => 'м', 'n' => 'н', 'o' => 'о',
385
            'p' => 'п', 'q' => 'к', 'r' => 'р', 's' => 'с', 't' => 'т',
386
            'u' => 'у', 'v' => 'в', 'w' => 'в', 'x' => 'кс', 'y' => 'й',
387
            'z' => 'з', 'A' => 'А', 'B' => 'Б', 'C' => 'Ц', 'D' => 'Д',
388
            'E' => 'Е', 'F' => 'Ф', 'G' => 'Г', 'H' => 'Х', 'I' => 'Ай',
389
            'J' => 'Дж', 'K' => 'К', 'L' => 'Л', 'M' => 'М', 'N' => 'Н',
390
            'O' => 'О', 'P' => 'П', 'Q' => 'К', 'R' => 'Р', 'S' => 'С',
391
            'T' => 'Т', 'U' => 'Ю', 'V' => 'В', 'W' => 'В', 'X' => 'Кс',
392
            'Y' => 'Й', 'Z' => 'З',
393
        ];
394
    }
395
396
    /**
397
     * Стартовая комната при создании персонажа
398
     * @return Room
399
     * @throws RoomNotFound
400
     */
401
    public function getStartRoom(): Room
402
    {
403
        $startRoom = $this->roomRepository->findOneByXandY(0, 0);
404
405
        if (!$startRoom) {
406
            throw new RoomNotFound();
407
        }
408
409
        return $startRoom;
410
    }
411
412
    /**
413
     * Стартовые предметы при создании персонажа
414
     * @param User $user
415
     */
416
    public function giveStarterItems(User $user)
417
    {
418
        $starterItemsIds = [
419
            'newbie-boots',
420
            'newbie-legs',
421
            'newbie-shirt',
422
            'tester-sword',
423
        ];
424
425
        $items = $this->itemRepository->findSeveralByIds($starterItemsIds);
426
427
        $this->takeItems($user, $items);
428
    }
429
430
    /**
431
     * Назначение вейтстейта юзеру
432
     * @param int $waitState
433
     * @return bool
434
     */
435
    public function addWaitstate(User $user, int $waitState): bool
436
    {
437
        $user->addWaitstate($waitState);
438
        $this->getEntityManager()->flush($user);
439
440
        return true;
441
    }
442
443
    /**
444
     * @return EntityManager
445
     */
446
    private function getEntityManager(): EntityManager
447
    {
448
        return $this->humanRepository->getEntityManager();
449
    }
450
451
    /**
452
     * @param User $user
453
     */
454
    public function dropWaitState(User $user)
455
    {
456
        $user->dropWaitState();
457
        $this->getEntityManager()->flush($user);
458
    }
459
}
460