Completed
Push — master ( e8b047...677a25 )
by Andreas
02:35
created

MongoGridFS::insertChunk()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 9
rs 9.6667
cc 1
eloc 6
nc 1
nop 3
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
        $file = $this->insertFile($record, $options);
205
        $this->insertChunksFromBytes($bytes, $file);
206
207
        return $file['_id'];
208
    }
209
210
    /**
211
     * Stores a file in the database
212
     *
213
     * @link http://php.net/manual/en/mongogridfs.storefile.php
214
     * @param string $filename The name of the file
215
     * @param array $extra Other metadata to add to the file saved
216
     * @param array $options Options for the store. "safe": Check that this store succeeded
217
     * @return mixed Returns the _id of the saved object
218
     * @throws MongoGridFSException
219
     * @throws Exception
220
     */
221
    public function storeFile($filename, array $extra = [], array $options = [])
222
    {
223
        $this->createChunksIndex();
224
225
        $record = $extra;
226
        if (is_string($filename)) {
227
            $record += [
228
                'md5' => md5_file($filename),
229
                'length' => filesize($filename),
230
                'filename' => $filename,
231
            ];
232
233
            $handle = fopen($filename, 'r');
234
            if (! $handle) {
235
                throw new MongoGridFSException('could not open file: ' . $filename);
236
            }
237
        } elseif (! is_resource($filename)) {
238
            throw new \Exception('first argument must be a string or stream resource');
239
        } else {
240
            $handle = $filename;
241
        }
242
243
        $file = $this->insertFile($record, $options);
244
        $length = $this->insertChunksFromFile($handle, $file);
245
246
        // Add length and MD5 if they were not present before
247
        $update = [];
248
        if (! isset($record['length'])) {
249
            $update['length'] = $length;
250
        }
251
        if (! isset($record['md5'])) {
252
            $update['md5'] = $this->getMd5ForFile($file['_id']);
253
        }
254
255
        if (count($update)) {
256
            $this->update(['_id' => $file['_id']], ['$set' => $update]);
257
        }
258
259
        return $file['_id'];
260
    }
261
262
    /**
263
     * Saves an uploaded file directly from a POST to the database
264
     *
265
     * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
266
     * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
267
     * @param array $metadata An array of extra fields for the uploaded file.
268
     * @return mixed Returns the _id of the uploaded file.
269
     * @throws MongoGridFSException
270
     */
271
    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...
272
    {
273
        if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
274
            throw new MongoGridFSException("Could not find uploaded file $name");
275
        }
276
        if (! isset($_FILES[$name]['tmp_name'])) {
277
            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
278
        }
279
280
        $uploadedFile = $_FILES[$name];
281
        $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
282
        $uploadedFile['name'] = (array) $uploadedFile['name'];
283
284
        if (count($uploadedFile['tmp_name']) > 1) {
285
            foreach ($uploadedFile['tmp_name'] as $key => $file) {
286
                $metadata['filename'] = $uploadedFile['name'][$key];
287
                $this->storeFile($file, $metadata);
288
            }
289
290
            return null;
291
        } else {
292
            $metadata += ['filename' => array_pop($uploadedFile['name'])];
293
            return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
294
        }
295
    }
296
297
    /**
298
     * Creates the index on the chunks collection
299
     */
300
    private function createChunksIndex()
301
    {
302
        $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
303
    }
304
305
    /**
306
     * Inserts a single chunk into the database
307
     *
308
     * @param mixed $fileId
309
     * @param string $data
310
     * @param int $chunkNumber
311
     * @return array|bool
312
     */
313
    private function insertChunk($fileId, $data, $chunkNumber)
314
    {
315
        $chunk = [
316
            'files_id' => $fileId,
317
            'n' => $chunkNumber,
318
            'data' => new MongoBinData($data),
319
        ];
320
        return $this->chunks->insert($chunk);
321
    }
322
323
    /**
324
     * Splits a string into chunks and writes them to the database
325
     *
326
     * @param string $bytes
327
     * @param array $record
328
     */
329
    private function insertChunksFromBytes($bytes, $record)
330
    {
331
        $chunkSize = $record['chunkSize'];
332
        $fileId = $record['_id'];
333
        $i = 0;
334
335
        $chunks = str_split($bytes, $chunkSize);
336
        foreach ($chunks as $chunk) {
337
            $this->insertChunk($fileId, $chunk, $i++);
338
        }
339
    }
340
341
    /**
342
     * Reads chunks from a file and writes them to the database
343
     *
344
     * @param resource $handle
345
     * @param array $record
346
     * @return int Returns the number of bytes written to the database
347
     */
348
    private function insertChunksFromFile($handle, $record)
349
    {
350
        $written = 0;
351
        $offset = 0;
352
        $i = 0;
353
354
        $fileId = $record['_id'];
355
        $chunkSize = $record['chunkSize'];
356
357
        rewind($handle);
358
        while (! feof($handle)) {
359
            $data = stream_get_contents($handle, $chunkSize);
360
            $this->insertChunk($fileId, $data, $i++);
361
            $written += strlen($data);
362
            $offset += $chunkSize;
363
        }
364
365
        return $written;
366
    }
367
368
    /**
369
     * Writes a file record to the database
370
     *
371
     * @param $record
372
     * @param array $options
373
     * @return array
374
     */
375
    private function insertFile($record, array $options = [])
376
    {
377
        $record += [
378
            '_id' => new MongoId(),
379
            'uploadDate' => new MongoDate(),
380
            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
381
        ];
382
383
        $this->insert($record, $options);
384
385
        return $record;
386
    }
387
388
    /**
389
     * Returns the MD5 string for a file previously stored to the database
390
     *
391
     * @param $id
392
     * @return string
393
     */
394
    private function getMd5ForFile($id)
395
    {
396
        $result = $this->db->command(['filemd5' => $id, 'root' => $this->prefix]);
397
        return $result['md5'];
398
    }
399
}
400