Completed
Push — master ( 0be8e1...585ca2 )
by Daniel
59:26 queued 46:02
created

FileUploader::upload()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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