Commit 78e69811 authored by Jeremy Mikola's avatar Jeremy Mikola

PHPLIB-232: Support typeMap option on GridFS Bucket

parent 31a8e20d
......@@ -31,6 +31,12 @@ replacement:
resource: "bucket"
parent: "database"
---
source:
file: apiargs-common-option.yaml
ref: typeMap
replacement:
parent: "database"
---
source:
file: apiargs-common-option.yaml
ref: writeConcern
......
......@@ -31,6 +31,10 @@ replacement:
resource: "bucket"
parent: "database"
---
source:
file: apiargs-MongoDBClient-method-construct-driverOptions.yaml
ref: typeMap
---
source:
file: apiargs-common-option.yaml
ref: writeConcern
......
......@@ -42,8 +42,8 @@ Errors/Exceptions
Behavior
--------
The selected bucket inherits options such as read preference and write concern
from the :phpclass:`Database <MongoDB\\Database>` object. Options may be
The selected bucket inherits options such as read preference and type
mapping from the :phpclass:`Database <MongoDB\\Database>` object. Options may be
overridden via the ``$options`` parameter.
Example
......
......@@ -19,7 +19,7 @@ Definition
.. code-block:: php
function getFileDocumentForStream($stream): object
function getFileDocumentForStream($stream): array|object
This method has the following parameters:
......@@ -28,7 +28,8 @@ Definition
Return Values
-------------
The metadata document associated with the GridFS stream.
The metadata document associated with the GridFS stream. The return type will
depend on the bucket's ``typeMap`` option.
.. todo: add examples
......
......@@ -28,8 +28,8 @@ Definition
Return Values
-------------
The ``_id`` field of the metadata document associated with the GridFS
stream.
The ``_id`` field of the metadata document associated with the GridFS stream.
The return type will depend on the bucket's ``typeMap`` option.
.. todo: add examples
......
......@@ -317,6 +317,7 @@ class Database
$options += [
'readConcern' => $this->readConcern,
'readPreference' => $this->readPreference,
'typeMap' => $this->typeMap,
'writeConcern' => $this->writeConcern,
];
......
......@@ -23,6 +23,11 @@ class Bucket
{
private static $defaultBucketName = 'fs';
private static $defaultChunkSizeBytes = 261120;
private static $defaultTypeMap = [
'array' => 'MongoDB\Model\BSONArray',
'document' => 'MongoDB\Model\BSONDocument',
'root' => 'MongoDB\Model\BSONDocument',
];
private static $streamWrapperProtocol = 'gridfs';
private $collectionWrapper;
......@@ -32,6 +37,7 @@ class Bucket
private $chunkSizeBytes;
private $readConcern;
private $readPreference;
private $typeMap;
private $writeConcern;
/**
......@@ -49,6 +55,8 @@ class Bucket
*
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
*
* * typeMap (array): Default type map for cursors and BSON documents.
*
* * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
*
* @param Manager $manager Manager instance from the driver
......@@ -79,6 +87,10 @@ class Bucket
throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
}
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
}
if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
}
......@@ -89,9 +101,10 @@ class Bucket
$this->chunkSizeBytes = $options['chunkSizeBytes'];
$this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern();
$this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference();
$this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap;
$this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern();
$collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'writeConcern' => 1]);
$collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'typeMap' => 1, 'writeConcern' => 1]);
$this->collectionWrapper = new CollectionWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions);
$this->registerStreamWrapper();
......@@ -112,6 +125,7 @@ class Bucket
'chunkSizeBytes' => $this->chunkSizeBytes,
'readConcern' => $this->readConcern,
'readPreference' => $this->readPreference,
'typeMap' => $this->typeMap,
'writeConcern' => $this->writeConcern,
];
}
......@@ -233,22 +247,15 @@ class Bucket
* Gets the file document of the GridFS file associated with a stream.
*
* @param resource $stream GridFS stream
* @return stdClass
* @return array|object
* @throws InvalidArgumentException
*/
public function getFileDocumentForStream($stream)
{
if ( ! is_resource($stream) || get_resource_type($stream) != "stream") {
throw InvalidArgumentException::invalidType('$stream', $stream, 'resource');
}
$file = $this->getRawFileDocumentForStream($stream);
$metadata = stream_get_meta_data($stream);
if (!$metadata['wrapper_data'] instanceof StreamWrapper) {
throw InvalidArgumentException::invalidType('$stream wrapper data', $metadata['wrapper_data'], 'MongoDB\Driver\GridFS\StreamWrapper');
}
return $metadata['wrapper_data']->getFile();
// Filter the raw document through the specified type map
return \MongoDB\BSON\toPHP(\MongoDB\BSON\fromPHP($file), $this->typeMap);
}
/**
......@@ -261,7 +268,13 @@ class Bucket
*/
public function getFileIdForStream($stream)
{
$file = $this->getFileDocumentForStream($stream);
$file = $this->getRawFileDocumentForStream($stream);
/* Filter the raw document through the specified type map, but override
* the root type so we can reliably access the ID.
*/
$typeMap = ['root' => 'stdClass'] + $this->typeMap;
$file = \MongoDB\BSON\toPHP(\MongoDB\BSON\fromPHP($file), $typeMap);
if ( ! isset($file->_id) && ! property_exists($file, '_id')) {
throw new CorruptFileException('file._id does not exist');
......@@ -466,6 +479,31 @@ class Bucket
return sprintf('%s.%s.files', $this->databaseName, $this->bucketName);
}
/**
* Gets the file document of the GridFS file associated with a stream.
*
* This returns the raw document from the StreamWrapper, which does not
* respect the Bucket's type map.
*
* @param resource $stream GridFS stream
* @return stdClass
* @throws InvalidArgumentException
*/
private function getRawFileDocumentForStream($stream)
{
if ( ! is_resource($stream) || get_resource_type($stream) != "stream") {
throw InvalidArgumentException::invalidType('$stream', $stream, 'resource');
}
$metadata = stream_get_meta_data($stream);
if (!$metadata['wrapper_data'] instanceof StreamWrapper) {
throw InvalidArgumentException::invalidType('$stream wrapper data', $metadata['wrapper_data'], 'MongoDB\Driver\GridFS\StreamWrapper');
}
return $metadata['wrapper_data']->getFile();
}
/**
* Opens a readable stream for the GridFS file.
*
......
......@@ -68,8 +68,8 @@ class CollectionWrapper
*/
public function dropCollections()
{
$this->filesCollection->drop();
$this->chunksCollection->drop();
$this->filesCollection->drop(['typeMap' => []]);
$this->chunksCollection->drop(['typeMap' => []]);
}
/**
......@@ -284,6 +284,7 @@ class CollectionWrapper
return null === $this->filesCollection->findOne([], [
'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
'projection' => ['_id' => 1],
'typeMap' => [],
]);
}
}
......@@ -57,6 +57,10 @@ class BucketFunctionalTest extends FunctionalTestCase
$options[][] = ['readPreference' => $value];
}
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['typeMap' => $value];
}
foreach ($this->getInvalidWriteConcernValues() as $value) {
$options[][] = ['writeConcern' => $value];
}
......@@ -314,6 +318,16 @@ class BucketFunctionalTest extends FunctionalTestCase
$this->assertSameDocuments($expected, $cursor);
}
public function testFindUsesTypeMap()
{
$this->bucket->uploadFromStream('a', $this->createStream('foo'));
$cursor = $this->bucket->find();
$fileDocument = current($cursor->toArray());
$this->assertInstanceOf('MongoDB\Model\BSONDocument', $fileDocument);
}
public function testGetBucketNameWithCustomValue()
{
$bucket = new Bucket($this->manager, $this->getDatabaseName(), ['bucketName' => 'custom_fs']);
......@@ -331,6 +345,18 @@ class BucketFunctionalTest extends FunctionalTestCase
$this->assertEquals($this->getDatabaseName(), $this->bucket->getDatabaseName());
}
public function testGetFileDocumentForStreamUsesTypeMap()
{
$metadata = ['foo' => 'bar'];
$stream = $this->bucket->openUploadStream('filename', ['_id' => 1, 'metadata' => $metadata]);
$fileDocument = $this->bucket->getFileDocumentForStream($stream);
$this->assertInstanceOf('MongoDB\Model\BSONDocument', $fileDocument);
$this->assertInstanceOf('MongoDB\Model\BSONDocument', $fileDocument['metadata']);
$this->assertSame(['foo' => 'bar'], $fileDocument['metadata']->getArrayCopy());
}
public function testGetFileDocumentForStreamWithReadableStream()
{
$metadata = ['foo' => 'bar'];
......@@ -366,6 +392,16 @@ class BucketFunctionalTest extends FunctionalTestCase
$this->bucket->getFileDocumentForStream($stream);
}
public function testGetFileIdForStreamUsesTypeMap()
{
$stream = $this->bucket->openUploadStream('filename', ['_id' => ['x' => 1]]);
$id = $this->bucket->getFileIdForStream($stream);
$this->assertInstanceOf('MongoDB\Model\BSONDocument', $id);
$this->assertSame(['x' => 1], $id->getArrayCopy());
}
public function testGetFileIdForStreamWithReadableStream()
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream('foobar'));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment