Completed
Push — master ( 11b317...37df4d )
by Lucas
09:27
created

FileManager::createOrUpdateRecord()   B

Complexity

Conditions 7
Paths 32

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 0
cts 12
cp 0
rs 8.2222
c 0
b 0
f 0
cc 7
eloc 8
nc 32
nop 3
crap 56
1
<?php
2
/**
3
 * Handles file specific actions
4
 */
5
6
namespace Graviton\FileBundle;
7
8
use Gaufrette\File;
9
use Gaufrette\FileSystem;
10
use Graviton\RestBundle\Model\DocumentModel;
11
use GravitonDyn\FileBundle\Document\File as FileDocument;
12
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
13
use Symfony\Component\HttpFoundation\File\UploadedFile;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
16
17
/**
18
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
19
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
20
 * @link     http://swisscom.ch
21
 */
22
class FileManager
23
{
24
    /**
25
     * @var FileSystem
26
     */
27
    private $fileSystem;
28
29
    /**
30
     * @var FileDocumentFactory
31
     */
32
    private $fileDocumentFactory;
33
34
    /**
35
     * FileManager constructor.
36
     *
37
     * @param FileSystem          $fileSystem          file system abstraction layer for s3 and more
38
     * @param FileDocumentFactory $fileDocumentFactory Instance to be used to create action entries.
39
     */
40
    public function __construct(FileSystem $fileSystem, FileDocumentFactory $fileDocumentFactory)
41
    {
42
        $this->fileSystem = $fileSystem;
43
        $this->fileDocumentFactory = $fileDocumentFactory;
44
    }
45
46
    /**
47
     * Indicates whether the file matching the specified key exists
48
     *
49
     * @param string $key Identifier to be found
50
     *
51
     * @return boolean TRUE if the file exists, FALSE otherwise
52
     */
53
    public function has($key)
54
    {
55
        return $this->fileSystem->has($key);
56
    }
57
58
    /**
59
     * Deletes the file matching the specified key
60
     *
61
     * @param string $key Identifier to be deleted
62
     *
63
     * @throws \RuntimeException when cannot read file
64
     *
65
     * @return boolean
66
     */
67
    public function delete($key)
0 ignored issues
show
Coding Style introduced by
function delete() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
68
    {
69
        return $this->fileSystem->delete($key);
70
    }
71
72
    /**
73
     * Reads the content from the file
74
     *
75
     * @param  string $key Key of the file
76
     *
77
     * @throws \Gaufrette\Exception\FileNotFound when file does not exist
78
     * @throws \RuntimeException                 when cannot read file
79
     *
80
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
81
     */
82
    public function read($key)
83
    {
84
        return $this->fileSystem->read($key);
85
    }
86
87
    /**
88
     * Stores uploaded files to CDN
89
     *
90
     * @param Request           $request  Current Http request
91
     * @param DocumentModel     $model    Model to be used to manage entity
92
     * @param FileDocument|null $fileData meta information about the file to be stored.
93
     *
94
     * @return array
95
     */
96
    public function saveFiles(Request $request, DocumentModel $model, FileDocument $fileData = null)
97
    {
98
        $inStore = [];
99
        $files = $this->extractUploadedFiles($request);
100
101
        foreach ($files as $key => $fileInfo) {
102
            /** @var FileDocument $record */
103
            $record = $this->createOrUpdateRecord($model, $fileData, $request->get('id'));
104
            $inStore[] = $record->getId();
105
106
            /** @var \Gaufrette\File $file */
107
            $file = $this->saveFile($record->getId(), $fileInfo['content']);
108
109
            $this->initOrUpdateMetadata(
110
                $record,
111
                $file->getSize(),
112
                $fileInfo,
113
                $fileData
114
            );
115
116
            $model->updateRecord($record->getId(), $record);
117
118
            // TODO NOTICE: ONLY UPLOAD OF ONE FILE IS CURRENTLY SUPPORTED
119
            break;
120
        }
121
122
        return $inStore;
123
    }
124
125
    /**
126
     * Save or update a file
127
     *
128
     * @param string $id   ID of file
129
     * @param String $data content to save
130
     *
131
     * @return File
132
     *
133
     * @throws BadRequestHttpException
134
     */
135
    public function saveFile($id, $data)
136
    {
137
        if (is_resource($data)) {
138
            throw new BadRequestHttpException('/file does not support storing resources');
139
        }
140
        $file = new File($id, $this->fileSystem);
141
        $file->setContent($data);
142
143
        return $file;
144
    }
145
146
    /**
147
     * Moves uploaded files to tmp directory
148
     *
149
     * @param Request $request Current http request
150
     *
151
     * @return array
152
     */
153
    private function extractUploadedFiles(Request $request)
154
    {
155
        $uploadedFiles = [];
156
157
        /** @var  $uploadedFile \Symfony\Component\HttpFoundation\File\UploadedFile */
158
        foreach ($request->files->all() as $field => $uploadedFile) {
159
            if (0 === $uploadedFile->getError()) {
160
                $uploadedFiles[$field] = [
161
                    'data' => [
162
                        'mimetype' => $uploadedFile->getMimeType(),
163
                        'filename' => $uploadedFile->getClientOriginalName()
164
                    ],
165
                    'content' => file_get_contents($uploadedFile->getPathName())
166
                ];
167
            } else {
168
                throw new UploadException($uploadedFile->getErrorMessage());
169
            }
170
        }
171
172
        if (empty($uploadedFiles)) {
173
            $uploadedFiles['upload'] = [
174
                'data' => [
175
                    'mimetype' => $request->headers->get('Content-Type'),
176
                    'filename' => ''
177
                ],
178
                'content' => $request->getContent()
179
            ];
180
        }
181
182
        return $uploadedFiles;
183
    }
184
185
    /**
186
     * Creates a new or updates an existing instance of the file document
187
     *
188
     * @param DocumentModel     $model    Document model
189
     * @param FileDocument|null $fileData File information
190
     * @param string            $id       Alternative Id to be checked
191
     *
192
     * @return FileDocument
193
     */
194
    private function createOrUpdateRecord(DocumentModel $model, FileDocument $fileData = null, $id = '')
195
    {
196
        $id = empty($id) && !empty($fileData) ? $fileData->getId() : $id;
197
198
        if (($recordExists = empty($record = $model->find($id))) && empty($record = $fileData)) {
199
            $entityClass = $model->getEntityClass();
200
201
            $record = new $entityClass();
202
        }
203
204
        if (!empty($id)) {
205
            $record->setId($id);
206
        }
207
208
        return $recordExists ? $model->updateRecord($record->getId(), $record) : $model->insertRecord($record);
0 ignored issues
show
Documentation introduced by
$record is of type null|object, but the function expects a object<Graviton\I18nBundle\Document\Translatable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $record defined by $fileData on line 198 can also be of type null; however, Graviton\RestBundle\Mode...ntModel::updateRecord() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
209
    }
210
211
    /**
212
     * Updates or initialzes the metadata information of the current entity.
213
     *
214
     * @param FileDocument $file     Document to be used
215
     * @param integer      $fileSize Size of the uploaded file
216
     * @param array        $fileInfo Additional info about the file
217
     * @param FileDocument $fileData File data to be updated
0 ignored issues
show
Documentation introduced by
Should the type for parameter $fileData not be null|FileDocument?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
218
     *
219
     * @return void
220
     */
221
    private function initOrUpdateMetaData(FileDocument $file, $fileSize, array $fileInfo, FileDocument $fileData = null)
222
    {
223
        if (empty($meta = $file->getMetadata()) && (empty($fileData) || empty($meta = $fileData->getMetadata()))) {
224
            $meta = $this->fileDocumentFactory->createFileMataData();
225
            $meta->setId($file->getId());
226
            $meta->setCreatedate(new \DateTime());
227
        }
228
229
        $meta->setModificationdate(new \DateTime());
230
        if (empty($meta->getFilename()) && !empty($fileInfo['data']['filename'])) {
231
            $meta->setFilename($fileInfo['data']['filename']);
232
        }
233
        if (!empty($fileInfo['data']['mimetype'])) {
234
            $meta->setMime($fileInfo['data']['mimetype']);
235
        }
236
        $meta->setSize($fileSize);
237
        $file->setMetadata($meta);
238
    }
239
240
    /**
241
     * Extracts different information sent in the request content.
242
     *
243
     * @param Request $request Current http request
244
     *
245
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,null|array<...ng,UploadedFile>|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
246
     */
247
    public function extractDataFromRequestContent(Request $request)
248
    {
249
        // split content
250
        $contentType = $request->headers->get('Content-Type');
251
        list(, $boundary) = explode('; boundary=', $contentType);
252
253
        // fix boundary dash count
254
        $boundary = '--' . $boundary;
255
256
        $content = $request->getContent();
257
        $contentBlocks = explode($boundary, $content, -1);
258
        $metadataInfo = '';
259
        $fileInfo = '';
260
261
        // determine content blocks usage
262
        foreach ($contentBlocks as $contentBlock) {
263
            if (empty($contentBlock)) {
264
                continue;
265
            }
266
            if (40 === strpos($contentBlock, 'upload')) {
267
                $fileInfo = $contentBlock;
268
                continue;
269
            }
270
            if (40 === strpos($contentBlock, 'metadata')) {
271
                $metadataInfo = $contentBlock;
272
                continue;
273
            }
274
        }
275
276
        $attributes = array_merge(
277
            $request->attributes->all(),
278
            $this->extractMetaDataFromContent($metadataInfo)
279
        );
280
        $files = $this->extractFileFromContent($fileInfo);
281
282
        return ['files' => $files, 'attributes' => $attributes];
283
    }
284
285
    /**
286
     * Extracts meta information from request content.
287
     *
288
     * @param string $metadataInfoString Information about metadata information
289
     *
290
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
291
     */
292
    private function extractMetaDataFromContent($metadataInfoString)
293
    {
294
        if (empty($metadataInfoString)) {
295
            return ['metadata' => '{}'];
296
        }
297
298
        $metadataInfo = explode("\r\n", ltrim($metadataInfoString));
299
        return ['metadata' => $metadataInfo[2]];
300
    }
301
302
    /**
303
     * Extracts file data from request content
304
     *
305
     * @param string $fileInfoString Information about uploaded files.
306
     *
307
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array<string,UploadedFile>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
308
     */
309
    private function extractFileFromContent($fileInfoString)
310
    {
311
        if (empty($fileInfoString)) {
312
            return null;
313
        }
314
315
        $fileInfo = explode("\r\n\r\n", ltrim($fileInfoString), 2);
316
317
        // write content to file ("upload_tmp_dir" || sys_get_temp_dir() )
318
        preg_match('@name=\"([^"]*)\";\sfilename=\"([^"]*)\"\s*Content-Type:\s([^"]*)@', $fileInfo[0], $matches);
319
        $originalName = $matches[2];
320
        $dir = ini_get('upload_tmp_dir');
321
        $dir = (empty($dir)) ? sys_get_temp_dir() : $dir;
322
        $file = $dir . '/' . $originalName;
323
        $fileContent = substr($fileInfo[1], 0, -2);
324
325
        // create file
326
        touch($file);
327
        $size = file_put_contents($file, $fileContent, LOCK_EX);
328
329
        $files = [
330
            $matches[1] => new UploadedFile(
331
                $file,
332
                $originalName,
333
                $matches[3],
334
                $size
335
            )
336
        ];
337
338
        return $files;
339
    }
340
}
341