From 87f24bf683aca15a4b8dbb3865f985e6327bee81 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 20 Dec 2023 15:54:48 +0100 Subject: [PATCH 1/2] feat: Add MongoConnectionFactory (#FRAM-153) --- src/MongoDB/Driver/MongoConnection.php | 222 +++++++++++++++--- src/MongoDB/Driver/MongoConnectionFactory.php | 38 +++ src/MongoDB/Driver/MongoDriver.php | 6 + src/MongoDB/Schema/MongoSchemaManager.php | 20 ++ .../Driver/MongoConnectionFactoryTest.php | 68 ++++++ .../MongoDB/Schema/MongoSchemaManagerTest.php | 10 + 6 files changed, 325 insertions(+), 39 deletions(-) create mode 100644 src/MongoDB/Driver/MongoConnectionFactory.php create mode 100644 tests/MongoDB/Driver/MongoConnectionFactoryTest.php diff --git a/src/MongoDB/Driver/MongoConnection.php b/src/MongoDB/Driver/MongoConnection.php index 9c5531b..a836d20 100644 --- a/src/MongoDB/Driver/MongoConnection.php +++ b/src/MongoDB/Driver/MongoConnection.php @@ -4,7 +4,6 @@ use Bdf\Prime\Connection\ConnectionInterface; use Bdf\Prime\Connection\Result\ResultSetInterface; -use Bdf\Prime\Exception\DBALException; use Bdf\Prime\MongoDB\Driver\Exception\MongoCommandException; use Bdf\Prime\MongoDB\Driver\Exception\MongoDBALException; use Bdf\Prime\MongoDB\Driver\ResultSet\CursorResultSet; @@ -30,13 +29,18 @@ use Bdf\Prime\Query\Factory\DefaultQueryFactory; use Bdf\Prime\Query\Factory\QueryFactoryInterface; use Bdf\Prime\Query\ReadCommandInterface; +use Closure; use Doctrine\Common\EventManager; use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection; use Doctrine\DBAL\ConnectionException; use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\DBAL\Result; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Statement; +use InvalidArgumentException; use MongoDB\Driver\BulkWrite; use MongoDB\Driver\Command; use MongoDB\Driver\Cursor; @@ -46,51 +50,61 @@ use MongoDB\Driver\Query; use MongoDB\Driver\WriteResult; +use function array_filter; +use function implode; + /** * Connection for mongoDb * - * @property Manager $_conn * @method \Bdf\Prime\Configuration getConfiguration() + * @final */ class MongoConnection extends Connection implements ConnectionInterface { - /** - * @var string - */ - protected $name; - - /** - * @var PrimeSchemaManager - */ - protected $schema; + protected string $name = ''; + protected ?PrimeSchemaManager $schema = null; /** - * Field for emulate transaction into MongoDB (will be added on + * Field for emulate transaction into MongoDB (will be added on each document) * * @var string */ - protected $transactionEmulationStateField = '__MONGO_CONNECTION_TRANSACTION__'; + protected string $transactionEmulationStateField = '__MONGO_CONNECTION_TRANSACTION__'; /** * Level for transaction emulation * * @var int */ - protected $transationLevel = 0; + protected int $transationLevel = 0; + + protected ?PrimePlatform $platform = null; + protected QueryFactoryInterface $factory; /** - * @var PrimePlatform + * The mongoDB connection + * Do not use directly, use $this->connection() instead */ - protected $platform; + private ?Manager $connection = null; + private array $parameters; /** - * @var QueryFactoryInterface + * @param $params + * @param Driver|Configuration|null $driver + * @param Configuration|null $config + * @param EventManager|null $eventManager + * @throws \Doctrine\DBAL\Exception */ - private $factory; - - public function __construct($params, Driver $driver, Configuration $config = null, EventManager $eventManager = null) + public function __construct($params, $driver = null, ?Configuration $config = null, EventManager $eventManager = null) { - parent::__construct($params, $driver, $config, $eventManager); + if ($config === null && $driver instanceof Configuration) { + $config = $driver; + $driver = null; + } + + parent::__construct($params, $driver ?? new MongoDriver(), $config, $eventManager); + + $this->parameters = $params; /** @psalm-suppress InvalidArgument */ $this->factory = new DefaultQueryFactory( @@ -132,7 +146,7 @@ public function getName(): string */ public function getDatabase(): string { - return $this->getParams()['dbname']; + return $this->parameters['dbname']; } /** @@ -154,7 +168,7 @@ public function platform(): PlatformInterface { if ($this->platform === null) { $this->platform = new PrimePlatform( - $this->getDatabasePlatform(), + new MongoPlatform(), $this->getConfiguration()->getTypes() ); } @@ -228,9 +242,7 @@ public function toDatabase($value, $type = null) public function executeSelect($collection, Query $query): Cursor { try { - $this->connect(); - - $cursor = $this->_conn->executeQuery($this->getNamespace($collection), $query); + $cursor = $this->connection()->executeQuery($this->getNamespace($collection), $query); $cursor->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']); return $cursor; @@ -249,9 +261,7 @@ public function executeSelect($collection, Query $query): Cursor public function executeWrite($collection, BulkWrite $query): WriteResult { try { - $this->connect(); - - return $this->_conn->executeBulkWrite($this->getNamespace($collection), $query); + return $this->connection()->executeBulkWrite($this->getNamespace($collection), $query); } catch (Exception $e) { throw new MongoDBALException('MongoDB : ' . $e->getMessage(), 0, $e); } @@ -273,6 +283,22 @@ public function executeUpdate($sql, array $params = [], array $types = []): int throw new \BadMethodCallException('Method ' . __METHOD__ . ' cannot be called on mongoDB connection'); } + /** + * {@inheritdoc} + */ + public function prepare(string $sql): Statement + { + throw new \BadMethodCallException('Method ' . __METHOD__ . ' cannot be called on mongoDB connection'); + } + + /** + * {@inheritdoc} + */ + public function lastInsertId($name = null) + { + throw new \BadMethodCallException('Method ' . __METHOD__ . ' cannot be called on mongoDB connection'); + } + /** * Run a command * @@ -285,9 +311,7 @@ public function executeUpdate($sql, array $params = [], array $types = []): int public function runCommand($command, $arguments = 1): Cursor { try { - $this->connect(); - - return $this->_conn->executeCommand( + return $this->connection()->executeCommand( $this->getDatabase(), Commands::create($command, $arguments)->get() ); @@ -309,9 +333,7 @@ public function runCommand($command, $arguments = 1): Cursor public function runAdminCommand($command, $arguments = 1) { try { - $this->connect(); - - return $this->_conn->executeCommand( + return $this->connection()->executeCommand( 'admin', Commands::create($command, $arguments)->get() ); @@ -337,7 +359,7 @@ public function beginTransaction() { $this->connect(); - foreach ($this->getSchemaManager()->listTableNames() as $collection) { + foreach ($this->schema()->getCollections() as $collection) { $bulk = new BulkWrite(); $bulk->update([], [ '$inc' => [ @@ -378,7 +400,7 @@ public function commit() throw ConnectionException::noActiveTransaction(); } - foreach ($this->getSchemaManager()->listTableNames() as $collection) { + foreach ($this->schema()->getCollections() as $collection) { $bulk = new BulkWrite(); $bulk->update([], [ '$inc' => [ @@ -403,7 +425,7 @@ public function rollBack() throw ConnectionException::noActiveTransaction(); } - foreach ($this->getSchemaManager()->listTableNames() as $collection) { + foreach ($this->schema()->getCollections() as $collection) { $bulk = new BulkWrite(); $bulk->delete([ @@ -450,7 +472,7 @@ public function execute(Compilable $query): ResultSetInterface return new CursorResultSet($this->runCommand($compiled)); } - throw new \InvalidArgumentException('Unsupported compiled query type ' . get_class($compiled)); + throw new InvalidArgumentException('Unsupported compiled query type ' . get_class($compiled)); } /** @@ -469,5 +491,127 @@ protected function getNamespace($collection) public function close(): void { parent::close(); + + if ($this->connection !== null) { + $this->connection = null; + } + } + + private function connection(): Manager + { + if ($this->connection !== null) { + return $this->connection; + } + + $params = $this->parameters; + $dsn = $this->buildDsn($params); + + return $this->connection = new Manager($dsn, array_filter($params)); + } + + private function buildDsn(array $params): string + { + $uri = 'mongodb://'; + + if (!empty($params['host'])) { + $uri .= $params['host']; + + if (!empty($params['port'])) { + $uri .= ':' . $params['port']; + } + + return $uri; + } + + if (!empty($params['hosts'])) { + $uri .= implode(',', $params['hosts']); + + return $uri; + } + + throw new InvalidArgumentException('Cannot build mongodb DSN'); + } + + // Mark all methods of doctrine's connection as deprecated + + /** + * @deprecated + */ + public function getDriver() + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::getDriver(); + } + + /** + * @deprecated + */ + public function getDatabasePlatform() + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::getDatabasePlatform(); + } + + /** + * @deprecated + */ + public function createExpressionBuilder(): ExpressionBuilder + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::createExpressionBuilder(); + } + + /** + * @deprecated + */ + public function isConnected() + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::isConnected(); + } + + /** + * @deprecated + */ + public function transactional(Closure $func) + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::transactional($func); + } + + /** + * @deprecated + */ + public function getNativeConnection() + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::getNativeConnection(); + } + + /** + * @deprecated + */ + public function createSchemaManager(): AbstractSchemaManager + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::createSchemaManager(); + } + + /** + * @deprecated + */ + public function convertToDatabaseValue($value, $type) + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::convertToDatabaseValue($value, $type); + } + + /** + * @deprecated + */ + public function convertToPHPValue($value, $type) + { + @trigger_error('Method ' . __METHOD__ . ' is deprecated without replacement.', E_USER_DEPRECATED); + return parent::convertToPHPValue($value, $type); } } diff --git a/src/MongoDB/Driver/MongoConnectionFactory.php b/src/MongoDB/Driver/MongoConnectionFactory.php new file mode 100644 index 0000000..331a999 --- /dev/null +++ b/src/MongoDB/Driver/MongoConnectionFactory.php @@ -0,0 +1,38 @@ +setName($connectionName); + + return $connection; + } + + /** + * {@inheritdoc} + */ + public function support(string $connectionName, array $parameters): bool + { + return $parameters['driver'] === 'mongodb' || $parameters['driver'] === 'mongo'; + } +} diff --git a/src/MongoDB/Driver/MongoDriver.php b/src/MongoDB/Driver/MongoDriver.php index 588edb3..73e4d25 100644 --- a/src/MongoDB/Driver/MongoDriver.php +++ b/src/MongoDB/Driver/MongoDriver.php @@ -25,6 +25,8 @@ class MongoDriver implements Driver */ public function connect(array $params, $username = null, $password = null, array $driverOptions = []) { + @trigger_error('MongoDriver is deprecated. Use MongoConnection directly.', E_USER_DEPRECATED); + if (!empty($username)) { $params['username'] = $username; } @@ -46,6 +48,8 @@ public function connect(array $params, $username = null, $password = null, array */ public function getDatabasePlatform() { + @trigger_error('MongoDriver is deprecated. Use MongoConnection directly.', E_USER_DEPRECATED); + return new MongoPlatform(); } @@ -54,6 +58,8 @@ public function getDatabasePlatform() */ public function getSchemaManager(Connection $conn, AbstractPlatform $platform) { + @trigger_error('MongoDriver is deprecated. Use MongoConnection directly.', E_USER_DEPRECATED); + return new MongoSchemasManager($conn, $platform); } diff --git a/src/MongoDB/Schema/MongoSchemaManager.php b/src/MongoDB/Schema/MongoSchemaManager.php index 23d407d..737a4c8 100644 --- a/src/MongoDB/Schema/MongoSchemaManager.php +++ b/src/MongoDB/Schema/MongoSchemaManager.php @@ -153,6 +153,26 @@ public function getDatabases(): array return $collections; } + /** + * Get all collection names of the current database + * + * @return list + */ + public function getCollections(): array + { + $list = $this->connection->runCommand('listCollections'); + + $collections = []; + + foreach ($list as $info) { + $collections[] = $info->name; + } + + return $collections; + } + + // @todo getTables + /** * {@inheritdoc} */ diff --git a/tests/MongoDB/Driver/MongoConnectionFactoryTest.php b/tests/MongoDB/Driver/MongoConnectionFactoryTest.php new file mode 100644 index 0000000..f57bb09 --- /dev/null +++ b/tests/MongoDB/Driver/MongoConnectionFactoryTest.php @@ -0,0 +1,68 @@ + [ + 'driver' => 'mongodb', + 'host' => $_ENV['MONGO_HOST'], + 'dbname' => 'TEST', + ], + ], + new ChainFactory([ + new MongoConnectionFactory(), + ]) + ) + ); + + $connection = $manager->getConnection('mongo'); + + $this->assertInstanceOf(MongoConnection::class, $connection); + $this->assertSame('mongo', $connection->getName()); + $this->assertSame([], $connection->schema()->getCollections()); + } + + public function test_create_with_mongo_driver() + { + $manager = new ConnectionManager( + new ConnectionRegistry( + [ + 'mongo' => [ + 'driver' => 'mongo', + 'host' => $_ENV['MONGO_HOST'], + 'dbname' => 'TEST', + ], + ], + new ChainFactory([ + new MongoConnectionFactory(), + ]) + ) + ); + + $connection = $manager->getConnection('mongo'); + + $this->assertInstanceOf(MongoConnection::class, $connection); + $this->assertSame('mongo', $connection->getName()); + $this->assertSame([], $connection->schema()->getCollections()); + } + + public function test_support() + { + $this->assertTrue((new MongoConnectionFactory())->support('mongo', ['driver' => 'mongodb'])); + $this->assertTrue((new MongoConnectionFactory())->support('mongo', ['driver' => 'mongo'])); + $this->assertFalse((new MongoConnectionFactory())->support('mongo', ['driver' => 'mysql'])); + } +} diff --git a/tests/MongoDB/Schema/MongoSchemaManagerTest.php b/tests/MongoDB/Schema/MongoSchemaManagerTest.php index 3f92c28..1a982ec 100644 --- a/tests/MongoDB/Schema/MongoSchemaManagerTest.php +++ b/tests/MongoDB/Schema/MongoSchemaManagerTest.php @@ -446,4 +446,14 @@ public function test_create_and_load_table_with_collation() ], ], $table->options()); } + + public function test_getCollections() + { + $this->assertSame([], $this->schema->getCollections()); + + $this->connection->runCommand('create', 'test_collection'); + $this->connection->runCommand('create', 'other'); + + $this->assertSame(['test_collection', 'other'], $this->schema->getCollections()); + } } From 68e34ed425b4e54d243b93388689eca7cea61d19 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 20 Dec 2023 16:09:01 +0100 Subject: [PATCH 2/2] test: Improve coverage for MongoConnection (#FRAM-153) --- tests/MongoDB/Driver/MongoConnectionTest.php | 16 ++++++++++++++++ tests/MongoDB/Schema/MongoSchemaManagerTest.php | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/MongoDB/Driver/MongoConnectionTest.php b/tests/MongoDB/Driver/MongoConnectionTest.php index 33d4c82..cc35aac 100644 --- a/tests/MongoDB/Driver/MongoConnectionTest.php +++ b/tests/MongoDB/Driver/MongoConnectionTest.php @@ -417,6 +417,22 @@ public function test_exceptions() $this->assertThrows(MongoCommandException::class, function () { $this->connection->runCommand('invalid'); }); $this->assertThrows(MongoCommandException::class, function () { $this->connection->runAdminCommand('invalid'); }); $this->assertThrows(MongoDBALException::class, function () { (new WriteQuery('invalid'))->update(['foo' => ['$bar' => []]], [])->execute($this->connection); }); + $this->assertThrows(\LogicException::class, function () { $this->connection->getNativeConnection(); }); + $this->assertThrows(\BadMethodCallException::class, function () { $this->connection->prepare('foo'); }); + $this->assertThrows(\BadMethodCallException::class, function () { $this->connection->lastInsertId(); }); + } + + public function test_coverage_deprecated() + { + $this->assertInstanceOf(MongoDriver::class, $this->connection->getDriver()); + $this->assertInstanceOf(MongoPlatform::class, $this->connection->getDatabasePlatform()); + $this->assertInstanceOf(MongoSchemasManager::class, $this->connection->createSchemaManager()); + $this->assertInstanceOf(MongoSchemasManager::class, $this->connection->getSchemaManager()); + $this->assertFalse($this->connection->isConnected()); + $this->assertInstanceOf(Manager::class, $this->connection->getWrappedConnection()); + $this->connection->transactional(function () {}); + $this->assertSame('foo', $this->connection->convertToDatabaseValue('foo', 'text')); + $this->assertSame('foo', $this->connection->convertToPHPValue('foo', 'text')); } private function assertThrows(string $exceptionClass, callable $task): void diff --git a/tests/MongoDB/Schema/MongoSchemaManagerTest.php b/tests/MongoDB/Schema/MongoSchemaManagerTest.php index 1a982ec..90c608f 100644 --- a/tests/MongoDB/Schema/MongoSchemaManagerTest.php +++ b/tests/MongoDB/Schema/MongoSchemaManagerTest.php @@ -454,6 +454,6 @@ public function test_getCollections() $this->connection->runCommand('create', 'test_collection'); $this->connection->runCommand('create', 'other'); - $this->assertSame(['test_collection', 'other'], $this->schema->getCollections()); + $this->assertEqualsCanonicalizing(['test_collection', 'other'], $this->schema->getCollections()); } }