diff --git a/CHANGELOG.md b/CHANGELOG.md index af376ec..6837362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,31 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased][unreleased] ### Added - Figure out a way to duplicate sqlsrv_field_metadata(). -- Figure out a way to duplicate sqlsrv_cancel() ```$stmt = null```? +- Figure out a way to duplicate sqlsrv_cancel() ### Changed -- Make client_info() more reliable. -- Figure out how/why sqlsrv's PHPTYPE_STR(EAM|ING) function return values are randomly wrong. When wrong, they are always the same wrong values. +- Make client_info() less reliant on external utils. +- Figure out how/why sqlsrv's PHPTYPE_STR(EAM|ING) function return values are randomly different. Though, when they are different, they are always the same different values. +- figure out how to retrieve client_info for dblib. test odbc functions... those utils may not be installed with unixodbc. -## [0.0.4] - +## [0.0.7] - 2016-08-25 +### Added +- dblib/sybase driver option (and made default) due to simpler setup. + +### Changed +- lowercase'd sqlshim classname and radsectors namespace. not sure why I camelcase'd them to begin with. +- made sqlshim class final. +- dynamicized the definition of SQLSRV constants. +- various other small updates and improvements. +- changed the way client_info retrieval functions are organized and accessed. + +### Fixed +- bug where prepare() options were not being processed at all. oops. + +### Removed +- ?-to-:tag conversion in prepare as it was highly unnecessary + +## [0.0.4] - 2015-09-21 ### Added - option parsing for prepare() - connection ref variable. @@ -45,6 +63,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - First alpha release. -[unreleased]: https://github.com/radsectors/sqlshim/compare/v0.0.3...HEAD +[unreleased]: https://github.com/radsectors/sqlshim/compare/v0.0.4...HEAD +[0.0.7]: https://github.com/radsectors/sqlshim/compare/v0.0.4...v0.0.7 +[0.0.4]: https://github.com/radsectors/sqlshim/compare/v0.0.3...v0.0.4 [0.0.3]: https://github.com/radsectors/sqlshim/compare/v0.0.2...v0.0.3 -[0.0.2]: https://github.com/radsectors/sqlshim/compare/v0.0.1...v0.0.2 \ No newline at end of file +[0.0.2]: https://github.com/radsectors/sqlshim/compare/v0.0.1...v0.0.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d086ef6..6152899 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,17 @@ # How to contribute -### Code Contributions -I've never sent or received pull requests, but that seems to be the way to go about it... so I'll suggest that route until I find something I like better. +### Pull Requests +1. Fork sqlshim +2. Create your feature branch: `git checkout -b my-new-feature` +3. Code some code! +3. Commit your changes `git commit -am 'Added some feature'` +4. Push your branch `git push origin my-new-feature` +5. Submit pull request -### Use Cases -sqlshim's biggest need right now is use cases (especially OS X). If you develop on a Mac and have unixODBC/FreeTDS/PHP set up (or if you don't, but are in need), then I need your help. - -Looking for any information to put toward building solid setup instructions for unixODBC/FreeTDS/PHP on OS X. +### Needs +User feedback. Has sqlshim worked for you? Has it failed? I want to know. ### Help Wanted -Please see sqlshim's [help wanted](https://github.com/radsectors/sqlshim/labels/help%20wanted) issues. \ No newline at end of file +Please see sqlshim's [help wanted](https://github.com/radsectors/sqlshim/labels/help%20wanted) issues. diff --git a/README.md b/README.md index a1b5a86..d34e872 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [sqlshim] - PHP sqlsrv for Linux/OS X +# **sqlshim** • PHP sqlsrv for Linux/OS X The **sqlshim** project aims to replicate [Microsoft SQL Server Driver for PHP][sqlsrv] (**sqlsrv**) on Linux/OS X. @@ -8,8 +8,8 @@ The **sqlshim** project aims to replicate [Microsoft SQL Server Driver for PHP][ ## Basic Usage -1. ```\RadSectors\SqlShim::init();``` -2. ```sqlsrv_connect( ... );``` +1. `\RadSectors\SqlShim::init();` +2. `sqlsrv_connect( ... );` 3. ??? 4. Profit!! @@ -18,7 +18,6 @@ The **sqlshim** project aims to replicate [Microsoft SQL Server Driver for PHP][ For more detailed documentation on **sqlshim**, please see the [sqlshim wiki](https://github.com/radsectors/sqlshim/wiki) ## Installation - See [wiki] for more detailed instructions. #### Composer @@ -30,7 +29,7 @@ require 'vendor/autoload.php'; #### Manual 1. Download the latest [release](https://github.com/radsectors/sqlshim/releases). -2. Extract ```src/sqlshim.php``` and ```src/globals.php``` to wherever you store your 3rd-party libraries. +2. Extract `src/sqlshim.php` and `src/globals.php` to wherever you store your 3rd-party libraries. 3. Include `sqlshim.php`. ```php require '/path/to/sqlshim.php'; @@ -54,6 +53,8 @@ You can hit me up on twitter [@radsectors](https://twitter.com/radsectors). ## Known Issues Please visit the [project on GitHub](https://github.com/radsectors/sqlshim) to view [outstanding issues](https://github.com/radsectors/sqlshim/issues). +## Credits + ## License **sqlshim** is licensed under the MIT license. See the [LICENSE](https://github.com/radsectors/sqlshim/blob/master/LICENSE) file for details. diff --git a/src/globals.php b/src/globals.php index 92da88b..2e9b1fe 100644 --- a/src/globals.php +++ b/src/globals.php @@ -13,6 +13,8 @@ // trying to dynamicize these functions, // but you can't really declare a function on the fly like this + // w/o using eval() which may be okay... + // the downside is that we lose docblocks... // foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $o) { // if ($o->name == 'init') { // continue; diff --git a/src/sqlshim.php b/src/sqlshim.php index a9aca40..583461f 100644 --- a/src/sqlshim.php +++ b/src/sqlshim.php @@ -1,6 +1,6 @@ 'dblib', 'driver' => 'sqlshim', 'tds_version' => '7.2', 'autotype_fields' => false, @@ -49,12 +51,14 @@ public static function init($options = []) switch ($opt) { case 'prefix': switch ($val) { - case 'odbc': + case 'sybase': + case 'mssql': + $val = 'dblib'; case 'dblib': + case 'odbc': self::$options->$opt = $val; break; default: - self::$options->$opt = 'odbc'; break; } break; @@ -102,6 +106,41 @@ public static function init($options = []) self::SCROLL_ABSOLUTE => \PDO::FETCH_ORI_ABS, self::SCROLL_RELATIVE => \PDO::FETCH_ORI_REL, ]; + self::$prepare_opts = array_flip([ + 'QueryTimeout', + 'SendStreamParamsAtExec', + 'Scrollable', + ]); + + self::$client_info = [ + 'odbc' => [ + 'DriverDLLName' => function () { + return basename(end(explode(' ', exec('cat /etc/odbcinst.ini | grep\Driver')))); + }, + 'DriverODBCVer' => function () { + return end(explode(' ', exec('isql --version'))); + }, + 'DriverVer' => function () { + return end(explode(' ', exec('tsql -C | grep Version'))); + }, + 'ExtensionVer' => function () { + return phpversion('pdo_odbc'); + }, + ], + 'dblib' => [ + 'DriverDLLName' => function () { + return basename(end(explode(' ', exec('echo nothing')))); + }, + 'DriverVer' => function () { + echo \PDO::ATTR_CLIENT_VERSION; + + return end(explode(' ', exec('echo nothing'))); + }, + 'ExtensionVer' => function () { + return phpversion('pdo_dblib'); + }, + ], + ]; // for global function registration $registered = false; @@ -119,11 +158,11 @@ public static function init($options = []) */ private static function log_err($e) { - if (is_array($e)) { + if (is_array($e) && count($e) > 2) { self::$errors[] = [ 'SQLSTATE' => $e[0], 'code' => $e[1], - 'message' => $e[2], + 'message' => $e[2] ?: '', ]; return; @@ -238,7 +277,7 @@ private static function calc_prec_scale($prec, $scale) private static function typify($row) { foreach ($row as &$value) { - //DBLIB database driver returns everything as strings, so this converts num's back to the correct data type + // DBLIB database driver returns everything as strings, so this converts num's back to the correct data type $value = self::convertDataType($value); } @@ -255,12 +294,9 @@ private static function convertDataType($string) return $string; } else { //is a float - $string = (float) filter_var($string, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); + $string = (float) filter_var($string, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); } - // } - // - // else - // { + // } else { // // is an int // $string = (int)filter_var($string, FILTER_SANITIZE_NUMBER_INT); // } @@ -277,6 +313,7 @@ private static function convertDataType($string) */ private static function guesstype($val) { + // TODO: 7-year-old comment http://php.net/manual/en/ref.pdo-dblib.php#89827 may be on to something if (self::$options->autotype_fields) { $num = is_numeric($val); $float = $num && strpos($val, '.') !== false; @@ -298,27 +335,6 @@ private static function guesstype($val) return $val; } - // client info helper functions. - private static function client_info_driver_dllname() - { - $return = basename(end(explode(' ', exec('cat /etc/odbcinst.ini | grep Driver')))); - - return $return; - } - private static function client_info_driver_odbcver() - { - return end(explode(' ', exec('isql --version'))); - } - private static function client_info_driver_ver() - { - return end(explode(' ', exec('tsql -C | grep Version'))); - } - private static function client_info_ext_ver() - { - return phpversion('pdo_odbc'); - // return (new \ReflectionExtension('pdo_odbc'))->getVersion(); - } - /* * sqlsrv constants */ @@ -469,19 +485,17 @@ public static function begin_transaction(\PDO $conn) public static function cancel(\PDOStatement $stmt) { // no PDO equivalent for this API + return true; } public static function client_info(\PDO $conn) { // \PDO::ATTR_CLIENT_VERSION (integer) - // REVIEW: client_info() - these system() calls may be too system-specific. or need some kind of prioritized alternatives. - // the first one won't even work reliably. - $return = [ - 'DriverDLLName' => self::client_info_driver_dllname(), - 'DriverODBCVer' => self::client_info_driver_odbcver(), - 'DriverVer' => self::client_info_driver_ver(), - 'ExtensionVer' => self::client_info_ext_ver(), - ]; + $return = []; + $info = self::$client_info[self::$options->prefix]; + foreach ($info as $i => $call) { + $return[$i] = $call(); + } return $return; } @@ -509,24 +523,31 @@ public static function connect($serverName, $connectionInfo) // IDEA: connect() - research prefixes? do something with them? $serverName = str_replace('tcp:', '', $serverName); // default port - list($connectionInfo['serverName'], $connectionInfo['port']) = explode(',', $serverName.',1433', 3); + list($connectionInfo['servername'], $connectionInfo['port']) = explode(',', $serverName.',1433', 2); // lowercase all keys $connectionInfo = array_change_key_case($connectionInfo); $cstrparams = [ - 'odbc' => [ - 'driver' => 'driver', - 'tds_version' => 'tds_version', - 'servername' => 'server', - 'port' => 'port', - 'database' => 'database', - 'characterset' => 'clientcharset', - ], 'dblib' => [ + 'tds_version' => 'version', 'servername' => 'host', 'database' => 'dbname', 'port' => 'port', 'characterset' => 'charset', + 'encrypt' => 'secure', // not used (http://php.net/manual/en/ref.pdo-dblib.connection.php) + 'trustservercertificate' => '', + 'logintimeout' => '', + ], + 'odbc' => [ + 'tds_version' => 'tds_version', + 'servername' => 'server', + 'database' => 'database', + 'port' => 'port', + 'characterset' => 'clientcharset', + 'encrypt' => '', + 'trustservercertificate' => '', + 'logintimeout' => '', + 'driver' => 'driver', ], ]; $cstr = self::$options->prefix.':'; @@ -634,9 +655,7 @@ public static function fetch(\PDOStatement $stmt, $row, $offset) public static function field_metadata(\PDOStatement $stmt) { // NOTE: field_metadata() - PDOStatement->getColumnMeta() is an "EXPERIMENTAL" function. And its request not supported by driver. - // HACK: field_metadata() - Implement our own? probably won't work (at first glance) - // check: http://community.sitepoint.com/t/pdo-getcolumnmeta-bug/3430/2 - // credit: http://vancelucas.com/ + // HACK: 7-year-old comment http://php.net/manual/en/ref.pdo-dblib.php#89827 may be on to something return false; // $metadata = []; // for ( $i=0; $i<$stmt->columnCount(); $i++ ) @@ -665,7 +684,8 @@ public static function get_field(\PDOStatement $stmt, $fieldIndex = 0, $getAsTyp public static function has_rows(\PDOStatement $stmt) { - // REVEW: this doesn't work unless $stmt->rowCount() works + // REVIEW: has_rows() - this doesn't work unless $stmt->rowCount() works. + // might just need: [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL] option set return (bool) $stmt->rowCount(); } @@ -682,16 +702,12 @@ public static function num_fields(\PDOStatement $stmt) public static function num_rows(\PDOStatement $stmt) { // REVIEW: num_rows() - $stmt->rowCount DOES NOT work for SELECTs in MSSQL PDO. - $conn = $stmt->conn; $sql = $stmt->queryString; if (stripos($sql, 'select') >= 0) { $sql = preg_replace('/SELECT .* FROM/', 'SELECT COUNT(*) AS count FROM', $sql); - - var_dump($sql); - $$cnt = $conn->query($sql); - $row = $cnt->fetch(\PDO::FETCH_NUM); + $row = $stmt->conn->query($sql)->fetch(\PDO::FETCH_NUM); if (is_array($row) && count($row)) { - return $row[key($row)]; + return reset($row); } } @@ -700,66 +716,40 @@ public static function num_rows(\PDOStatement $stmt) public static function prepare(\PDO $conn, $sql, $params = [], $options = []) { - // REVIEW: prepare() - is the ?-to-:tag conversion necessary? since ?s are apparently supported. - $i = 1; - $count = -1; - do { - $sql = preg_replace('/\?/', ":var$i", $sql, 1, $found); - ++$i; - ++$count; - } while ($found); - - //*****Use this for parameters if part above does not work! - // $occurences = mb_substr_count($sql, "?"); - // for ($x=0; $x<=$occurences; $x++) { - // // Should add error handling if parameter is blank - // if (ctype_digit($sqlparams[$x])){ - // $sql = preg_replace('/\?/', $params[$x], $sql, 1); - // } else { - // // Not sure why it needs single quotes instead of double. - // $sql = preg_replace('/\?/', '\''.$params[$x].'\'', $sql, 1); - // } - // } - - $sql = stripslashes(($sql)); - - // translate options array - $optionsin = $options; - $options = []; + // translate options + $options = array_intersect_key($options, self::$prepare_opts); foreach ($options as $opt => $val) { switch ($opt) { case 'QueryTimeout': if (is_numeric($val)) { $conn->setAttribute(\PDO::SQLSRV_ATTR_QUERY_TIMEOUT, intval($val)); } - break; + break; case 'SendStreamParamsAtExec': - // ??? - break; + // TODO: prepare() - figure out what this is and what to do with it + break; case 'Scrollable': if (isset(self::$tabcursor[$val])) { $options[\PDO::ATTR_CURSOR] = self::$tabcursor[$val]; } - break; + break; default: - break; + break; } } + $sql = stripslashes($sql); // REVIEW: purpose? + if (!is_array($params)) { $params = []; } + $count = mb_substr_count($sql, '?'); + $params = array_slice(array_pad($params, $count, null), 0, $count, false); try { $stmt = $conn->prepare($sql, $options); - $i = 1; - foreach (array_slice($params, 0, $count) as $var) { - if ($i > $count) { - break; - } - $bound = $stmt->bindValue(":var$i", $var); - // if ( !$bound ) { echo "fail $i:$var
"; } - ++$i; + foreach ($params as $i => $var) { + $bound = $stmt->bindValue($i + 1, $var); } $stmt->conn = $conn; // for ref return $stmt; diff --git a/tests/GeneralTest.php b/tests/GeneralTest.php index 925019b..92bd7e7 100644 --- a/tests/GeneralTest.php +++ b/tests/GeneralTest.php @@ -134,7 +134,7 @@ public function testConnection($init) */ public function testClientInfo($con) { - //var_dump(sqlsrv_client_info($con)); + var_dump(sqlsrv_client_info($con)); } /**