Completed
Pull Request — master (#20)
by
unknown
05:09 queued 26s
created

MongoGridFS::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
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
        try {
254
            $file = $this->insertFile($record, $options);
255
        } catch (MongoException $e) {
256
            throw new MongoGridFSException('Cannot insert file record', 0, $e);
257
        }
258
259
        try {
260
            $length = $this->insertChunksFromFile($handle, $file);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->insertFile($record, $options) on line 254 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...
261
        } catch (MongoException $e) {
262
            $this->delete($file['_id']);
263
            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
264
        }
265
266
267
        // Add length and MD5 if they were not present before
268
        $update = [];
269
        if (! isset($record['length'])) {
270
            $update['length'] = $length;
271
        }
272
        if (! isset($record['md5'])) {
273
            try {
274
                $update['md5'] = $this->getMd5ForFile($file['_id']);
275
            } catch (MongoException $e) {
276
                throw new MongoGridFSException('Error computing MD5 checksum', 0, $e);
277
            }
278
        }
279
280
        if (count($update)) {
281
            try {
282
                $result = $this->update(['_id' => $file['_id']], ['$set' => $update]);
283
                if (! $this->isOKResult($result)) {
284
                    throw new MongoGridFSException('Error updating file record');
285
                }
286
            } catch (MongoException $e) {
287
                $this->delete($file['_id']);
288
                throw new MongoGridFSException('Error updating file record', 0, $e);
289
            }
290
291
        }
292
293
        return $file['_id'];
294
    }
295
296
    /**
297
     * Saves an uploaded file directly from a POST to the database
298
     *
299
     * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
300
     * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
301
     * @param array $metadata An array of extra fields for the uploaded file.
302
     * @return mixed Returns the _id of the uploaded file.
303
     * @throws MongoGridFSException
304
     */
305
    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...
306
    {
307
        if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
308
            throw new MongoGridFSException("Could not find uploaded file $name");
309
        }
310
        if (! isset($_FILES[$name]['tmp_name'])) {
311
            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
312
        }
313
314
        $uploadedFile = $_FILES[$name];
315
        $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
316
        $uploadedFile['name'] = (array) $uploadedFile['name'];
317
318
        if (count($uploadedFile['tmp_name']) > 1) {
319
            foreach ($uploadedFile['tmp_name'] as $key => $file) {
320
                $metadata['filename'] = $uploadedFile['name'][$key];
321
                $this->storeFile($file, $metadata);
322
            }
323
324
            return null;
325
        } else {
326
            $metadata += ['filename' => array_pop($uploadedFile['name'])];
327
            return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
328
        }
329
    }
330
331
    /**
332
     * Creates the index on the chunks collection
333
     */
334
    private function createChunksIndex()
335
    {
336
        try {
337
            $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
338
        } catch (MongoDuplicateKeyException $e) {}
1 ignored issue
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
339
340
    }
341
342
    /**
343
     * Inserts a single chunk into the database
344
     *
345
     * @param mixed $fileId
346
     * @param string $data
347
     * @param int $chunkNumber
348
     * @return array|bool
349
     */
350
    private function insertChunk($fileId, $data, $chunkNumber)
351
    {
352
        $chunk = [
353
            'files_id' => $fileId,
354
            'n' => $chunkNumber,
355
            'data' => new MongoBinData($data),
356
        ];
357
358
        $result = $this->chunks->insert($chunk);
359
360
        if (! $this->isOKResult($result)) {
361
            throw new \MongoException('error inserting chunk');
362
        }
363
364
        return $result;
365
    }
366
367
    /**
368
     * Splits a string into chunks and writes them to the database
369
     *
370
     * @param string $bytes
371
     * @param array $record
372
     */
373
    private function insertChunksFromBytes($bytes, $record)
374
    {
375
        $chunkSize = $record['chunkSize'];
376
        $fileId = $record['_id'];
377
        $i = 0;
378
379
        $chunks = str_split($bytes, $chunkSize);
380
        foreach ($chunks as $chunk) {
381
            $this->insertChunk($fileId, $chunk, $i++);
382
        }
383
    }
384
385
    /**
386
     * Reads chunks from a file and writes them to the database
387
     *
388
     * @param resource $handle
389
     * @param array $record
390
     * @return int Returns the number of bytes written to the database
391
     */
392
    private function insertChunksFromFile($handle, $record)
393
    {
394
        $written = 0;
395
        $offset = 0;
396
        $i = 0;
397
398
        $fileId = $record['_id'];
399
        $chunkSize = $record['chunkSize'];
400
401
        rewind($handle);
402
        while (! feof($handle)) {
403
            $data = stream_get_contents($handle, $chunkSize);
404
            $this->insertChunk($fileId, $data, $i++);
405
            $written += strlen($data);
406
            $offset += $chunkSize;
407
        }
408
409
        return $written;
410
    }
411
412
    /**
413
     * Writes a file record to the database
414
     *
415
     * @param $record
416
     * @param array $options
417
     * @return array
418
     */
419
    private function insertFile($record, array $options = [])
420
    {
421
        $record += [
422
            '_id' => new MongoId(),
423
            'uploadDate' => new MongoDate(),
424
            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
425
        ];
426
427
        $result = $this->insert($record, $options);
428
429
        if (! $this->isOKResult($result)) {
430
            throw new \MongoException('error inserting file');
431
        }
432
433
        return $record;
434
    }
435
436
    /**
437
     * Returns the MD5 string for a file previously stored to the database
438
     *
439
     * @param $id
440
     * @return string
441
     */
442
    private function getMd5ForFile($id)
443
    {
444
        $result = $this->db->command(['filemd5' => $id, 'root' => $this->prefix]);
445
        return $result['md5'];
446
    }
447
448
    private function isOKResult($result)
449
    {
450
        return (is_array($result) && $result['ok'] == 1.0) ||
451
               (is_bool($result) && $result);
452
    }
453
}
454