Completed
Pull Request — master (#20)
by
unknown
02:34
created

MongoGridFS::storeUpload()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 25
rs 8.439
cc 6
eloc 16
nc 5
nop 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 */
15
16
class MongoGridFS extends MongoCollection
1 ignored issue
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
17
{
18
    const DEFAULT_CHUNK_SIZE = 262144; // 256 kb
19
20
    const ASCENDING = 1;
21
    const DESCENDING = -1;
22
23
    /**
24
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunks
25
     * @var $chunks MongoCollection
26
     */
27
    public $chunks;
28
29
    /**
30
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.filesname
31
     * @var $filesName string
32
     */
33
    protected $filesName;
34
35
    /**
36
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunksname
37
     * @var $chunksName string
38
     */
39
    protected $chunksName;
40
41
    /**
42
     * @var MongoDB
43
     */
44
    private $database;
45
46
    private $prefix;
47
48
    /**
49
     * Files as stored across two collections, the first containing file meta
50
     * information, the second containing chunks of the actual file. By default,
51
     * fs.files and fs.chunks are the collection names used.
52
     *
53
     * @link http://php.net/manual/en/mongogridfs.construct.php
54
     * @param MongoDB $db Database
55
     * @param string $prefix [optional] <p>Optional collection name prefix.</p>
56
     * @param mixed $chunks  [optional]
57
     * @return MongoGridFS
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
58
     * @throws \Exception
59
     */
60
    public function __construct(MongoDB $db, $prefix = "fs", $chunks = null)
61
    {
62
        if ($chunks) {
63
            trigger_error("The 'chunks' argument is deprecated and ignored", E_DEPRECATED);
64
        }
65
        if (empty($prefix)) {
66
            throw new \Exception('MongoGridFS::__construct(): invalid prefix');
67
        }
68
69
        $this->database = $db;
70
        $this->prefix = $prefix;
71
        $this->filesName = $prefix . '.files';
72
        $this->chunksName = $prefix . '.chunks';
73
74
        $this->chunks = $db->selectCollection($this->chunksName);
75
76
        parent::__construct($db, $this->filesName);
77
    }
78
79
    /**
80
     * Delete a file from the database
81
     *
82
     * @link http://php.net/manual/en/mongogridfs.delete.php
83
     * @param mixed $id _id of the file to remove
84
     * @return boolean Returns true if the remove was successfully sent to the database.
85
     */
86
    public function delete($id)
87
    {
88
        $this->createChunksIndex();
89
90
        $this->chunks->remove(['files_id' => $id], ['justOne' => false]);
91
        return parent::remove(['_id' => $id]);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (remove() instead of delete()). Are you sure this is correct? If so, you might want to change this to $this->remove().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
92
    }
93
94
    /**
95
     * Drops the files and chunks collections
96
     * @link http://php.net/manual/en/mongogridfs.drop.php
97
     * @return array The database response
98
     */
99
    public function drop()
100
    {
101
        $this->chunks->drop();
102
        return parent::drop();
103
    }
104
105
    /**
106
     * @link http://php.net/manual/en/mongogridfs.find.php
107
     * @param array $query The query
108
     * @param array $fields Fields to return
109
     * @param array $options Options for the find command
0 ignored issues
show
Bug introduced by
There is no parameter named $options. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
110
     * @return MongoGridFSCursor A MongoGridFSCursor
111
     */
112 View Code Duplication
    public function find(array $query = [], array $fields = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
113
    {
114
        $cursor = new MongoGridFSCursor($this, $this->db->getConnection(), (string) $this, $query, $fields);
115
        $cursor->setReadPreference($this->getReadPreference());
0 ignored issues
show
Documentation introduced by
$this->getReadPreference() is of type array, but the function expects a string.

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...
116
117
        return $cursor;
118
    }
119
120
    /**
121
     * Returns a single file matching the criteria
122
     *
123
     * @link http://www.php.net/manual/en/mongogridfs.findone.php
124
     * @param array $query The fields for which to search.
125
     * @param array $fields Fields of the results to return.
126
     * @param array $options Options for the find command
127
     * @return MongoGridFSFile|null
128
     */
129
    public function findOne(array $query = [], array $fields = [], array $options = [])
130
    {
131
        if (is_string($query)) {
132
            $query = ['filename' => $query];
133
        }
134
135
        $items = iterator_to_array($this->find($query, $fields)->limit(1));
136
        return current($items);
137
    }
138
139
    /**
140
     * Retrieve a file from the database
141
     *
142
     * @link http://www.php.net/manual/en/mongogridfs.get.php
143
     * @param mixed $id _id of the file to find.
144
     * @return MongoGridFSFile|null
145
     */
146
    public function get($id)
147
    {
148
        return $this->findOne(['_id' => $id]);
149
    }
150
151
    /**
152
     * Stores a file in the database
153
     *
154
     * @link http://php.net/manual/en/mongogridfs.put.php
155
     * @param string $filename The name of the file
156
     * @param array $extra Other metadata to add to the file saved
157
     * @param array $options An array of options for the insert operations executed against the chunks and files collections.
158
     * @return mixed Returns the _id of the saved object
159
     */
160
    public function put($filename, array $extra = [], array $options = [])
161
    {
162
        return $this->storeFile($filename, $extra, $options);
163
    }
164
165
    /**
166
     * Removes files from the collections
167
     *
168
     * @link http://www.php.net/manual/en/mongogridfs.remove.php
169
     * @param array $criteria Description of records to remove.
170
     * @param array $options Options for remove.
171
     * @throws MongoCursorException
172
     * @return boolean
173
     */
174
    public function remove(array $criteria = [], array $options = [])
175
    {
176
        $this->createChunksIndex();
177
178
        $matchingFiles = parent::find($criteria, ['_id' => 1]);
1 ignored issue
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (find() instead of remove()). Are you sure this is correct? If so, you might want to change this to $this->find().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
179
        $ids = [];
180
        foreach ($matchingFiles as $file) {
181
            $ids[] = $file['_id'];
182
        }
183
        $this->chunks->remove(['files_id' => ['$in' => $ids]], ['justOne' => false] + $options);
184
        return parent::remove(['_id' => ['$in' => $ids]], ['justOne' => false] + $options);
185
    }
186
187
    /**
188
     * Chunkifies and stores bytes in the database
189
     * @link http://php.net/manual/en/mongogridfs.storebytes.php
190
     * @param string $bytes A string of bytes to store
191
     * @param array $extra Other metadata to add to the file saved
192
     * @param array $options Options for the store. "safe": Check that this store succeeded
193
     * @return mixed The _id of the object saved
194
     */
195
    public function storeBytes($bytes, array $extra = [], array $options = [])
196
    {
197
        $this->createChunksIndex();
198
199
        $record = $extra + [
200
            'length' => mb_strlen($bytes, '8bit'),
201
            'md5' => md5($bytes),
202
        ];
203
204
        try {
205
            $file = $this->insertFile($record, $options);
206
        } catch (MongoException $e) {
207
            throw new MongoGridFSException('Cannot insert file record', 0, $e);
208
        }
209
210
        try {
211
            $this->insertChunksFromBytes($bytes, $file);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->insertFile($record, $options) on line 205 can also be of type object; however, MongoGridFS::insertChunksFromBytes() does only seem to accept array, 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...
212
        } catch (MongoException $e) {
213
            $this->delete($file['_id']);
214
            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
215
        }
216
217
        return $file['_id'];
218
    }
219
220
    /**
221
     * Stores a file in the database
222
     *
223
     * @link http://php.net/manual/en/mongogridfs.storefile.php
224
     * @param string $filename The name of the file
225
     * @param array $extra Other metadata to add to the file saved
226
     * @param array $options Options for the store. "safe": Check that this store succeeded
227
     * @return mixed Returns the _id of the saved object
228
     * @throws MongoGridFSException
229
     * @throws Exception
230
     */
231
    public function storeFile($filename, array $extra = [], array $options = [])
232
    {
233
        $this->createChunksIndex();
234
235
        $record = $extra;
236
        if (is_string($filename)) {
237
            $record += [
238
                'md5' => md5_file($filename),
239
                'length' => filesize($filename),
240
                'filename' => $filename,
241
            ];
242
243
            $handle = fopen($filename, 'r');
244
            if (! $handle) {
245
                throw new MongoGridFSException('could not open file: ' . $filename);
246
            }
247
        } elseif (! is_resource($filename)) {
248
            throw new \Exception('first argument must be a string or stream resource');
249
        } else {
250
            $handle = $filename;
251
        }
252
253
        $md5 = null;
254
        try {
255
            $file = $this->insertFile($record, $options);
256
        } catch (MongoException $e) {
257
            throw new MongoGridFSException('Cannot insert file record', 0, $e);
258
        }
259
260
        try {
261
            $length = $this->insertChunksFromFile($handle, $file, $md5);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->insertFile($record, $options) on line 255 can also be of type object; however, MongoGridFS::insertChunksFromFile() does only seem to accept array, 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...
262
        } catch (MongoException $e) {
263
            $this->delete($file['_id']);
264
            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
265
        }
266
267
268
        // Add length and MD5 if they were not present before
269
        $update = [];
270
        if (! isset($record['length'])) {
271
            $update['length'] = $length;
272
        }
273
        if (! isset($record['md5'])) {
274
            try {
275
                $update['md5'] = $md5;
276
            } catch (MongoException $e) {
277
                throw new MongoGridFSException('Error computing MD5 checksum', 0, $e);
278
            }
279
        }
280
281
        if (count($update)) {
282
            try {
283
                $result = $this->update(['_id' => $file['_id']], ['$set' => $update]);
284
                if (! $this->isOKResult($result)) {
285
                    throw new MongoGridFSException('Error updating file record');
286
                }
287
            } catch (MongoException $e) {
288
                $this->delete($file['_id']);
289
                throw new MongoGridFSException('Error updating file record', 0, $e);
290
            }
291
292
        }
293
294
        return $file['_id'];
295
    }
296
297
    /**
298
     * Saves an uploaded file directly from a POST to the database
299
     *
300
     * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
301
     * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
302
     * @param array $metadata An array of extra fields for the uploaded file.
303
     * @return mixed Returns the _id of the uploaded file.
304
     * @throws MongoGridFSException
305
     */
306
    public function storeUpload($name, array $metadata = [])
0 ignored issues
show
Coding Style introduced by
storeUpload uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
307
    {
308
        if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
309
            throw new MongoGridFSException("Could not find uploaded file $name");
310
        }
311
        if (! isset($_FILES[$name]['tmp_name'])) {
312
            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
313
        }
314
315
        $uploadedFile = $_FILES[$name];
316
        $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
317
        $uploadedFile['name'] = (array) $uploadedFile['name'];
318
319
        if (count($uploadedFile['tmp_name']) > 1) {
320
            foreach ($uploadedFile['tmp_name'] as $key => $file) {
321
                $metadata['filename'] = $uploadedFile['name'][$key];
322
                $this->storeFile($file, $metadata);
323
            }
324
325
            return null;
326
        } else {
327
            $metadata += ['filename' => array_pop($uploadedFile['name'])];
328
            return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
329
        }
330
    }
331
332
    /**
333
     * Creates the index on the chunks collection
334
     */
335
    private function createChunksIndex()
336
    {
337
        try {
338
            $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
339
        } catch (MongoDuplicateKeyException $e) {}
1 ignored issue
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
340
341
    }
342
343
    /**
344
     * Inserts a single chunk into the database
345
     *
346
     * @param mixed $fileId
347
     * @param string $data
348
     * @param int $chunkNumber
349
     * @return array|bool
350
     */
351
    private function insertChunk($fileId, $data, $chunkNumber)
352
    {
353
        $chunk = [
354
            'files_id' => $fileId,
355
            'n' => $chunkNumber,
356
            'data' => new MongoBinData($data),
357
        ];
358
359
        $result = $this->chunks->insert($chunk);
360
361
        if (! $this->isOKResult($result)) {
362
            throw new \MongoException('error inserting chunk');
363
        }
364
365
        return $result;
366
    }
367
368
    /**
369
     * Splits a string into chunks and writes them to the database
370
     *
371
     * @param string $bytes
372
     * @param array $record
373
     */
374
    private function insertChunksFromBytes($bytes, $record)
375
    {
376
        $chunkSize = $record['chunkSize'];
377
        $fileId = $record['_id'];
378
        $i = 0;
379
380
        $chunks = str_split($bytes, $chunkSize);
381
        foreach ($chunks as $chunk) {
382
            $this->insertChunk($fileId, $chunk, $i++);
383
        }
384
    }
385
386
    /**
387
     * Reads chunks from a file and writes them to the database
388
     *
389
     * @param resource $handle
390
     * @param array $record
391
     * @param string $md5
392
     * @return int Returns the number of bytes written to the database
393
     */
394
    private function insertChunksFromFile($handle, $record, &$md5)
395
    {
396
        $written = 0;
397
        $offset = 0;
398
        $i = 0;
399
400
        $fileId = $record['_id'];
401
        $chunkSize = $record['chunkSize'];
402
403
        $hash = hash_init('md5');
404
405
        rewind($handle);
406
        while (! feof($handle)) {
407
            $data = stream_get_contents($handle, $chunkSize);
408
            hash_update($hash, $data);
409
            $this->insertChunk($fileId, $data, $i++);
410
            $written += strlen($data);
411
            $offset += $chunkSize;
412
        }
413
414
        $md5 = hash_final($hash);
415
416
        return $written;
417
    }
418
419
    /**
420
     * Writes a file record to the database
421
     *
422
     * @param $record
423
     * @param array $options
424
     * @return array
425
     */
426
    private function insertFile($record, array $options = [])
427
    {
428
        $record += [
429
            '_id' => new MongoId(),
430
            'uploadDate' => new MongoDate(),
431
            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
432
        ];
433
434
        $result = $this->insert($record, $options);
435
436
        if (! $this->isOKResult($result)) {
437
            throw new \MongoException('error inserting file');
438
        }
439
440
        return $record;
441
    }
442
443
    private function isOKResult($result)
444
    {
445
        return (is_array($result) && $result['ok'] == 1.0) ||
446
               (is_bool($result) && $result);
447
    }
448
}
449