diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index 41aef9c6c66..47e17a86e88 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -151,7 +151,9 @@ protected function configuration($name) throw new InvalidArgumentException("Database [{$name}] not configured."); } - return $config; + $urlParser = new UrlParser; + + return $urlParser->parseDatabaseConfigWithUrl($config); } /** diff --git a/src/Illuminate/Database/UrlParser.php b/src/Illuminate/Database/UrlParser.php new file mode 100644 index 00000000000..9a1f15a9f39 --- /dev/null +++ b/src/Illuminate/Database/UrlParser.php @@ -0,0 +1,212 @@ + 'sqlsrv', + 'mysql2' => 'mysql', // Amazon RDS, for some weird reason + 'postgres' => 'pgsql', + 'postgresql' => 'pgsql', + 'sqlite3' => 'sqlite', + ]; + + /** + * The different components of parsed url. + * + * @var array + */ + protected $parsedUrl; + + /** + * Get all of the current drivers aliases. + * + * @return array + */ + public static function getDriverAliases(): array + { + return static::$driverAliases; + } + + /** + * Add the driver alias to the driver aliases array. + * + * @param string $alias + * @param string $driver + * @return void + */ + public static function addDriverAlias($alias, $driver) + { + static::$driverAliases[$alias] = $driver; + } + + /** + * Transform the url string or config array with url key to a parsed classic config array. + * + * @param array|string $config + * @return array + */ + public function parseDatabaseConfigWithUrl($config): array + { + if (is_string($config)) { + $config = ['url' => $config]; + } + + $url = $config['url'] ?? null; + $config = Arr::except($config, 'url'); + + if (! $url) { + return $config; + } + + $this->parsedUrl = $this->parseUrl($url); + + return array_merge( + $config, + $this->getMainAttributes(), + $this->getOtherOptions() + ); + } + + /** + * Decode the string url, to an array of all of its components. + * + * @param string $url + * @return array + */ + protected function parseUrl($url): array + { + // sqlite3?:///... => sqlite3?://null/... or else the URL will be invalid + $url = preg_replace('#^(sqlite3?):///#', '$1://null/', $url); + + $parsedUrl = parse_url($url); + + if ($parsedUrl === false) { + throw new InvalidArgumentException('Malformed parameter "url".'); + } + + return $this->parseStringsToNativeTypes(array_map('rawurldecode', $parsedUrl)); + } + + /** + * Convert string casted values to there native types. + * Ex: 'false' => false, '42' => 42, 'foo' => 'foo' + * + * @param string $url + * @return mixed + */ + protected function parseStringsToNativeTypes($value) + { + if (is_array($value)) { + return array_map([$this, 'parseStringsToNativeTypes'], $value); + } + + if (! is_string($value)) { + return $value; + } + + $parsedValue = json_decode($value, true); + + if (json_last_error() === JSON_ERROR_NONE) { + return $parsedValue; + } + + return $value; + } + + /** + * Return the main attributes of the database connection config from url. + * + * @return array + */ + protected function getMainAttributes(): array + { + return array_filter([ + 'driver' => $this->getDriver(), + 'database' => $this->getDatabase(), + 'host' => $this->getInUrl('host'), + 'port' => $this->getInUrl('port'), + 'username' => $this->getInUrl('user'), + 'password' => $this->getInUrl('pass'), + ], function ($value) { + return $value !== null; + }); + } + + /** + * Find connection driver from url. + * + * @return string|null + */ + protected function getDriver() + { + $alias = $this->getInUrl('scheme'); + + if (! $alias) { + return null; + } + + return static::$driverAliases[$alias] ?? $alias; + } + + /** + * Get a component of the parsed url. + * + * @param string $key + * @return string|null + */ + protected function getInUrl($key) + { + return $this->parsedUrl[$key] ?? null; + } + + /** + * Find connection database from url. + * + * @return string|null + */ + protected function getDatabase() + { + $path = $this->getInUrl('path'); + + if (! $path) { + return null; + } + + return substr($path, 1); + } + + /** + * Return all the options added to the url with query params. + * + * @return array + */ + protected function getOtherOptions(): array + { + $queryString = $this->getInUrl('query'); + + if (! $queryString) { + return []; + } + + $query = []; + + parse_str($queryString, $query); + + return $this->parseStringsToNativeTypes($query); + } +} diff --git a/tests/Database/DatabaseConnectionFactoryTest.php b/tests/Database/DatabaseConnectionFactoryTest.php index 72c92dfa000..e03cf4ddb5a 100755 --- a/tests/Database/DatabaseConnectionFactoryTest.php +++ b/tests/Database/DatabaseConnectionFactoryTest.php @@ -24,6 +24,10 @@ protected function setUp(): void 'database' => ':memory:', ]); + $this->db->addConnection([ + 'url' => 'sqlite:///:memory:', + ], 'url'); + $this->db->addConnection([ 'driver' => 'sqlite', 'read' => [ @@ -44,15 +48,47 @@ protected function tearDown(): void public function testConnectionCanBeCreated() { - $this->assertInstanceOf(PDO::class, $this->db->connection()->getPdo()); - $this->assertInstanceOf(PDO::class, $this->db->connection()->getReadPdo()); - $this->assertInstanceOf(PDO::class, $this->db->connection('read_write')->getPdo()); - $this->assertInstanceOf(PDO::class, $this->db->connection('read_write')->getReadPdo()); + $this->assertInstanceOf(PDO::class, $this->db->getConnection()->getPdo()); + $this->assertInstanceOf(PDO::class, $this->db->getConnection()->getReadPdo()); + $this->assertInstanceOf(PDO::class, $this->db->getConnection('read_write')->getPdo()); + $this->assertInstanceOf(PDO::class, $this->db->getConnection('read_write')->getReadPdo()); + $this->assertInstanceOf(PDO::class, $this->db->getConnection('url')->getPdo()); + $this->assertInstanceOf(PDO::class, $this->db->getConnection('url')->getReadPdo()); + } + + public function testConnectionFromUrlHasProperConfig() + { + $this->db->addConnection([ + 'url' => 'mysql://root:pass@db/local?strict=true', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + ], 'url-config'); + + $this->assertEquals([ + 'name' => 'url-config', + 'driver' => 'mysql', + 'database' => 'local', + 'host' => 'db', + 'username' => 'root', + 'password' => 'pass', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + ], $this->db->getConnection('url-config')->getConfig()); } public function testSingleConnectionNotCreatedUntilNeeded() { - $connection = $this->db->connection(); + $connection = $this->db->getConnection(); $pdo = new ReflectionProperty(get_class($connection), 'pdo'); $pdo->setAccessible(true); $readPdo = new ReflectionProperty(get_class($connection), 'readPdo'); @@ -64,7 +100,7 @@ public function testSingleConnectionNotCreatedUntilNeeded() public function testReadWriteConnectionsNotCreatedUntilNeeded() { - $connection = $this->db->connection('read_write'); + $connection = $this->db->getConnection('read_write'); $pdo = new ReflectionProperty(get_class($connection), 'pdo'); $pdo->setAccessible(true); $readPdo = new ReflectionProperty(get_class($connection), 'readPdo'); @@ -105,13 +141,11 @@ public function testCustomConnectorsCanBeResolvedViaContainer() public function testSqliteForeignKeyConstraints() { $this->db->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'foreign_key_constraints' => true, + 'url' => 'sqlite:///:memory:?foreign_key_constraints=true', ], 'constraints_set'); - $this->assertEquals(0, $this->db->connection()->select('PRAGMA foreign_keys')[0]->foreign_keys); + $this->assertEquals(0, $this->db->getConnection()->select('PRAGMA foreign_keys')[0]->foreign_keys); - $this->assertEquals(1, $this->db->connection('constraints_set')->select('PRAGMA foreign_keys')[0]->foreign_keys); + $this->assertEquals(1, $this->db->getConnection('constraints_set')->select('PRAGMA foreign_keys')[0]->foreign_keys); } } diff --git a/tests/Database/DatabaseUrlParserTest.php b/tests/Database/DatabaseUrlParserTest.php new file mode 100644 index 00000000000..27245af801b --- /dev/null +++ b/tests/Database/DatabaseUrlParserTest.php @@ -0,0 +1,339 @@ +assertEquals($expectedOutput, (new UrlParser)->parseDatabaseConfigWithUrl($config)); + } + + public function testDriversAliases() + { + $this->assertEquals([ + 'mssql' => 'sqlsrv', + 'mysql2' => 'mysql', + 'postgres' => 'pgsql', + 'postgresql' => 'pgsql', + 'sqlite3' => 'sqlite', + ], UrlParser::getDriverAliases()); + + UrlParser::addDriverAlias('some-particular-alias', 'mysql'); + + $this->assertEquals([ + 'mssql' => 'sqlsrv', + 'mysql2' => 'mysql', + 'postgres' => 'pgsql', + 'postgresql' => 'pgsql', + 'sqlite3' => 'sqlite', + 'some-particular-alias' => 'mysql', + ], UrlParser::getDriverAliases()); + + $this->assertEquals([ + 'driver' => 'mysql', + ], (new UrlParser)->parseDatabaseConfigWithUrl('some-particular-alias://null')); + } + + public function databaseUrls() + { + return [ + 'simple URL' => [ + 'mysql://foo:bar@localhost/baz', + [ + 'driver' => 'mysql', + 'username' => 'foo', + 'password' => 'bar', + 'host' => 'localhost', + 'database' => 'baz', + ], + ], + 'simple URL with port' => [ + 'mysql://foo:bar@localhost:134/baz', + [ + 'driver' => 'mysql', + 'username' => 'foo', + 'password' => 'bar', + 'host' => 'localhost', + 'port' => 134, + 'database' => 'baz', + ], + ], + 'sqlite relative URL with host' => [ + 'sqlite://localhost/foo/database.sqlite', + [ + 'database' => 'foo/database.sqlite', + 'driver' => 'sqlite', + 'host' => 'localhost', + ], + ], + 'sqlite absolute URL with host' => [ + 'sqlite://localhost//tmp/database.sqlite', + [ + 'database' => '/tmp/database.sqlite', + 'driver' => 'sqlite', + 'host' => 'localhost', + ], + ], + 'sqlite relative URL without host' => [ + 'sqlite:///foo/database.sqlite', + [ + 'database' => 'foo/database.sqlite', + 'driver' => 'sqlite', + ], + ], + 'sqlite absolute URL without host' => [ + 'sqlite:////tmp/database.sqlite', + [ + 'database' => '/tmp/database.sqlite', + 'driver' => 'sqlite', + ], + ], + 'sqlite memory' => [ + 'sqlite:///:memory:', + [ + 'database' => ':memory:', + 'driver' => 'sqlite', + ], + ], + 'params parsed from URL override individual params' => [ + [ + 'url' => 'mysql://foo:bar@localhost/baz', + 'password' => 'lulz', + 'driver' => 'sqlite', + ], + [ + 'username' => 'foo', + 'password' => 'bar', + 'host' => 'localhost', + 'database' => 'baz', + 'driver' => 'mysql', + ], + ], + 'params not parsed from URL but individual params are preserved' => [ + [ + 'url' => 'mysql://foo:bar@localhost/baz', + 'port' => 134, + ], + [ + 'username' => 'foo', + 'password' => 'bar', + 'host' => 'localhost', + 'port' => 134, + 'database' => 'baz', + 'driver' => 'mysql', + ], + ], + 'query params from URL are used as extra params' => [ + 'url' => 'mysql://foo:bar@localhost/database?charset=UTF-8', + [ + 'driver' => 'mysql', + 'database' => 'database', + 'host' => 'localhost', + 'username' => 'foo', + 'password' => 'bar', + 'charset' => 'UTF-8', + ], + ], + 'simple URL with driver set apart' => [ + [ + 'url' => '//foo:bar@localhost/baz', + 'driver' => 'sqlsrv', + ], + [ + 'username' => 'foo', + 'password' => 'bar', + 'host' => 'localhost', + 'database' => 'baz', + 'driver' => 'sqlsrv', + ], + ], + 'simple URL with percent encoding' => [ + 'mysql://foo%3A:bar%2F@localhost/baz+baz%40', + [ + 'username' => 'foo:', + 'password' => 'bar/', + 'host' => 'localhost', + 'database' => 'baz+baz@', + 'driver' => 'mysql', + ], + ], + 'simple URL with percent sign in password' => [ + 'mysql://foo:bar%25bar@localhost/baz', + [ + 'username' => 'foo', + 'password' => 'bar%bar', + 'host' => 'localhost', + 'database' => 'baz', + 'driver' => 'mysql', + ], + ], + 'URL with mssql alias driver' => [ + 'mssql://null', + [ + 'driver' => 'sqlsrv', + ], + ], + 'URL with sqlsrv alias driver' => [ + 'sqlsrv://null', + [ + 'driver' => 'sqlsrv', + ], + ], + 'URL with mysql alias driver' => [ + 'mysql://null', + [ + 'driver' => 'mysql', + ], + ], + 'URL with mysql2 alias driver' => [ + 'mysql2://null', + [ + 'driver' => 'mysql', + ], + ], + 'URL with postgres alias driver' => [ + 'postgres://null', + [ + 'driver' => 'pgsql', + ], + ], + 'URL with postgresql alias driver' => [ + 'postgresql://null', + [ + 'driver' => 'pgsql', + ], + ], + 'URL with pgsql alias driver' => [ + 'pgsql://null', + [ + 'driver' => 'pgsql', + ], + ], + 'URL with sqlite alias driver' => [ + 'sqlite://null', + [ + 'driver' => 'sqlite', + ], + ], + 'URL with sqlite3 alias driver' => [ + 'sqlite3://null', + [ + 'driver' => 'sqlite', + ], + ], + + 'URL with unknown driver' => [ + 'foo://null', + [ + 'driver' => 'foo', + ], + ], + 'Sqlite with foreign_key_constraints' => [ + 'sqlite:////absolute/path/to/database.sqlite?foreign_key_constraints=true', + [ + 'driver' => 'sqlite', + 'database' => '/absolute/path/to/database.sqlite', + 'foreign_key_constraints' => true, + ], + ], + + 'Most complex example with read and write subarrays all in string' => [ + 'mysql://root:@null/database?read[host][]=192.168.1.1&write[host][]=196.168.1.2&sticky=true&charset=utf8mb4&collation=utf8mb4_unicode_ci&prefix=', + [ + 'read' => [ + 'host' => ['192.168.1.1'], + ], + 'write' => [ + 'host' => ['196.168.1.2'], + ], + 'sticky' => true, + 'driver' => 'mysql', + 'database' => 'database', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + ], + ], + + 'Full example from doc that prove that there isn\'t any Breaking Change' => [ + [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'forge', + 'username' => 'forge', + 'password' => '', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => ['foo' => 'bar'], + ], + [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'forge', + 'username' => 'forge', + 'password' => '', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => ['foo' => 'bar'], + ], + ], + + 'Full example from doc with url overwriting parameters' => [ + [ + 'url' => 'mysql://root:pass@db/local', + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'forge', + 'username' => 'forge', + 'password' => '', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => ['foo' => 'bar'], + ], + [ + 'driver' => 'mysql', + 'host' => 'db', + 'port' => '3306', + 'database' => 'local', + 'username' => 'root', + 'password' => 'pass', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => ['foo' => 'bar'], + ], + ], + ]; + } +}