diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8626f27a..0345701b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.
+## [4.5.0] - upcoming
+
+* Add GridFS integration for Laravel File Storage by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2985)
+
## [4.4.0] - 2024-05-31
* Support collection name prefix by @GromNaN in [#2930](https://github.com/mongodb/laravel-mongodb/pull/2930)
diff --git a/composer.json b/composer.json
index 84229b00f..af060bb3c 100644
--- a/composer.json
+++ b/composer.json
@@ -34,6 +34,8 @@
},
"require-dev": {
"mongodb/builder": "^0.2",
+ "league/flysystem-gridfs": "^3.28",
+ "league/flysystem-read-only": "^3.0",
"phpunit/phpunit": "^10.3",
"orchestra/testbench": "^8.0|^9.0",
"mockery/mockery": "^1.4.4",
@@ -45,6 +47,7 @@
"illuminate/bus": "< 10.37.2"
},
"suggest": {
+ "league/flysystem-gridfs": "Filesystem storage in MongoDB with GridFS",
"mongodb/builder": "Provides a fluent aggregation builder for MongoDB pipelines"
},
"minimum-stability": "dev",
diff --git a/docs/filesystems.txt b/docs/filesystems.txt
new file mode 100644
index 000000000..065b5c08f
--- /dev/null
+++ b/docs/filesystems.txt
@@ -0,0 +1,159 @@
+.. _laravel-filesystems:
+
+==================
+GridFS Filesystems
+==================
+
+.. facet::
+ :name: genre
+ :values: tutorial
+
+.. meta::
+ :keywords: php framework, gridfs, code example
+
+Overview
+--------
+
+You can use the
+`GridFS Adapter for Flysystem `__
+to store large files in MongoDB.
+GridFS lets you store files of unlimited size in the same database as your data.
+
+
+Configuration
+-------------
+
+Before using the GridFS driver, install the Flysystem GridFS package through the
+Composer package manager by running the following command:
+
+.. code-block:: bash
+
+ composer require league/flysystem-gridfs
+
+Configure `Laravel File Storage `__
+to use the ``gridfs`` driver in ``config/filesystems.php``:
+
+.. code-block:: php
+
+ 'disks' => [
+ 'gridfs' => [
+ 'driver' => 'gridfs',
+ 'connection' => 'mongodb',
+ // 'database' => null,
+ // 'bucket' => 'fs',
+ // 'prefix' => '',
+ // 'read-only' => false,
+ // 'throw' => false,
+ ],
+ ],
+
+You can configure the disk the following settings in ``config/filesystems.php``:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 25 75
+
+ * - Setting
+ - Description
+
+ * - ``driver``
+ - **Required**. Specifies the filesystem driver to use. Must be ``gridfs`` for MongoDB.
+
+ * - ``connection``
+ - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified.
+
+ * - ``database``
+ - Name of the MongoDB database for the GridFS bucket. The driver uses the database of the connection if a database is not specified.
+
+ * - ``bucket``
+ - Name or instance of the GridFS bucket. A database can contain multiple buckets identified by their name. Defaults to ``fs``.
+
+ * - ``prefix``
+ - Specifies a prefix for the name of the files that are stored in the bucket. Using a distinct bucket is recommended
+ in order to store the files in a different collection, instead of using a prefix.
+ The prefix should not start with a leading slash ``/``.
+
+ * - ``read-only``
+ - If ``true``, writing to the GridFS bucket is disabled. Write operations will return ``false`` or throw exceptions
+ depending on the configuration of ``throw``. Defaults to ``false``.
+
+ * - ``throw``
+ - If ``true``, exceptions are thrown when an operation cannot be performed. If ``false``,
+ operations return ``true`` on success and ``false`` on error. Defaults to ``false``.
+
+You can also use a factory or a service name to create an instance of ``MongoDB\GridFS\Bucket``.
+In this case, the options ``connection`` and ``database`` are ignored:
+
+.. code-block:: php
+
+ use Illuminate\Foundation\Application;
+ use MongoDB\GridFS\Bucket;
+
+ 'disks' => [
+ 'gridfs' => [
+ 'driver' => 'gridfs',
+ 'bucket' => static function (Application $app): Bucket {
+ return $app['db']->connection('mongodb')
+ ->getMongoDB()
+ ->selectGridFSBucket([
+ 'bucketName' => 'avatars',
+ 'chunkSizeBytes' => 261120,
+ ]);
+ },
+ ],
+ ],
+
+Usage
+-----
+
+A benefit of using Laravel Filesystem is that it provides a common interface
+for all the supported file systems. You can use the ``gridfs`` disk in the same
+way as the ``local`` disk.
+
+.. code-block:: php
+
+ $disk = Storage::disk('gridfs');
+
+ // Write the file "hello.txt" into GridFS
+ $disk->put('hello.txt', 'Hello World!');
+
+ // Read the file
+ echo $disk->get('hello.txt'); // Hello World!
+
+To learn more Laravel File Storage, see
+`Laravel File Storage `__
+in the Laravel documentation.
+
+Versioning
+----------
+
+File names in GridFS are metadata in file documents, which are identified by
+unique ObjectIDs. If multiple documents share the same file name, they are
+considered "revisions" and further distinguished by creation timestamps.
+
+The Laravel MongoDB integration uses the GridFS Flysystem adapter. It interacts
+with file revisions in the following ways:
+
+- Reading a file reads the last revision of this file name
+- Writing a file creates a new revision for this file name
+- Renaming a file renames all the revisions of this file name
+- Deleting a file deletes all the revisions of this file name
+
+The GridFS Adapter for Flysystem does not provide access to a specific revision
+of a filename. You must use the
+`GridFS API `__
+if you need to work with revisions, as shown in the following code:
+
+.. code-block:: php
+
+ // Create a bucket service from the MongoDB connection
+ /** @var \MongoDB\GridFS\Bucket $bucket */
+ $bucket = $app['db']->connection('mongodb')->getMongoDB()->selectGridFSBucket();
+
+ // Download the last but one version of a file
+ $bucket->openDownloadStreamByName('hello.txt', ['revision' => -2])
+
+.. note::
+
+ If you use a prefix the Filesystem component, you will have to handle it
+ by yourself when using the GridFS API directly.
diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php
index 50c042230..0932048c9 100644
--- a/src/MongoDBServiceProvider.php
+++ b/src/MongoDBServiceProvider.php
@@ -4,15 +4,28 @@
namespace MongoDB\Laravel;
+use Closure;
use Illuminate\Cache\CacheManager;
use Illuminate\Cache\Repository;
+use Illuminate\Filesystem\FilesystemAdapter;
+use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
+use InvalidArgumentException;
+use League\Flysystem\Filesystem;
+use League\Flysystem\GridFS\GridFSAdapter;
+use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter;
+use MongoDB\GridFS\Bucket;
use MongoDB\Laravel\Cache\MongoStore;
use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Queue\MongoConnector;
+use RuntimeException;
use function assert;
+use function class_exists;
+use function get_debug_type;
+use function is_string;
+use function sprintf;
class MongoDBServiceProvider extends ServiceProvider
{
@@ -66,5 +79,59 @@ public function register()
return new MongoConnector($this->app['db']);
});
});
+
+ $this->registerFlysystemAdapter();
+ }
+
+ private function registerFlysystemAdapter(): void
+ {
+ // GridFS adapter for filesystem
+ $this->app->resolving('filesystem', static function (FilesystemManager $filesystemManager) {
+ $filesystemManager->extend('gridfs', static function (Application $app, array $config) {
+ if (! class_exists(GridFSAdapter::class)) {
+ throw new RuntimeException('GridFS adapter for Flysystem is missing. Try running "composer require league/flysystem-gridfs"');
+ }
+
+ $bucket = $config['bucket'] ?? null;
+
+ if ($bucket instanceof Closure) {
+ // Get the bucket from a factory function
+ $bucket = $bucket($app, $config);
+ } elseif (is_string($bucket) && $app->has($bucket)) {
+ // Get the bucket from a service
+ $bucket = $app->get($bucket);
+ } elseif (is_string($bucket) || $bucket === null) {
+ // Get the bucket from the database connection
+ $connection = $app['db']->connection($config['connection']);
+ if (! $connection instanceof Connection) {
+ throw new InvalidArgumentException(sprintf('The database connection "%s" does not use the "mongodb" driver.', $config['connection'] ?? $app['config']['database.default']));
+ }
+
+ $bucket = $connection->getMongoClient()
+ ->selectDatabase($config['database'] ?? $connection->getDatabaseName())
+ ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs', 'disableMD5' => true]);
+ }
+
+ if (! $bucket instanceof Bucket) {
+ throw new InvalidArgumentException(sprintf('Unexpected value for GridFS "bucket" configuration. Expecting "%s". Got "%s"', Bucket::class, get_debug_type($bucket)));
+ }
+
+ $adapter = new GridFSAdapter($bucket, $config['prefix'] ?? '');
+
+ /** @see FilesystemManager::createFlysystem() */
+ if ($config['read-only'] ?? false) {
+ if (! class_exists(ReadOnlyFilesystemAdapter::class)) {
+ throw new RuntimeException('Read-only Adapter for Flysystem is missing. Try running "composer require league/flysystem-read-only"');
+ }
+
+ $adapter = new ReadOnlyFilesystemAdapter($adapter);
+ }
+
+ /** Prevent using backslash on Windows in {@see FilesystemAdapter::__construct()} */
+ $config['directory_separator'] = '/';
+
+ return new FilesystemAdapter(new Filesystem($adapter, $config), $adapter, $config);
+ });
+ });
}
}
diff --git a/tests/FilesystemsTest.php b/tests/FilesystemsTest.php
new file mode 100644
index 000000000..3b9fa8e5f
--- /dev/null
+++ b/tests/FilesystemsTest.php
@@ -0,0 +1,150 @@
+getBucket()->drop();
+
+ parent::tearDown();
+ }
+
+ public static function provideValidOptions(): Generator
+ {
+ yield 'connection-minimal' => [
+ [
+ 'driver' => 'gridfs',
+ 'connection' => 'mongodb',
+ ],
+ ];
+
+ yield 'connection-full' => [
+ [
+ 'driver' => 'gridfs',
+ 'connection' => 'mongodb',
+ 'database' => env('MONGODB_DATABASE', 'unittest'),
+ 'bucket' => 'fs',
+ 'prefix' => 'foo/',
+ ],
+ ];
+
+ yield 'bucket-service' => [
+ [
+ 'driver' => 'gridfs',
+ 'bucket' => 'bucket',
+ ],
+ ];
+
+ yield 'bucket-factory' => [
+ [
+ 'driver' => 'gridfs',
+ 'bucket' => static fn (Application $app) => $app['db']
+ ->connection('mongodb')
+ ->getMongoDB()
+ ->selectGridFSBucket(),
+ ],
+ ];
+ }
+
+ #[DataProvider('provideValidOptions')]
+ public function testValidOptions(array $options)
+ {
+ // Service used by "bucket-service"
+ $this->app->singleton('bucket', static fn (Application $app) => $app['db']
+ ->connection('mongodb')
+ ->getMongoDB()
+ ->selectGridFSBucket());
+
+ $this->app['config']->set('filesystems.disks.' . $this->dataName(), $options);
+
+ $disk = Storage::disk($this->dataName());
+ $disk->put($filename = $this->dataName() . '.txt', $value = microtime());
+ $this->assertEquals($value, $disk->get($filename), 'File saved');
+ }
+
+ public static function provideInvalidOptions(): Generator
+ {
+ yield 'not-mongodb-connection' => [
+ ['driver' => 'gridfs', 'connection' => 'sqlite'],
+ 'The database connection "sqlite" does not use the "mongodb" driver.',
+ ];
+
+ yield 'factory-not-bucket' => [
+ ['driver' => 'gridfs', 'bucket' => static fn () => new stdClass()],
+ 'Unexpected value for GridFS "bucket" configuration. Expecting "MongoDB\GridFS\Bucket". Got "stdClass"',
+ ];
+
+ yield 'service-not-bucket' => [
+ ['driver' => 'gridfs', 'bucket' => 'bucket'],
+ 'Unexpected value for GridFS "bucket" configuration. Expecting "MongoDB\GridFS\Bucket". Got "stdClass"',
+ ];
+ }
+
+ #[DataProvider('provideInvalidOptions')]
+ public function testInvalidOptions(array $options, string $message)
+ {
+ // Service used by "service-not-bucket"
+ $this->app->singleton('bucket', static fn () => new stdClass());
+
+ $this->app['config']->set('filesystems.disks.' . $this->dataName(), $options);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage($message);
+
+ Storage::disk($this->dataName());
+ }
+
+ public function testReadOnlyAndThrowOptions()
+ {
+ $this->app['config']->set('filesystems.disks.gridfs-readonly', [
+ 'driver' => 'gridfs',
+ 'connection' => 'mongodb',
+ // Use ReadOnlyAdapter
+ 'read-only' => true,
+ // Throw exceptions
+ 'throw' => true,
+ ]);
+
+ $disk = Storage::disk('gridfs-readonly');
+
+ $this->expectException(UnableToWriteFile::class);
+ $this->expectExceptionMessage('This is a readonly adapter.');
+
+ $disk->put('file.txt', '');
+ }
+
+ public function testPrefix()
+ {
+ $this->app['config']->set('filesystems.disks.gridfs-prefix', [
+ 'driver' => 'gridfs',
+ 'connection' => 'mongodb',
+ 'prefix' => 'foo/bar/',
+ ]);
+
+ $disk = Storage::disk('gridfs-prefix');
+ $disk->put('hello/world.txt', 'Hello World!');
+ $this->assertSame('Hello World!', $disk->get('hello/world.txt'));
+ $this->assertSame('Hello World!', stream_get_contents($this->getBucket()->openDownloadStreamByName('foo/bar/hello/world.txt')), 'File name is prefixed in the bucket');
+ }
+
+ private function getBucket(): Bucket
+ {
+ return DB::connection('mongodb')->getMongoDB()->selectGridFSBucket();
+ }
+}