UploadableFileManager   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 118
dl 0
loc 219
ccs 0
cts 125
cp 0
rs 9.52
c 4
b 0
f 1
wmc 36

11 Methods

Rating   Name   Duplication   Size   Complexity  
A addDeletedField() 0 3 1
A __construct() 0 10 1
A setUploadedFilesFromFileBag() 0 11 3
A processClonedUploadable() 0 20 5
A storeFilesMetadata() 0 16 6
A deleteFiles() 0 7 2
A copyFilepath() 0 22 4
A removeFilepath() 0 12 3
A deleteFileForField() 0 5 2
A getFileResponse() 0 35 5
A persistFiles() 0 37 4
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components 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\ApiComponentsBundle\Helper\Uploadable;
15
16
use Doctrine\Common\Collections\ArrayCollection;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\Persistence\ManagerRegistry;
19
use Liip\ImagineBundle\Service\FilterService;
20
use Silverback\ApiComponentsBundle\Annotation\UploadableField;
21
use Silverback\ApiComponentsBundle\AttributeReader\UploadableAttributeReader;
22
use Silverback\ApiComponentsBundle\Entity\Utility\ImagineFiltersInterface;
23
use Silverback\ApiComponentsBundle\Flysystem\FilesystemProvider;
24
use Silverback\ApiComponentsBundle\Imagine\CacheManager;
25
use Silverback\ApiComponentsBundle\Imagine\FlysystemDataLoader;
26
use Silverback\ApiComponentsBundle\Model\Uploadable\UploadedDataUriFile;
27
use Silverback\ApiComponentsBundle\Utility\ClassMetadataTrait;
28
use Symfony\Component\HttpFoundation\File\File;
29
use Symfony\Component\HttpFoundation\FileBag;
30
use Symfony\Component\HttpFoundation\HeaderUtils;
31
use Symfony\Component\HttpFoundation\Response;
32
use Symfony\Component\HttpFoundation\StreamedResponse;
33
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
34
use Symfony\Component\PropertyAccess\PropertyAccess;
35
36
/**
37
 * @author Daniel West <[email protected]>
38
 */
39
class UploadableFileManager
40
{
41
    use ClassMetadataTrait;
42
43
    private UploadableAttributeReader $annotationReader;
44
    private FilesystemProvider $filesystemProvider;
45
    private FlysystemDataLoader $flysystemDataLoader;
46
    private FileInfoCacheManager $fileInfoCacheManager;
47
    private ?CacheManager $imagineCacheManager;
48
    private ?FilterService $filterService;
49
    private ArrayCollection $deletedFields;
50
51
    public function __construct(ManagerRegistry $registry, UploadableAttributeReader $annotationReader, FilesystemProvider $filesystemProvider, FlysystemDataLoader $flysystemDataLoader, FileInfoCacheManager $fileInfoCacheManager, ?CacheManager $imagineCacheManager, ?FilterService $filterService = null)
52
    {
53
        $this->initRegistry($registry);
54
        $this->annotationReader = $annotationReader;
55
        $this->filesystemProvider = $filesystemProvider;
56
        $this->flysystemDataLoader = $flysystemDataLoader;
57
        $this->fileInfoCacheManager = $fileInfoCacheManager;
58
        $this->imagineCacheManager = $imagineCacheManager;
59
        $this->filterService = $filterService;
60
        $this->deletedFields = new ArrayCollection();
61
    }
62
63
    public function addDeletedField($field): void
64
    {
65
        $this->deletedFields->add($field);
66
    }
67
68
    public function processClonedUploadable(object $oldObject, object $newObject): object
69
    {
70
        if (!$this->annotationReader->isConfigured($oldObject)) {
71
            throw new \InvalidArgumentException('The old object is not configured as uploadable');
72
        }
73
74
        if ($oldObject::class !== $newObject::class) {
75
            throw new \InvalidArgumentException('The objects must be the same class');
76
        }
77
78
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
79
        $configuredProperties = $this->annotationReader->getConfiguredProperties($oldObject, false);
80
        foreach ($configuredProperties as $fieldConfiguration) {
81
            if ($propertyAccessor->getValue($oldObject, $fieldConfiguration->property)) {
82
                $newPath = $this->copyFilepath($oldObject, $fieldConfiguration);
83
                $propertyAccessor->setValue($newObject, $fieldConfiguration->property, $newPath);
84
            }
85
        }
86
87
        return $newObject;
88
    }
89
90
    public function setUploadedFilesFromFileBag(object $object, FileBag $fileBag): void
91
    {
92
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
93
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, false);
94
95
        /**
96
         * @var UploadableField[] $configuredProperties
97
         */
98
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
99
            if ($file = $fileBag->get($fileProperty)) {
100
                $propertyAccessor->setValue($object, $fileProperty, $file);
101
            }
102
        }
103
    }
104
105
    public function storeFilesMetadata(object $object): void
106
    {
107
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, true);
108
        $classMetadata = $this->getClassMetadata($object);
109
110
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
111
            // Let the data loader which should be configured for imagine to know which adapter to use
112
            $this->flysystemDataLoader->setAdapter($fieldConfiguration->adapter);
113
114
            $filename = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
115
            if ($filename && $object instanceof ImagineFiltersInterface && $this->filterService) {
116
                $filters = $object->getImagineFilters($fileProperty, null);
117
                foreach ($filters as $filter) {
118
                    // This will trigger the cached file to be store
119
                    // When cached files are store we save the file info
120
                    $this->filterService->getUrlOfFilteredImage($filename, $filter);
121
                }
122
            }
123
        }
124
    }
125
126
    public function persistFiles(object $object): void
127
    {
128
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
129
        $classMetadata = $this->getClassMetadata($object);
130
131
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, true);
132
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
133
            // this is null if null is submitted as the value... also null if not submitted
134
            /** @var File|UploadedDataUriFile|null $file */
135
            $file = $propertyAccessor->getValue($object, $fileProperty);
136
            if (!$file) {
137
                // so we need to know if it was a deleted field from the denormalizer
138
                if ($this->deletedFields->contains($fieldConfiguration->property)) {
139
                    $this->deleteFileForField($object, $classMetadata, $fieldConfiguration);
140
                    $classMetadata->setFieldValue($object, $fieldConfiguration->property, null);
141
                }
142
                continue;
143
            }
144
145
            $this->deleteFileForField($object, $classMetadata, $fieldConfiguration);
146
            $filesystem = $this->filesystemProvider->getFilesystem($fieldConfiguration->adapter);
147
148
            $path = $fieldConfiguration->prefix ?? '';
149
            $path .= $file->getFilename();
150
            $stream = fopen($file->getRealPath(), 'r');
151
            $filesystem->writeStream(
152
                $path,
153
                $stream,
154
                [
155
                    'mimetype' => $file->getMimeType(),
156
                    'metadata' => [
157
                        'contentType' => $file->getMimeType(),
158
                    ],
159
                ]
160
            );
161
            $classMetadata->setFieldValue($object, $fieldConfiguration->property, $path);
162
            $propertyAccessor->setValue($object, $fileProperty, null);
163
        }
164
    }
165
166
    public function deleteFiles(object $object): void
167
    {
168
        $classMetadata = $this->getClassMetadata($object);
169
170
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, true);
171
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
172
            $this->deleteFileForField($object, $classMetadata, $fieldConfiguration);
173
        }
174
    }
175
176
    private function deleteFileForField(object $object, ClassMetadata $classMetadata, UploadableField $fieldConfiguration): void
177
    {
178
        $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
179
        if ($currentFilepath) {
180
            $this->removeFilepath($object, $fieldConfiguration);
181
        }
182
    }
183
184
    public function getFileResponse(object $object, string $property, bool $forceDownload = false): Response
185
    {
186
        try {
187
            $reflectionProperty = new \ReflectionProperty($object, $property);
188
        } catch (\ReflectionException $exception) {
189
            throw new NotFoundHttpException($exception->getMessage());
190
        }
191
        if (!$this->annotationReader->isFieldConfigured($reflectionProperty)) {
192
            throw new NotFoundHttpException(sprintf('field configuration not found for %s', $property));
193
        }
194
195
        $propertyConfiguration = $this->annotationReader->getPropertyConfiguration($reflectionProperty);
196
197
        $filesystem = $this->filesystemProvider->getFilesystem($propertyConfiguration->adapter);
198
199
        $classMetadata = $this->getClassMetadata($object);
200
201
        $filePath = $classMetadata->getFieldValue($object, $propertyConfiguration->property);
202
        if (empty($filePath)) {
203
            return new Response('The file path for this resource is empty', Response::HTTP_NOT_FOUND);
204
        }
205
        $response = new StreamedResponse();
206
        $response->setCallback(
207
            static function () use ($filesystem, $filePath) {
208
                $outputStream = fopen('php://output', 'w');
209
                $fileStream = $filesystem->readStream($filePath);
210
                stream_copy_to_stream($fileStream, $outputStream);
211
            }
212
        );
213
        $response->headers->set('Content-Type', $filesystem->mimeType($filePath));
214
215
        $disposition = HeaderUtils::makeDisposition($forceDownload ? HeaderUtils::DISPOSITION_ATTACHMENT : HeaderUtils::DISPOSITION_INLINE, $filePath);
216
        $response->headers->set('Content-Disposition', $disposition);
217
218
        return $response;
219
    }
220
221
    private function removeFilepath(object $object, UploadableField $fieldConfiguration): void
222
    {
223
        $classMetadata = $this->getClassMetadata($object);
224
225
        $filesystem = $this->filesystemProvider->getFilesystem($fieldConfiguration->adapter);
226
        $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
227
        $this->fileInfoCacheManager->deleteCaches([$currentFilepath], [null]);
228
        if ($this->imagineCacheManager) {
229
            $this->imagineCacheManager->remove([$currentFilepath], null);
230
        }
231
        if ($filesystem->fileExists($currentFilepath)) {
232
            $filesystem->delete($currentFilepath);
233
        }
234
    }
235
236
    private function copyFilepath(object $object, UploadableField $fieldConfiguration): ?string
237
    {
238
        $classMetadata = $this->getClassMetadata($object);
239
240
        $filesystem = $this->filesystemProvider->getFilesystem($fieldConfiguration->adapter);
241
        $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
242
        if (!$filesystem->fileExists($currentFilepath)) {
243
            return null;
244
        }
245
        $pathInfo = pathinfo($currentFilepath);
246
        $basename = $pathInfo['filename'];
247
        $extension = $pathInfo['extension'] ?? null;
248
        if (!empty($extension)) {
249
            $extension = sprintf('.%s', $extension);
250
        }
251
        $num = 1;
252
        while ($filesystem->fileExists($newFilepath = sprintf('%s_%d%s', $basename, $num, $extension))) {
253
            ++$num;
254
        }
255
        $filesystem->copy($currentFilepath, $newFilepath);
256
257
        return $newFilepath;
258
    }
259
}
260