From 4fff744a247d360cb7b0b5f641d951f27d37013c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 24 May 2024 10:59:46 +0200 Subject: [PATCH] Add support for GridFS adapter Can be configured using a Doctrine MongoDB ODM, a MongoDB\Client configuration or a service providing a MongoDB\GridFS\Bucket instance. --- README.md | 1 + composer.json | 2 + docs/6-gridfs.md | 88 ++++++++++++ src/Adapter/AdapterDefinitionFactory.php | 1 + .../GridFSAdapterDefinitionBuilder.php | 104 ++++++++++++++ .../GridFSAdapterDefinitionBuilderTest.php | 129 ++++++++++++++++++ 6 files changed, 325 insertions(+) create mode 100644 docs/6-gridfs.md create mode 100644 src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php create mode 100644 tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php diff --git a/README.md b/README.md index f2eb7b4..2cd1c88 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ to interact with your storage. 3. [Interacting with FTP and SFTP servers](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/3-interacting-with-ftp-and-sftp-servers.md) 4. [Using a lazy adapter to switch storage backend using an environment variable](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/4-using-lazy-adapter-to-switch-at-runtime.md) 5. [Creating a custom adapter](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/5-creating-a-custom-adapter.md) +6. [MongoDB GridFS](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/6-gridfs.md) * [Security issue disclosure procedure](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/A-security-disclosure-procedure.md) * [Configuration reference](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/B-configuration-reference.md) diff --git a/composer.json b/composer.json index ac1e9e5..dbb07b0 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,13 @@ "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { + "doctrine/mongodb-odm": "^2.0", "league/flysystem-async-aws-s3": "^3.1", "league/flysystem-aws-s3-v3": "^3.1", "league/flysystem-azure-blob-storage": "^3.1", "league/flysystem-ftp": "^3.1", "league/flysystem-google-cloud-storage": "^3.1", + "league/flysystem-gridfs": "^3.28", "league/flysystem-memory": "^3.1", "league/flysystem-read-only": "^3.15", "league/flysystem-sftp-v3": "^3.1", diff --git a/docs/6-gridfs.md b/docs/6-gridfs.md new file mode 100644 index 0000000..8c6ebc4 --- /dev/null +++ b/docs/6-gridfs.md @@ -0,0 +1,88 @@ +# MongoDB GridFS + +GridFS stores files in a MongoDB database. + +Install the GridFS adapter: + +``` +composer require league/flysystem-gridfs +``` + +## With `doctrine/mongodb-odm-bundle` + +For applications that uses Doctrine MongoDB ODM, set the `doctrine_connection` name to use: + +```yaml +# config/packages/flysystem.yaml + +flysystem: + storages: + users.storage: + adapter: 'gridfs' + options: + # Name of a Doctrine MongoDB ODM connection + doctrine_connection: 'default' + # Use the default DB from the Doctrine MongoDB ODM configuration + database: ~ + bucket: 'fs' +``` + +## With a Full Configuration + +To initialize the GridFS bucket from configuration, set the `mongodb_uri` and `database` options, others are optional. + +```yaml +# config/packages/flysystem.yaml + +flysystem: + storages: + users.storage: + adapter: 'gridfs' + options: + # MongoDB client configuration + mongodb_uri: '%env(MONGODB_URI)%' + mongodb_uri_options: [] + mongodb_driver_options: [] + # Database name is required + database: '%env(MONGODB_DB)%' + bucket: 'fs' +``` + +```dotenv +# .env + +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_DB=flysystem +``` + +## With a Bucket Service + +For a more advanced configuration, create a service for +[`MongoDB\GridFS\Bucket`](https://www.mongodb.com/docs/php-library/current/tutorial/gridfs/): + +```yaml +# config/packages/flysystem.yaml + +services: + mongodb_client: + class: 'MongoDB\Client' + arguments: + - '%env(MONGODB_URI)%' + + mongodb_database: + class: 'MongoDB\Database' + factory: ['mongodb_client', 'selectDatabase'] + arguments: ['%env(MONGODB_DB)%'] + + mongodb_gridfs_bucket: + class: 'MongoDB\GridFS\Bucket' + factory: ['@mongodb_database', 'selectGridFSBucket'] + +flysystem: + storages: + users.storage: + adapter: 'gridfs' + options: + # Service name + bucket: 'mongodb_gridfs_bucket' +``` diff --git a/src/Adapter/AdapterDefinitionFactory.php b/src/Adapter/AdapterDefinitionFactory.php index 809c263..0aa50d6 100644 --- a/src/Adapter/AdapterDefinitionFactory.php +++ b/src/Adapter/AdapterDefinitionFactory.php @@ -34,6 +34,7 @@ public function __construct() new Builder\AzureAdapterDefinitionBuilder(), new Builder\FtpAdapterDefinitionBuilder(), new Builder\GcloudAdapterDefinitionBuilder(), + new Builder\GridFSAdapterDefinitionBuilder(), new Builder\LocalAdapterDefinitionBuilder(), new Builder\MemoryAdapterDefinitionBuilder(), new Builder\SftpAdapterDefinitionBuilder(), diff --git a/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php b/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php new file mode 100644 index 0000000..cb6be84 --- /dev/null +++ b/src/Adapter/Builder/GridFSAdapterDefinitionBuilder.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace League\FlysystemBundle\Adapter\Builder; + +use Doctrine\ODM\MongoDB\DocumentManager; +use League\Flysystem\GridFS\GridFSAdapter; +use MongoDB\Client; +use MongoDB\GridFS\Bucket; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Jérôme Tamarelle + * + * @internal + */ +class GridFSAdapterDefinitionBuilder extends AbstractAdapterDefinitionBuilder +{ + public function getName(): string + { + return 'gridfs'; + } + + protected function getRequiredPackages(): array + { + return [ + GridFSAdapter::class => 'league/flysystem-gridfs', + ]; + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->define('bucket')->default(null)->allowedTypes('string', 'null'); + $resolver->define('prefix')->default('')->allowedTypes('string'); + $resolver->define('database')->default(null)->allowedTypes('string', 'null'); + $resolver->define('doctrine_connection')->allowedTypes('string'); + $resolver->define('mongodb_uri')->allowedTypes('string'); + $resolver->define('mongodb_uri_options')->default([])->allowedTypes('array'); + $resolver->define('mongodb_driver_options')->default([])->allowedTypes('array'); + } + + /** + * @param array{bucket:string|null, prefix:string, database:string|null, doctrine_connection?:string, mongodb_uri?:string, mongodb_uri_options:array, mongodb_driver_options:array} $options + */ + protected function configureDefinition(Definition $definition, array $options, ?string $defaultVisibilityForDirectories): void + { + if (isset($options['doctrine_connection'])) { + if (isset($options['mongodb_uri'])) { + throw new InvalidArgumentException('In GridFS configuration, "doctrine_connection" and "mongodb_uri" options cannot be set together.'); + } + $bucket = new Definition(Bucket::class); + $bucket->setFactory([self::class, 'initializeBucketFromDocumentManager']); + $bucket->setArguments([ + new Reference(sprintf('doctrine_mongodb.odm.%s_document_manager', $options['doctrine_connection'])), + $options['database'], + $options['bucket'], + ]); + } elseif (isset($options['mongodb_uri'])) { + $bucket = new Definition(Bucket::class); + $bucket->setFactory([self::class, 'initializeBucketFromConfig']); + $bucket->setArguments([ + $options['mongodb_uri'], + $options['mongodb_uri_options'], + $options['mongodb_driver_options'], + $options['database'] ?? throw new InvalidArgumentException('MongoDB "database" name is required for Flysystem GridFS configuration'), + $options['bucket'], + ]); + } elseif ($options['bucket']) { + $bucket = new Reference($options['bucket']); + } else { + throw new InvalidArgumentException('Flysystem GridFS configuration requires a "bucket" service name, a "mongodb_uri" or a "doctrine_connection" name'); + } + + $definition->setClass(GridFSAdapter::class); + $definition->setArgument(0, $bucket); + $definition->setArgument(1, $options['prefix']); + } + + public static function initializeBucketFromDocumentManager(DocumentManager $documentManager, ?string $dbName, ?string $bucketName): Bucket + { + return $documentManager + ->getClient() + ->selectDatabase($dbName ?? $documentManager->getConfiguration()->getDefaultDB()) + ->selectGridFSBucket(['bucketName' => $bucketName ?? 'fs', 'disableMD5' => true]); + } + + public static function initializeBucketFromConfig(string $uri, array $uriOptions, array $driverOptions, ?string $dbName, ?string $bucketName): Bucket + { + return (new Client($uri, $uriOptions, $driverOptions)) + ->selectDatabase($dbName) + ->selectGridFSBucket(['bucketName' => $bucketName ?? 'fs', 'disableMD5' => true]); + } +} diff --git a/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php b/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php new file mode 100644 index 0000000..c7ea289 --- /dev/null +++ b/tests/Adapter/Builder/GridFSAdapterDefinitionBuilderTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\League\FlysystemBundle\Adapter\Builder; + +use Doctrine\ODM\MongoDB\Configuration; +use Doctrine\ODM\MongoDB\DocumentManager; +use League\Flysystem\GridFS\GridFSAdapter; +use League\FlysystemBundle\Adapter\Builder\GridFSAdapterDefinitionBuilder; +use MongoDB\Client; +use MongoDB\GridFS\Bucket; +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +class GridFSAdapterDefinitionBuilderTest extends TestCase +{ + public function createBuilder(): GridFSAdapterDefinitionBuilder + { + return new GridFSAdapterDefinitionBuilder(); + } + + public static function provideValidOptions(): \Generator + { + yield 'doctrine_minimal' => [[ + 'doctrine_connection' => 'default', + ]]; + + yield 'doctrine_full' => [[ + 'doctrine_connection' => 'custom', + 'database' => 'testing', + 'bucket' => 'avatars', + ]]; + + yield 'config_minimal' => [[ + 'mongodb_uri' => 'mongodb://localhost:27017/', + 'database' => 'testing', + ]]; + + yield 'config_full' => [[ + 'mongodb_uri' => 'mongodb://server1:27017,server2:27017/', + 'mongodb_uri_options' => ['appname' => 'flysystem'], + 'mongodb_driver_options' => ['disableClientPersistence' => false], + 'database' => 'testing', + 'bucket' => 'avatars', + ]]; + + yield 'service' => [[ + 'bucket' => 'bucket', + ]]; + } + + /** + * @dataProvider provideValidOptions + */ + public function testCreateDefinition($options) + { + $this->assertSame(GridFSAdapter::class, $this->createBuilder()->createDefinition($options, null)->getClass()); + } + + public static function provideInvalidOptions(): \Generator + { + yield 'empty' => [ + [], + 'Flysystem GridFS configuration requires a "bucket" service name, a "mongodb_uri" or a "doctrine_connection" name', + ]; + + yield 'no database with mongodb_uri' => [ + ['mongodb_uri' => 'mongodb://127.0.0.1:27017/'], + 'MongoDB "database" name is required for Flysystem GridFS configuration', + ]; + + yield 'both doctrine_connection and mongodb_uri' => [ + ['doctrine_connection' => 'default', 'mongodb_uri' => 'mongodb://127.0.0.1:27017/'], + 'In GridFS configuration, "doctrine_connection" and "mongodb_uri" options cannot be set together.', + ]; + } + + /** + * @dataProvider provideInvalidOptions + */ + public function testInvalidOptions(array $options, string $message) + { + $builder = $this->createBuilder(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + $builder->createDefinition($options, null); + } + + public function testInitializeBucketFromDocumentManager() + { + $client = new Client(); + $config = new Configuration(); + $config->setDefaultDB('testing'); + $dm = $this->createMock(DocumentManager::class); + $dm->expects($this->once())->method('getClient')->willReturn($client); + $dm->expects($this->once())->method('getConfiguration')->willReturn($config); + + $bucket = GridFSAdapterDefinitionBuilder::initializeBucketFromDocumentManager($dm, null, 'avatars'); + + $this->assertInstanceOf(Bucket::class, $bucket); + $this->assertSame('testing', $bucket->getDatabaseName()); + $this->assertSame('avatars', $bucket->getBucketName()); + } + + public function testInitializeBucketFromConfig() + { + $bucket = GridFSAdapterDefinitionBuilder::initializeBucketFromConfig( + 'mongodb://server:27017/', + ['appname' => 'flysystem'], + ['disableClientPersistence' => false], + 'testing', + 'avatars' + ); + + $this->assertInstanceOf(Bucket::class, $bucket); + $this->assertSame('testing', $bucket->getDatabaseName()); + $this->assertSame('avatars', $bucket->getBucketName()); + } +}