Skip to content

Commit

Permalink
separate connection string parsing from connection
Browse files Browse the repository at this point in the history
  • Loading branch information
koenpunt committed Feb 9, 2015
1 parent 9113cc2 commit 0ffaef0
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 104 deletions.
1 change: 1 addition & 0 deletions ActiveRecord.php
Expand Up @@ -15,6 +15,7 @@
require __DIR__.'/lib/Table.php';
require __DIR__.'/lib/ConnectionManager.php';
require __DIR__.'/lib/Connection.php';
require __DIR__.'/lib/ConnectionInfo.php';
require __DIR__.'/lib/Serialization.php';
require __DIR__.'/lib/Expressions.php';
require __DIR__.'/lib/SQLBuilder.php';
Expand Down
130 changes: 27 additions & 103 deletions lib/Connection.php
Expand Up @@ -86,35 +86,45 @@ abstract class Connection
* A connection name that is set in ActiveRecord\Config
* If null it will use the default connection specified by ActiveRecord\Config->set_default_connection
* @return Connection
* @see parse_connection_url
* @see ConnectionInfo::from_connection_url
*/
public static function instance($connection_string_or_connection_name=null)
public static function instance($connection_info_or_name=null)
{
$config = Config::instance();

if (strpos($connection_string_or_connection_name, '://') === false)
if (!is_array($connection_info_or_name))
{
$connection_string = $connection_string_or_connection_name ?
$config->get_connection($connection_string_or_connection_name) :
$config->get_default_connection_string();
if (strpos($connection_info_or_name, '://') === false)
{
$connection_url = $connection_info_or_name ?
$config->get_connection($connection_info_or_name) :
$config->get_default_connection_string();
}
else
{
$connection_url = $connection_info_or_name;
}

if (!$connection_url)
throw new DatabaseException("Empty connection string");

$connection_info = ConnectionInfo::from_connection_url($connection_url);
}
else
$connection_string = $connection_string_or_connection_name;

if (!$connection_string)
throw new DatabaseException("Empty connection string");
{
$connection_info = new ConnectionInfo($connection_info_or_name);
}

$info = static::parse_connection_url($connection_string);
$fqclass = static::load_adapter_class($info->protocol);
$fqclass = static::load_adapter_class($connection_info->protocol);

try {
$connection = new $fqclass($info);
$connection->protocol = $info->protocol;
$connection = new $fqclass($connection_info);
$connection->protocol = $connection_info->protocol;
$connection->logging = $config->get_logging();
$connection->logger = $connection->logging ? $config->get_logger() : null;

if (isset($info->charset))
$connection->set_encoding($info->charset);
if (isset($connection_info->charset))
$connection->set_encoding($connection_info->charset);
} catch (PDOException $e) {
throw new DatabaseException($e);
}
Expand All @@ -140,92 +150,6 @@ private static function load_adapter_class($adapter)
return $fqclass;
}

/**
* Use this for any adapters that can take connection info in the form below
* to set the adapters connection info.
*
* <code>
* protocol://username:password@host[:port]/dbname
* protocol://urlencoded%20username:urlencoded%20password@host[:port]/dbname?decode=true
* protocol://username:password@unix(/some/file/path)/dbname
* </code>
*
* Sqlite has a special syntax, as it does not need a database name or user authentication:
*
* <code>
* sqlite://file.db
* sqlite://../relative/path/to/file.db
* sqlite://unix(/absolute/path/to/file.db)
* sqlite://windows(c%2A/absolute/path/to/file.db)
* </code>
*
* @param string $connection_url A connection URL
* @return object the parsed URL as an object.
*/
public static function parse_connection_url($connection_url)
{
$url = @parse_url($connection_url);

if (!isset($url['host']))
throw new DatabaseException('Database host must be specified in the connection string. If you want to specify an absolute filename, use e.g. sqlite://unix(/path/to/file)');

$info = new \stdClass();
$info->protocol = $url['scheme'];
$info->host = $url['host'];
$info->db = isset($url['path']) ? substr($url['path'], 1) : null;
$info->user = isset($url['user']) ? $url['user'] : null;
$info->pass = isset($url['pass']) ? $url['pass'] : null;

$allow_blank_db = ($info->protocol == 'sqlite');

if ($info->host == 'unix(')
{
$socket_database = $info->host . '/' . $info->db;

if ($allow_blank_db)
$unix_regex = '/^unix\((.+)\)\/?().*$/';
else
$unix_regex = '/^unix\((.+)\)\/(.+)$/';

if (preg_match_all($unix_regex, $socket_database, $matches) > 0)
{
$info->host = $matches[1][0];
$info->db = $matches[2][0];
}
} elseif (substr($info->host, 0, 8) == 'windows(')
{
$info->host = urldecode(substr($info->host, 8) . '/' . substr($info->db, 0, -1));
$info->db = null;
}

if ($allow_blank_db && $info->db)
$info->host .= '/' . $info->db;

if (isset($url['port']))
$info->port = $url['port'];

if (strpos($connection_url, 'decode=true') !== false)
{
if ($info->user)
$info->user = urldecode($info->user);

if ($info->pass)
$info->pass = urldecode($info->pass);
}

if (isset($url['query']))
{
foreach (explode('/&/', $url['query']) as $pair) {
list($name, $value) = explode('=', $pair);

if ($name == 'charset')
$info->charset = $value;
}
}

return $info;
}

/**
* Class Connection is a singleton. Access it via instance().
*
Expand All @@ -246,7 +170,7 @@ protected function __construct($info)
else
$host = "unix_socket=$info->host";

$this->connection = new PDO("$info->protocol:$host;dbname=$info->db", $info->user, $info->pass, static::$PDO_OPTIONS);
$this->connection = new PDO("$info->protocol:$host;dbname=$info->database", $info->username, $info->password, static::$PDO_OPTIONS);
} catch (PDOException $e) {
throw new DatabaseException($e);
}
Expand Down
111 changes: 111 additions & 0 deletions lib/ConnectionInfo.php
@@ -0,0 +1,111 @@
<?php

namespace ActiveRecord;

class ConnectionInfo {

public $protocol = null;
public $host = null;
public $port = null;
public $database = null;

public $username = null;
public $password = null;

public function __construct($input = array()){
foreach($input as $prop => $value){
$this->{$prop} = $value;
}
}

/**
* Parses a connection url and return a ConnectionInfo object
*
* Use this for any adapters that can take connection info in the form below
* to set the adapters connection info.
*
* <code>
* protocol://username:password@host[:port]/dbname
* protocol://urlencoded%20username:urlencoded%20password@host[:port]/dbname?decode=true
* protocol://username:password@unix(/some/file/path)/dbname
* </code>
*
* Sqlite has a special syntax, as it does not need a database name or user authentication:
*
* <code>
* sqlite://file.db
* sqlite://../relative/path/to/file.db
* sqlite://unix(/absolute/path/to/file.db)
* sqlite://windows(c%2A/absolute/path/to/file.db)
* </code>
*
* @param string $connection_url A connection URL
* @return object the parsed URL as an object.
*/
public static function from_connection_url($connection_url){
$url = @parse_url($connection_url);

if (!isset($url['host']))
throw new DatabaseException('Database host must be specified in the connection string. If you want to specify an absolute filename, use e.g. sqlite://unix(/path/to/file)');

$info = new self();
$info->protocol = $url['scheme'];
$info->host = $url['host'];
if(isset($url['path'])){
$info->database = substr($url['path'], 1);
}
$info->username = isset($url['user']) ? $url['user'] : null;
$info->password = isset($url['pass']) ? $url['pass'] : null;

$allow_blank_db = ($info->protocol == 'sqlite');

if ($info->host == 'unix(')
{
$socket_database = $info->host . '/' . $info->database;

if ($allow_blank_db)
$unix_regex = '/^unix\((.+)\)\/?().*$/';
else
$unix_regex = '/^unix\((.+)\)\/(.+)$/';

if (preg_match_all($unix_regex, $socket_database, $matches) > 0)
{
$info->host = $matches[1][0];
$info->database = $matches[2][0];
}
} elseif (substr($info->host, 0, 8) == 'windows(')
{
$info->host = urldecode(substr($info->host, 8) . '/' . substr($info->database, 0, -1));
$info->database = null;
}

if ($allow_blank_db && $info->database)
$info->host .= '/' . $info->database;

if (isset($url['port']))
$info->port = $url['port'];

if (strpos($connection_url, 'decode=true') !== false)
{
if ($info->username)
$info->username = urldecode($info->username);

if ($info->password)
$info->password = urldecode($info->password);
}

if (isset($url['query']))
{

foreach (explode('/&/', $url['query']) as $pair) {
list($name, $value) = explode('=', $pair);

if ($name == 'charset')
$info->charset = $value;
}
}

return $info;
}

}
2 changes: 1 addition & 1 deletion lib/adapters/OciAdapter.php
Expand Up @@ -22,7 +22,7 @@ protected function __construct($info)
{
try {
$this->dsn_params = isset($info->charset) ? ";charset=$info->charset" : "";
$this->connection = new PDO("oci:dbname=//$info->host/$info->db$this->dsn_params",$info->user,$info->pass,static::$PDO_OPTIONS);
$this->connection = new PDO("oci:dbname=//$info->host/$info->database$this->dsn_params",$info->username,$info->password,static::$PDO_OPTIONS);
} catch (PDOException $e) {
throw new DatabaseException($e);
}
Expand Down
94 changes: 94 additions & 0 deletions test/ConnectionInfoTest.php
@@ -0,0 +1,94 @@
<?php

use ActiveRecord\ConnectionInfo;

class ConnectionInfoTest extends SnakeCase_PHPUnit_Framework_TestCase
{
/**
* @expectedException ActiveRecord\DatabaseException
*/
public function test_connection_info_from_should_throw_exception_when_no_host()
{
ConnectionInfo::from_connection_url('mysql://user:pass@');
}

public function test_connection_info()
{
$info = ConnectionInfo::from_connection_url('mysql://user:pass@127.0.0.1:3306/dbname');
$this->assert_equals('mysql',$info->protocol);
$this->assert_equals('user',$info->username);
$this->assert_equals('pass',$info->password);
$this->assert_equals('127.0.0.1',$info->host);
$this->assert_equals(3306,$info->port);
$this->assert_equals('dbname',$info->database);
}

public function test_gh_103_sqlite_connection_string_relative()
{
$info = ConnectionInfo::from_connection_url('sqlite://../some/path/to/file.db');
$this->assert_equals('../some/path/to/file.db', $info->host);
}

/**
* @expectedException ActiveRecord\DatabaseException
*/
public function test_gh_103_sqlite_connection_string_absolute()
{
$info = ConnectionInfo::from_connection_url('sqlite:///some/path/to/file.db');
}

public function test_gh_103_sqlite_connection_string_unix()
{
$info = ConnectionInfo::from_connection_url('sqlite://unix(/some/path/to/file.db)');
$this->assert_equals('/some/path/to/file.db', $info->host);

$info = ConnectionInfo::from_connection_url('sqlite://unix(/some/path/to/file.db)/');
$this->assert_equals('/some/path/to/file.db', $info->host);

$info = ConnectionInfo::from_connection_url('sqlite://unix(/some/path/to/file.db)/dummy');
$this->assert_equals('/some/path/to/file.db', $info->host);
}

public function test_gh_103_sqlite_connection_string_windows()
{
$info = ConnectionInfo::from_connection_url('sqlite://windows(c%3A/some/path/to/file.db)');
$this->assert_equals('c:/some/path/to/file.db', $info->host);
}

public function test_parse_connection_url_with_unix_sockets()
{
$info = ConnectionInfo::from_connection_url('mysql://user:password@unix(/tmp/mysql.sock)/database');
$this->assert_equals('/tmp/mysql.sock',$info->host);
}

public function test_parse_connection_url_with_decode_option()
{
$info = ConnectionInfo::from_connection_url('mysql://h%20az:h%40i@127.0.0.1/test?decode=true');
$this->assert_equals('h az',$info->username);
$this->assert_equals('h@i',$info->password);
}

public function test_encoding()
{
$info = ConnectionInfo::from_connection_url('mysql://test:test@127.0.0.1/test?charset=utf8');
$this->assert_equals('utf8', $info->charset);
}

public function test_connection_info_from_array(){
$info = new ConnectionInfo(array(
'protocol' => 'mysql',
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'dbname',
'username' => 'user',
'password' => 'pass'
));
$this->assert_equals('mysql',$info->protocol);
$this->assert_equals('user',$info->username);
$this->assert_equals('pass',$info->password);
$this->assert_equals('127.0.0.1',$info->host);
$this->assert_equals(3306,$info->port);
$this->assert_equals('dbname',$info->database);
}

}

0 comments on commit 0ffaef0

Please sign in to comment.