Passed
Pull Request — master (#7124)
by
unknown
09:36
created

Version20251130111400::migrateAttemptFeedback()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 85
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 51
nop 2
dl 0
loc 85
rs 8.4468
c 1
b 0
f 0
nc 12

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
8
9
use Chamilo\CoreBundle\Entity\Asset;
10
use Chamilo\CoreBundle\Entity\ResourceFile;
11
use Chamilo\CoreBundle\Entity\ResourceNode;
12
use Chamilo\CoreBundle\Entity\ResourceType;
13
use Chamilo\CoreBundle\Helpers\CreateUploadedFileHelper;
14
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
15
use Chamilo\CoreBundle\Repository\AssetRepository;
16
use Doctrine\DBAL\Schema\Schema;
17
use Throwable;
18
19
final class Version20251130111400 extends AbstractMigrationChamilo
20
{
21
    private const DRY_RUN = false;
22
23
    public function getDescription(): string
24
    {
25
        return 'Migrate attempt_file / attempt_feedback assets to ResourceNode/ResourceFile and fill resource_node_id.';
26
    }
27
28
    public function up(Schema $schema): void
29
    {
30
        $this->entityManager->clear();
0 ignored issues
show
Bug introduced by
The method clear() does not exist on null. ( Ignorable by Annotation )

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

30
        $this->entityManager->/** @scrutinizer ignore-call */ 
31
                              clear();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
31
32
        /** @var AssetRepository $assetRepo */
33
        $assetRepo = $this->container->get(AssetRepository::class);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

33
        /** @scrutinizer ignore-call */ 
34
        $assetRepo = $this->container->get(AssetRepository::class);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
34
35
        $resourceTypeRepo = $this->entityManager->getRepository(ResourceType::class);
36
37
        $attemptFileType = $resourceTypeRepo->findOneBy(['title' => 'attempt_file']);
38
        $attemptFeedbackType = $resourceTypeRepo->findOneBy(['title' => 'attempt_feedback']);
39
40
        if (null === $attemptFileType || null === $attemptFeedbackType) {
41
            error_log('[MIGRATION] ResourceType "attempt_file" or "attempt_feedback" not found. Aborting data migration.');
42
43
            return;
44
        }
45
46
        $this->migrateAttemptFiles($assetRepo, $attemptFileType);
47
        $this->migrateAttemptFeedback($assetRepo, $attemptFeedbackType);
48
    }
49
50
    public function down(Schema $schema): void
51
    {
52
        // No-op: data migration is not reversible in a safe way.
53
    }
54
55
    private function migrateAttemptFiles(
56
        AssetRepository $assetRepo,
57
        ResourceType $attemptFileType
58
    ): void {
59
        $sql = 'SELECT id, attempt_id, asset_id
60
                FROM attempt_file
61
                WHERE asset_id IS NOT NULL
62
                  AND resource_node_id IS NULL';
63
64
        $rows = $this->connection->executeQuery($sql)->fetchAllAssociative();
65
66
        $total = \count($rows);
67
        $processed = 0;
68
        $migrated = 0;
69
70
        error_log('[MIGRATION][attempt_file] Candidates='.(string) $total);
71
72
        foreach ($rows as $row) {
73
            $processed++;
74
75
            $rowId = $row['id'];         // binary(16) UUID
76
            $assetId = $row['asset_id']; // binary(16) UUID
77
78
            $rowIdHex = bin2hex($rowId);
79
            $assetIdHex = bin2hex($assetId);
80
81
            error_log(
82
                sprintf(
83
                    '[MIGRATION][attempt_file] Processing rowId=%s assetId=%s',
84
                    $rowIdHex,
85
                    $assetIdHex
86
                )
87
            );
88
89
            try {
90
                /** @var Asset|null $asset */
91
                $asset = $assetRepo->find($assetId);
92
93
                if (null === $asset) {
94
                    error_log(
95
                        sprintf(
96
                            '[MIGRATION][attempt_file] Missing Asset for attempt_file.id=%s (assetId=%s)',
97
                            $rowIdHex,
98
                            $assetIdHex
99
                        )
100
                    );
101
102
                    continue;
103
                }
104
105
                $ok = $this->migrateSingleAssetToResourceNode(
106
                    'attempt_file',
107
                    'attempt_file',
108
                    $rowId,
109
                    $asset,
110
                    $assetRepo,
111
                    $attemptFileType
112
                );
113
114
                if ($ok) {
115
                    $migrated++;
116
                }
117
            } catch (Throwable $e) {
118
                error_log(
119
                    sprintf(
120
                        '[MIGRATION][attempt_file] Error for attempt_file.id=%s: %s',
121
                        $rowIdHex,
122
                        $e->getMessage()
123
                    )
124
                );
125
            }
126
127
            if (0 === $processed % 50) {
128
                // Free some memory
129
                $this->entityManager->clear(ResourceNode::class);
130
                $this->entityManager->clear(ResourceFile::class);
131
                $this->entityManager->clear(Asset::class);
132
            }
133
        }
134
135
        error_log(
136
            sprintf(
137
                '[MIGRATION][attempt_file] Processed=%d, Migrated=%d (Total=%d)',
138
                $processed,
139
                $migrated,
140
                $total
141
            )
142
        );
143
    }
144
145
    private function migrateAttemptFeedback(
146
        AssetRepository $assetRepo,
147
        ResourceType $attemptFeedbackType
148
    ): void {
149
        $sql = 'SELECT id, attempt_id, asset_id
150
                FROM attempt_feedback
151
                WHERE asset_id IS NOT NULL
152
                  AND resource_node_id IS NULL';
153
154
        $rows = $this->connection->executeQuery($sql)->fetchAllAssociative();
155
156
        $total = \count($rows);
157
        $processed = 0;
158
        $migrated = 0;
159
160
        error_log('[MIGRATION][attempt_feedback] Candidates='.(string) $total);
161
162
        foreach ($rows as $row) {
163
            $processed++;
164
165
            $rowId = $row['id'];         // binary(16) UUID
166
            $assetId = $row['asset_id']; // binary(16) UUID
167
168
            $rowIdHex = bin2hex($rowId);
169
            $assetIdHex = bin2hex($assetId);
170
171
            error_log(
172
                sprintf(
173
                    '[MIGRATION][attempt_feedback] Processing rowId=%s assetId=%s',
174
                    $rowIdHex,
175
                    $assetIdHex
176
                )
177
            );
178
179
            try {
180
                /** @var Asset|null $asset */
181
                $asset = $assetRepo->find($assetId);
182
183
                if (null === $asset) {
184
                    error_log(
185
                        sprintf(
186
                            '[MIGRATION][attempt_feedback] Missing Asset for attempt_feedback.id=%s (assetId=%s)',
187
                            $rowIdHex,
188
                            $assetIdHex
189
                        )
190
                    );
191
192
                    continue;
193
                }
194
195
                $ok = $this->migrateSingleAssetToResourceNode(
196
                    'attempt_feedback',
197
                    'attempt_feedback',
198
                    $rowId,
199
                    $asset,
200
                    $assetRepo,
201
                    $attemptFeedbackType
202
                );
203
204
                if ($ok) {
205
                    $migrated++;
206
                }
207
            } catch (Throwable $e) {
208
                error_log(
209
                    sprintf(
210
                        '[MIGRATION][attempt_feedback] Error for attempt_feedback.id=%s: %s',
211
                        $rowIdHex,
212
                        $e->getMessage()
213
                    )
214
                );
215
            }
216
217
            if (0 === $processed % 50) {
218
                $this->entityManager->clear(ResourceNode::class);
219
                $this->entityManager->clear(ResourceFile::class);
220
                $this->entityManager->clear(Asset::class);
221
            }
222
        }
223
224
        error_log(
225
            sprintf(
226
                '[MIGRATION][attempt_feedback] Processed=%d, Migrated=%d (Total=%d)',
227
                $processed,
228
                $migrated,
229
                $total
230
            )
231
        );
232
    }
233
234
    /**
235
     * Shared logic for attempt_file and attempt_feedback:
236
     *  - Read Asset content using AssetRepository.
237
     *  - Create a ResourceNode with the given ResourceType.
238
     *  - Create a ResourceFile with setFile() (Vich) using the Asset content.
239
     *  - Update resource_node_id in the corresponding table.
240
     *
241
     * @return bool true if the migration succeeded, false if it was skipped.
242
     */
243
    private function migrateSingleAssetToResourceNode(
244
        string $context,
245
        string $tableName,
246
        string $rowId,
247
        Asset $asset,
248
        AssetRepository $assetRepo,
249
        ResourceType $resourceType
250
    ): bool {
251
        $content = $assetRepo->getAssetContent($asset);
252
253
        if (!\is_string($content) || '' === $content) {
254
            $filePath = '';
255
            $exists = null;
256
257
            try {
258
                $filePath = $assetRepo->getStorage()->resolveUri($asset);
259
                $exists = $assetRepo->getFileSystem()->fileExists($filePath);
260
            } catch (Throwable $e) {
261
                error_log(
262
                    sprintf(
263
                        '[MIGRATION][%s] Error while checking filesystem for Asset id=%s: %s',
264
                        $context,
265
                        (string) $asset->getId(),
266
                        $e->getMessage()
267
                    )
268
                );
269
            }
270
271
            error_log(
272
                sprintf(
273
                    '[MIGRATION][%s] Empty content for Asset id=%s, title=%s, mime=%s, size=%d, path=%s, exists=%s',
274
                    $context,
275
                    (string) $asset->getId(),
276
                    (string) $asset->getTitle(),
277
                    (string) $asset->getMimeType(),
278
                    (int) $asset->getSize(),
279
                    $filePath,
280
                    true === $exists ? 'yes' : 'no'
281
                )
282
            );
283
284
            return false;
285
        }
286
287
        $originalName = $asset->getTitle();
288
        $mimeType = 'application/octet-stream';
289
290
        if (method_exists($asset, 'getMimeType') && null !== $asset->getMimeType()) {
291
            $mimeType = (string) $asset->getMimeType();
292
        }
293
294
        if (self::DRY_RUN) {
295
            error_log(
296
                sprintf(
297
                    '[MIGRATION][DRY_RUN][%s] Would create ResourceNode/ResourceFile for table=%s rowId=%s',
298
                    $context,
299
                    $tableName,
300
                    bin2hex($rowId)
301
                )
302
            );
303
304
            return true;
305
        }
306
307
        $node = new ResourceNode();
308
        $node->setTitle($originalName ?: 'attempt_' . $context);
309
        $node->setResourceType($resourceType);
310
311
        $this->entityManager->persist($node);
312
313
        $uploadedFile = CreateUploadedFileHelper::fromString(
314
            $originalName ?: 'file_' . $context,
315
            $mimeType,
316
            $content
317
        );
318
319
        $resourceFile = new ResourceFile();
320
        $resourceFile->setResourceNode($node);
321
        $resourceFile->setFile($uploadedFile);
322
        $this->entityManager->persist($resourceFile);
323
        $this->entityManager->flush();
324
325
        $this->connection->update(
326
            $tableName,
327
            ['resource_node_id' => $node->getId()],
328
            ['id' => $rowId]
329
        );
330
331
        error_log(
332
            sprintf(
333
                '[MIGRATION][%s] Migrated rowId=%s to ResourceNode id=%d',
334
                $context,
335
                bin2hex($rowId),
336
                (int) $node->getId()
337
            )
338
        );
339
340
        return true;
341
    }
342
}
343