Skip to content

Commit 7da7372

Browse files
authored
PHPLIB-1220: Support codec option for GridFS buckets (#1149)
* PHPLIB-1220: Support codec option for GridFS buckets * Support null as an uploadDate in test codec * Add test to ensure bucket codec can be overridden * Omit instanceof check for codec * Forbid specifying codec and typeMap in bucket options * Use null as default value for length * Convert embedded metadata document to stdClass
1 parent f38224d commit 7da7372

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed

src/GridFS/Bucket.php

+27
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
namespace MongoDB\GridFS;
1919

2020
use Iterator;
21+
use MongoDB\BSON\Document;
22+
use MongoDB\Codec\DocumentCodec;
2123
use MongoDB\Collection;
2224
use MongoDB\Driver\CursorInterface;
2325
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
@@ -35,6 +37,7 @@
3537
use MongoDB\Operation\Find;
3638

3739
use function array_intersect_key;
40+
use function array_key_exists;
3841
use function assert;
3942
use function fopen;
4043
use function get_resource_type;
@@ -75,6 +78,8 @@ class Bucket
7578

7679
private const STREAM_WRAPPER_PROTOCOL = 'gridfs';
7780

81+
private ?DocumentCodec $codec = null;
82+
7883
private CollectionWrapper $collectionWrapper;
7984

8085
private string $databaseName;
@@ -142,6 +147,10 @@ public function __construct(Manager $manager, string $databaseName, array $optio
142147
throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes']));
143148
}
144149

150+
if (isset($options['codec']) && ! $options['codec'] instanceof DocumentCodec) {
151+
throw InvalidArgumentException::invalidType('"codec" option', $options['codec'], DocumentCodec::class);
152+
}
153+
145154
if (! is_bool($options['disableMD5'])) {
146155
throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
147156
}
@@ -162,10 +171,15 @@ public function __construct(Manager $manager, string $databaseName, array $optio
162171
throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
163172
}
164173

174+
if (isset($options['codec']) && isset($options['typeMap'])) {
175+
throw InvalidArgumentException::cannotCombineCodecAndTypeMap();
176+
}
177+
165178
$this->manager = $manager;
166179
$this->databaseName = $databaseName;
167180
$this->bucketName = $options['bucketName'];
168181
$this->chunkSizeBytes = $options['chunkSizeBytes'];
182+
$this->codec = $options['codec'] ?? null;
169183
$this->disableMD5 = $options['disableMD5'];
170184
$this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern();
171185
$this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference();
@@ -188,6 +202,7 @@ public function __debugInfo()
188202
{
189203
return [
190204
'bucketName' => $this->bucketName,
205+
'codec' => $this->codec,
191206
'databaseName' => $this->databaseName,
192207
'disableMD5' => $this->disableMD5,
193208
'manager' => $this->manager,
@@ -309,6 +324,10 @@ public function drop()
309324
*/
310325
public function find($filter = [], array $options = [])
311326
{
327+
if ($this->codec && ! array_key_exists('codec', $options)) {
328+
$options['codec'] = $this->codec;
329+
}
330+
312331
return $this->collectionWrapper->findFiles($filter, $options);
313332
}
314333

@@ -326,6 +345,10 @@ public function find($filter = [], array $options = [])
326345
*/
327346
public function findOne($filter = [], array $options = [])
328347
{
348+
if ($this->codec && ! array_key_exists('codec', $options)) {
349+
$options['codec'] = $this->codec;
350+
}
351+
329352
return $this->collectionWrapper->findOneFile($filter, $options);
330353
}
331354

@@ -381,6 +404,10 @@ public function getFileDocumentForStream($stream)
381404
{
382405
$file = $this->getRawFileDocumentForStream($stream);
383406

407+
if ($this->codec) {
408+
return $this->codec->decode(Document::fromPHP($file));
409+
}
410+
384411
// Filter the raw document through the specified type map
385412
return apply_type_map_to_document($file, $this->typeMap);
386413
}

src/GridFS/WritableStream.php

+2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ public function __construct(CollectionWrapper $collectionWrapper, string $filena
136136
'_id' => $options['_id'],
137137
'chunkSize' => $this->chunkSize,
138138
'filename' => $filename,
139+
'length' => null,
140+
'uploadDate' => null,
139141
] + array_intersect_key($options, ['aliases' => 1, 'contentType' => 1, 'metadata' => 1]);
140142
}
141143

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Fixtures\Codec;
4+
5+
use DateTimeImmutable;
6+
use MongoDB\BSON\Document;
7+
use MongoDB\BSON\UTCDateTime;
8+
use MongoDB\Codec\DecodeIfSupported;
9+
use MongoDB\Codec\DocumentCodec;
10+
use MongoDB\Codec\EncodeIfSupported;
11+
use MongoDB\Exception\UnsupportedValueException;
12+
use MongoDB\Tests\Fixtures\Document\TestFile;
13+
14+
use function assert;
15+
16+
final class TestFileCodec implements DocumentCodec
17+
{
18+
use DecodeIfSupported;
19+
use EncodeIfSupported;
20+
21+
public function canDecode($value): bool
22+
{
23+
return $value instanceof Document;
24+
}
25+
26+
public function decode($value): TestFile
27+
{
28+
if (! $value instanceof Document) {
29+
throw UnsupportedValueException::invalidDecodableValue($value);
30+
}
31+
32+
$fileObject = new TestFile();
33+
$fileObject->id = $value->get('_id');
34+
$fileObject->length = (int) $value->get('length');
35+
$fileObject->chunkSize = (int) $value->get('chunkSize');
36+
$fileObject->filename = (string) $value->get('filename');
37+
38+
$uploadDate = $value->get('uploadDate');
39+
if ($uploadDate instanceof UTCDateTime) {
40+
$fileObject->uploadDate = DateTimeImmutable::createFromMutable($uploadDate->toDateTime());
41+
}
42+
43+
if ($value->has('metadata')) {
44+
$metadata = $value->get('metadata');
45+
assert($metadata instanceof Document);
46+
$fileObject->metadata = $metadata->toPHP();
47+
}
48+
49+
return $fileObject;
50+
}
51+
52+
public function canEncode($value): bool
53+
{
54+
return $value instanceof TestFile;
55+
}
56+
57+
public function encode($value): Document
58+
{
59+
if (! $value instanceof TestFile) {
60+
throw UnsupportedValueException::invalidEncodableValue($value);
61+
}
62+
63+
return Document::fromPHP([
64+
'_id' => $value->id,
65+
'length' => $value->length,
66+
'chunkSize' => $value->chunkSize,
67+
'uploadDate' => new UTCDateTime($value->uploadDate),
68+
'filename' => $value->filename,
69+
]);
70+
}
71+
}

tests/Fixtures/Document/TestFile.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
/*
3+
* Copyright 2023-present MongoDB, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace MongoDB\Tests\Fixtures\Document;
19+
20+
use DateTimeImmutable;
21+
use stdClass;
22+
23+
final class TestFile
24+
{
25+
public $id;
26+
public int $length;
27+
public int $chunkSize;
28+
public ?DateTimeImmutable $uploadDate = null;
29+
public string $filename;
30+
public ?stdClass $metadata = null;
31+
}

tests/GridFS/BucketFunctionalTest.php

+125
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
use MongoDB\Model\BSONDocument;
1616
use MongoDB\Model\IndexInfo;
1717
use MongoDB\Operation\ListIndexes;
18+
use MongoDB\Tests\Fixtures\Codec\TestDocumentCodec;
19+
use MongoDB\Tests\Fixtures\Codec\TestFileCodec;
20+
use MongoDB\Tests\Fixtures\Document\TestFile;
21+
use stdClass;
1822

1923
use function array_merge;
2024
use function call_user_func;
@@ -68,6 +72,7 @@ public function provideInvalidConstructorOptions()
6872
return $this->createOptionDataProvider([
6973
'bucketName' => $this->getInvalidStringValues(true),
7074
'chunkSizeBytes' => $this->getInvalidIntegerValues(true),
75+
'codec' => $this->getInvalidDocumentCodecValues(),
7176
'disableMD5' => $this->getInvalidBooleanValues(true),
7277
'readConcern' => $this->getInvalidReadConcernValues(),
7378
'readPreference' => $this->getInvalidReadPreferenceValues(),
@@ -83,6 +88,17 @@ public function testConstructorShouldRequireChunkSizeBytesOptionToBePositive():
8388
new Bucket($this->manager, $this->getDatabaseName(), ['chunkSizeBytes' => 0]);
8489
}
8590

91+
public function testConstructorWithCodecAndTypeMapOptions(): void
92+
{
93+
$options = [
94+
'codec' => new TestDocumentCodec(),
95+
'typeMap' => ['root' => 'array', 'document' => 'array'],
96+
];
97+
98+
$this->expectExceptionObject(InvalidArgumentException::cannotCombineCodecAndTypeMap());
99+
new Bucket($this->manager, $this->getDatabaseName(), $options);
100+
}
101+
86102
/** @dataProvider provideInputDataAndExpectedChunks */
87103
public function testDelete($input, $expectedChunks): void
88104
{
@@ -317,6 +333,41 @@ public function testFindUsesTypeMap(): void
317333
$this->assertInstanceOf(BSONDocument::class, $fileDocument);
318334
}
319335

336+
public function testFindUsesCodec(): void
337+
{
338+
$this->bucket->uploadFromStream('a', $this->createStream('foo'));
339+
340+
$cursor = $this->bucket->find([], ['codec' => new TestFileCodec()]);
341+
$fileDocument = current($cursor->toArray());
342+
343+
$this->assertInstanceOf(TestFile::class, $fileDocument);
344+
$this->assertSame('a', $fileDocument->filename);
345+
}
346+
347+
public function testFindInheritsBucketCodec(): void
348+
{
349+
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['codec' => new TestFileCodec()]);
350+
$bucket->uploadFromStream('a', $this->createStream('foo'));
351+
352+
$cursor = $bucket->find();
353+
$fileDocument = current($cursor->toArray());
354+
355+
$this->assertInstanceOf(TestFile::class, $fileDocument);
356+
$this->assertSame('a', $fileDocument->filename);
357+
}
358+
359+
public function testFindResetsInheritedBucketCodec(): void
360+
{
361+
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['codec' => new TestFileCodec()]);
362+
$bucket->uploadFromStream('a', $this->createStream('foo'));
363+
364+
$cursor = $bucket->find([], ['codec' => null]);
365+
$fileDocument = current($cursor->toArray());
366+
367+
$this->assertInstanceOf(BSONDocument::class, $fileDocument);
368+
$this->assertSame('a', $fileDocument->filename);
369+
}
370+
320371
public function testFindOne(): void
321372
{
322373
$this->bucket->uploadFromStream('a', $this->createStream('foo'));
@@ -339,6 +390,64 @@ public function testFindOne(): void
339390
$this->assertSameDocument(['filename' => 'b', 'length' => 6], $fileDocument);
340391
}
341392

393+
public function testFindOneUsesCodec(): void
394+
{
395+
$this->bucket->uploadFromStream('a', $this->createStream('foo'));
396+
$this->bucket->uploadFromStream('b', $this->createStream('foobar'));
397+
$this->bucket->uploadFromStream('c', $this->createStream('foobarbaz'));
398+
399+
$fileDocument = $this->bucket->findOne(
400+
['length' => ['$lte' => 6]],
401+
[
402+
'sort' => ['length' => -1],
403+
'codec' => new TestFileCodec(),
404+
],
405+
);
406+
407+
$this->assertInstanceOf(TestFile::class, $fileDocument);
408+
$this->assertSame('b', $fileDocument->filename);
409+
$this->assertSame(6, $fileDocument->length);
410+
}
411+
412+
public function testFindOneInheritsBucketCodec(): void
413+
{
414+
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['codec' => new TestFileCodec()]);
415+
416+
$bucket->uploadFromStream('a', $this->createStream('foo'));
417+
$bucket->uploadFromStream('b', $this->createStream('foobar'));
418+
$bucket->uploadFromStream('c', $this->createStream('foobarbaz'));
419+
420+
$fileDocument = $bucket->findOne(
421+
['length' => ['$lte' => 6]],
422+
['sort' => ['length' => -1]],
423+
);
424+
425+
$this->assertInstanceOf(TestFile::class, $fileDocument);
426+
$this->assertSame('b', $fileDocument->filename);
427+
$this->assertSame(6, $fileDocument->length);
428+
}
429+
430+
public function testFindOneResetsInheritedBucketCodec(): void
431+
{
432+
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['codec' => new TestFileCodec()]);
433+
434+
$bucket->uploadFromStream('a', $this->createStream('foo'));
435+
$bucket->uploadFromStream('b', $this->createStream('foobar'));
436+
$bucket->uploadFromStream('c', $this->createStream('foobarbaz'));
437+
438+
$fileDocument = $bucket->findOne(
439+
['length' => ['$lte' => 6]],
440+
[
441+
'sort' => ['length' => -1],
442+
'codec' => null,
443+
],
444+
);
445+
446+
$this->assertInstanceOf(BSONDocument::class, $fileDocument);
447+
$this->assertSame('b', $fileDocument->filename);
448+
$this->assertSame(6, $fileDocument->length);
449+
}
450+
342451
public function testGetBucketNameWithCustomValue(): void
343452
{
344453
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['bucketName' => 'custom_fs']);
@@ -388,6 +497,22 @@ public function testGetFileDocumentForStreamUsesTypeMap(): void
388497
$this->assertSame(['foo' => 'bar'], $fileDocument['metadata']->getArrayCopy());
389498
}
390499

500+
public function testGetFileDocumentForStreamUsesCodec(): void
501+
{
502+
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['codec' => new TestFileCodec()]);
503+
504+
$metadata = ['foo' => 'bar'];
505+
$stream = $bucket->openUploadStream('filename', ['_id' => 1, 'metadata' => $metadata]);
506+
507+
$fileDocument = $bucket->getFileDocumentForStream($stream);
508+
509+
$this->assertInstanceOf(TestFile::class, $fileDocument);
510+
511+
$this->assertSame('filename', $fileDocument->filename);
512+
$this->assertInstanceOf(stdClass::class, $fileDocument->metadata);
513+
$this->assertSame($metadata, (array) $fileDocument->metadata);
514+
}
515+
391516
public function testGetFileDocumentForStreamWithReadableStream(): void
392517
{
393518
$metadata = ['foo' => 'bar'];

0 commit comments

Comments
 (0)