Passed
Push — master ( b12002...2bc8e2 )
by Daniel
05:39
created

UploadableFileManager::processClonedUploadable()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 11
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 20
ccs 0
cts 12
cp 0
crap 30
rs 9.6111
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\Persistence\ManagerRegistry;
18
use Liip\ImagineBundle\Service\FilterService;
19
use Silverback\ApiComponentsBundle\Annotation\UploadableField;
20
use Silverback\ApiComponentsBundle\AnnotationReader\UploadableAnnotationReader;
21
use Silverback\ApiComponentsBundle\Entity\Utility\ImagineFiltersInterface;
22
use Silverback\ApiComponentsBundle\Flysystem\FilesystemProvider;
23
use Silverback\ApiComponentsBundle\Imagine\CacheManager;
24
use Silverback\ApiComponentsBundle\Imagine\FlysystemDataLoader;
25
use Silverback\ApiComponentsBundle\Model\Uploadable\UploadedDataUriFile;
26
use Silverback\ApiComponentsBundle\Utility\ClassMetadataTrait;
27
use Symfony\Component\HttpFoundation\File\File;
28
use Symfony\Component\HttpFoundation\FileBag;
29
use Symfony\Component\HttpFoundation\HeaderUtils;
30
use Symfony\Component\HttpFoundation\Response;
31
use Symfony\Component\HttpFoundation\StreamedResponse;
32
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
33
use Symfony\Component\PropertyAccess\PropertyAccess;
34
35
/**
36
 * @author Daniel West <[email protected]>
37
 */
38
class UploadableFileManager
39
{
40
    use ClassMetadataTrait;
41
42
    private UploadableAnnotationReader $annotationReader;
43
    private FilesystemProvider $filesystemProvider;
44
    private FlysystemDataLoader $flysystemDataLoader;
45
    private FileInfoCacheManager $fileInfoCacheManager;
46
    private ?CacheManager $imagineCacheManager;
47
    private ?FilterService $filterService;
48
    private ArrayCollection $deletedFields;
49
50
    public function __construct(ManagerRegistry $registry, UploadableAnnotationReader $annotationReader, FilesystemProvider $filesystemProvider, FlysystemDataLoader $flysystemDataLoader, FileInfoCacheManager $fileInfoCacheManager, ?CacheManager $imagineCacheManager, ?FilterService $filterService = null)
51
    {
52
        $this->initRegistry($registry);
53
        $this->annotationReader = $annotationReader;
54
        $this->filesystemProvider = $filesystemProvider;
55
        $this->flysystemDataLoader = $flysystemDataLoader;
56
        $this->fileInfoCacheManager = $fileInfoCacheManager;
57
        $this->imagineCacheManager = $imagineCacheManager;
58
        $this->filterService = $filterService;
59
        $this->deletedFields = new ArrayCollection();
60
    }
61
62
    public function addDeletedField($field): void
63
    {
64
        $this->deletedFields->add($field);
65
    }
66
67
    public function processClonedUploadable(object $oldObject, object $newObject): object
68
    {
69
        if (!$this->annotationReader->isConfigured($oldObject)) {
70
            throw new \InvalidArgumentException('The old object is not configured as uploadable');
71
        }
72
73
        if (\get_class($oldObject) !== \get_class($newObject)) {
74
            throw new \InvalidArgumentException('The objects must be the same class');
75
        }
76
77
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
78
        $configuredProperties = $this->annotationReader->getConfiguredProperties($oldObject, false);
79
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
80
            if ($propertyAccessor->getValue($oldObject, $fieldConfiguration->property)) {
81
                $newPath = $this->copyFilepath($oldObject, $fieldConfiguration);
82
                $propertyAccessor->setValue($newObject, $fieldConfiguration->property, $newPath);
83
            }
84
        }
85
86
        return $newObject;
87
    }
88
89
    public function setUploadedFilesFromFileBag(object $object, FileBag $fileBag): void
90
    {
91
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
92
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, false);
93
94
        /**
95
         * @var UploadableField[] $configuredProperties
96
         */
97
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
98
            if ($file = $fileBag->get($fileProperty)) {
99
                $propertyAccessor->setValue($object, $fileProperty, $file);
100
            }
101
        }
102
    }
103
104
    public function storeFilesMetadata(object $object): void
105
    {
106
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, true);
107
        $classMetadata = $this->getClassMetadata($object);
108
109
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
110
            // Let the data loader which should be configured for imagine to know which adapter to use
111
            $this->flysystemDataLoader->setAdapter($fieldConfiguration->adapter);
112
113
            $filename = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
114
            if ($filename && $object instanceof ImagineFiltersInterface && $this->filterService) {
115
                $filters = $object->getImagineFilters($fileProperty, null);
116
                foreach ($filters as $filter) {
117
                    // This will trigger the cached file to be store
118
                    // When cached files are store we save the file info
119
                    $this->filterService->getUrlOfFilteredImage($filename, $filter);
120
                }
121
            }
122
        }
123
    }
124
125
    public function persistFiles(object $object): void
126
    {
127
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
128
        $classMetadata = $this->getClassMetadata($object);
129
130
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, true);
131
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
132
            // this is null if null is submitted as the value and null if not submitted
133
            /** @var File|UploadedDataUriFile|null $file */
134
            $file = $propertyAccessor->getValue($object, $fileProperty);
135
            if (!$file) {
136
                if ($this->deletedFields->contains($fieldConfiguration->property)) {
137
                    // this will not have been updated yet, original database value - string file path
138
                    $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
139
                    if ($currentFilepath) {
140
                        $this->removeFilepath($object, $fieldConfiguration);
141
                        // file path set to null
142
                        $classMetadata->setFieldValue($object, $fieldConfiguration->property, null);
143
                    }
144
                }
145
                continue;
146
            }
147
148
            $filesystem = $this->filesystemProvider->getFilesystem($fieldConfiguration->adapter);
0 ignored issues
show
Bug introduced by
It seems like $fieldConfiguration->adapter can also be of type null; however, parameter $name of Silverback\ApiComponents...ovider::getFilesystem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

148
            $filesystem = $this->filesystemProvider->getFilesystem(/** @scrutinizer ignore-type */ $fieldConfiguration->adapter);
Loading history...
149
150
            $path = $fieldConfiguration->prefix ?? '';
151
            $path .= $file->getFilename();
152
            $stream = fopen($file->getRealPath(), 'r');
153
            $filesystem->writeStream(
154
                $path,
155
                $stream,
156
                [
157
                    'mimetype' => $file->getMimeType(),
158
                ]
159
            );
160
            $classMetadata->setFieldValue($object, $fieldConfiguration->property, $path);
161
            $propertyAccessor->setValue($object, $fileProperty, null);
162
        }
163
    }
164
165
    public function deleteFiles(object $object): void
166
    {
167
        $classMetadata = $this->getClassMetadata($object);
168
169
        $configuredProperties = $this->annotationReader->getConfiguredProperties($object, true);
170
        foreach ($configuredProperties as $fileProperty => $fieldConfiguration) {
171
            $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
172
            if ($currentFilepath) {
173
                $this->removeFilepath($object, $fieldConfiguration);
174
            }
175
        }
176
    }
177
178
    public function getFileResponse(object $object, string $property, bool $forceDownload = false): Response
179
    {
180
        try {
181
            $reflectionProperty = new \ReflectionProperty($object, $property);
182
        } catch (\ReflectionException $exception) {
183
            throw new NotFoundHttpException($exception->getMessage());
184
        }
185
        if (!$this->annotationReader->isFieldConfigured($reflectionProperty)) {
186
            throw new NotFoundHttpException(sprintf('field configuration not found for %s', $property));
187
        }
188
189
        $propertyConfiguration = $this->annotationReader->getPropertyConfiguration($reflectionProperty);
190
191
        $filesystem = $this->filesystemProvider->getFilesystem($propertyConfiguration->adapter);
0 ignored issues
show
Bug introduced by
It seems like $propertyConfiguration->adapter can also be of type null; however, parameter $name of Silverback\ApiComponents...ovider::getFilesystem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

191
        $filesystem = $this->filesystemProvider->getFilesystem(/** @scrutinizer ignore-type */ $propertyConfiguration->adapter);
Loading history...
192
193
        $classMetadata = $this->getClassMetadata($object);
194
195
        $filePath = $classMetadata->getFieldValue($object, $propertyConfiguration->property);
196
        if (empty($filePath)) {
197
            return new Response('The file path for this resource is empty', Response::HTTP_NOT_FOUND);
198
        }
199
        $response = new StreamedResponse();
200
        $response->setCallback(
201
            static function () use ($filesystem, $filePath) {
202
                $outputStream = fopen('php://output', 'w');
203
                $fileStream = $filesystem->readStream($filePath);
204
                stream_copy_to_stream($fileStream, $outputStream);
205
            }
206
        );
207
        $response->headers->set('Content-Type', $filesystem->mimeType($filePath));
208
209
        $disposition = HeaderUtils::makeDisposition($forceDownload ? HeaderUtils::DISPOSITION_ATTACHMENT : HeaderUtils::DISPOSITION_INLINE, $filePath);
210
        $response->headers->set('Content-Disposition', $disposition);
211
212
        return $response;
213
    }
214
215
    private function removeFilepath(object $object, UploadableField $fieldConfiguration): void
216
    {
217
        $classMetadata = $this->getClassMetadata($object);
218
219
        $filesystem = $this->filesystemProvider->getFilesystem($fieldConfiguration->adapter);
0 ignored issues
show
Bug introduced by
It seems like $fieldConfiguration->adapter can also be of type null; however, parameter $name of Silverback\ApiComponents...ovider::getFilesystem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

219
        $filesystem = $this->filesystemProvider->getFilesystem(/** @scrutinizer ignore-type */ $fieldConfiguration->adapter);
Loading history...
220
        $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
221
        $this->fileInfoCacheManager->deleteCaches([$currentFilepath], [null]);
222
        if ($this->imagineCacheManager) {
223
            $this->imagineCacheManager->remove([$currentFilepath], null);
224
        }
225
        if ($filesystem->fileExists($currentFilepath)) {
226
            $filesystem->delete($currentFilepath);
227
        }
228
    }
229
230
    private function copyFilepath(object $object, UploadableField $fieldConfiguration)
231
    {
232
        $classMetadata = $this->getClassMetadata($object);
233
234
        $filesystem = $this->filesystemProvider->getFilesystem($fieldConfiguration->adapter);
0 ignored issues
show
Bug introduced by
It seems like $fieldConfiguration->adapter can also be of type null; however, parameter $name of Silverback\ApiComponents...ovider::getFilesystem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

234
        $filesystem = $this->filesystemProvider->getFilesystem(/** @scrutinizer ignore-type */ $fieldConfiguration->adapter);
Loading history...
235
        $currentFilepath = $classMetadata->getFieldValue($object, $fieldConfiguration->property);
236
        if (!$filesystem->fileExists($currentFilepath)) {
237
            return null;
238
        }
239
        [ 'filename' => $basename, 'extension' => $extension ] = pathinfo($currentFilepath);
240
        if (!empty($extension)) {
241
            $extension = sprintf('.%s', $extension);
242
        }
243
        $num = 1;
244
        while ($filesystem->fileExists($newFilepath = sprintf('%s_%d%s', $basename, $num, $extension))) {
245
            ++$num;
246
        }
247
        $filesystem->copy($currentFilepath, $newFilepath);
248
249
        return $newFilepath;
250
    }
251
}
252