Completed
Push — v2 ( 02db1b...ed3360 )
by Daniel
04:31
created

FileUploader::getNewFilename()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 14
ccs 0
cts 10
cp 0
crap 6
rs 9.9666
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Component Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentBundle\File;
15
16
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
17
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18
use ApiPlatform\Core\Validator\ValidatorInterface as ApiValidator;
19
use Doctrine\ORM\EntityManagerInterface;
20
use RuntimeException;
21
use Silverback\ApiComponentBundle\Entity\Utility\FileInterface;
22
use Symfony\Component\Filesystem\Filesystem;
23
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
24
use Symfony\Component\HttpFoundation\File\File;
25
use Symfony\Component\HttpFoundation\File\UploadedFile;
26
use Symfony\Component\PropertyAccess\PropertyAccess;
27
use Symfony\Component\PropertyAccess\PropertyAccessor;
28
use Symfony\Component\Validator\ConstraintViolation;
29
use Symfony\Component\Validator\ConstraintViolationList;
30
use Symfony\Component\Validator\Validator\ValidatorInterface;
31
32
class FileUploader
33
{
34
    private EntityManagerInterface $em;
35
    private ResourceMetadataFactoryInterface $resourceMetadataFactory;
36
    private ValidatorInterface $validator;
37
    private ApiValidator $apiValidator;
38
    private PropertyAccessor $propertyAccessor;
39
    private array $rootPath;
40
41
    public function __construct(
42
        EntityManagerInterface $em,
43
        ResourceMetadataFactoryInterface $resourceMetadataFactory,
44
        ValidatorInterface $validator,
45
        ApiValidator $apiValidator,
46
        array $rootPaths = []
47
    ) {
48
        $this->em = $em;
49
        $this->resourceMetadataFactory = $resourceMetadataFactory;
50
        $this->validator = $validator;
51
        $this->apiValidator = $apiValidator;
52
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
53
        $this->rootPath = $rootPaths['uploads'];
54
    }
55
56
    private function getRealPath(string $moveToDir, string $filename): string
57
    {
58
        return rtrim($moveToDir, '/') . '/' . $filename;
59
    }
60
61
    private function getNewFilename(string $moveToDir, UploadedFile $file): string
62
    {
63
        $fs = new Filesystem();
64
65
        $ext = $file->guessExtension();
66
        $basename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
67
        $filename = "$basename.$ext";
68
        $i = 0;
69
        while ($fs->exists($this->getRealPath($moveToDir, $filename))) {
70
            ++$i;
71
            $filename = "$basename.$i.$ext";
72
        }
73
74
        return $filename;
75
    }
76
77
    private function validateNewFile($entity, $field, UploadedFile $file, array $validationGroups): void
78
    {
79
        $errors = $this->validator->validatePropertyValue($entity, $field, $file->getRealPath(), $validationGroups);
80
        if (null !== $errors && \count($errors)) {
81
            throw new ValidationException($errors);
82
        }
83
    }
84
85
    private function unlinkFile(File $currentFile): void
86
    {
87
        $oldFilePath = $currentFile->getRealPath();
88
        if (!is_writable($oldFilePath)) {
89
            throw new RuntimeException('The existing file cannot be deleted. File upload aborted');
90
        }
91
        unlink($currentFile->getRealPath());
92
    }
93
94
    public function upload(FileInterface $entity, string $field, UploadedFile $file, string $itemOperationName = 'post'): FileInterface
95
    {
96
        if ('' === $file->getFilename()) {
97
            $template = 'The file was not uploaded. It is likely that the file size was larger than %s';
98
            throw new ValidationException(new ConstraintViolationList([new ConstraintViolation(sprintf($template, ini_get('upload_max_filesize')), $template, [ini_get('upload_max_filesize')], $file, 'filename', $file->getFilename())]));
99
        }
100
        $resourceMetadata = $this->resourceMetadataFactory->create(\get_class($entity));
101
        $validationGroups = $resourceMetadata->getOperationAttribute(
102
            ['item_operation_name' => $itemOperationName],
103
            'validation_groups',
104
            [],
105
            true
106
        );
107
108
        /** @var File|string|null $currentFile */
109
        $currentFile = $this->propertyAccessor->getValue($entity, $field);
110
111
        // Set to the new file and validate it before we upload and persist any changes
112
        $this->validateNewFile($entity, $field, $file, $validationGroups);
113
114
        // Validation passed, remove old file first (in case we don't have permission to do it)
115
        if ($currentFile) {
116
            try {
117
                $this->unlinkFile(new File($currentFile));
118
            } catch (FileNotFoundException $e) {
119
                // If the file did not exist, there's no problem if it was not found as we are trying to delete it anyway
120
            }
121
        }
122
123
        // Old file removed, let's update!
124
        $moveToDir = sprintf('%s/%s', $this->rootPath, $entity->getDirectory());
0 ignored issues
show
Bug introduced by
$this->rootPath of type array is incompatible with the type string expected by parameter $args of sprintf(). ( Ignorable by Annotation )

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

124
        $moveToDir = sprintf('%s/%s', /** @scrutinizer ignore-type */ $this->rootPath, $entity->getDirectory());
Loading history...
125
        $filename = $this->getNewFilename($moveToDir, $file);
126
        $movedFile = $file->move($moveToDir, $filename);
127
        $this->propertyAccessor->setValue($entity, $field, $movedFile->getRealPath());
128
        $this->apiValidator->validate($entity, ['groups' => $validationGroups]);
129
        $this->em->persist($entity);
130
        $this->em->flush();
131
132
        return $entity;
133
    }
134
}
135