diff --git a/docker-compose.yml b/docker-compose.yml index 542ed21af..16c6d946c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - './:/mqdev' environment: - AMQP_DSN=amqp://guest:guest@rabbitmq:5672/mqdev + - DOCTINE_DSN=mysql://root:rootpass@mysql/mqdev - SYMFONY__RABBITMQ__HOST=rabbitmq - SYMFONY__RABBITMQ__USER=guest - SYMFONY__RABBITMQ__PASSWORD=guest diff --git a/pkg/dbal/DbalConnectionFactory.php b/pkg/dbal/DbalConnectionFactory.php index aca0ce678..176458f0d 100644 --- a/pkg/dbal/DbalConnectionFactory.php +++ b/pkg/dbal/DbalConnectionFactory.php @@ -19,21 +19,33 @@ class DbalConnectionFactory implements PsrConnectionFactory private $connection; /** + * The config could be an array, string DSN or null. In case of null it will attempt to connect to mysql localhost with default credentials. + * * $config = [ * 'connection' => [] - dbal connection options. see http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html * 'table_name' => 'enqueue', - database table name. * 'polling_interval' => '1000', - How often query for new messages (milliseconds) * 'lazy' => true, - Use lazy database connection (boolean) - * ]. + * ] + * + * or + * + * mysql://user:pass@localhost:3606/db?charset=UTF-8 * - * @param $config + * @param array|string|null $config */ - public function __construct(array $config = []) + public function __construct($config = 'mysql://') { - $this->config = array_replace([ - 'connection' => [], - 'lazy' => true, - ], $config); + if (empty($config)) { + $config = $this->parseDsn('mysql://'); + } elseif (is_string($config)) { + $config = $this->parseDsn($config); + } elseif (is_array($config)) { + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + + $this->config = $config; } /** @@ -74,4 +86,50 @@ private function establishConnection() return $this->connection; } + + /** + * @param string $dsn + * + * @return array + */ + private function parseDsn($dsn) + { + if (false === strpos($dsn, '://')) { + throw new \LogicException(sprintf('The given DSN "%s" is not valid. Must contain "://".', $dsn)); + } + + list($schema, $rest) = explode('://', $dsn, 2); + + $supported = [ + 'db2' => true, + 'ibm_db2' => true, + 'mssql' => true, + 'pdo_sqlsrv' => true, + 'mysql' => true, + 'mysql2' => true, + 'pdo_mysql' => true, + 'pgsql' => true, + 'postgres' => true, + 'postgresql' => true, + 'pdo_pgsql' => true, + 'sqlite' => true, + 'sqlite3' => true, + 'pdo_sqlite' => true, + ]; + + if (false == isset($supported[$schema])) { + throw new \LogicException(sprintf( + 'The given DSN schema "%s" is not supported. There are supported schemes: "%s".', + $schema, + implode('", "', array_keys($supported)) + )); + } + + return [ + 'lazy' => true, + 'connection' => [ + 'url' => empty($rest) ? $schema.'://root@localhost' : $dsn, + ], + ]; + } } diff --git a/pkg/dbal/DbalContext.php b/pkg/dbal/DbalContext.php index 2a7d85071..e6a7b5d17 100644 --- a/pkg/dbal/DbalContext.php +++ b/pkg/dbal/DbalContext.php @@ -43,7 +43,11 @@ public function __construct($connection, array $config = []) } elseif (is_callable($connection)) { $this->connectionFactory = $connection; } else { - throw new \InvalidArgumentException('The connection argument must be either Doctrine\DBAL\Connection or callable that returns Doctrine\DBAL\Connection.'); + throw new \InvalidArgumentException(sprintf( + 'The connection argument must be either %s or callable that returns %s.', + Connection::class, + Connection::class + )); } } diff --git a/pkg/dbal/Symfony/DbalTransportFactory.php b/pkg/dbal/Symfony/DbalTransportFactory.php index d5be5c43c..28c3ac05e 100644 --- a/pkg/dbal/Symfony/DbalTransportFactory.php +++ b/pkg/dbal/Symfony/DbalTransportFactory.php @@ -33,7 +33,16 @@ public function __construct($name = 'dbal') public function addConfiguration(ArrayNodeDefinition $builder) { $builder + ->beforeNormalization() + ->ifString() + ->then(function ($v) { + return ['dsn' => $v]; + }) + ->end() ->children() + ->scalarNode('dsn') + ->info('The Doctrine DBAL DSN. Other parameters are ignored if set') + ->end() ->variableNode('connection') ->treatNullLike([]) ->info('Doctrine DBAL connection options. See http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html') @@ -66,6 +75,9 @@ public function createConnectionFactory(ContainerBuilder $container, array $conf if (false == empty($config['dbal_connection_name'])) { $factory = new Definition(ManagerRegistryConnectionFactory::class); $factory->setArguments([new Reference('doctrine'), $config]); + } elseif (false == empty($config['dsn'])) { + $factory = new Definition(DbalConnectionFactory::class); + $factory->setArguments([$config['dsn']]); } elseif (false == empty($config['connection'])) { $factory = new Definition(DbalConnectionFactory::class); $factory->setArguments([$config]); diff --git a/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php b/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php new file mode 100644 index 000000000..39a29563d --- /dev/null +++ b/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php @@ -0,0 +1,110 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); + + new DbalConnectionFactory(new \stdClass()); + } + + public function testThrowIfSchemeIsNotSupported() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN schema "http" is not supported. There are supported schemes: "db2", "ibm_db2", "mssql", "pdo_sqlsrv", "mysql", "mysql2", "pdo_mysql", "pgsql", "postgres", "postgresql", "pdo_pgsql", "sqlite", "sqlite3", "pdo_sqlite"'); + + new DbalConnectionFactory('http://example.com'); + } + + public function testThrowIfDsnCouldNotBeParsed() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN "invalidDSN" is not valid. Must contain "://".'); + + new DbalConnectionFactory('invalidDSN'); + } + + /** + * @dataProvider provideConfigs + * + * @param mixed $config + * @param mixed $expectedConfig + */ + public function testShouldParseConfigurationAsExpected($config, $expectedConfig) + { + $factory = new DbalConnectionFactory($config); + + $this->assertAttributeEquals($expectedConfig, 'config', $factory); + } + + public static function provideConfigs() + { + yield [ + null, + [ + 'lazy' => true, + 'connection' => [ + 'url' => 'mysql://root@localhost', + ], + ], + ]; + + yield [ + 'mysql://', + [ + 'lazy' => true, + 'connection' => [ + 'url' => 'mysql://root@localhost', + ], + ], + ]; + + yield [ + 'pgsql://', + [ + 'lazy' => true, + 'connection' => [ + 'url' => 'pgsql://root@localhost', + ], + ], + ]; + + yield [ + 'mysql://user:pass@host:10000/db', + [ + 'lazy' => true, + 'connection' => [ + 'url' => 'mysql://user:pass@host:10000/db', + ], + ], + ]; + + yield [ + [], + [ + 'lazy' => true, + 'connection' => [ + 'url' => 'mysql://root@localhost', + ], + ], + ]; + + yield [ + ['table_name' => 'a_queue_table', 'connection' => ['foo' => 'fooVal', 'bar' => 'barVal']], + ['table_name' => 'a_queue_table', 'connection' => ['foo' => 'fooVal', 'bar' => 'barVal']], + ]; + } +} diff --git a/pkg/dbal/Tests/DbalConnectionFactoryTest.php b/pkg/dbal/Tests/DbalConnectionFactoryTest.php index c9f8c0f6e..4c8ec75ff 100644 --- a/pkg/dbal/Tests/DbalConnectionFactoryTest.php +++ b/pkg/dbal/Tests/DbalConnectionFactoryTest.php @@ -23,7 +23,7 @@ public function testCouldBeConstructedWithEmptyConfiguration() $this->assertAttributeEquals([ 'lazy' => true, - 'connection' => [], + 'connection' => ['url' => 'mysql://root@localhost'], ], 'config', $factory); } diff --git a/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php b/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php index 5e1f2ff6f..7f809da43 100644 --- a/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php +++ b/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php @@ -61,6 +61,25 @@ public function testShouldAllowAddConfiguration() ], $config); } + public function testShouldAllowAddConfigurationAsString() + { + $transport = new DbalTransportFactory(); + $tb = new TreeBuilder(); + $rootNode = $tb->root('foo'); + + $transport->addConfiguration($rootNode); + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), ['mysqlDSN']); + + $this->assertEquals([ + 'dsn' => 'mysqlDSN', + 'dbal_connection_name' => null, + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, + ], $config); + } + public function testShouldCreateDbalConnectionFactory() { $container = new ContainerBuilder(); @@ -89,6 +108,26 @@ public function testShouldCreateDbalConnectionFactory() ], $factory->getArgument(0)); } + public function testShouldCreateConnectionFactoryFromDsnString() + { + $container = new ContainerBuilder(); + + $transport = new DbalTransportFactory(); + + $serviceId = $transport->createConnectionFactory($container, [ + 'dsn' => 'theDSN', + 'connection' => [], + 'lazy' => true, + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + ]); + + $this->assertTrue($container->hasDefinition($serviceId)); + $factory = $container->getDefinition($serviceId); + $this->assertEquals(DbalConnectionFactory::class, $factory->getClass()); + $this->assertSame('theDSN', $factory->getArgument(0)); + } + public function testShouldCreateManagerRegistryConnectionFactory() { $container = new ContainerBuilder(); diff --git a/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php b/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php index 4fb4f5038..43b8f174f 100644 --- a/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php +++ b/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php @@ -70,7 +70,7 @@ public function load(array $configs, ContainerBuilder $container) $this->factories[$name]->createDriver($container, $transportConfig); } - if (false == isset($config['transport'][$config['transport']['default']['alias']])) { + if (isset($config['transport']['default']['alias']) && false == isset($config['transport'][$config['transport']['default']['alias']])) { throw new \LogicException(sprintf('Transport is not enabled: %s', $config['transport']['default']['alias'])); } @@ -82,7 +82,7 @@ public function load(array $configs, ContainerBuilder $container) $config['client']['router_queue'], $config['client']['default_processor_queue'], $config['client']['router_processor'], - $config['transport'][$config['transport']['default']['alias']], + isset($config['transport']['default']['alias']) ? $config['transport'][$config['transport']['default']['alias']] : [], ]); $container->setParameter('enqueue.client.router_queue_name', $config['client']['router_queue']); diff --git a/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php b/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php index 20c12f5ad..b5043aa26 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php @@ -5,6 +5,7 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\RouteCollectionBuilder; @@ -24,6 +25,10 @@ class CustomAppKernel extends Kernel public function setEnqueueConfig(array $config) { + $fs = new Filesystem(); + $fs->remove(sys_get_temp_dir().'/EnqueueBundleCustom/cache'); + $fs->mkdir(sys_get_temp_dir().'/EnqueueBundleCustom/cache'); + $this->enqueueConfig = array_replace_recursive($this->enqueueConfig, $config); } diff --git a/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php b/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php index 02859da2d..a2c65492c 100644 --- a/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php @@ -49,6 +49,12 @@ public function provideEnqueueConfigs() ], ]]; + yield 'default_dbal_as_dsn' => [[ + 'transport' => [ + 'default' => getenv('DOCTINE_DSN'), + ], + ]]; + yield 'stomp' => [[ 'transport' => [ 'default' => 'stomp', @@ -99,13 +105,13 @@ public function provideEnqueueConfigs() yield 'fs_dsn' => [[ 'transport' => [ 'default' => 'fs', - 'fs' => 'file:/'.sys_get_temp_dir(), + 'fs' => 'file://'.sys_get_temp_dir(), ], ]]; yield 'default_fs_as_dsn' => [[ 'transport' => [ - 'default' => 'file:/'.sys_get_temp_dir(), + 'default' => 'file://'.sys_get_temp_dir(), ], ]]; @@ -113,16 +119,25 @@ public function provideEnqueueConfigs() 'transport' => [ 'default' => 'dbal', 'dbal' => [ - 'dbname' => getenv('SYMFONY__DB__NAME'), - 'user' => getenv('SYMFONY__DB__USER'), - 'password' => getenv('SYMFONY__DB__PASSWORD'), - 'host' => getenv('SYMFONY__DB__HOST'), - 'port' => getenv('SYMFONY__DB__PORT'), - 'driver' => getenv('SYMFONY__DB__DRIVER'), + 'connection' => [ + 'dbname' => getenv('SYMFONY__DB__NAME'), + 'user' => getenv('SYMFONY__DB__USER'), + 'password' => getenv('SYMFONY__DB__PASSWORD'), + 'host' => getenv('SYMFONY__DB__HOST'), + 'port' => getenv('SYMFONY__DB__PORT'), + 'driver' => getenv('SYMFONY__DB__DRIVER'), + ], ], ], ]]; + yield 'dbal_dsn' => [[ + 'transport' => [ + 'default' => 'dbal', + 'dbal' => getenv('DOCTINE_DSN'), + ], + ]]; + yield 'sqs' => [[ 'transport' => [ 'default' => 'sqs', @@ -218,6 +233,7 @@ protected function customSetUp(array $enqueueConfig) $this->client = static::createClient(['enqueue_config' => $enqueueConfig]); $this->client->getKernel()->boot(); + static::$kernel = $this->client->getKernel(); $this->container = static::$kernel->getContainer(); /** @var DriverInterface $driver */ diff --git a/pkg/enqueue/Symfony/DefaultTransportFactory.php b/pkg/enqueue/Symfony/DefaultTransportFactory.php index 634a58af1..dcc54cf8b 100644 --- a/pkg/enqueue/Symfony/DefaultTransportFactory.php +++ b/pkg/enqueue/Symfony/DefaultTransportFactory.php @@ -4,6 +4,8 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\Symfony\AmqpTransportFactory; +use Enqueue\Dbal\DbalConnectionFactory; +use Enqueue\Dbal\Symfony\DbalTransportFactory; use Enqueue\Fs\FsConnectionFactory; use Enqueue\Fs\Symfony\FsTransportFactory; use Enqueue\Null\NullConnectionFactory; @@ -142,6 +144,10 @@ private function findFactory($dsn) return new FsTransportFactory('default_fs'); } + if ($connectionFactory instanceof DbalConnectionFactory) { + return new DbalTransportFactory('default_dbal'); + } + if ($connectionFactory instanceof NullConnectionFactory) { return new NullTransportFactory('default_null'); } diff --git a/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php b/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php index ecc106641..f554e616f 100644 --- a/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php +++ b/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php @@ -3,6 +3,7 @@ namespace Enqueue\Tests\Functions; use Enqueue\AmqpExt\AmqpConnectionFactory; +use Enqueue\Dbal\DbalConnectionFactory; use Enqueue\Fs\FsConnectionFactory; use Enqueue\Null\NullConnectionFactory; use PHPUnit\Framework\TestCase; @@ -57,5 +58,9 @@ public static function provideDSNs() yield ['file:///foo/bar/baz', FsConnectionFactory::class]; yield ['null://', NullConnectionFactory::class]; + + yield ['mysql://', DbalConnectionFactory::class]; + + yield ['pgsql://', DbalConnectionFactory::class]; } } diff --git a/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php index 890f3bca5..953145988 100644 --- a/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php +++ b/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php @@ -255,5 +255,9 @@ public static function provideDSNs() yield ['null://', 'default_null']; yield ['file://', 'default_fs']; + + yield ['mysql://', 'default_dbal']; + + yield ['pgsql://', 'default_dbal']; } } diff --git a/pkg/enqueue/functions.php b/pkg/enqueue/functions.php index 223ee353e..aa76c57a6 100644 --- a/pkg/enqueue/functions.php +++ b/pkg/enqueue/functions.php @@ -4,6 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\Consumption\QueueConsumer; +use Enqueue\Dbal\DbalConnectionFactory; use Enqueue\Fs\FsConnectionFactory; use Enqueue\Null\NullConnectionFactory; use Enqueue\Psr\PsrConnectionFactory; @@ -30,6 +31,23 @@ function dsn_to_connection_factory($dsn) $map['null'] = NullConnectionFactory::class; } + if (class_exists(DbalConnectionFactory::class)) { + $map['db2'] = DbalConnectionFactory::class; + $map['ibm_db2'] = DbalConnectionFactory::class; + $map['mssql'] = DbalConnectionFactory::class; + $map['pdo_sqlsrv'] = DbalConnectionFactory::class; + $map['mysql'] = DbalConnectionFactory::class; + $map['mysql2'] = DbalConnectionFactory::class; + $map['pdo_mysql'] = DbalConnectionFactory::class; + $map['pgsql'] = DbalConnectionFactory::class; + $map['postgres'] = DbalConnectionFactory::class; + $map['postgresql'] = DbalConnectionFactory::class; + $map['pdo_pgsql'] = DbalConnectionFactory::class; + $map['sqlite'] = DbalConnectionFactory::class; + $map['sqlite3'] = DbalConnectionFactory::class; + $map['pdo_sqlite'] = DbalConnectionFactory::class; + } + list($scheme) = explode('://', $dsn); if (false == $scheme || false === strpos($dsn, '://')) { throw new \LogicException(sprintf('The scheme could not be parsed from DSN "%s"', $dsn)); diff --git a/pkg/stomp/Symfony/StompTransportFactory.php b/pkg/stomp/Symfony/StompTransportFactory.php index c7ed2f4ec..238a80224 100644 --- a/pkg/stomp/Symfony/StompTransportFactory.php +++ b/pkg/stomp/Symfony/StompTransportFactory.php @@ -34,7 +34,7 @@ public function addConfiguration(ArrayNodeDefinition $builder) $builder ->children() ->scalarNode('host')->defaultValue('localhost')->cannotBeEmpty()->end() - ->integerNode('port')->min(1)->defaultValue(61613)->end() + ->scalarNode('port')->defaultValue(61613)->end() ->scalarNode('login')->defaultValue('guest')->cannotBeEmpty()->end() ->scalarNode('password')->defaultValue('guest')->cannotBeEmpty()->end() ->scalarNode('vhost')->defaultValue('/')->cannotBeEmpty()->end()