Completed
Push — master ( 4e608a...fcc2ac )
by
unknown
02:01 queued 37s
created

Version20251130111400::migrateAttemptFiles()   B

Complexity

Conditions 8
Paths 39

Size

Total Lines 119
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 75
nc 39
nop 2
dl 0
loc 119
rs 7.301
c 2
b 0
f 0

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\Migrations\AbstractMigrationChamilo;
14
use Chamilo\CoreBundle\Repository\AssetRepository;
15
use Doctrine\DBAL\Schema\Schema;
16
use Doctrine\Persistence\ManagerRegistry;
17
use Symfony\Component\HttpFoundation\File\UploadedFile;
18
use Throwable;
19
20
final class Version20251130111400 extends AbstractMigrationChamilo
21
{
22
    private const DRY_RUN = false;
23
24
    public function getDescription(): string
25
    {
26
        return 'Migrate attempt_file / attempt_feedback assets to ResourceNode/ResourceFile and fill resource_node_id, then cleanup asset references and orphan nodes.';
27
    }
28
29
    /**
30
     * This migration runs outside a global DB transaction.
31
     * We want to be able to skip broken rows without rolling back everything.
32
     */
33
    public function isTransactional(): bool
34
    {
35
        return false;
36
    }
37
38
    public function up(Schema $schema): void
39
    {
40
        $this->ensureEntityManagerIsOpen();
41
42
        $resourceTypeRepo = $this->entityManager->getRepository(ResourceType::class);
0 ignored issues
show
Bug introduced by
The method getRepository() 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

42
        /** @scrutinizer ignore-call */ 
43
        $resourceTypeRepo = $this->entityManager->getRepository(ResourceType::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...
43
44
        /** @var ResourceType|null $attemptFileType */
45
        $attemptFileType = $resourceTypeRepo->findOneBy(['title' => 'attempt_file']);
46
        /** @var ResourceType|null $attemptFeedbackType */
47
        $attemptFeedbackType = $resourceTypeRepo->findOneBy(['title' => 'attempt_feedback']);
48
49
        if (null === $attemptFileType || null === $attemptFeedbackType) {
50
            error_log('[MIGRATION] ResourceType "attempt_file" or "attempt_feedback" not found. Aborting data migration.');
51
52
            return;
53
        }
54
55
        /** @var AssetRepository $assetRepo */
56
        $assetRepo = $this->getAssetRepository();
57
58
        // 1) Main migrations (create ResourceNode/ResourceFile, clear asset_id, delete unused Asset).
59
        $this->migrateAttemptFiles($assetRepo, $attemptFileType);
60
        $this->migrateAttemptFeedback($assetRepo, $attemptFeedbackType);
61
62
        // 2) Cleanup: ensure there are no resource_node_id values without a ResourceFile.
63
        $this->cleanupOrphanNodesWithoutFile();
64
    }
65
66
    public function down(Schema $schema): void
67
    {
68
        // No-op: data migration is not reversible in a safe way.
69
    }
70
71
    /**
72
     * Migrate and/or cleanup attempt_file rows:
73
     * - If resource_node_id IS NULL: create ResourceNode/ResourceFile and then clear asset_id.
74
     * - If resource_node_id IS NOT NULL: only clear asset_id (row was already migrated).
75
     */
76
    private function migrateAttemptFiles(
77
        AssetRepository $assetRepo,
78
        ResourceType $attemptFileType
79
    ): void {
80
        $sql = 'SELECT id, attempt_id, asset_id, resource_node_id
81
                FROM attempt_file
82
                WHERE asset_id IS NOT NULL';
83
84
        $stmt = $this->connection->executeQuery($sql);
85
86
        $processed = 0;
87
        $migrated = 0;
88
        $cleanedOnly = 0;
89
90
        error_log('[MIGRATION][attempt_file] Starting migration for attempt_file rows.');
91
92
        while (false !== ($row = $stmt->fetchAssociative())) {
93
            $processed++;
94
95
            $rowId = $row['id'];             // binary(16) UUID or similar
96
            $attemptId = $row['attempt_id']; // int or other scalar
97
            $assetId = $row['asset_id'];     // binary(16) UUID
98
            $resourceNodeId = $row['resource_node_id']; // int|null
99
100
            $rowIdForLog = $this->formatIdForLog($rowId);
101
            $attemptIdForLog = $this->formatIdForLog($attemptId);
102
            $assetIdForLog = $this->formatIdForLog($assetId);
103
104
            if (0 === $processed % 200) {
105
                error_log(
106
                    sprintf(
107
                        '[MIGRATION][attempt_file] Progress: processed=%d, migrated=%d, cleanedOnly=%d (last rowId=%s)',
108
                        $processed,
109
                        $migrated,
110
                        $cleanedOnly,
111
                        $rowIdForLog
112
                    )
113
                );
114
            }
115
116
            try {
117
                $this->ensureEntityManagerIsOpen();
118
119
                /** @var Asset|null $asset */
120
                $asset = $assetRepo->find($assetId);
121
122
                if (null === $asset) {
123
                    // Asset record is gone, just clear the asset_id reference.
124
                    $this->connection->update(
125
                        'attempt_file',
126
                        ['asset_id' => null],
127
                        ['id' => $rowId]
128
                    );
129
130
                    error_log(
131
                        sprintf(
132
                            '[MIGRATION][attempt_file] Cleared asset_id for attempt_file.id=%s (attemptId=%s assetId=%s) because Asset is missing.',
133
                            $rowIdForLog,
134
                            $attemptIdForLog,
135
                            $assetIdForLog
136
                        )
137
                    );
138
139
                    continue;
140
                }
141
142
                if (null === $resourceNodeId) {
143
                    // Not migrated yet: create ResourceNode/ResourceFile then cleanup the Asset reference.
144
                    $ok = $this->migrateSingleAssetToResourceNode(
145
                        $assetRepo,
146
                        'attempt_file',
147
                        'attempt_file',
148
                        $rowId,
149
                        $asset,
150
                        $attemptFileType
151
                    );
152
153
                    if ($ok) {
154
                        $migrated++;
155
                    }
156
                } else {
157
                    // Already has a resource_node_id: only cleanup asset_id and possibly delete the Asset if unused.
158
                    $this->clearAssetReference(
159
                        $assetRepo,
160
                        'attempt_file',
161
                        'attempt_file',
162
                        $rowId,
163
                        $rowIdForLog,
164
                        $asset
165
                    );
166
                    $cleanedOnly++;
167
                }
168
            } catch (Throwable $e) {
169
                error_log(
170
                    sprintf(
171
                        '[MIGRATION][attempt_file] Error for attempt_file.id=%s (attemptId=%s): %s',
172
                        $rowIdForLog,
173
                        $attemptIdForLog,
174
                        $e->getMessage()
175
                    )
176
                );
177
178
                $this->ensureEntityManagerIsOpen();
179
            }
180
181
            if (0 === $processed % 50) {
182
                // Free some memory for long-running migrations.
183
                $this->entityManager->clear(ResourceNode::class);
184
                $this->entityManager->clear(ResourceFile::class);
185
                $this->entityManager->clear(Asset::class);
186
            }
187
        }
188
189
        error_log(
190
            sprintf(
191
                '[MIGRATION][attempt_file] Finished. Processed=%d, Migrated=%d, CleanedOnly=%d',
192
                $processed,
193
                $migrated,
194
                $cleanedOnly
195
            )
196
        );
197
    }
198
199
    /**
200
     * Same logic as attempt_file but for attempt_feedback.
201
     */
202
    private function migrateAttemptFeedback(
203
        AssetRepository $assetRepo,
204
        ResourceType $attemptFeedbackType
205
    ): void {
206
        $sql = 'SELECT id, attempt_id, asset_id, resource_node_id
207
                FROM attempt_feedback
208
                WHERE asset_id IS NOT NULL';
209
210
        $stmt = $this->connection->executeQuery($sql);
211
212
        $processed = 0;
213
        $migrated = 0;
214
        $cleanedOnly = 0;
215
216
        error_log('[MIGRATION][attempt_feedback] Starting migration for attempt_feedback rows.');
217
218
        while (false !== ($row = $stmt->fetchAssociative())) {
219
            $processed++;
220
221
            $rowId = $row['id'];             // binary(16) UUID or similar
222
            $attemptId = $row['attempt_id']; // int or other scalar
223
            $assetId = $row['asset_id'];     // binary(16) UUID
224
            $resourceNodeId = $row['resource_node_id']; // int|null
225
226
            $rowIdForLog = $this->formatIdForLog($rowId);
227
            $attemptIdForLog = $this->formatIdForLog($attemptId);
228
            $assetIdForLog = $this->formatIdForLog($assetId);
229
230
            if (0 === $processed % 200) {
231
                error_log(
232
                    sprintf(
233
                        '[MIGRATION][attempt_feedback] Progress: processed=%d, migrated=%d, cleanedOnly=%d (last rowId=%s)',
234
                        $processed,
235
                        $migrated,
236
                        $cleanedOnly,
237
                        $rowIdForLog
238
                    )
239
                );
240
            }
241
242
            try {
243
                $this->ensureEntityManagerIsOpen();
244
245
                /** @var Asset|null $asset */
246
                $asset = $assetRepo->find($assetId);
247
248
                if (null === $asset) {
249
                    // Asset record is gone, just clear the asset_id reference.
250
                    $this->connection->update(
251
                        'attempt_feedback',
252
                        ['asset_id' => null],
253
                        ['id' => $rowId]
254
                    );
255
256
                    error_log(
257
                        sprintf(
258
                            '[MIGRATION][attempt_feedback] Cleared asset_id for attempt_feedback.id=%s (attemptId=%s assetId=%s) because Asset is missing.',
259
                            $rowIdForLog,
260
                            $attemptIdForLog,
261
                            $assetIdForLog
262
                        )
263
                    );
264
265
                    continue;
266
                }
267
268
                if (null === $resourceNodeId) {
269
                    $ok = $this->migrateSingleAssetToResourceNode(
270
                        $assetRepo,
271
                        'attempt_feedback',
272
                        'attempt_feedback',
273
                        $rowId,
274
                        $asset,
275
                        $attemptFeedbackType
276
                    );
277
278
                    if ($ok) {
279
                        $migrated++;
280
                    }
281
                } else {
282
                    $this->clearAssetReference(
283
                        $assetRepo,
284
                        'attempt_feedback',
285
                        'attempt_feedback',
286
                        $rowId,
287
                        $rowIdForLog,
288
                        $asset
289
                    );
290
                    $cleanedOnly++;
291
                }
292
            } catch (Throwable $e) {
293
                error_log(
294
                    sprintf(
295
                        '[MIGRATION][attempt_feedback] Error for attempt_feedback.id=%s (attemptId=%s): %s',
296
                        $rowIdForLog,
297
                        $attemptIdForLog,
298
                        $e->getMessage()
299
                    )
300
                );
301
302
                $this->ensureEntityManagerIsOpen();
303
            }
304
305
            if (0 === $processed % 50) {
306
                $this->entityManager->clear(ResourceNode::class);
307
                $this->entityManager->clear(ResourceFile::class);
308
                $this->entityManager->clear(Asset::class);
309
            }
310
        }
311
312
        error_log(
313
            sprintf(
314
                '[MIGRATION][attempt_feedback] Finished. Processed=%d, Migrated=%d, CleanedOnly=%d',
315
                $processed,
316
                $migrated,
317
                $cleanedOnly
318
            )
319
        );
320
    }
321
322
    /**
323
     * Shared logic for attempt_file and attempt_feedback:
324
     *  - Create ResourceNode with the given ResourceType.
325
     *  - Create ResourceFile with setFile() (Vich) using an UploadedFile from the Asset.
326
     *  - Update resource_node_id AND clear asset_id in the corresponding table.
327
     *  - If the Asset has no more references, delete it (DB + filesystem).
328
     *
329
     * @return bool true if the migration succeeded, false if it was skipped.
330
     */
331
    private function migrateSingleAssetToResourceNode(
332
        AssetRepository $assetRepo,
333
        string $context,
334
        string $tableName,
335
        string $rowId,
336
        Asset $asset,
337
        ResourceType $resourceType
338
    ): bool {
339
        $rowIdForLog = $this->formatIdForLog($rowId);
340
341
        if (self::DRY_RUN) {
342
            error_log(
343
                sprintf(
344
                    '[MIGRATION][DRY_RUN][%s] Would create ResourceNode/ResourceFile for table=%s rowId=%s (Asset id=%s)',
345
                    $context,
346
                    $tableName,
347
                    $rowIdForLog,
348
                    (string) $asset->getId()
349
                )
350
            );
351
352
            return true;
353
        }
354
355
        // 1) Build an UploadedFile from the existing Asset file using streams.
356
        $uploadedFile = $this->createUploadedFileFromAsset($assetRepo, $asset, $context);
357
358
        if (null === $uploadedFile) {
359
            error_log(
360
                sprintf(
361
                    '[MIGRATION][%s] Skipping rowId=%s because UploadedFile could not be created (Asset id=%s).',
362
                    $context,
363
                    $rowIdForLog,
364
                    (string) $asset->getId()
365
                )
366
            );
367
368
            return false;
369
        }
370
371
        try {
372
            // 2) Create ResourceNode
373
            $this->ensureEntityManagerIsOpen();
374
375
            $node = new ResourceNode();
376
            $node->setTitle($asset->getTitle() ?: 'attempt_' . $context);
377
            $node->setResourceType($resourceType);
378
            $this->entityManager->persist($node);
379
380
            // 3) Create ResourceFile with Vich file
381
            $resourceFile = new ResourceFile();
382
            $resourceFile->setResourceNode($node);
383
            $resourceFile->setFile($uploadedFile);
384
            $this->entityManager->persist($resourceFile);
385
386
            $this->entityManager->flush();
387
388
            // 4) Update the link table: set resource_node_id AND clear asset_id
389
            $this->connection->update(
390
                $tableName,
391
                [
392
                    'resource_node_id' => $node->getId(),
393
                    'asset_id' => null,
394
                ],
395
                ['id' => $rowId]
396
            );
397
398
            // 5) Clean up the temporary file if it still exists
399
            $realPath = $uploadedFile->getRealPath();
400
            if ($realPath && is_file($realPath)) {
401
                @unlink($realPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

401
                /** @scrutinizer ignore-unhandled */ @unlink($realPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
402
            }
403
404
            // 6) Delete Asset if there are no more references in attempt_file/attempt_feedback
405
            $this->cleanupAssetIfUnused($assetRepo, $asset);
406
407
            error_log(
408
                sprintf(
409
                    '[MIGRATION][%s] Migrated rowId=%s to ResourceNode id=%d and cleared asset reference (Asset id=%s)',
410
                    $context,
411
                    $rowIdForLog,
412
                    (int) $node->getId(),
413
                    (string) $asset->getId()
414
                )
415
            );
416
417
            return true;
418
        } catch (Throwable $e) {
419
            error_log(
420
                sprintf(
421
                    '[MIGRATION][%s] Failed to migrate rowId=%s (Asset id=%s): %s',
422
                    $context,
423
                    $rowIdForLog,
424
                    (string) $asset->getId(),
425
                    $e->getMessage()
426
                )
427
            );
428
429
            $this->ensureEntityManagerIsOpen();
430
431
            $realPath = $uploadedFile->getRealPath();
432
            if ($realPath && is_file($realPath)) {
433
                @unlink($realPath);
434
            }
435
436
            return false;
437
        }
438
    }
439
440
    /**
441
     * Clear asset_id for a single row and delete the Asset if there are no remaining references.
442
     */
443
    private function clearAssetReference(
444
        AssetRepository $assetRepo,
445
        string $context,
446
        string $tableName,
447
        string $rowId,
448
        string $rowIdForLog,
449
        Asset $asset
450
    ): void {
451
        // Clear asset_id for this row
452
        $this->connection->update(
453
            $tableName,
454
            ['asset_id' => null],
455
            ['id' => $rowId]
456
        );
457
458
        // Try to delete the Asset if it has no more references
459
        $this->cleanupAssetIfUnused($assetRepo, $asset);
460
461
        error_log(
462
            sprintf(
463
                '[MIGRATION][%s] Cleared asset_id for %s.id=%s (Asset id=%s, maybe deleted if unused).',
464
                $context,
465
                $tableName,
466
                $rowIdForLog,
467
                (string) $asset->getId()
468
            )
469
        );
470
    }
471
472
    /**
473
     * Delete the Asset (DB + filesystem) if it is no longer referenced
474
     * by attempt_file or attempt_feedback.
475
     */
476
    private function cleanupAssetIfUnused(AssetRepository $assetRepo, Asset $asset): void
477
    {
478
        $assetId = $asset->getId();
479
480
        $refs = (int) $this->connection->fetchOne(
481
            'SELECT
482
                 (SELECT COUNT(*) FROM attempt_file WHERE asset_id = :asset_id) +
483
                 (SELECT COUNT(*) FROM attempt_feedback WHERE asset_id = :asset_id) AS total_refs',
484
            ['asset_id' => $assetId]
485
        );
486
487
        if ($refs > 0) {
488
            // Still referenced somewhere, do not delete.
489
            return;
490
        }
491
492
        // No more references: delete Asset from DB and filesystem
493
        $assetRepo->delete($asset);
494
495
        error_log(
496
            sprintf(
497
                '[MIGRATION][asset] Deleted Asset id=%s because it has no remaining references.',
498
                $this->formatIdForLog($assetId)
499
            )
500
        );
501
    }
502
503
    /**
504
     * Cleanup step after migration:
505
     * Ensure that there are no resource_node_id values without a ResourceFile
506
     * in attempt_file / attempt_feedback, and delete orphan ResourceNode entries.
507
     */
508
    private function cleanupOrphanNodesWithoutFile(): void
509
    {
510
        error_log('[MIGRATION][cleanup] Starting orphan ResourceNode cleanup for attempts.');
511
512
        // 1) attempt_file: nodes without ResourceFile
513
        $nodeIds = $this->connection->fetchFirstColumn(
514
            'SELECT DISTINCT af.resource_node_id
515
             FROM attempt_file af
516
             LEFT JOIN resource_file rf ON rf.resource_node_id = af.resource_node_id
517
             WHERE af.resource_node_id IS NOT NULL
518
               AND rf.id IS NULL'
519
        );
520
521
        if (!empty($nodeIds)) {
522
            // Remove resource_node_id from attempt_file
523
            $this->connection->executeStatement(
524
                'UPDATE attempt_file
525
                 SET resource_node_id = NULL
526
                 WHERE resource_node_id IN (?)',
527
                [$nodeIds],
528
                [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
529
            );
530
531
            // Delete orphan ResourceNode entries that do not have any ResourceFile
532
            $this->connection->executeStatement(
533
                'DELETE rn
534
                 FROM resource_node rn
535
                 LEFT JOIN resource_file rf ON rf.resource_node_id = rn.id
536
                 WHERE rn.id IN (?)
537
                   AND rf.id IS NULL',
538
                [$nodeIds],
539
                [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
540
            );
541
542
            error_log(
543
                sprintf(
544
                    '[MIGRATION][cleanup] Cleaned orphan attempt_file nodes without ResourceFile. Count=%d',
545
                    \count($nodeIds)
546
                )
547
            );
548
        } else {
549
            error_log('[MIGRATION][cleanup] No orphan attempt_file nodes without ResourceFile found.');
550
        }
551
552
        // 2) attempt_feedback: nodes without ResourceFile
553
        $fbNodeIds = $this->connection->fetchFirstColumn(
554
            'SELECT DISTINCT af.resource_node_id
555
             FROM attempt_feedback af
556
             LEFT JOIN resource_file rf ON rf.resource_node_id = af.resource_node_id
557
             WHERE af.resource_node_id IS NOT NULL
558
               AND rf.id IS NULL'
559
        );
560
561
        if (!empty($fbNodeIds)) {
562
            $this->connection->executeStatement(
563
                'UPDATE attempt_feedback
564
                 SET resource_node_id = NULL
565
                 WHERE resource_node_id IN (?)',
566
                [$fbNodeIds],
567
                [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
568
            );
569
570
            $this->connection->executeStatement(
571
                'DELETE rn
572
                 FROM resource_node rn
573
                 LEFT JOIN resource_file rf ON rf.resource_node_id = rn.id
574
                 WHERE rn.id IN (?)
575
                   AND rf.id IS NULL',
576
                [$fbNodeIds],
577
                [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
578
            );
579
580
            error_log(
581
                sprintf(
582
                    '[MIGRATION][cleanup] Cleaned orphan attempt_feedback nodes without ResourceFile. Count=%d',
583
                    \count($fbNodeIds)
584
                )
585
            );
586
        } else {
587
            error_log('[MIGRATION][cleanup] No orphan attempt_feedback nodes without ResourceFile found.');
588
        }
589
    }
590
591
    /**
592
     * Create an UploadedFile from an Asset using Flysystem streams.
593
     * This avoids loading the whole file content as a single string in memory.
594
     */
595
    private function createUploadedFileFromAsset(
596
        AssetRepository $assetRepo,
597
        Asset $asset,
598
        string $context
599
    ): ?UploadedFile {
600
        $filePath = '';
601
        $stream = null;
602
        $tmpPath = false;
603
604
        try {
605
            $filePath = $assetRepo->getStorage()->resolveUri($asset);
606
            $fs = $assetRepo->getFileSystem();
607
608
            if (!$fs->fileExists($filePath)) {
609
                error_log(
610
                    sprintf(
611
                        '[MIGRATION][%s] File does not exist in filesystem for Asset id=%s, path=%s',
612
                        $context,
613
                        (string) $asset->getId(),
614
                        $filePath
615
                    )
616
                );
617
618
                return null;
619
            }
620
621
            $stream = $fs->readStream($filePath);
622
            if (false === $stream) {
623
                error_log(
624
                    sprintf(
625
                        '[MIGRATION][%s] Could not open read stream for Asset id=%s, path=%s',
626
                        $context,
627
                        (string) $asset->getId(),
628
                        $filePath
629
                    )
630
                );
631
632
                return null;
633
            }
634
635
            $tmpPath = tempnam(sys_get_temp_dir(), 'asset_migrate_');
636
            if (false === $tmpPath) {
637
                error_log(
638
                    sprintf(
639
                        '[MIGRATION][%s] Failed to create temporary file for Asset id=%s',
640
                        $context,
641
                        (string) $asset->getId()
642
                    )
643
                );
644
645
                fclose($stream);
646
647
                return null;
648
            }
649
650
            $tmpHandle = fopen($tmpPath, 'wb');
651
            if (false === $tmpHandle) {
652
                error_log(
653
                    sprintf(
654
                        '[MIGRATION][%s] Failed to open temporary file for writing: %s (Asset id=%s)',
655
                        $context,
656
                        $tmpPath,
657
                        (string) $asset->getId()
658
                    )
659
                );
660
661
                fclose($stream);
662
                @unlink($tmpPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

662
                /** @scrutinizer ignore-unhandled */ @unlink($tmpPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
663
664
                return null;
665
            }
666
667
            stream_copy_to_stream($stream, $tmpHandle);
668
            fclose($stream);
669
            fclose($tmpHandle);
670
671
            $originalName = $asset->getTitle() ?: basename($filePath);
672
673
            $mimeType = 'application/octet-stream';
674
            if (method_exists($asset, 'getMimeType') && null !== $asset->getMimeType()) {
675
                $mimeType = (string) $asset->getMimeType();
676
            }
677
678
            // last argument "true" => test mode, do not check HTTP upload.
679
            return new UploadedFile($tmpPath, $originalName, $mimeType, null, true);
680
        } catch (Throwable $e) {
681
            error_log(
682
                sprintf(
683
                    '[MIGRATION][%s] Exception while creating UploadedFile for Asset id=%s, path=%s: %s',
684
                    $context,
685
                    (string) $asset->getId(),
686
                    $filePath,
687
                    $e->getMessage()
688
                )
689
            );
690
691
            if (is_resource($stream)) {
692
                fclose($stream);
693
            }
694
695
            if (false !== $tmpPath && is_file($tmpPath)) {
696
                @unlink($tmpPath);
697
            }
698
699
            return null;
700
        }
701
    }
702
703
    /**
704
     * Always get an EntityManager that is open and in sync with Doctrine.
705
     * This is important if a previous flush closed the EM.
706
     */
707
    private function ensureEntityManagerIsOpen(): void
708
    {
709
        /** @var ManagerRegistry $doctrine */
710
        $doctrine = $this->container->get('doctrine');
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

710
        /** @scrutinizer ignore-call */ 
711
        $doctrine = $this->container->get('doctrine');

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...
711
712
        $em = $doctrine->getManager();
713
714
        if (!$em->isOpen()) {
715
            $doctrine->resetManager();
716
            $em = $doctrine->getManager();
717
718
            error_log('[MIGRATION] EntityManager was closed and has been reset.');
719
        }
720
721
        $this->entityManager = $em;
722
    }
723
724
    /**
725
     * Always get an AssetRepository bound to the current EntityManager.
726
     */
727
    private function getAssetRepository(): AssetRepository
728
    {
729
        /** @var AssetRepository $assetRepo */
730
        $assetRepo = $this->container->get(AssetRepository::class);
731
732
        return $assetRepo;
733
    }
734
735
    /**
736
     * Format any kind of primary key / UUID value for logging.
737
     * - If it is a binary string, log hex.
738
     * - If it is an int, log the numeric value.
739
     * - If it is null, log "NULL".
740
     */
741
    private function formatIdForLog(mixed $value): string
742
    {
743
        if (null === $value) {
744
            return 'NULL';
745
        }
746
747
        if (\is_string($value)) {
748
            return bin2hex($value);
749
        }
750
751
        return (string) $value;
752
    }
753
}
754