Completed
Push — master ( 4afe07...53b41d )
by Andreas
02:43
created

MongoGridFS::insertChunksFromFile()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 24
rs 8.9713
cc 2
eloc 16
nc 2
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);
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...
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
        $md5 = null;
244
        $file = $this->insertFile($record, $options);
245
        $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 244 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...
246
247
        // Add length and MD5 if they were not present before
248
        $update = [];
249
        if (! isset($record['length'])) {
250
            $update['length'] = $length;
251
        }
252
        if (! isset($record['md5'])) {
253
            $update['md5'] = $md5;
254
        }
255
256
        if (count($update)) {
257
            $this->update(['_id' => $file['_id']], ['$set' => $update]);
258
        }
259
260
        return $file['_id'];
261
    }
262
263
    /**
264
     * Saves an uploaded file directly from a POST to the database
265
     *
266
     * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
267
     * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
268
     * @param array $metadata An array of extra fields for the uploaded file.
269
     * @return mixed Returns the _id of the uploaded file.
270
     * @throws MongoGridFSException
271
     */
272
    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...
273
    {
274
        if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
275
            throw new MongoGridFSException("Could not find uploaded file $name");
276
        }
277
        if (! isset($_FILES[$name]['tmp_name'])) {
278
            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
279
        }
280
281
        $uploadedFile = $_FILES[$name];
282
        $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
283
        $uploadedFile['name'] = (array) $uploadedFile['name'];
284
285
        if (count($uploadedFile['tmp_name']) > 1) {
286
            foreach ($uploadedFile['tmp_name'] as $key => $file) {
287
                $metadata['filename'] = $uploadedFile['name'][$key];
288
                $this->storeFile($file, $metadata);
289
            }
290
291
            return null;
292
        } else {
293
            $metadata += ['filename' => array_pop($uploadedFile['name'])];
294
            return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
295
        }
296
    }
297
298
    /**
299
     * Creates the index on the chunks collection
300
     */
301
    private function createChunksIndex()
302
    {
303
        $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
304
    }
305
306
    /**
307
     * Inserts a single chunk into the database
308
     *
309
     * @param mixed $fileId
310
     * @param string $data
311
     * @param int $chunkNumber
312
     * @return array|bool
313
     */
314
    private function insertChunk($fileId, $data, $chunkNumber)
315
    {
316
        $chunk = [
317
            'files_id' => $fileId,
318
            'n' => $chunkNumber,
319
            'data' => new MongoBinData($data),
320
        ];
321
        return $this->chunks->insert($chunk);
322
    }
323
324
    /**
325
     * Splits a string into chunks and writes them to the database
326
     *
327
     * @param string $bytes
328
     * @param array $record
329
     */
330
    private function insertChunksFromBytes($bytes, $record)
331
    {
332
        $chunkSize = $record['chunkSize'];
333
        $fileId = $record['_id'];
334
        $i = 0;
335
336
        $chunks = str_split($bytes, $chunkSize);
337
        foreach ($chunks as $chunk) {
338
            $this->insertChunk($fileId, $chunk, $i++);
339
        }
340
    }
341
342
    /**
343
     * Reads chunks from a file and writes them to the database
344
     *
345
     * @param resource $handle
346
     * @param array $record
347
     * @param string $md5
348
     * @return int Returns the number of bytes written to the database
349
     */
350
    private function insertChunksFromFile($handle, $record, &$md5)
351
    {
352
        $written = 0;
353
        $offset = 0;
354
        $i = 0;
355
356
        $fileId = $record['_id'];
357
        $chunkSize = $record['chunkSize'];
358
359
        $hash = hash_init('md5');
360
361
        rewind($handle);
362
        while (! feof($handle)) {
363
            $data = stream_get_contents($handle, $chunkSize);
364
            hash_update($hash, $data);
365
            $this->insertChunk($fileId, $data, $i++);
366
            $written += strlen($data);
367
            $offset += $chunkSize;
368
        }
369
370
        $md5 = hash_final($hash);
371
372
        return $written;
373
    }
374
375
    /**
376
     * Writes a file record to the database
377
     *
378
     * @param $record
379
     * @param array $options
380
     * @return array
381
     */
382
    private function insertFile($record, array $options = [])
383
    {
384
        $record += [
385
            '_id' => new MongoId(),
386
            'uploadDate' => new MongoDate(),
387
            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
388
        ];
389
390
        $this->insert($record, $options);
391
392
        return $record;
393
    }
394
}
395