Skip to content

Commit b1147cd

Browse files
authored
PHPLIB-1206 Add bucket alises for context resolver using GridFS StreamWrapper (#1138)
1 parent 91f3367 commit b1147cd

10 files changed

+678
-50
lines changed

docs/reference/class/MongoDBGridFSBucket.txt

+1
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ Methods
5555
/reference/method/MongoDBGridFSBucket-openDownloadStream
5656
/reference/method/MongoDBGridFSBucket-openDownloadStreamByName
5757
/reference/method/MongoDBGridFSBucket-openUploadStream
58+
/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias
5859
/reference/method/MongoDBGridFSBucket-rename
5960
/reference/method/MongoDBGridFSBucket-uploadFromStream
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
===========================================================
2+
MongoDB\\GridFS\\Bucket::registerGlobalStreamWrapperAlias()
3+
===========================================================
4+
5+
.. versionadded:: 1.18
6+
7+
.. default-domain:: mongodb
8+
9+
.. contents:: On this page
10+
:local:
11+
:backlinks: none
12+
:depth: 1
13+
:class: singlecol
14+
15+
Definition
16+
----------
17+
18+
.. phpmethod:: MongoDB\\GridFS\\Bucket::registerGlobalStreamWrapperAlias()
19+
20+
Registers an alias for the bucket, which enables files within the bucket to
21+
be accessed using a basic filename string (e.g.
22+
`gridfs://<bucket-alias>/<filename>`).
23+
24+
.. code-block:: php
25+
26+
function registerGlobalStreamWrapperAlias(string $alias): void
27+
28+
Parameters
29+
----------
30+
31+
``$alias`` : array
32+
A non-empty string used to identify the GridFS bucket when accessing files
33+
using the ``gridfs://`` stream wrapper.
34+
35+
Behavior
36+
--------
37+
38+
After registering an alias for the bucket, the most recent revision of a file
39+
can be accessed using a filename string in the form ``gridfs://<bucket-alias>/<filename>``.
40+
41+
Supported stream functions:
42+
43+
- :php:`copy() <copy>`
44+
- :php:`file_exists() <file_exists>`
45+
- :php:`file_get_contents() <file_get_contents>`
46+
- :php:`file_put_contents() <file_put_contents>`
47+
- :php:`filemtime() <filemtime>`
48+
- :php:`filesize() <filesize>`
49+
- :php:`file() <file>`
50+
- :php:`fopen() <fopen>` (with "r", "rb", "w", and "wb" modes)
51+
52+
In read mode, the stream context can contain the option ``gridfs['revision']``
53+
to specify the revision number of the file to read. If omitted, the most recent
54+
revision is read (revision ``-1``).
55+
56+
In write mode, the stream context can contain the option ``gridfs['chunkSizeBytes']``.
57+
If omitted, the defaults are inherited from the ``Bucket`` instance option.
58+
59+
Example
60+
-------
61+
62+
Read and write to a GridFS bucket using the ``gridfs://`` stream wrapper
63+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
64+
65+
The following example demonstrates how to register an alias for a GridFS bucket
66+
and use the functions ``file_exists()``, ``file_get_contents()``, and
67+
``file_put_contents()`` to read and write to the bucket.
68+
69+
Each call to these functions makes a request to the server.
70+
71+
.. code-block:: php
72+
73+
<?php
74+
75+
$database = (new MongoDB\Client)->selectDatabase('test');
76+
$bucket = $database->selectGridFSBucket();
77+
78+
$bucket->registerGlobalStreamWrapperAlias('mybucket');
79+
80+
var_dump(file_exists('gridfs://mybucket/hello.txt'));
81+
82+
file_put_contents('gridfs://mybucket/hello.txt', 'Hello, GridFS!');
83+
84+
var_dump(file_exists('gridfs://mybucket/hello.txt'));
85+
86+
echo file_get_contents('gridfs://mybucket/hello.txt');
87+
88+
The output would then resemble:
89+
90+
.. code-block:: none
91+
92+
bool(false)
93+
bool(true)
94+
Hello, GridFS!
95+
96+
Read a specific revision of a file
97+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
98+
99+
Using a stream context, you can specify the revision number of the file to
100+
read. If omitted, the most recent revision is read.
101+
102+
.. code-block:: php
103+
104+
<?php
105+
106+
$database = (new MongoDB\Client)->selectDatabase('test');
107+
$bucket = $database->selectGridFSBucket();
108+
109+
$bucket->registerGlobalStreamWrapperAlias('mybucket');
110+
111+
// Creating revision 0
112+
$handle = fopen('gridfs://mybucket/hello.txt', 'w');
113+
fwrite($handle, 'Hello, GridFS! (v0)');
114+
fclose($handle);
115+
116+
// Creating revision 1
117+
$handle = fopen('gridfs://mybucket/hello.txt', 'w');
118+
fwrite($handle, 'Hello, GridFS! (v1)');
119+
fclose($handle);
120+
121+
// Read revision 0
122+
$context = stream_context_create([
123+
'gridfs' => ['revision' => 0],
124+
]);
125+
$handle = fopen('gridfs://mybucket/hello.txt', 'r', false, $context);
126+
echo fread($handle, 1024);
127+
128+
The output would then resemble:
129+
130+
.. code-block:: none
131+
132+
Hello, GridFS! (v0)

examples/gridfs-stream-wrapper.php

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/**
4+
* For applications that need to interact with GridFS using only a filename string,
5+
* a bucket can be registered with an alias. Files can then be accessed using the
6+
* following pattern: gridfs://<bucket-alias>/<filename>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace MongoDB\Examples;
12+
13+
use MongoDB\Client;
14+
15+
use function file_exists;
16+
use function file_get_contents;
17+
use function file_put_contents;
18+
use function getenv;
19+
use function stream_context_create;
20+
21+
use const PHP_EOL;
22+
23+
require __DIR__ . '/../vendor/autoload.php';
24+
25+
$client = new Client(getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/');
26+
$bucket = $client->test->selectGridFSBucket();
27+
$bucket->drop();
28+
29+
// Register the alias "mybucket" for default bucket of the "test" database
30+
$bucket->registerGlobalStreamWrapperAlias('mybucket');
31+
32+
echo 'File exists: ';
33+
echo file_exists('gridfs://mybucket/hello.txt') ? 'yes' : 'no';
34+
echo PHP_EOL;
35+
36+
echo 'Writing file';
37+
file_put_contents('gridfs://mybucket/hello.txt', 'Hello, GridFS!');
38+
echo PHP_EOL;
39+
40+
echo 'File exists: ';
41+
echo file_exists('gridfs://mybucket/hello.txt') ? 'yes' : 'no';
42+
echo PHP_EOL;
43+
44+
echo 'Reading file: ';
45+
echo file_get_contents('gridfs://mybucket/hello.txt');
46+
echo PHP_EOL;
47+
48+
echo 'Writing new version of the file';
49+
file_put_contents('gridfs://mybucket/hello.txt', 'Hello, GridFS! (v2)');
50+
echo PHP_EOL;
51+
52+
echo 'Reading new version of the file: ';
53+
echo file_get_contents('gridfs://mybucket/hello.txt');
54+
echo PHP_EOL;
55+
56+
echo 'Reading previous version of the file: ';
57+
$context = stream_context_create(['gridfs' => ['revision' => -2]]);
58+
echo file_get_contents('gridfs://mybucket/hello.txt', false, $context);
59+
echo PHP_EOL;

psalm-baseline.xml

+10-14
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@
8181
<code><![CDATA[$options['revision']]]></code>
8282
<code><![CDATA[$options['revision']]]></code>
8383
</MixedArgument>
84+
<MixedArgumentTypeCoercion>
85+
<code>$context</code>
86+
</MixedArgumentTypeCoercion>
8487
</file>
8588
<file src="src/GridFS/ReadableStream.php">
8689
<MixedArgument>
@@ -89,20 +92,13 @@
8992
</MixedArgument>
9093
</file>
9194
<file src="src/GridFS/StreamWrapper.php">
92-
<MixedArgument>
93-
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
94-
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
95-
<code><![CDATA[$context[$this->protocol]['file']]]></code>
96-
<code><![CDATA[$context[$this->protocol]['filename']]]></code>
97-
<code><![CDATA[$context[$this->protocol]['options']]]></code>
98-
</MixedArgument>
99-
<MixedArrayAccess>
100-
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
101-
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
102-
<code><![CDATA[$context[$this->protocol]['file']]]></code>
103-
<code><![CDATA[$context[$this->protocol]['filename']]]></code>
104-
<code><![CDATA[$context[$this->protocol]['options']]]></code>
105-
</MixedArrayAccess>
95+
<InvalidArgument>
96+
<code>$context</code>
97+
<code>$context</code>
98+
</InvalidArgument>
99+
<MixedAssignment>
100+
<code>$context</code>
101+
</MixedAssignment>
106102
</file>
107103
<file src="src/Model/BSONArray.php">
108104
<MixedAssignment>

src/GridFS/Bucket.php

+72
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use MongoDB\Exception\UnsupportedException;
3232
use MongoDB\GridFS\Exception\CorruptFileException;
3333
use MongoDB\GridFS\Exception\FileNotFoundException;
34+
use MongoDB\GridFS\Exception\LogicException;
3435
use MongoDB\GridFS\Exception\StreamException;
3536
use MongoDB\Model\BSONArray;
3637
use MongoDB\Model\BSONDocument;
@@ -39,6 +40,7 @@
3940
use function array_intersect_key;
4041
use function array_key_exists;
4142
use function assert;
43+
use function explode;
4244
use function fopen;
4345
use function get_resource_type;
4446
use function in_array;
@@ -54,6 +56,7 @@
5456
use function MongoDB\BSON\toJSON;
5557
use function property_exists;
5658
use function sprintf;
59+
use function str_contains;
5760
use function stream_context_create;
5861
use function stream_copy_to_stream;
5962
use function stream_get_meta_data;
@@ -587,6 +590,29 @@ public function openUploadStream(string $filename, array $options = [])
587590
return fopen($path, 'w', false, $context);
588591
}
589592

593+
/**
594+
* Register an alias to enable basic filename access for this bucket.
595+
*
596+
* For applications that need to interact with GridFS using only a filename
597+
* string, a bucket can be registered with an alias. Files can then be
598+
* accessed using the following pattern:
599+
*
600+
* gridfs://<bucket-alias>/<filename>
601+
*
602+
* Read operations will always target the most recent revision of a file.
603+
*
604+
* @param non-empty-string string $alias The alias to use for the bucket
605+
*/
606+
public function registerGlobalStreamWrapperAlias(string $alias): void
607+
{
608+
if ($alias === '' || str_contains($alias, '/')) {
609+
throw new InvalidArgumentException(sprintf('The bucket alias must be a non-empty string without any slash, "%s" given', $alias));
610+
}
611+
612+
// Use a closure to expose the private method into another class
613+
StreamWrapper::setContextResolver($alias, fn (string $path, string $mode, array $context) => $this->resolveStreamContext($path, $mode, $context));
614+
}
615+
590616
/**
591617
* Renames the GridFS file with the specified ID.
592618
*
@@ -756,4 +782,50 @@ private function registerStreamWrapper(): void
756782

757783
StreamWrapper::register(self::STREAM_WRAPPER_PROTOCOL);
758784
}
785+
786+
/**
787+
* Create a stream context from the path and mode provided to fopen().
788+
*
789+
* @see StreamWrapper::setContextResolver()
790+
*
791+
* @param string $path The full url provided to fopen(). It contains the filename.
792+
* gridfs://database_name/collection_name.files/file_name
793+
* @param array{revision?: int, chunkSizeBytes?: int, disableMD5?: bool} $context The options provided to fopen()
794+
*
795+
* @return array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}
796+
*
797+
* @throws FileNotFoundException
798+
* @throws LogicException
799+
*/
800+
private function resolveStreamContext(string $path, string $mode, array $context): array
801+
{
802+
// Fallback to an empty filename if the path does not contain one: "gridfs://alias"
803+
$filename = explode('/', $path, 4)[3] ?? '';
804+
805+
if ($mode === 'r' || $mode === 'rb') {
806+
$file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $context['revision'] ?? -1);
807+
808+
if (! is_object($file)) {
809+
throw FileNotFoundException::byFilenameAndRevision($filename, $context['revision'] ?? -1, $path);
810+
}
811+
812+
return [
813+
'collectionWrapper' => $this->collectionWrapper,
814+
'file' => $file,
815+
];
816+
}
817+
818+
if ($mode === 'w' || $mode === 'wb') {
819+
return [
820+
'collectionWrapper' => $this->collectionWrapper,
821+
'filename' => $filename,
822+
'options' => $context + [
823+
'chunkSizeBytes' => $this->chunkSizeBytes,
824+
'disableMD5' => $this->disableMD5,
825+
],
826+
];
827+
}
828+
829+
throw LogicException::openModeNotSupported($mode);
830+
}
759831
}

0 commit comments

Comments
 (0)