Completed
Pull Request — master (#63)
by Andreas
21:41
created

MongoGridFS::__sleep()   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 0
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
17
{
18
    const ASCENDING = 1;
19
    const DESCENDING = -1;
20
21
    /**
22
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunks
23
     * @var $chunks MongoCollection
24
     */
25
    public $chunks;
26
27
    /**
28
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.filesname
29
     * @var $filesName string
30
     */
31
    protected $filesName;
32
33
    /**
34
     * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunksname
35
     * @var $chunksName string
36
     */
37
    protected $chunksName;
38
39
    /**
40
     * @var MongoDB
41
     */
42
    private $database;
43
44
    private $prefix;
45
46
    private $defaultChunkSize = 261120;
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
     * @throws \Exception
58
     */
59
    public function __construct(MongoDB $db, $prefix = "fs", $chunks = null)
60
    {
61
        if ($chunks) {
62
            trigger_error("The 'chunks' argument is deprecated and ignored", E_DEPRECATED);
63
        }
64
        if (empty($prefix)) {
65
            throw new \Exception('MongoGridFS::__construct(): invalid prefix');
66
        }
67
68
        $this->database = $db;
69
        $this->prefix = $prefix;
70
        $this->filesName = $prefix . '.files';
71
        $this->chunksName = $prefix . '.chunks';
72
73
        $this->chunks = $db->selectCollection($this->chunksName);
74
75
        parent::__construct($db, $this->filesName);
76
    }
77
78
    /**
79
     * Delete a file from the database
80
     *
81
     * @link http://php.net/manual/en/mongogridfs.delete.php
82
     * @param mixed $id _id of the file to remove
83
     * @return boolean Returns true if the remove was successfully sent to the database.
84
     */
85
    public function delete($id)
86
    {
87
        $this->createChunksIndex();
88
89
        $this->chunks->remove(['files_id' => $id], ['justOne' => false]);
90
        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...
91
    }
92
93
    /**
94
     * Drops the files and chunks collections
95
     * @link http://php.net/manual/en/mongogridfs.drop.php
96
     * @return array The database response
97
     */
98
    public function drop()
99
    {
100
        $this->chunks->drop();
101
        return parent::drop();
102
    }
103
104
    /**
105
     * @link http://php.net/manual/en/mongogridfs.find.php
106
     * @param array $query The query
107
     * @param array $fields Fields to return
108
     * @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...
109
     * @return MongoGridFSCursor A MongoGridFSCursor
110
     */
111 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...
112
    {
113
        $cursor = new MongoGridFSCursor($this, $this->db->getConnection(), (string) $this, $query, $fields);
114
        $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...
115
116
        return $cursor;
117
    }
118
119
    /**
120
     * Returns a single file matching the criteria
121
     *
122
     * @link http://www.php.net/manual/en/mongogridfs.findone.php
123
     * @param array $query The fields for which to search.
124
     * @param array $fields Fields of the results to return.
125
     * @param array $options Options for the find command
126
     * @return MongoGridFSFile|null
127
     */
128
    public function findOne(array $query = [], array $fields = [], array $options = [])
129
    {
130
        if (is_string($query)) {
131
            $query = ['filename' => $query];
132
        }
133
134
        $items = iterator_to_array($this->find($query, $fields)->limit(1));
135
        return count($items) ? current($items) : null;
136
    }
137
138
    /**
139
     * Retrieve a file from the database
140
     *
141
     * @link http://www.php.net/manual/en/mongogridfs.get.php
142
     * @param mixed $id _id of the file to find.
143
     * @return MongoGridFSFile|null
144
     */
145
    public function get($id)
146
    {
147
        return $this->findOne(['_id' => $id]);
148
    }
149
150
    /**
151
     * Stores a file in the database
152
     *
153
     * @link http://php.net/manual/en/mongogridfs.put.php
154
     * @param string $filename The name of the file
155
     * @param array $extra Other metadata to add to the file saved
156
     * @param array $options An array of options for the insert operations executed against the chunks and files collections.
157
     * @return mixed Returns the _id of the saved object
158
     */
159
    public function put($filename, array $extra = [], array $options = [])
160
    {
161
        return $this->storeFile($filename, $extra, $options);
162
    }
163
164
    /**
165
     * Removes files from the collections
166
     *
167
     * @link http://www.php.net/manual/en/mongogridfs.remove.php
168
     * @param array $criteria Description of records to remove.
169
     * @param array $options Options for remove.
170
     * @throws MongoCursorException
171
     * @return boolean
172
     */
173
    public function remove(array $criteria = [], array $options = [])
174
    {
175
        $this->createChunksIndex();
176
177
        $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...
178
        $ids = [];
179
        foreach ($matchingFiles as $file) {
180
            $ids[] = $file['_id'];
181
        }
182
        $this->chunks->remove(['files_id' => ['$in' => $ids]], ['justOne' => false] + $options);
183
        return parent::remove(['_id' => ['$in' => $ids]], ['justOne' => false] + $options);
184
    }
185
186
    /**
187
     * Chunkifies and stores bytes in the database
188
     * @link http://php.net/manual/en/mongogridfs.storebytes.php
189
     * @param string $bytes A string of bytes to store
190
     * @param array $extra Other metadata to add to the file saved
191
     * @param array $options Options for the store. "safe": Check that this store succeeded
192
     * @return mixed The _id of the object saved
193
     */
194
    public function storeBytes($bytes, array $extra = [], array $options = [])
195
    {
196
        $this->createChunksIndex();
197
198
        $record = $extra + [
199
            'length' => mb_strlen($bytes, '8bit'),
200
            'md5' => md5($bytes),
201
        ];
202
203
        try {
204
            $file = $this->insertFile($record, $options);
205
        } catch (MongoException $e) {
206
            throw new MongoGridFSException('Could not store file: '. $e->getMessage(), 0, $e);
207
        }
208
209
        try {
210
            $this->insertChunksFromBytes($bytes, $file);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->insertFile($record, $options) on line 204 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...
211
        } catch (MongoException $e) {
212
            $this->delete($file['_id']);
213
            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
214
        }
215
216
        return $file['_id'];
217
    }
218
219
    /**
220
     * Stores a file in the database
221
     *
222
     * @link http://php.net/manual/en/mongogridfs.storefile.php
223
     * @param string $filename The name of the file
224
     * @param array $extra Other metadata to add to the file saved
225
     * @param array $options Options for the store. "safe": Check that this store succeeded
226
     * @return mixed Returns the _id of the saved object
227
     * @throws MongoGridFSException
228
     * @throws Exception
229
     */
230
    public function storeFile($filename, array $extra = [], array $options = [])
231
    {
232
        $this->createChunksIndex();
233
234
        $record = $extra;
235
        if (is_string($filename)) {
236
            $record += [
237
                'md5' => md5_file($filename),
238
                'length' => filesize($filename),
239
                'filename' => $filename,
240
            ];
241
242
            $handle = fopen($filename, 'r');
243
            if (! $handle) {
244
                throw new MongoGridFSException('could not open file: ' . $filename);
245
            }
246
        } elseif (! is_resource($filename)) {
247
            throw new \Exception('first argument must be a string or stream resource');
248
        } else {
249
            $handle = $filename;
250
        }
251
252
        $md5 = null;
253
        try {
254
            $file = $this->insertFile($record, $options);
255
        } catch (MongoException $e) {
256
            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
257
        }
258
259
        try {
260
            $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 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('Could not store file: ' . $e->getMessage(), 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'] = $md5;
275
            } catch (MongoException $e) {
276
                throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 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('Could not store file');
285
                }
286
            } catch (MongoException $e) {
287
                $this->delete($file['_id']);
288
                throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 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
     * @param string $md5
391
     * @return int Returns the number of bytes written to the database
392
     */
393
    private function insertChunksFromFile($handle, $record, &$md5)
394
    {
395
        $written = 0;
396
        $offset = 0;
397
        $i = 0;
398
399
        $fileId = $record['_id'];
400
        $chunkSize = $record['chunkSize'];
401
402
        $hash = hash_init('md5');
403
404
        rewind($handle);
405
        while (! feof($handle)) {
406
            $data = stream_get_contents($handle, $chunkSize);
407
            hash_update($hash, $data);
408
            $this->insertChunk($fileId, $data, $i++);
409
            $written += strlen($data);
410
            $offset += $chunkSize;
411
        }
412
413
        $md5 = hash_final($hash);
414
415
        return $written;
416
    }
417
418
    /**
419
     * Writes a file record to the database
420
     *
421
     * @param $record
422
     * @param array $options
423
     * @return array
424
     */
425
    private function insertFile($record, array $options = [])
426
    {
427
        $record += [
428
            '_id' => new MongoId(),
429
            'uploadDate' => new MongoDate(),
430
            'chunkSize' => $this->defaultChunkSize,
431
        ];
432
433
        $result = $this->insert($record, $options);
434
435
        if (! $this->isOKResult($result)) {
436
            throw new \MongoException('error inserting file');
437
        }
438
439
        return $record;
440
    }
441
442
    private function isOKResult($result)
443
    {
444
        return (is_array($result) && $result['ok'] == 1.0) ||
445
               (is_bool($result) && $result);
446
    }
447
448
    /**
449
     * @return array
450
     */
451
    public function __sleep()
452
    {
453
        return ['chunks', 'chunksName', 'database', 'defaultChunkSize', 'filesName', 'prefix'] + parent::__sleep();
454
    }
455
}
456