BulkUpsert   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 199
dl 0
loc 344
rs 6
c 0
b 0
f 0
wmc 55

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A dispatchRankingMessages() 0 12 2
B updatePlayerChartLibs() 0 48 10
A extractId() 0 15 6
F processPlayerChartData() 0 161 27
B __invoke() 0 73 9

How to fix   Complexity   

Complex Class

Complex classes like BulkUpsert 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.

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 BulkUpsert, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
// src/Controller/PlayerChart/BulkUpsert.php
4
declare(strict_types=1);
5
6
namespace VideoGamesRecords\CoreBundle\Controller\PlayerChart;
7
8
use Doctrine\ORM\EntityManagerInterface;
9
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
10
use Symfony\Component\HttpFoundation\JsonResponse;
11
use Symfony\Component\HttpFoundation\Request;
12
use Symfony\Component\HttpFoundation\Response;
13
use Symfony\Component\Messenger\MessageBusInterface;
14
use Symfony\Component\Serializer\SerializerInterface;
15
use Symfony\Component\Validator\Validator\ValidatorInterface;
16
use Symfony\Contracts\Translation\TranslatorInterface;
17
use VideoGamesRecords\CoreBundle\Entity\Chart;
18
use VideoGamesRecords\CoreBundle\Entity\ChartLib;
19
use VideoGamesRecords\CoreBundle\Entity\Player;
20
use VideoGamesRecords\CoreBundle\Entity\PlayerChart;
21
use VideoGamesRecords\CoreBundle\Entity\PlayerChartLib;
22
use VideoGamesRecords\CoreBundle\Entity\PlayerChartStatus;
23
use VideoGamesRecords\CoreBundle\Entity\Platform;
24
use VideoGamesRecords\CoreBundle\Message\Player\UpdatePlayerChartRank;
25
use Zenstruck\Messenger\Monitor\Stamp\DescriptionStamp;
0 ignored issues
show
Bug introduced by
The type Zenstruck\Messenger\Monitor\Stamp\DescriptionStamp was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
27
class BulkUpsert extends AbstractController
28
{
29
    private EntityManagerInterface $entityManager;
30
    private SerializerInterface $serializer;
31
    private ValidatorInterface $validator;
32
    private MessageBusInterface $messageBus;
33
    private TranslatorInterface $translator;
34
35
    public function __construct(
36
        EntityManagerInterface $entityManager,
37
        SerializerInterface $serializer,
38
        ValidatorInterface $validator,
39
        MessageBusInterface $messageBus,
40
        TranslatorInterface $translator
41
    ) {
42
        $this->entityManager = $entityManager;
43
        $this->serializer = $serializer;
44
        $this->validator = $validator;
45
        $this->messageBus = $messageBus;
46
        $this->translator = $translator;
47
    }
48
49
    public function __invoke(Request $request): JsonResponse
50
    {
51
        $this->denyAccessUnlessGranted('ROLE_PLAYER');
52
53
        $content = json_decode($request->getContent(), true);
54
55
        if (!isset($content['playerCharts']) || !is_array($content['playerCharts'])) {
56
            return new JsonResponse(['error' => 'playerCharts array is required'], Response::HTTP_BAD_REQUEST);
57
        }
58
59
        $playerCharts = [];
60
        $errors = [];
61
        $chartIds = [];
62
        $createdCount = 0;
63
        $updatedCount = 0;
64
65
        // Désactiver l'auto-flush pour les performances
66
        $this->entityManager->getConnection()->beginTransaction();
67
68
        try {
69
            foreach ($content['playerCharts'] as $index => $playerChartData) {
70
                $result = $this->processPlayerChartData($playerChartData, $index, $errors);
71
72
                if ($result !== null) {
73
                    $this->entityManager->persist($result['playerChart']);
74
                    $playerCharts[] = $result['playerChart'];
75
                    $chartIds[] = $result['playerChart']->getChart()->getId();
76
77
                    if ($result['isUpdate']) {
78
                        $updatedCount++;
79
                    } else {
80
                        $createdCount++;
81
                    }
82
                }
83
            }
84
85
            if (!empty($errors)) {
86
                $this->entityManager->getConnection()->rollBack();
87
                return new JsonResponse(['errors' => $errors], Response::HTTP_BAD_REQUEST);
88
            }
89
90
            // Flush toutes les entités en une fois
91
            $this->entityManager->flush();
92
93
            // Mettre à jour le game.lastScore avec le dernier PlayerChart du tableau
94
            if (!empty($playerCharts)) {
95
                $lastPlayerChart = end($playerCharts);
96
                $game = $lastPlayerChart->getChart()->getGroup()->getGame();
97
                $game->setLastScore($lastPlayerChart);
98
                $this->entityManager->flush();
99
            }
100
101
            $this->entityManager->getConnection()->commit();
102
103
            // Envoyer les messages de mise à jour des rangs de manière groupée
104
            $this->dispatchRankingMessages($chartIds);
105
106
            return new JsonResponse([
107
                'message' => $this->translator->trans(
108
                    'playerChart.bulk_upsert_success',
109
                    ['%total%' => count($playerCharts), '%created%' => $createdCount, '%updated%' => $updatedCount],
110
                    'VgrCore'
111
                ),
112
                'created' => $createdCount,
113
                'updated' => $updatedCount,
114
                'total' => count($playerCharts),
115
                'ranking_updates_dispatched' => count(array_unique($chartIds))
116
            ], Response::HTTP_OK);
117
        } catch (\Exception $e) {
118
            $this->entityManager->getConnection()->rollBack();
119
            return new JsonResponse(
120
                ['error' => 'Database error: ' . $e->getMessage()],
121
                Response::HTTP_INTERNAL_SERVER_ERROR
122
            );
123
        }
124
    }
125
126
    private function processPlayerChartData(array $data, int $index, array &$errors): ?array
127
    {
128
        try {
129
            // Extraction des IDs depuis les formats API Platform ou simples
130
            $chartId = $this->extractId($data['chart'] ?? null);
131
            $playerId = $this->extractId($data['player'] ?? null);
132
133
            // Validation des données requises
134
            if (!$chartId || !$playerId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chartId of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $playerId of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
135
                $errors[$index] = 'chart and player are required fields';
136
                return null;
137
            }
138
139
            // Récupération des entités référencées
140
            $chart = $this->entityManager->getRepository(Chart::class)->find($chartId);
141
            if (!$chart) {
142
                $errors[$index] = sprintf('Chart with id %s not found', $chartId);
143
                return null;
144
            }
145
146
            $player = $this->entityManager->getRepository(Player::class)->find($playerId);
147
            if (!$player) {
148
                $errors[$index] = sprintf('Player with id %s not found', $playerId);
149
                return null;
150
            }
151
152
            // Vérifier si c'est une mise à jour ou une création
153
            $isUpdate = false;
154
            $playerChart = null;
155
            $playerChartId = $data['id'] ?? null;
156
157
            if ($playerChartId) {
158
                // Mode modification : récupérer l'entité existante
159
                $playerChart = $this->entityManager->getRepository(PlayerChart::class)->find($playerChartId);
160
                if (!$playerChart) {
161
                    $errors[$index] = sprintf('PlayerChart with id %s not found', $playerChartId);
162
                    return null;
163
                }
164
165
                // Vérifier que l'utilisateur peut modifier ce PlayerChart
166
                if ($playerChart->getPlayer()->getId() !== $player->getId()) {
167
                    $errors[$index] = 'Cannot modify PlayerChart of another player';
168
                    return null;
169
                }
170
171
                $isUpdate = true;
172
            } else {
173
                // Mode création : vérifier qu'il n'existe pas déjà
174
                $existingPlayerChart = $this->entityManager->getRepository(PlayerChart::class)
175
                    ->findOneBy(['chart' => $chart, 'player' => $player]);
176
177
                if ($existingPlayerChart) {
178
                    $errors[$index] = sprintf(
179
                        'PlayerChart already exists for player %d and chart %d',
180
                        $player->getId(),
181
                        $chart->getId()
182
                    );
183
                    return null;
184
                }
185
186
                // Création du PlayerChart
187
                $playerChart = new PlayerChart();
188
                $playerChart->setChart($chart);
189
                $playerChart->setPlayer($player);
190
            }
191
192
            // Status - gérer les formats API Platform et simples
193
            $statusId = $this->extractId($data['status'] ?? null) ?? 1;
194
            $status = $this->entityManager->getRepository(PlayerChartStatus::class)->find($statusId);
195
            if (!$status) {
196
                $errors[$index] = 'Invalid status provided';
197
                return null;
198
            }
199
            $playerChart->setStatus($status);
200
            $playerChart->setProof();
201
202
            // Platform optionnelle - gérer les formats API Platform et simples
203
            if (isset($data['platform'])) {
204
                $platformId = $this->extractId($data['platform']);
205
                if ($platformId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $platformId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
206
                    $platform = $this->entityManager->getRepository(Platform::class)->find($platformId);
207
                    if (!$platform) {
208
                        $errors[$index] = sprintf('Platform with id %s not found', $platformId);
209
                        return null;
210
                    }
211
                    $playerChart->setPlatform($platform);
212
                }
213
            }
214
215
            // Gestion des libs (valeurs du score)
216
            if (isset($data['libs']) && is_array($data['libs'])) {
217
                if ($isUpdate) {
218
                    // En mode édition, mettre à jour les libs existantes
219
                    $this->updatePlayerChartLibs($playerChart, $data['libs'], $index, $errors);
220
                } else {
221
                    // En mode création, ajouter les nouvelles libs
222
                    foreach ($data['libs'] as $libData) {
223
                        $chartLibId = $this->extractId($libData['libChart'] ?? $libData['chartLib'] ?? null);
224
                        $parseValue = $libData['parseValue'] ?? null;
225
                        $value = $libData['value'] ?? null;
226
227
                        if (!$chartLibId || ($parseValue === null && $value === null)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chartLibId of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
228
                            $errors[$index] = 'libChart/chartLib and parseValue (or value) are required for each lib';
229
                            return null;
230
                        }
231
232
                        $chartLib = $this->entityManager
233
                            ->getRepository(ChartLib::class)
234
                            ->find($chartLibId);
235
236
                        if (!$chartLib) {
237
                            $errors[$index] = sprintf('ChartLib with id %s not found', $chartLibId);
238
                            return null;
239
                        }
240
241
                        $playerChartLib = new PlayerChartLib();
242
                        $playerChartLib->setLibChart($chartLib);
243
244
                        if ($parseValue !== null) {
245
                            // Si parseValue est fourni, l'utiliser et appeler setValueFromPaseValue()
246
                            $playerChartLib->setParseValue($parseValue);
247
                            $playerChartLib->setValueFromPaseValue();
248
                        } else {
249
                            // Sinon utiliser value directement (rétrocompatibilité)
250
                            $playerChartLib->setValue($value);
251
                        }
252
253
                        $playerChart->addLib($playerChartLib);
254
                    }
255
                }
256
257
                if (!empty($errors)) {
258
                    return null;
259
                }
260
            }
261
262
            // Mettre à jour lastUpdate avec la date du jour et forcer le statut à 1
263
            $playerChart->setLastUpdate(new \DateTime());
264
            $defaultStatus = $this->entityManager->getRepository(PlayerChartStatus::class)->find(1);
265
            if ($defaultStatus) {
266
                $playerChart->setStatus($defaultStatus);
267
            }
268
269
            // Validation de l'entité
270
            $violations = $this->validator->validate($playerChart);
271
            if (count($violations) > 0) {
272
                $violationMessages = [];
273
                foreach ($violations as $violation) {
274
                    $violationMessages[] = $violation->getMessage();
275
                }
276
                $errors[$index] = implode(', ', $violationMessages);
277
                return null;
278
            }
279
280
            return [
281
                'playerChart' => $playerChart,
282
                'isUpdate' => $isUpdate
283
            ];
284
        } catch (\Exception $e) {
285
            $errors[$index] = 'Error processing PlayerChart: ' . $e->getMessage();
286
            return null;
287
        }
288
    }
289
290
    private function extractId($value): ?int
291
    {
292
        if (is_numeric($value)) {
293
            return (int) $value;
294
        }
295
296
        if (is_string($value) && preg_match('/\/(\d+)$/', $value, $matches)) {
297
            return (int) $matches[1];
298
        }
299
300
        if (is_array($value) && isset($value['id'])) {
301
            return (int) $value['id'];
302
        }
303
304
        return null;
305
    }
306
307
    private function updatePlayerChartLibs(PlayerChart $playerChart, array $libsData, int $index, array &$errors): void
308
    {
309
        // Créer un index des libs existantes par chartLibId
310
        $existingLibs = [];
311
        foreach ($playerChart->getLibs() as $existingLib) {
312
            $existingLibs[$existingLib->getLibChart()->getId()] = $existingLib;
313
        }
314
315
        // Traiter chaque lib des données
316
        foreach ($libsData as $libData) {
317
            $chartLibId = $this->extractId($libData['libChart'] ?? $libData['chartLib'] ?? null);
318
            $parseValue = $libData['parseValue'] ?? null;
319
            $value = $libData['value'] ?? null;
320
321
            if (!$chartLibId || ($parseValue === null && $value === null)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chartLibId of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
322
                $errors[$index] = 'libChart/chartLib and parseValue (or value) are required for each lib';
323
                return;
324
            }
325
326
            if (isset($existingLibs[$chartLibId])) {
327
                // Mettre à jour la lib existante
328
                if ($parseValue !== null) {
329
                    $existingLibs[$chartLibId]->setParseValue($parseValue);
330
                    $existingLibs[$chartLibId]->setValueFromPaseValue();
331
                } else {
332
                    $existingLibs[$chartLibId]->setValue($value);
333
                }
334
            } else {
335
                // Créer une nouvelle lib si elle n'existe pas
336
                $chartLib = $this->entityManager->getRepository(ChartLib::class)
337
                    ->find($chartLibId);
338
339
                if (!$chartLib) {
340
                    $errors[$index] = sprintf('ChartLib with id %s not found', $chartLibId);
341
                    return;
342
                }
343
344
                $playerChartLib = new PlayerChartLib();
345
                $playerChartLib->setLibChart($chartLib);
346
347
                if ($parseValue !== null) {
348
                    $playerChartLib->setParseValue($parseValue);
349
                    $playerChartLib->setValueFromPaseValue();
350
                } else {
351
                    $playerChartLib->setValue($value);
352
                }
353
354
                $playerChart->addLib($playerChartLib);
355
            }
356
        }
357
    }
358
359
    private function dispatchRankingMessages(array $chartIds): void
360
    {
361
        // Envoyer un message par chart unique pour éviter la duplication
362
        $uniqueChartIds = array_unique($chartIds);
363
364
        foreach ($uniqueChartIds as $chartId) {
365
            $message = new UpdatePlayerChartRank($chartId);
366
            $this->messageBus->dispatch(
367
                $message,
368
                [
369
                    new DescriptionStamp(
370
                        sprintf('Bulk update player-ranking for chart [%d]', $chartId)
371
                    )
372
                ]
373
            );
374
        }
375
    }
376
}
377