Browse Source

Merge pull request #4861 from cakephp/3.0-dsn-support

Added preliminary support for DSN. Refs #4396
José Lorenzo Rodríguez 11 years ago
parent
commit
18cb33b935

+ 5 - 5
.travis.yml

@@ -6,9 +6,9 @@ php:
   - 5.6
 
 env:
-  - DB=mysql db_class='Cake\Database\Driver\Mysql' db_dsn='mysql:host=0.0.0.0;dbname=cakephp_test' db_database='cakephp_test' db_login='travis' db_password=''
-  - DB=pgsql db_class='Cake\Database\Driver\Postgres' db_dsn='pgsql:host=127.0.0.1;dbname=cakephp_test' db_database="cakephp_test" db_login='postgres' db_password=''
-  - DB=sqlite db_class='Cake\Database\Driver\Sqlite' db_dsn='sqlite::memory:'
+  - DB=mysql db_dsn='mysql://travis@0.0.0.0/cakephp_test'
+  - DB=pgsql db_dsn='postgres://postgres@127.0.0.1/cakephp_test'
+  - DB=sqlite db_dsn='sqlite:///memory:'
 
 services:
   - memcached
@@ -21,9 +21,9 @@ matrix:
     - php: 5.4
       env: PHPCS=1
     - php: hhvm-nightly
-      env: HHVM=1 DB=sqlite db_class='Cake\Database\Driver\Sqlite' db_dsn='sqlite::memory:'
+      env: HHVM=1 DB=sqlite db_dsn='sqlite:///memory:'
     - php: hhvm-nightly
-      env: HHVM=1 DB=mysql db_class='Cake\Database\Driver\Mysql' db_dsn='mysql:host=0.0.0.0;dbname=cakephp_test' db_database='cakephp_test' db_login='travis' db_password=''
+      env: HHVM=1 DB=mysql db_dsn='mysql://travis@0.0.0.0/cakephp_test'
 
 before_script:
   - composer self-update

+ 1 - 5
appveyor.yml

@@ -7,14 +7,10 @@ branches:
     - 3.0
 environment:
   global:
-    db_class: 'Cake\Database\Driver\SqlServer'
-    db_database: 'cakephp'
-    db_login: 'sa'
-    db_password: 'Password12!'
     PHP: "C:/PHP"
   matrix:
       - db: 2012
-        db_dsn: 'sqlsrv:Server=.\SQL2012SP1;Database=cakephp;MultipleActiveResultSets=false'
+        db_dsn: 'sqlserver://sa:Password12!@.\SQL2012SP1/cakephp?MultipleActiveResultSets=false'
 services:
   - mssql2012sp1
 init:

+ 4 - 15
phpunit.xml.dist

@@ -33,27 +33,16 @@
 	</listeners>
 	<php>
 		<!-- SQLite
-		<env name="db_class" value="Cake\Database\Driver\Sqlite"/>
-		<env name="db_dsn" value="sqlite::memory:"/>
+		<env name="db_dsn" value="sqlite://:memory:"/>
 		-->
 		<!-- Postgres
-		<env name="db_class" value="Cake\Database\Driver\Postgres"/>
-		<env name="db_database" value="cake_test"/>
-		<env name="db_login" value=""/>
-		<env name="db_password" value=""/>
+		<env name="db_dsn" value="postgres://localhost/cake_test"/>
 		-->
 		<!-- Mysql
-		<env name="db_class" value="Cake\Database\Driver\Mysql"/>
-		<env name="db_dsn" value="mysql:host=localhost;dbname=cake_test"/>
-		<env name="db_database" value=""/>
-		<env name="db_login" value=""/>
-		<env name="db_password" value=""/>
+		<env name="db_dsn" value="mysql://localhost/cake_test"/>
 		-->
 		<!-- SQL Server
-		<env name="db_class" value="Cake\Database\Driver\Sqlserver"/>
-		<env name="db_dsn" value="sqlsrv:Server=localhost;Database=cake_test;MultipleActiveResultSets=false"/>
-		<env name="db_login" value=""/>
-		<env name="db_password" value=""/>
+		<env name="db_dsn" value="sqlsrv://localhost/cake_test"/>
 		-->
 	</php>
 </phpunit>

+ 16 - 0
src/Cache/Cache.php

@@ -68,6 +68,22 @@ class Cache {
 	use StaticConfigTrait;
 
 /**
+ * An array mapping url schemes to fully qualified caching engine
+ * class names.
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'apc' => 'Cake\Cache\Engine\ApcEngine',
+		'file' => 'Cake\Cache\Engine\FileEngine',
+		'memcached' => 'Cake\Cache\Engine\MemcachedEngine',
+		'null' => 'Cake\Cache\Engine\NullEngine',
+		'redis' => 'Cake\Cache\Engine\RedisEngine',
+		'wincache' => 'Cake\Cache\Engine\WincacheEngine',
+		'xcache' => 'Cake\Cache\Engine\XcacheEngine',
+	];
+
+/**
  * Flag for tracking whether or not caching is enabled.
  *
  * @var bool

+ 10 - 4
src/Cache/Engine/MemcachedEngine.php

@@ -45,7 +45,7 @@ class MemcachedEngine extends CacheEngine {
  * - `duration` Specify how long items in this cache configuration last.
  * - `groups` List of groups or 'tags' associated to every key stored in this config.
  *    handy for deleting a complete group from cache.
- * - `login` Login to access the Memcache server
+ * - `username` Login to access the Memcache server
  * - `password` Password to access the Memcache server
  * - `persistent` The name of the persistent connection. All configurations using
  *    the same persistent value will share a single underlying connection.
@@ -67,7 +67,7 @@ class MemcachedEngine extends CacheEngine {
 		'compress' => false,
 		'duration' => 3600,
 		'groups' => [],
-		'login' => null,
+		'username' => null,
 		'password' => null,
 		'persistent' => false,
 		'prefix' => 'cake_',
@@ -147,14 +147,20 @@ class MemcachedEngine extends CacheEngine {
 			}
 		}
 
-		if ($this->_config['login'] !== null && $this->_config['password'] !== null) {
+		if (empty($this->_config['username']) && !empty($this->_config['login'])) {
+			throw new InvalidArgumentException(
+				'Please pass "username" instead of "login" for connecting to Memcached'
+			);
+		}
+
+		if ($this->_config['username'] !== null && $this->_config['password'] !== null) {
 			if (!method_exists($this->_Memcached, 'setSaslAuthData')) {
 				throw new InvalidArgumentException(
 					'Memcached extension is not build with SASL support'
 				);
 			}
 			$this->_Memcached->setSaslAuthData(
-				$this->_config['login'],
+				$this->_config['username'],
 				$this->_config['password']
 			);
 		}

+ 118 - 0
src/Core/StaticConfigTrait.php

@@ -15,6 +15,8 @@
 namespace Cake\Core;
 
 use BadMethodCallException;
+use InvalidArgumentException;
+use UnexpectedValueException;
 
 /**
  * A trait that provides a set of static methods to manage configuration
@@ -78,12 +80,21 @@ trait StaticConfigTrait {
 			}
 			return;
 		}
+
 		if (isset(static::$_config[$key])) {
 			throw new BadMethodCallException(sprintf('Cannot reconfigure existing key "%s"', $key));
 		}
+
 		if (is_object($config)) {
 			$config = ['className' => $config];
 		}
+
+		if (isset($config['url'])) {
+			$parsed = static::parseDsn($config['url']);
+			unset($config['url']);
+			$config = $parsed + $config;
+		}
+
 		if (isset($config['engine']) && empty($config['className'])) {
 			$config['className'] = $config['engine'];
 			unset($config['engine']);
@@ -123,4 +134,111 @@ trait StaticConfigTrait {
 		return array_keys(static::$_config);
 	}
 
+/**
+ * Parses a DSN into a valid connection configuration
+ *
+ * This method allows setting a DSN using formatting similar to that used by PEAR::DB.
+ * The following is an example of its usage:
+ *
+ * {{{
+ * $dsn = 'mysql://user:pass@localhost/database?';
+ * $config = ConnectionManager::parseDsn($dsn);
+ *
+ * $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS';
+ * $config = Log::parseDsn($dsn);
+ *
+ * $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null';
+ * $config = Email::parseDsn($dsn);
+ *
+ * $dsn = 'file:///?className=\My\Cache\Engine\FileEngine';
+ * $config = Cache::parseDsn($dsn);
+ *
+ * $dsn = 'File://?prefix=myapp_cake_core_&serialize=true&duration=+2 minutes&path=/tmp/persistent/';
+ * $config = Cache::parseDsn($dsn);
+ * }}}
+ *
+ * For all classes, the value of `scheme` is set as the value of both the `className`
+ * unless they have been otherwise specified.
+ *
+ * Note that querystring arguments are also parsed and set as values in the returned configuration.
+ *
+ * @param string $dsn The DSN string to convert to a configuration array
+ * @return array The configuration array to be stored after parsing the DSN
+ * @throws \InvalidArgumentException If not passed a string
+ */
+	public static function parseDsn($dsn) {
+		if (empty($dsn)) {
+			return [];
+		}
+
+		if (!is_string($dsn)) {
+			throw new InvalidArgumentException('Only strings can be passed to parseDsn');
+		}
+
+		if (preg_match("/^([\w\\\]+)/", $dsn, $matches)) {
+			$scheme = $matches[1];
+			$dsn = preg_replace("/^([\w\\\]+)/", 'file', $dsn);
+		}
+
+		$parsed = parse_url($dsn);
+
+		if ($parsed === false) {
+			return $dsn;
+		}
+
+		$parsed['scheme'] = $scheme;
+		$query = '';
+
+		if (isset($parsed['query'])) {
+			$query = $parsed['query'];
+			unset($parsed['query']);
+		}
+
+		parse_str($query, $queryArgs);
+
+		foreach ($queryArgs as $key => $value) {
+			if ($value === 'true') {
+				$queryArgs[$key] = true;
+			} elseif ($value === 'false') {
+				$queryArgs[$key] = false;
+			} elseif ($value === 'null') {
+				$queryArgs[$key] = null;
+			}
+		}
+
+		if (isset($parsed['user'])) {
+			$parsed['username'] = $parsed['user'];
+		}
+
+		if (isset($parsed['pass'])) {
+			$parsed['password'] = $parsed['pass'];
+		}
+
+		unset($parsed['pass'], $parsed['user']);
+		$parsed = $queryArgs + $parsed;
+
+		if (empty($parsed['className'])) {
+			$classMap = static::dsnClassMap();
+
+			$parsed['className'] = $parsed['scheme'];
+			if (isset($classMap[$parsed['scheme']])) {
+				$parsed['className'] = $classMap[$parsed['scheme']];
+			}
+		}
+
+		return $parsed;
+	}
+
+/**
+ * return or update the dsn class map for this class
+ *
+ * @param array|null $map additions/edits to the class map to apply
+ * @return array
+ */
+	public static function dsnClassMap($map = null) {
+		if ($map) {
+			static::$_dsnClassMap = $map + static::$_dsnClassMap;
+		}
+		return static::$_dsnClassMap;
+	}
 }

+ 1 - 1
src/Database/Connection.php

@@ -659,7 +659,7 @@ class Connection {
 	public function __debugInfo() {
 		$secrets = [
 			'password' => '*****',
-			'login' => '*****',
+			'username' => '*****',
 			'host' => '*****',
 			'database' => '*****',
 			'port' => '*****',

+ 6 - 9
src/Database/Driver/Mysql.php

@@ -30,7 +30,7 @@ class Mysql extends \Cake\Database\Driver {
 	protected $_baseConfig = [
 		'persistent' => true,
 		'host' => 'localhost',
-		'login' => 'root',
+		'username' => 'root',
 		'password' => '',
 		'database' => 'cake',
 		'port' => '3306',
@@ -38,7 +38,6 @@ class Mysql extends \Cake\Database\Driver {
 		'encoding' => 'utf8',
 		'timezone' => null,
 		'init' => [],
-		'dsn' => null
 	];
 
 /**
@@ -74,15 +73,13 @@ class Mysql extends \Cake\Database\Driver {
 			$config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca'];
 		}
 
-		if (empty($config['dsn'])) {
-			if (empty($config['unix_socket'])) {
-				$config['dsn'] = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']};charset={$config['encoding']}";
-			} else {
-				$config['dsn'] = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
-			}
+		if (empty($config['unix_socket'])) {
+			$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']};charset={$config['encoding']}";
+		} else {
+			$dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
 		}
 
-		$this->_connect($config);
+		$this->_connect($dsn, $config);
 
 		if (!empty($config['init'])) {
 			foreach ((array)$config['init'] as $command) {

+ 11 - 3
src/Database/Driver/PDODriverTrait.php

@@ -15,6 +15,7 @@
 namespace Cake\Database\Driver;
 
 use Cake\Database\Statement\PDOStatement;
+use InvalidArgumentException;
 use PDO;
 
 /**
@@ -32,13 +33,20 @@ trait PDODriverTrait {
 /**
  * Establishes a connection to the database server
  *
+ * @param string $dsn A Driver-specific PDO-DSN
  * @param array $config configuration to be used for creating connection
  * @return bool true on success
+ * @throws InvalidArgumentException
  */
-	protected function _connect(array $config) {
+	protected function _connect($dsn, array $config) {
+		if (empty($config['username']) && !empty($config['login'])) {
+			throw new InvalidArgumentException(
+				'Please pass "username" instead of "login" for connecting to the database'
+			);
+		}
 		$connection = new PDO(
-			$config['dsn'],
-			$config['login'],
+			$dsn,
+			$config['username'],
 			$config['password'],
 			$config['flags']
 		);

+ 3 - 7
src/Database/Driver/Postgres.php

@@ -30,7 +30,7 @@ class Postgres extends \Cake\Database\Driver {
 	protected $_baseConfig = [
 		'persistent' => true,
 		'host' => 'localhost',
-		'login' => 'root',
+		'username' => 'root',
 		'password' => '',
 		'database' => 'cake',
 		'schema' => 'public',
@@ -39,7 +39,6 @@ class Postgres extends \Cake\Database\Driver {
 		'timezone' => null,
 		'flags' => [],
 		'init' => [],
-		'dsn' => null
 	];
 
 /**
@@ -57,11 +56,8 @@ class Postgres extends \Cake\Database\Driver {
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 		];
 
-		if (empty($config['dsn'])) {
-			$config['dsn'] = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
-		}
-
-		$this->_connect($config);
+		$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
+		$this->_connect($dsn, $config);
 		$this->_connection = $connection = $this->connection();
 		if (!empty($config['encoding'])) {
 			$this->setEncoding($config['encoding']);

+ 3 - 7
src/Database/Driver/Sqlite.php

@@ -31,13 +31,12 @@ class Sqlite extends \Cake\Database\Driver {
  */
 	protected $_baseConfig = [
 		'persistent' => false,
-		'login' => null,
+		'username' => null,
 		'password' => null,
 		'database' => ':memory:',
 		'encoding' => 'utf8',
 		'flags' => [],
 		'init' => [],
-		'dsn' => null
 	];
 
 /**
@@ -55,11 +54,8 @@ class Sqlite extends \Cake\Database\Driver {
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 		];
 
-		if (empty($config['dsn'])) {
-			$config['dsn'] = "sqlite:{$config['database']}";
-		}
-
-		$this->_connect($config);
+		$dsn = "sqlite:{$config['database']}";
+		$this->_connect($dsn, $config);
 
 		if (!empty($config['init'])) {
 			foreach ((array)$config['init'] as $command) {

+ 3 - 6
src/Database/Driver/Sqlserver.php

@@ -34,14 +34,13 @@ class Sqlserver extends \Cake\Database\Driver {
 	protected $_baseConfig = [
 		'persistent' => false,
 		'host' => 'localhost\SQLEXPRESS',
-		'login' => '',
+		'username' => '',
 		'password' => '',
 		'database' => 'cake',
 		'encoding' => PDO::SQLSRV_ENCODING_UTF8,
 		'flags' => [],
 		'init' => [],
 		'settings' => [],
-		'dsn' => null
 	];
 
 /**
@@ -62,11 +61,9 @@ class Sqlserver extends \Cake\Database\Driver {
 		if (!empty($config['encoding'])) {
 			$config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
 		}
-		if (empty($config['dsn'])) {
-			$config['dsn'] = "sqlsrv:Server={$config['host']};Database={$config['database']};MultipleActiveResultSets=false";
-		}
 
-		$this->_connect($config);
+		$dsn = "sqlsrv:Server={$config['host']};Database={$config['database']};MultipleActiveResultSets=false";
+		$this->_connect($dsn, $config);
 
 		$connection = $this->connection();
 		if (!empty($config['init'])) {

+ 54 - 0
src/Datasource/ConnectionManager.php

@@ -31,6 +31,7 @@ class ConnectionManager {
 
 	use StaticConfigTrait {
 		config as protected _config;
+		parseDsn as protected _parseDsn;
 	}
 
 /**
@@ -41,6 +42,18 @@ class ConnectionManager {
 	protected static $_aliasMap = [];
 
 /**
+ * An array mapping url schemes to fully qualified driver class names
+ *
+ * @return array
+ */
+	protected static $_dsnClassMap = [
+		'mysql' => 'Cake\Database\Driver\Mysql',
+		'postgres' => 'Cake\Database\Driver\Postgres',
+		'sqlite' => 'Cake\Database\Driver\Sqlite',
+		'sqlserver' => 'Cake\Database\Driver\Sqlserver',
+	];
+
+/**
  * The ConnectionRegistry used by the manager.
  *
  * @var \Cake\Datasource\ConnectionRegistry
@@ -66,6 +79,47 @@ class ConnectionManager {
 	}
 
 /**
+ * Parses a DSN into a valid connection configuration
+ *
+ * This method allows setting a DSN using formatting similar to that used by PEAR::DB.
+ * The following is an example of its usage:
+ *
+ * {{{
+ * $dsn = 'mysql://user:pass@localhost/database';
+ * $config = ConnectionManager::parseDsn($dsn);
+ *
+ * $dsn = 'Cake\Database\Driver\Mysql://localhost:3306/database?className=Cake\Database\Connection';
+ * $config = ConnectionManager::parseDsn($dsn);
+ *
+ * $dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql';
+ * $config = ConnectionManager::parseDsn($dsn);
+ * }}}
+ *
+ * For all classes, the value of `scheme` is set as the value of both the `className` and `driver`
+ * unless they have been otherwise specified.
+ *
+ * Note that querystring arguments are also parsed and set as values in the returned configuration.
+ *
+ * @param array $config An array with a `url` key mapping to a string DSN
+ * @return array The configuration array to be stored after parsing the DSN
+ */
+	public static function parseDsn($config = null) {
+		$config = static::_parseDsn($config);
+
+		if (isset($config['path']) && empty($config['database'])) {
+			$config['database'] = substr($config['path'], 1);
+		}
+
+		if (empty($config['driver'])) {
+			$config['driver'] = $config['className'];
+			$config['className'] = 'Cake\Database\Connection';
+		}
+
+		unset($config['path']);
+		return $config;
+	}
+
+/**
  * Set one or more connection aliases.
  *
  * Connection aliases allow you to rename active connections without overwriting

+ 11 - 0
src/Log/Log.php

@@ -108,6 +108,17 @@ class Log {
 	}
 
 /**
+ * An array mapping url schemes to fully qualified Log engine class names
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'console' => 'Cake\Log\Engine\ConsoleLog',
+		'file' => 'Cake\Log\Engine\FileLog',
+		'syslog' => 'Cake\Log\Engine\SyslogLog',
+	];
+
+/**
  * Internal flag for tracking whether or not configuration has been changed.
  *
  * @var bool

+ 19 - 0
src/Network/Email/Email.php

@@ -304,6 +304,17 @@ class Email {
 	protected $_boundary = null;
 
 /**
+ * An array mapping url schemes to fully qualified Transport class names
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'debug' => 'Cake\Network\Email\DebugTransport',
+		'mail' => 'Cake\Network\Email\MailTransport',
+		'smtp' => 'Cake\Network\Email\SmtpTransport',
+	];
+
+/**
  * Configuration profiles for transports.
  *
  * @var array
@@ -1174,9 +1185,17 @@ class Email {
 		if (isset(static::$_transportConfig[$key])) {
 			throw new BadMethodCallException(sprintf('Cannot modify an existing config "%s"', $key));
 		}
+
 		if (is_object($config)) {
 			$config = ['className' => $config];
 		}
+
+		if (isset($config['url'])) {
+			$parsed = static::parseDsn($config['url']);
+			unset($config['url']);
+			$config = $parsed + $config;
+		}
+
 		static::$_transportConfig[$key] = $config;
 	}
 

+ 2 - 2
tests/TestCase/Cache/Engine/MemcachedEngineTest.php

@@ -111,7 +111,7 @@ class MemcachedEngineTest extends TestCase {
 			'servers' => array('127.0.0.1'),
 			'persistent' => false,
 			'compress' => false,
-			'login' => null,
+			'username' => null,
 			'password' => null,
 			'groups' => array(),
 			'serialize' => 'php',
@@ -356,7 +356,7 @@ class MemcachedEngineTest extends TestCase {
 			'engine' => 'Memcached',
 			'servers' => array('127.0.0.1:11211'),
 			'persistent' => false,
-			'login' => 'test',
+			'username' => 'test',
 			'password' => 'password'
 		);
 

+ 446 - 0
tests/TestCase/Core/StaticConfigTraitTest.php

@@ -0,0 +1,446 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Core;
+
+use Cake\Core\StaticConfigTrait;
+use Cake\TestSuite\TestCase;
+use PHPUnit_Framework_Test;
+
+/**
+ * TestConnectionManagerStaticConfig
+ */
+class TestConnectionManagerStaticConfig {
+
+	use StaticConfigTrait {
+		parseDsn as protected _parseDsn;
+	}
+
+/**
+ * Parse a DSN
+ *
+ * @param string $config The config to parse.
+ * @return array
+ */
+	public static function parseDsn($config = null) {
+		$config = static::_parseDsn($config);
+
+		if (isset($config['path']) && empty($config['database'])) {
+			$config['database'] = substr($config['path'], 1);
+		}
+
+		if (empty($config['driver'])) {
+			$config['driver'] = $config['className'];
+			$config['className'] = 'Cake\Database\Connection';
+		}
+
+		unset($config['path']);
+		return $config;
+	}
+
+/**
+ * Database driver class map.
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'mysql' => 'Cake\Database\Driver\Mysql',
+		'postgres' => 'Cake\Database\Driver\Postgres',
+		'sqlite' => 'Cake\Database\Driver\Sqlite',
+		'sqlserver' => 'Cake\Database\Driver\Sqlserver',
+	];
+
+}
+
+/**
+ * TestCacheStaticConfig
+ */
+class TestCacheStaticConfig {
+
+	use StaticConfigTrait;
+
+/**
+ * Cache driver class map.
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'apc' => 'Cake\Cache\Engine\ApcEngine',
+		'file' => 'Cake\Cache\Engine\FileEngine',
+		'memcached' => 'Cake\Cache\Engine\MemcachedEngine',
+		'null' => 'Cake\Cache\Engine\NullEngine',
+		'redis' => 'Cake\Cache\Engine\RedisEngine',
+		'wincache' => 'Cake\Cache\Engine\WincacheEngine',
+		'xcache' => 'Cake\Cache\Engine\XcacheEngine',
+	];
+
+}
+
+/**
+ * TestEmailStaticConfig
+ */
+class TestEmailStaticConfig {
+
+	use StaticConfigTrait;
+
+/**
+ * Email driver class map.
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'debug' => 'Cake\Network\Email\DebugTransport',
+		'mail' => 'Cake\Network\Email\MailTransport',
+		'smtp' => 'Cake\Network\Email\SmtpTransport',
+	];
+
+}
+
+/**
+ * TestLogStaticConfig
+ */
+class TestLogStaticConfig {
+
+	use StaticConfigTrait;
+
+/**
+ * Log engine class map.
+ *
+ * @var array
+ */
+	protected static $_dsnClassMap = [
+		'console' => 'Cake\Log\Engine\ConsoleLog',
+		'file' => 'Cake\Log\Engine\FileLog',
+		'syslog' => 'Cake\Log\Engine\SyslogLog',
+	];
+
+}
+
+/**
+ * StaticConfigTraitTest class
+ *
+ */
+class StaticConfigTraitTest extends TestCase {
+
+/**
+ * setup method
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+		$this->subject = $this->getObjectForTrait('Cake\Core\StaticConfigTrait');
+	}
+
+/**
+ * teardown method
+ *
+ * @return void
+ */
+	public function tearDown() {
+		unset($this->subject);
+		parent::tearDown();
+	}
+
+/**
+ * Tests simple usage of parseDsn
+ *
+ * @return void
+ */
+	public function testSimpleParseDsn() {
+		$className = get_class($this->subject);
+		$this->assertSame([], $className::parseDsn(''));
+	}
+
+/**
+ * Tests that failing to pass a string to parseDsn will throw an exception
+ *
+ * @expectedException InvalidArgumentException
+ * @return void
+ */
+	public function testParseBadType() {
+		$className = get_class($this->subject);
+		$className::parseDsn(['url' => 'http://:80']);
+	}
+
+/**
+ * Tests parsing different DSNs
+ *
+ * @return void
+ */
+	public function testCustomParseDsn() {
+		$dsn = 'mysql://localhost:3306/database';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'driver' => 'Cake\Database\Driver\Mysql',
+			'host' => 'localhost',
+			'database' => 'database',
+			'port' => 3306,
+			'scheme' => 'mysql',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mysql://user:password@localhost:3306/database';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'driver' => 'Cake\Database\Driver\Mysql',
+			'host' => 'localhost',
+			'password' => 'password',
+			'database' => 'database',
+			'port' => 3306,
+			'scheme' => 'mysql',
+			'username' => 'user',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'sqlite:///memory:';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'driver' => 'Cake\Database\Driver\Sqlite',
+			'database' => 'memory:',
+			'scheme' => 'sqlite',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'sqlite:///?database=memory:';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'driver' => 'Cake\Database\Driver\Sqlite',
+			'database' => 'memory:',
+			'scheme' => 'sqlite',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'sqlserver://sa:Password12!@.\SQL2012SP1/cakephp?MultipleActiveResultSets=false';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'driver' => 'Cake\Database\Driver\Sqlserver',
+			'host' => '.\SQL2012SP1',
+			'MultipleActiveResultSets' => false,
+			'password' => 'Password12!',
+			'database' => 'cakephp',
+			'scheme' => 'sqlserver',
+			'username' => 'sa',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+	}
+
+/**
+ * Tests className/driver value setting
+ *
+ * @return void
+ */
+	public function testParseDsnClassnameDriver() {
+		$dsn = 'mysql://localhost:3306/database';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'database' => 'database',
+			'driver' => 'Cake\Database\Driver\Mysql',
+			'host' => 'localhost',
+			'port' => 3306,
+			'scheme' => 'mysql',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mysql://user:password@localhost:3306/database';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'database' => 'database',
+			'driver' => 'Cake\Database\Driver\Mysql',
+			'host' => 'localhost',
+			'password' => 'password',
+			'port' => 3306,
+			'scheme' => 'mysql',
+			'username' => 'user',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mysql://localhost/database?className=Custom\Driver';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'database' => 'database',
+			'driver' => 'Custom\Driver',
+			'host' => 'localhost',
+			'scheme' => 'mysql',
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mysql://localhost:3306/database?className=Custom\Driver';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'database' => 'database',
+			'driver' => 'Custom\Driver',
+			'host' => 'localhost',
+			'scheme' => 'mysql',
+			'port' => 3306,
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+
+		$dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql';
+		$expected = [
+			'className' => 'Cake\Database\Connection',
+			'database' => 'database',
+			'driver' => 'Cake\Database\Driver\Mysql',
+			'host' => 'localhost',
+			'scheme' => 'Cake\Database\Connection',
+			'port' => 3306,
+		];
+		$this->assertEquals($expected, TestConnectionManagerStaticConfig::parseDsn($dsn));
+	}
+
+/**
+ * Tests parsing querystring values
+ *
+ * @return void
+ */
+	public function testParseDsnQuerystring() {
+		$dsn = 'file:///?url=test';
+		$expected = [
+			'className' => 'Cake\Log\Engine\FileLog',
+			'path' => '/',
+			'scheme' => 'file',
+			'url' => 'test',
+		];
+		$this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
+
+		$dsn = 'file:///?file=debug&key=value';
+		$expected = [
+			'className' => 'Cake\Log\Engine\FileLog',
+			'file' => 'debug',
+			'key' => 'value',
+			'path' => '/',
+			'scheme' => 'file',
+		];
+		$this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
+
+		$dsn = 'file:///tmp?file=debug&types[]=notice&types[]=info&types[]=debug';
+		$expected = [
+			'className' => 'Cake\Log\Engine\FileLog',
+			'file' => 'debug',
+			'path' => '/tmp',
+			'scheme' => 'file',
+			'types' => ['notice', 'info', 'debug'],
+		];
+		$this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mail:///?timeout=30&key=true&key2=false&client=null&tls=null';
+		$expected = [
+			'className' => 'Cake\Network\Email\MailTransport',
+			'client' => null,
+			'key' => true,
+			'key2' => false,
+			'path' => '/',
+			'scheme' => 'mail',
+			'timeout' => '30',
+			'tls' => null,
+		];
+		$this->assertEquals($expected, TestEmailStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mail://true:false@null/1?timeout=30&key=true&key2=false&client=null&tls=null';
+		$expected = [
+			'className' => 'Cake\Network\Email\MailTransport',
+			'client' => null,
+			'host' => 'null',
+			'key' => true,
+			'key2' => false,
+			'password' => 'false',
+			'path' => '/1',
+			'scheme' => 'mail',
+			'timeout' => '30',
+			'tls' => null,
+			'username' => 'true',
+		];
+		$this->assertEquals($expected, TestEmailStaticConfig::parseDsn($dsn));
+
+		$dsn = 'mail://user:secret@localhost:25?timeout=30&client=null&tls=null';
+		$expected = [
+			'className' => 'Cake\Network\Email\MailTransport',
+			'client' => null,
+			'host' => 'localhost',
+			'password' => 'secret',
+			'port' => 25,
+			'scheme' => 'mail',
+			'timeout' => '30',
+			'tls' => null,
+			'username' => 'user',
+		];
+		$this->assertEquals($expected, TestEmailStaticConfig::parseDsn($dsn));
+
+		$dsn = 'file:///?prefix=myapp_cake_core_&serialize=true&duration=%2B2 minutes';
+		$expected = [
+			'className' => 'Cake\Log\Engine\FileLog',
+			'duration' => '+2 minutes',
+			'path' => '/',
+			'prefix' => 'myapp_cake_core_',
+			'scheme' => 'file',
+			'serialize' => true,
+		];
+		$this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
+	}
+
+/**
+ * Tests loading a single plugin
+ *
+ * @return void
+ */
+	public function testParseDsnPathSetting() {
+		$dsn = 'file:///';
+		$expected = [
+			'className' => 'Cake\Log\Engine\FileLog',
+			'path' => '/',
+			'scheme' => 'file',
+		];
+		$this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
+
+		$dsn = 'file:///?path=/tmp/persistent/';
+		$expected = [
+			'className' => 'Cake\Log\Engine\FileLog',
+			'path' => '/tmp/persistent/',
+			'scheme' => 'file',
+		];
+		$this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
+	}
+
+/**
+ * Test that the dsn map can be updated/append to
+ *
+ * @return void
+ */
+	public function testCanUpdateClassMap() {
+		$expected = [
+			'console' => 'Cake\Log\Engine\ConsoleLog',
+			'file' => 'Cake\Log\Engine\FileLog',
+			'syslog' => 'Cake\Log\Engine\SyslogLog',
+		];
+		$result = TestLogStaticConfig::dsnClassMap();
+		$this->assertEquals($expected, $result, "The class map should match the class property");
+
+		$expected = [
+			'console' => 'Special\EngineLog',
+			'file' => 'Cake\Log\Engine\FileLog',
+			'syslog' => 'Cake\Log\Engine\SyslogLog',
+		];
+		$result = TestLogStaticConfig::dsnClassMap(['console' => 'Special\EngineLog']);
+		$this->assertEquals($expected, $result, "Should be possible to change the map");
+
+		$expected = [
+			'console' => 'Special\EngineLog',
+			'file' => 'Cake\Log\Engine\FileLog',
+			'syslog' => 'Cake\Log\Engine\SyslogLog',
+			'my' => 'Special\OtherLog'
+		];
+		$result = TestLogStaticConfig::dsnClassMap(['my' => 'Special\OtherLog']);
+		$this->assertEquals($expected, $result, "Should be possible to add to the map");
+	}
+
+}

+ 2 - 2
tests/TestCase/Database/ConnectionTest.php

@@ -114,8 +114,8 @@ class ConnectionTest extends TestCase {
  */
 	public function testWrongCredentials() {
 		$config = ConnectionManager::config('test');
-		$this->skipIf(isset($config['dsn']), 'Datasource has dsn, skipping.');
-		$connection = new Connection(['database' => '_probably_not_there_'] + ConnectionManager::config('test'));
+		$this->skipIf(isset($config['url']), 'Datasource has dsn, skipping.');
+		$connection = new Connection(['database' => '/dev/nonexistent'] + ConnectionManager::config('test'));
 		$connection->connect();
 	}
 

+ 6 - 6
tests/TestCase/Database/Driver/MysqlTest.php

@@ -45,10 +45,11 @@ class MysqlTest extends TestCase {
  */
 	public function testConnectionConfigDefault() {
 		$driver = $this->getMock('Cake\Database\Driver\Mysql', ['_connect']);
+		$dsn = 'mysql:host=localhost;port=3306;dbname=cake;charset=utf8';
 		$expected = [
 			'persistent' => true,
 			'host' => 'localhost',
-			'login' => 'root',
+			'username' => 'root',
 			'password' => '',
 			'database' => 'cake',
 			'port' => '3306',
@@ -56,7 +57,6 @@ class MysqlTest extends TestCase {
 			'encoding' => 'utf8',
 			'timezone' => null,
 			'init' => [],
-			'dsn' => 'mysql:host=localhost;port=3306;dbname=cake;charset=utf8'
 		];
 
 		$expected['flags'] += [
@@ -65,7 +65,7 @@ class MysqlTest extends TestCase {
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 		];
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 		$driver->connect([]);
 	}
 
@@ -79,7 +79,7 @@ class MysqlTest extends TestCase {
 			'persistent' => false,
 			'host' => 'foo',
 			'database' => 'bar',
-			'login' => 'user',
+			'username' => 'user',
 			'password' => 'pass',
 			'port' => 3440,
 			'flags' => [1 => true, 2 => false],
@@ -92,8 +92,8 @@ class MysqlTest extends TestCase {
 			['_connect', 'connection'],
 			[$config]
 		);
+		$dsn = 'mysql:host=foo;port=3440;dbname=bar;charset=a-language';
 		$expected = $config;
-		$expected['dsn'] = 'mysql:host=foo;port=3440;dbname=bar;charset=a-language';
 		$expected['init'][] = "SET time_zone = 'Antartica'";
 		$expected['flags'] += [
 			PDO::ATTR_PERSISTENT => false,
@@ -108,7 +108,7 @@ class MysqlTest extends TestCase {
 		$connection->expects($this->exactly(3))->method('exec');
 
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 		$driver->expects($this->any())->method('connection')
 			->will($this->returnValue($connection));
 		$driver->connect($config);

+ 7 - 7
tests/TestCase/Database/Driver/PostgresTest.php

@@ -34,10 +34,11 @@ class PostgresTest extends TestCase {
  */
 	public function testConnectionConfigDefault() {
 		$driver = $this->getMock('Cake\Database\Driver\Postgres', ['_connect', 'connection']);
+		$dsn = 'pgsql:host=localhost;port=5432;dbname=cake';
 		$expected = [
 			'persistent' => true,
 			'host' => 'localhost',
-			'login' => 'root',
+			'username' => 'root',
 			'password' => '',
 			'database' => 'cake',
 			'schema' => 'public',
@@ -46,7 +47,6 @@ class PostgresTest extends TestCase {
 			'timezone' => null,
 			'flags' => [],
 			'init' => [],
-			'dsn' => 'pgsql:host=localhost;port=5432;dbname=cake'
 		];
 
 		$expected['flags'] += [
@@ -68,7 +68,7 @@ class PostgresTest extends TestCase {
 		$connection->expects($this->exactly(2))->method('exec');
 
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 		$driver->expects($this->any())->method('connection')
 			->will($this->returnValue($connection));
 
@@ -85,7 +85,7 @@ class PostgresTest extends TestCase {
 			'persistent' => false,
 			'host' => 'foo',
 			'database' => 'bar',
-			'login' => 'user',
+			'username' => 'user',
 			'password' => 'pass',
 			'port' => 3440,
 			'flags' => [1 => true, 2 => false],
@@ -99,9 +99,9 @@ class PostgresTest extends TestCase {
 			['_connect', 'connection'],
 			[$config]
 		);
+		$dsn = 'pgsql:host=foo;port=3440;dbname=bar';
 
 		$expected = $config;
-		$expected['dsn'] = 'pgsql:host=foo;port=3440;dbname=bar';
 		$expected['flags'] += [
 			PDO::ATTR_PERSISTENT => false,
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
@@ -125,7 +125,7 @@ class PostgresTest extends TestCase {
 
 		$driver->connection($connection);
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 
 		$driver->expects($this->any())->method('connection')
 			->will($this->returnValue($connection));
@@ -142,7 +142,7 @@ class PostgresTest extends TestCase {
 		$driver = $this->getMock(
 			'Cake\Database\Driver\Postgres',
 			['_connect', 'connection'],
-			[['dsn' => 'foo']]
+			[[]]
 		);
 		$connection = $this
 			->getMockBuilder('\Cake\Database\Connection')

+ 6 - 6
tests/TestCase/Database/Driver/SqliteTest.php

@@ -32,15 +32,15 @@ class SqliteTest extends TestCase {
  */
 	public function testConnectionConfigDefault() {
 		$driver = $this->getMock('Cake\Database\Driver\Sqlite', ['_connect']);
+		$dsn = 'sqlite::memory:';
 		$expected = [
 			'persistent' => false,
 			'database' => ':memory:',
 			'encoding' => 'utf8',
-			'login' => null,
+			'username' => null,
 			'password' => null,
 			'flags' => [],
 			'init' => [],
-			'dsn' => 'sqlite::memory:'
 		];
 
 		$expected['flags'] += [
@@ -48,7 +48,7 @@ class SqliteTest extends TestCase {
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 		];
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 		$driver->connect([]);
 	}
 
@@ -71,10 +71,10 @@ class SqliteTest extends TestCase {
 			['_connect', 'connection'],
 			[$config]
 		);
+		$dsn = 'sqlite:bar.db';
 
 		$expected = $config;
-		$expected += ['login' => null, 'password' => null];
-		$expected['dsn'] = 'sqlite:bar.db';
+		$expected += ['username' => null, 'password' => null];
 		$expected['flags'] += [
 			PDO::ATTR_PERSISTENT => true,
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
@@ -86,7 +86,7 @@ class SqliteTest extends TestCase {
 		$connection->expects($this->exactly(2))->method('exec');
 
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 		$driver->expects($this->any())->method('connection')
 			->will($this->returnValue($connection));
 		$driver->connect($config);

+ 5 - 5
tests/TestCase/Database/Driver/SqlserverTest.php

@@ -45,7 +45,7 @@ class SqlserverTest extends TestCase {
 		$config = [
 			'persistent' => false,
 			'host' => 'foo',
-			'login' => 'Administrator',
+			'username' => 'Administrator',
 			'password' => 'blablabla',
 			'database' => 'bar',
 			'encoding' => 'a-language',
@@ -58,9 +58,9 @@ class SqlserverTest extends TestCase {
 			['_connect', 'connection'],
 			[$config]
 		);
+		$dsn = 'sqlsrv:Server=foo;Database=bar;MultipleActiveResultSets=false';
 
 		$expected = $config;
-		$expected['dsn'] = 'sqlsrv:Server=foo;Database=bar;MultipleActiveResultSets=false';
 		$expected['flags'] += [
 			PDO::ATTR_PERSISTENT => false,
 			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
@@ -83,7 +83,7 @@ class SqlserverTest extends TestCase {
 
 		$driver->connection($connection);
 		$driver->expects($this->once())->method('_connect')
-			->with($expected);
+			->with($dsn, $expected);
 
 		$driver->expects($this->any())->method('connection')
 			->will($this->returnValue($connection));
@@ -100,7 +100,7 @@ class SqlserverTest extends TestCase {
 		$driver = $this->getMock(
 			'Cake\Database\Driver\Sqlserver',
 			['_connect', 'connection', '_version'],
-			[['dsn' => 'foo']]
+			[[]]
 		);
 		$driver
 			->expects($this->any())
@@ -154,7 +154,7 @@ class SqlserverTest extends TestCase {
 		$driver = $this->getMock(
 			'Cake\Database\Driver\Sqlserver',
 			['_connect', 'connection', '_version'],
-			[['dsn' => 'foo']]
+			[[]]
 		);
 		$driver
 			->expects($this->any())

+ 3 - 12
tests/bootstrap.php

@@ -97,20 +97,11 @@ Cache::config([
 ]);
 
 // Ensure default test connection is defined
-if (!getenv('db_class')) {
-	putenv('db_class=Cake\Database\Driver\Sqlite');
-	putenv('db_dsn=sqlite::memory:');
+if (!getenv('db_dsn')) {
+	putenv('db_dsn=sqlite://:memory:');
 }
 
-ConnectionManager::config('test', [
-	'className' => 'Cake\Database\Connection',
-	'driver' => getenv('db_class'),
-	'dsn' => getenv('db_dsn'),
-	'database' => getenv('db_database'),
-	'login' => getenv('db_login'),
-	'password' => getenv('db_password'),
-	'timezone' => 'UTC'
-]);
+ConnectionManager::config('test', ['url' => getenv('db_dsn')]);
 
 Configure::write('Session', [
 	'defaults' => 'php'