MongoGridFS   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 439
Duplicated Lines 1.59 %

Coupling/Cohesion

Components 2
Dependencies 8

Importance

Changes 0
Metric Value
dl 7
loc 439
rs 8.48
c 0
b 0
f 0
wmc 49
lcom 2
cbo 8

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 3
A delete() 0 7 1
A drop() 0 5 1
A find() 7 7 1
A findOne() 0 9 3
A get() 0 4 1
A put() 0 4 1
A remove() 0 12 2
A storeBytes() 0 24 3
C storeFile() 0 64 12
B storeUpload() 0 25 6
A createChunksIndex() 0 7 2
A insertChunk() 0 16 2
A insertChunksFromBytes() 0 11 2
A insertChunksFromFile() 0 24 2
A insertFile() 0 16 2
A isOKResult() 0 5 4
A __sleep() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MongoGridFS often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MongoGridFS, and based on these observations, apply Extract Interface, too.

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
if (class_exists('MongoGridFS', false)) {
17
    return;
18
}
19
20
class MongoGridFS extends MongoCollection
21
{
22
    const ASCENDING = 1;
23
    const DESCENDING = -1;
24
25
    /**
26
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunks
27
     * @var $chunks MongoCollection
28
     */
29
    public $chunks;
30
31
    /**
32
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.filesname
33
     * @var $filesName string
34
     */
35
    protected $filesName;
36
37
    /**
38
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunksname
39
     * @var $chunksName string
40
     */
41
    protected $chunksName;
42
43
    /**
44
     * @var MongoDB
45
     */
46
    private $database;
47
48
    private $prefix;
49
50
    private $defaultChunkSize = 261120;
51
52
    /**
53
     * Files as stored across two collections, the first containing file meta
54
     * information, the second containing chunks of the actual file. By default,
55
     * fs.files and fs.chunks are the collection names used.
56
     *
57
     * @link http://php.net/manual/en/mongogridfs.construct.php
58
     * @param MongoDB $db Database
59
     * @param string $prefix [optional] <p>Optional collection name prefix.</p>
60
     * @param mixed $chunks  [optional]
61
     * @throws \Exception
62
     */
63
    public function __construct(MongoDB $db, $prefix = "fs", $chunks = null)
64
    {
65
        if ($chunks) {
66
            trigger_error("The 'chunks' argument is deprecated and ignored", E_USER_DEPRECATED);
67
        }
68
        if (empty($prefix)) {
69
            throw new \Exception('MongoGridFS::__construct(): invalid prefix');
70
        }
71
72
        $this->database = $db;
73
        $this->prefix = (string) $prefix;
74
        $this->filesName = $prefix . '.files';
75
        $this->chunksName = $prefix . '.chunks';
76
77
        $this->chunks = $db->selectCollection($this->chunksName);
78
79
        parent::__construct($db, $this->filesName);
80
    }
81
82
    /**
83
     * Delete a file from the database
84
     *
85
     * @link http://php.net/manual/en/mongogridfs.delete.php
86
     * @param mixed $id _id of the file to remove
87
     * @return boolean Returns true if the remove was successfully sent to the database.
88
     */
89
    public function delete($id)
90
    {
91
        $this->createChunksIndex();
92
93
        $this->chunks->remove(['files_id' => $id], ['justOne' => false]);
94
        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...
95
    }
96
97
    /**
98
     * Drops the files and chunks collections
99
     * @link http://php.net/manual/en/mongogridfs.drop.php
100
     * @return array The database response
101
     */
102
    public function drop()
103
    {
104
        $this->chunks->drop();
105
        return parent::drop();
106
    }
107
108
    /**
109
     * @link http://php.net/manual/en/mongogridfs.find.php
110
     * @param array $query The query
111
     * @param array $fields Fields to return
112
     * @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...
113
     * @return MongoGridFSCursor A MongoGridFSCursor
114
     */
115 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...
116
    {
117
        $cursor = new MongoGridFSCursor($this, $this->db->getConnection(), (string) $this, $query, $fields);
118
        $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...
119
120
        return $cursor;
121
    }
122
123
    /**
124
     * Returns a single file matching the criteria
125
     *
126
     * @link http://www.php.net/manual/en/mongogridfs.findone.php
127
     * @param mixed $query The fields for which to search or a filename to search for.
128
     * @param array $fields Fields of the results to return.
129
     * @param array $options Options for the find command
130
     * @return MongoGridFSFile|null
131
     */
132
    public function findOne($query = [], array $fields = [], array $options = [])
133
    {
134
        if (! is_array($query)) {
135
            $query = ['filename' => (string) $query];
136
        }
137
138
        $items = iterator_to_array($this->find($query, $fields)->limit(1));
139
        return count($items) ? current($items) : null;
140
    }
141
142
    /**
143
     * Retrieve a file from the database
144
     *
145
     * @link http://www.php.net/manual/en/mongogridfs.get.php
146
     * @param mixed $id _id of the file to find.
147
     * @return MongoGridFSFile|null
148
     */
149
    public function get($id)
150
    {
151
        return $this->findOne(['_id' => $id]);
152
    }
153
154
    /**
155
     * Stores a file in the database
156
     *
157
     * @link http://php.net/manual/en/mongogridfs.put.php
158
     * @param string $filename The name of the file
159
     * @param array $extra Other metadata to add to the file saved
160
     * @param array $options An array of options for the insert operations executed against the chunks and files collections.
161
     * @return mixed Returns the _id of the saved object
162
     */
163
    public function put($filename, array $extra = [], array $options = [])
164
    {
165
        return $this->storeFile($filename, $extra, $options);
166
    }
167
168
    /**
169
     * Removes files from the collections
170
     *
171
     * @link http://www.php.net/manual/en/mongogridfs.remove.php
172
     * @param array $criteria Description of records to remove.
173
     * @param array $options Options for remove.
174
     * @throws MongoCursorException
175
     * @return boolean
176
     */
177
    public function remove(array $criteria = [], array $options = [])
178
    {
179
        $this->createChunksIndex();
180
181
        $matchingFiles = parent::find($criteria, ['_id' => 1]);
182
        $ids = [];
183
        foreach ($matchingFiles as $file) {
184
            $ids[] = $file['_id'];
185
        }
186
        $this->chunks->remove(['files_id' => ['$in' => $ids]], ['justOne' => false] + $options);
187
        return parent::remove(['_id' => ['$in' => $ids]], ['justOne' => false] + $options);
188
    }
189
190
    /**
191
     * Chunkifies and stores bytes in the database
192
     * @link http://php.net/manual/en/mongogridfs.storebytes.php
193
     * @param string $bytes A string of bytes to store
194
     * @param array $extra Other metadata to add to the file saved
195
     * @param array $options Options for the store. "safe": Check that this store succeeded
196
     * @return mixed The _id of the object saved
197
     */
198
    public function storeBytes($bytes, array $extra = [], array $options = [])
199
    {
200
        $this->createChunksIndex();
201
202
        $record = $extra + [
203
            'length' => mb_strlen($bytes, '8bit'),
204
            'md5' => md5($bytes),
205
        ];
206
207
        try {
208
            $file = $this->insertFile($record, $options);
209
        } catch (MongoException $e) {
210
            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e);
211
        }
212
213
        try {
214
            $this->insertChunksFromBytes($bytes, $file);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->insertFile($record, $options) on line 208 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...
215
        } catch (MongoException $e) {
216
            $this->delete($file['_id']);
217
            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e);
218
        }
219
220
        return $file['_id'];
221
    }
222
223
    /**
224
     * Stores a file in the database
225
     *
226
     * @link http://php.net/manual/en/mongogridfs.storefile.php
227
     * @param string $filename The name of the file
228
     * @param array $extra Other metadata to add to the file saved
229
     * @param array $options Options for the store. "safe": Check that this store succeeded
230
     * @return mixed Returns the _id of the saved object
231
     * @throws MongoGridFSException
232
     * @throws Exception
233
     */
234
    public function storeFile($filename, array $extra = [], array $options = [])
235
    {
236
        $this->createChunksIndex();
237
238
        $record = $extra;
239
        if (is_string($filename)) {
240
            $record += [
241
                'md5' => md5_file($filename),
242
                'length' => filesize($filename),
243
                'filename' => $filename,
244
            ];
245
246
            $handle = fopen($filename, 'r');
247
            if (! $handle) {
248
                throw new MongoGridFSException('could not open file: ' . $filename);
249
            }
250
        } elseif (! is_resource($filename)) {
251
            throw new \Exception('first argument must be a string or stream resource');
252
        } else {
253
            $handle = $filename;
254
        }
255
256
        $md5 = null;
257
        try {
258
            $file = $this->insertFile($record, $options);
259
        } catch (MongoException $e) {
260
            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e);
261
        }
262
263
        try {
264
            $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 258 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...
265
        } catch (MongoException $e) {
266
            $this->delete($file['_id']);
267
            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e);
268
        }
269
270
271
        // Add length and MD5 if they were not present before
272
        $update = [];
273
        if (! isset($record['length'])) {
274
            $update['length'] = $length;
275
        }
276
        if (! isset($record['md5'])) {
277
            try {
278
                $update['md5'] = $md5;
279
            } catch (MongoException $e) {
280
                throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e);
281
            }
282
        }
283
284
        if (count($update)) {
285
            try {
286
                $result = $this->update(['_id' => $file['_id']], ['$set' => $update]);
287
                if (! $this->isOKResult($result)) {
288
                    throw new MongoGridFSException('Could not store file');
289
                }
290
            } catch (MongoException $e) {
291
                $this->delete($file['_id']);
292
                throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), $e->getCode(), $e);
293
            }
294
        }
295
296
        return $file['_id'];
297
    }
298
299
    /**
300
     * Saves an uploaded file directly from a POST to the database
301
     *
302
     * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
303
     * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
304
     * @param array $metadata An array of extra fields for the uploaded file.
305
     * @return mixed Returns the _id of the uploaded file.
306
     * @throws MongoGridFSException
307
     */
308
    public function storeUpload($name, array $metadata = [])
309
    {
310
        if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
311
            throw new MongoGridFSException("Could not find uploaded file $name");
312
        }
313
        if (! isset($_FILES[$name]['tmp_name'])) {
314
            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
315
        }
316
317
        $uploadedFile = $_FILES[$name];
318
        $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
319
        $uploadedFile['name'] = (array) $uploadedFile['name'];
320
321
        if (count($uploadedFile['tmp_name']) > 1) {
322
            foreach ($uploadedFile['tmp_name'] as $key => $file) {
323
                $metadata['filename'] = $uploadedFile['name'][$key];
324
                $this->storeFile($file, $metadata);
325
            }
326
327
            return null;
328
        } else {
329
            $metadata += ['filename' => array_pop($uploadedFile['name'])];
330
            return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
331
        }
332
    }
333
334
    /**
335
     * Creates the index on the chunks collection
336
     */
337
    private function createChunksIndex()
338
    {
339
        try {
340
            $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
341
        } catch (MongoDuplicateKeyException $e) {
342
        }
343
    }
344
345
    /**
346
     * Inserts a single chunk into the database
347
     *
348
     * @param mixed $fileId
349
     * @param string $data
350
     * @param int $chunkNumber
351
     * @return array|bool
352
     */
353
    private function insertChunk($fileId, $data, $chunkNumber)
354
    {
355
        $chunk = [
356
            'files_id' => $fileId,
357
            'n' => $chunkNumber,
358
            'data' => new MongoBinData($data),
359
        ];
360
361
        $result = $this->chunks->insert($chunk);
362
363
        if (! $this->isOKResult($result)) {
364
            throw new \MongoException('error inserting chunk');
365
        }
366
367
        return $result;
368
    }
369
370
    /**
371
     * Splits a string into chunks and writes them to the database
372
     *
373
     * @param string $bytes
374
     * @param array $record
375
     */
376
    private function insertChunksFromBytes($bytes, $record)
377
    {
378
        $chunkSize = $record['chunkSize'];
379
        $fileId = $record['_id'];
380
        $i = 0;
381
382
        $chunks = str_split($bytes, $chunkSize);
383
        foreach ($chunks as $chunk) {
384
            $this->insertChunk($fileId, $chunk, $i++);
385
        }
386
    }
387
388
    /**
389
     * Reads chunks from a file and writes them to the database
390
     *
391
     * @param resource $handle
392
     * @param array $record
393
     * @param string $md5
394
     * @return int Returns the number of bytes written to the database
395
     */
396
    private function insertChunksFromFile($handle, $record, &$md5)
397
    {
398
        $written = 0;
399
        $offset = 0;
400
        $i = 0;
401
402
        $fileId = $record['_id'];
403
        $chunkSize = $record['chunkSize'];
404
405
        $hash = hash_init('md5');
406
407
        rewind($handle);
408
        while (! feof($handle)) {
409
            $data = stream_get_contents($handle, $chunkSize);
410
            hash_update($hash, $data);
411
            $this->insertChunk($fileId, $data, $i++);
412
            $written += strlen($data);
413
            $offset += $chunkSize;
414
        }
415
416
        $md5 = hash_final($hash);
417
418
        return $written;
419
    }
420
421
    /**
422
     * Writes a file record to the database
423
     *
424
     * @param $record
425
     * @param array $options
426
     * @return array
427
     */
428
    private function insertFile($record, array $options = [])
429
    {
430
        $record += [
431
            '_id' => new MongoId(),
432
            'uploadDate' => new MongoDate(),
433
            'chunkSize' => $this->defaultChunkSize,
434
        ];
435
436
        $result = $this->insert($record, $options);
437
438
        if (! $this->isOKResult($result)) {
439
            throw new \MongoException('error inserting file');
440
        }
441
442
        return $record;
443
    }
444
445
    private function isOKResult($result)
446
    {
447
        return (is_array($result) && $result['ok'] == 1.0) ||
448
               (is_bool($result) && $result);
449
    }
450
451
    /**
452
     * @return array
453
     */
454
    public function __sleep()
455
    {
456
        return ['chunks', 'chunksName', 'database', 'defaultChunkSize', 'filesName', 'prefix'] + parent::__sleep();
457
    }
458
}
459