├── .gitignore ├── tests ├── bootstrap.php ├── config-sample.php └── GeneralTest.php ├── .markdownlint.json ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── phpunit.xml ├── CONTRIBUTING.md ├── README.md ├── composer.json ├── LICENSE ├── CHANGELOG.md └── src ├── globals.php └── sqlshim.php /.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | vendor 3 | docs 4 | composer.lock 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Listen for XDebug", 9 | "type": "php", 10 | "request": "launch", 11 | "port": 9000, 12 | 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "unit tests", 8 | "type": "shell", 9 | "command": "${workspaceRoot}/vendor/bin/phpunit", 10 | "group": { 11 | "kind": "test", 12 | "isDefault": true 13 | }, 14 | "presentation": { 15 | "echo": true, 16 | "reveal": "always", 17 | "focus": true, 18 | "panel": "shared" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | 4 | ## Donations 5 | [Monetary contributions](http://paypal.me/radsectors) are also appreciated! 6 | 7 | 8 | ## Feedback 9 | Has sqlshim worked for you? Has it failed? We want to know! 10 | * [Discord](https://discord.gg/S9zbe8m) 11 | * [Slack](https://radsectors.slack.com/messages/C0C7U0QN9/) 12 | * [Issue tracker](https://github.com/radsectors/sqlshim/issues) 13 | * [Twitter](https://twitter.com/radsectors) 14 | 15 | 16 | ## Specific Needs 17 | Please see our [help wanted](https://github.com/radsectors/sqlshim/labels/help%20wanted) issues. 18 | 19 | 20 | ## Pull Requests 21 | 1. Fork sqlshim 22 | 2. Create your feature branch: `git checkout -b my-new-feature` 23 | 3. Code some code! 24 | 3. Commit your changes `git commit -am 'Added x feature'` 25 | 4. Push your branch `git push origin my-new-feature` 26 | 5. Submit pull request 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **sqlshim** • sqlsrv for Linux/OS X 2 | 3 | **sqlshim** replicates [sqlsrv (Microsoft SQL Server Driver for PHP)](https://github.com/Microsoft/msphpsql) functionality on Linux/OS X. 4 | 5 | It began as a small set of spoofed sqlsrv functions and has since evolved into a full-on sqlsrv replacement. 6 | 7 | It is currently in `alpha` and is primarily intended for use in dev environments or non-mission-critical applications. But I can't tell you what to do. 8 | 9 | 10 | ## Getting Started 11 | 1. `$ composer require "radsectors/sqlshim:dev-master"` 12 | 2. `=5.4.0" 20 | }, 21 | "minimum-stability": "stable", 22 | "prefer-stable": true, 23 | "require-dev": { 24 | "phpunit/phpunit": "*", 25 | "radsectors/urp": "dev-master", 26 | "squizlabs/php_codesniffer": "^3.2" 27 | }, 28 | "suggest": { 29 | 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "radsectors\\": "src" 34 | }, 35 | "files": ["src/globals.php"] 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "radsectors\\test\\": "tests" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 radsectors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased][unreleased] 6 | ### Added 7 | - Figure out a way to duplicate `sqlsrv_field_metadata()`. 8 | - Figure out a way to duplicate `sqlsrv_cancel()` 9 | 10 | ### Changed 11 | - Figure out how/why sqlsrv's PHPTYPE_STR(EAM|ING) function return values are 12 | randomly different. 13 | - PHPTYPE_DECIMAL and SQLTYPE_NUMERIC need fixing. 14 | - Improve test modules. 15 | 16 | ### Deprecated 17 | - odbc support 18 | 19 | ### Removed 20 | - odbc support 21 | 22 | 23 | ## [0.0.10] - 2016-11-23 24 | ### Added 25 | - `get_field()` 26 | - Legit error now logged for invalid `configure()` and `get_config()` parameters. 27 | 28 | ### Changed 29 | - Improved error logging. Maybe. I hope? 30 | - Renamed some internal functions. 31 | - Some PHPTYPE_* functions needed to be updated. 32 | - Began deprecating odbc bits. 33 | 34 | ### Fixed 35 | - ServerName:Port parsing. 36 | 37 | 38 | ## [0.0.9] - 2016-09-25 39 | ### Added 40 | - `configure()` and `get_config()` functions. 41 | - client info for dblib. 42 | 43 | ### Chanaged 44 | - `init()` is now called automatically by `globals.php` which is now autoloaded 45 | via composer. This will affect users who are using custom init config options 46 | as well as those who `include()` manually. 47 | - improved `server_info()` 48 | - cleaned up some unnecessary static vars. 49 | 50 | ### Removed 51 | - ODBC config options. they were never used or tested anyway. 52 | 53 | 54 | ## [0.0.8] - 2016-09-08 55 | ### Changed 56 | - improved `guesstype()` function. 57 | 58 | ### Fixed 59 | - some camelcasing 60 | 61 | ### Removed 62 | - `convertDataType()` function. 63 | 64 | 65 | ## [0.0.7] - 2016-08-25 66 | ### Added 67 | - dblib/sybase driver option (and made default) due to simpler setup. 68 | 69 | ### Changed 70 | - lowercase'd sqlshim classname and radsectors namespace. not sure why I 71 | camelcase'd them to begin with. 72 | - made sqlshim class final. 73 | - dynamicized the definition of SQLSRV constants. 74 | - various other small updates and improvements. 75 | - changed the way client_info retrieval functions are organized and accessed. 76 | 77 | ### Fixed 78 | - bug where `prepare()` options were not being processed at all. oops. 79 | 80 | ### Removed 81 | - ?-to-:tag conversion in prepare as it was highly unnecessary 82 | 83 | 84 | ## [0.0.4] - 2015-09-21 85 | ### Added 86 | - option parsing for prepare() 87 | - connection ref variable. 88 | 89 | ### Removed 90 | - has_rows() and num_rows() were assumed to be working, but do not. no longer pretending. 91 | 92 | ### Fixed 93 | - bug improperly handling invalid parameters. 94 | 95 | 96 | ## [0.0.3] - 2015-06-17 97 | ### Added 98 | - Worked out some ugly logic for SQLTYPE_(DECIMAL|NUMERIC) functions. 99 | 100 | ### Changed 101 | - Improved and optimized SQLTYPE functions. Reduced repeated code. 102 | 103 | 104 | ## [0.0.2] - 2015-06-09 105 | ### Added 106 | - Logic to a few more functions. 107 | 108 | ### Changed 109 | - Renamed class constants (removed SQLSRV_ prefixes.) 110 | - Improved (I think) error logging function. 111 | 112 | ### Fixed 113 | - A glaring bug where the fetch functions return false instead of null when 114 | there are no records to fetch. 115 | 116 | 117 | ## [0.0.1] - 2015-05-28 118 | ### Added 119 | - First alpha release. 120 | 121 | [unreleased]: https://github.com/radsectors/sqlshim/compare/v0.0.10...HEAD 122 | [0.0.10]: https://github.com/radsectors/sqlshim/compare/v0.0.9...v0.0.10 123 | [0.0.9]: https://github.com/radsectors/sqlshim/compare/v0.0.8...v0.0.9 124 | [0.0.8]: https://github.com/radsectors/sqlshim/compare/v0.0.7...v0.0.8 125 | [0.0.7]: https://github.com/radsectors/sqlshim/compare/v0.0.4...v0.0.7 126 | [0.0.4]: https://github.com/radsectors/sqlshim/compare/v0.0.3...v0.0.4 127 | [0.0.3]: https://github.com/radsectors/sqlshim/compare/v0.0.2...v0.0.3 128 | [0.0.2]: https://github.com/radsectors/sqlshim/compare/v0.0.1...v0.0.2 129 | -------------------------------------------------------------------------------- /tests/GeneralTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($exists); 38 | 39 | return $exists; 40 | } 41 | 42 | /** 43 | * @depends testInit 44 | * @requires extension sqlsrv 45 | */ 46 | public function testConstants($init) 47 | { 48 | echo "\nsqlshim and slqsrv present.\nRunning const comparison test...\n"; 49 | // test PHPTYPE constants 50 | $constants = get_defined_constants(true)['sqlsrv']; 51 | foreach ($constants as $const => $v) { 52 | if (strpos($const, 'SQLSRV_') === 0) { 53 | $cval = constant('\radsectors\sqlshim::'.str_replace('SQLSRV_', '', $const)); 54 | $gval = constant($const); 55 | $compare = ($gval === $cval); 56 | if (!$compare) { 57 | echo "$const: srv: $gval / shim: $cval\n"; 58 | } else { 59 | $this->assertTrue($compare); 60 | } 61 | } 62 | } 63 | 64 | // test PHPTYPE functions 65 | $functions = [ 66 | // 'PHPTYPE_STREAM' => ['rock', 'hello', '', 0, 1, 2, true, false, 'char', 'binary', null], 67 | // 'PHPTYPE_STRING' => ['rock', 'hello', '', 0, 1, 2, true, false, 'char', 'binary', null], 68 | 'SQLTYPE_BINARY' => [-8001, -8000, -1, 0, 1, 8000, 8001], 69 | 'SQLTYPE_CHAR' => [-8001, -8000, -1, 0, 1, 8000, 8001], 70 | 'SQLTYPE_DECIMAL' => [[-3, 256], [-3, 256]], 71 | 'SQLTYPE_NCHAR' => [-8001, -8000, -1, 0, 1, 8000, 8001], 72 | 'SQLTYPE_NUMERIC' => [[-3, 256], [-3, 256]], 73 | 'SQLTYPE_NVARCHAR' => [-8001, -8000, -1, 0, 1, 8000, 8001], 74 | 'SQLTYPE_VARBINARY' => [-8001, -8000, -1, 0, 1, 8000, 8001], 75 | 'SQLTYPE_VARCHAR' => [-8001, -8000, -1, 0, 1, 8000, 8001], 76 | ]; 77 | // $functions = get_extension_funcs('sqlsrv'); 78 | $tryfunc = function ($func, $args) { 79 | $compare = null; 80 | $cval = call_user_func_array("\\radsectors\\sqlshim::$func", $args); 81 | $gval = call_user_func_array("sqlsrv_$func", $args); 82 | $compare = ($gval === $cval); 83 | if (!$compare) { 84 | echo "$func(".implode(',', $args)."): srv: $gval / shim: $cval\n"; 85 | } else { 86 | $this->assertTrue($compare); 87 | } 88 | return $compare; 89 | }; 90 | foreach ($functions as $func => $args) { 91 | if (strstr($func, 'DECIMAL') || strstr($func, 'NUMERIC')) { 92 | $p1 = $args[0][0]; 93 | $p2 = $args[1][0]; 94 | for ($args[0][0]; $p1 <= $args[0][1]; $p1++) { 95 | for ($args[1][0]; $p2 <= $args[1][1]; $p2++) { 96 | $tryfunc($func, [$p1, $p2]); 97 | } 98 | } 99 | } else { 100 | foreach ($args as $arg) { 101 | $tryfunc($func, [$arg]); 102 | } 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * @depends testInit 109 | */ 110 | public function testConfigure($init) 111 | { 112 | echo "\nConfigure function test.\n"; 113 | // TODO: change to NOT test for default values 114 | $this->assertTrue(sqlsrv_configure('ClientBufferMaxKBSize', 10240)); 115 | $this->assertTrue(sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ERROR)); 116 | $this->assertTrue(sqlsrv_configure('LogSubsystems', SQLSRV_LOG_SYSTEM_OFF)); 117 | $this->assertTrue(sqlsrv_configure('WarningsReturnAsErrors', true)); 118 | 119 | $this->assertTrue(sqlsrv_get_config('ClientBufferMaxKBSize') == 10240); 120 | $this->assertTrue(sqlsrv_get_config('LogSeverity') == SQLSRV_LOG_SEVERITY_ERROR); 121 | $this->assertTrue(sqlsrv_get_config('LogSubsystems') == SQLSRV_LOG_SYSTEM_OFF); 122 | $this->assertTrue(sqlsrv_get_config('WarningsReturnAsErrors') == true); 123 | } 124 | 125 | /** 126 | * @depends testInit 127 | */ 128 | public function testConnection($init) 129 | { 130 | if (!$init) { 131 | return false; 132 | } 133 | 134 | echo "\nConnection test... "; 135 | $con = sqlsrv_connect( 136 | HOSTNAME, 137 | [ 138 | 'Database' => DATABASE, 139 | 'UID' => USERNAME, 140 | 'PWD' => PASSWORD, 141 | 'CharacterSet' => 'UTF-8', 142 | ] 143 | ); 144 | 145 | if (is_object($con) && get_class($con) == 'PDO') { 146 | echo "sqlshim PDO connection.\n"; 147 | } elseif (is_resource($con) && get_resource_type($con) == 'SQL Server Connection') { 148 | echo "sqlsrv connection.\n"; 149 | } else { 150 | urp::failed(sqlsrv_errors()); 151 | } 152 | $this->assertTrue($con !== false); 153 | 154 | return $con; 155 | } 156 | 157 | /** 158 | * @depends testConnection 159 | */ 160 | public function testClientInfo($con) 161 | { 162 | echo "\nClient Info function test.\n"; 163 | // TODO: make better tests 164 | $client_info = sqlsrv_client_info($con); 165 | urp::info($client_info); 166 | $this->assertTrue(is_array($client_info)); 167 | } 168 | 169 | /** 170 | * @depends testConnection 171 | */ 172 | public function testServer($con) 173 | { 174 | echo "\nServer info test."; 175 | // TODO: make better tests 176 | $this->assertTrue(is_array(sqlsrv_server_info($con))); 177 | } 178 | 179 | /** 180 | * @depends testConnection 181 | */ 182 | public function testQueries($con) 183 | { 184 | echo "\nQuery tests..."; 185 | if ($con !== false) { 186 | $stmt = sqlsrv_query( 187 | $con, 188 | 'SELECT * FROM Northwind.Customers;', 189 | null, 190 | ['Scrollable' => SQLSRV_CURSOR_KEYSET] 191 | ); 192 | $rows = []; 193 | // var_dump(sqlsrv_num_rows($stmt)); 194 | // exit; 195 | while ($row = sqlsrv_fetch_array($stmt)) { 196 | $rows[] = $row; 197 | } 198 | $this->assertCount(91, $rows); 199 | 200 | // var_dump(sqlsrv_field_metadata($stmt)); 201 | 202 | $params = [ 203 | ['UK', SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)], 204 | ['Sweden', SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)], 205 | ['Mexico', SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)], 206 | ]; 207 | // pq($params); 208 | 209 | $stmt = sqlsrv_query($con, 'SELECT * FROM Northwind.Customers WHERE Country IN (?, ?, ?);', $params); 210 | $rows = []; 211 | while ($row = sqlsrv_fetch_object($stmt)) { 212 | $rows[] = $row; 213 | } 214 | // pq($rows); 215 | $this->assertCount(14, $rows); 216 | } 217 | } 218 | 219 | /** 220 | * @depends testConnection 221 | */ 222 | public function testStoredProcedure($con) 223 | { 224 | echo "\nStored procedure test."; 225 | if ($con !== false) { 226 | $stmt = sqlsrv_query($con, '{ CALL SalesByCategory( ?, ? ) }', ['Meat/Poultry', null]); 227 | $rows = []; 228 | if ($stmt) { 229 | while ($row = sqlsrv_fetch_array($stmt)) { 230 | $rows[] = $row; 231 | } 232 | } 233 | // var_dump($rows); 234 | } 235 | 236 | return false; 237 | } 238 | 239 | /** 240 | * @depends testConnection 241 | */ 242 | public function testTransactions($con) 243 | { 244 | echo "\nTransaction test."; 245 | if ($con !== false) { 246 | $stmt = sqlsrv_begin_transaction($con); 247 | // sqlsrv_prepare($cono, ) 248 | sqlsrv_rollback($con); 249 | } 250 | 251 | return false; 252 | } 253 | 254 | /** 255 | * @depends testConnection 256 | */ 257 | public function testDataTypes($con) 258 | { 259 | if ($con !== false) { 260 | return $con; 261 | } 262 | 263 | return false; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/globals.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT 11 | * @link https://github.com/radsectors/sqlshim 12 | */ 13 | namespace { 14 | 15 | use radsectors\sqlshim; 16 | 17 | if (sqlshim::init()) { 18 | // dynamicaly define all public constants in sqlshim 19 | $ref = new \ReflectionClass('\radsectors\sqlshim'); 20 | foreach ($ref->getConstants() as $const => $value) { 21 | define("SQLSRV_$const", $value); 22 | } 23 | 24 | // trying to dynamicize these functions, 25 | // but you can't really declare a function on the fly like this 26 | // w/o using eval() which may be okay... 27 | // the downside is that we lose docblocks... 28 | // foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $o) { 29 | // if ($o->name == 'init') { 30 | // continue; 31 | // } 32 | // echo "$o->name\n"; 33 | // } 34 | // exit; 35 | 36 | /** 37 | * SQLSRV_PHPTYPE_STREAM. 38 | * 39 | * @param string $encoding 40 | * 41 | * @return int 42 | */ 43 | function SQLSRV_PHPTYPE_STREAM($encoding) 44 | { 45 | return sqlshim::PHPTYPE_STREAM($encoding); 46 | } 47 | 48 | /** 49 | * SQLSRV_PHPTYPE_STRING. 50 | * 51 | * @param string $encoding 52 | * 53 | * @return int 54 | */ 55 | function SQLSRV_PHPTYPE_STRING($encoding) 56 | { 57 | return sqlshim::PHPTYPE_STRING($encoding); 58 | } 59 | 60 | /** 61 | * SQLSRV_SQLTYPE_BINARY. 62 | * 63 | * @param int $byteCount 64 | * 65 | * @return int 66 | */ 67 | function SQLSRV_SQLTYPE_BINARY($byteCount) 68 | { 69 | return sqlshim::SQLTYPE_BINARY($byteCount); 70 | } 71 | 72 | /** 73 | * SQLSRV_SQLTYPE_CHAR. 74 | * 75 | * @param int $charCount 76 | * 77 | * @return int 78 | */ 79 | function SQLSRV_SQLTYPE_CHAR($charCount) 80 | { 81 | return sqlshim::SQLTYPE_CHAR($charCount); 82 | } 83 | 84 | /** 85 | * SQLSRV_SQLTYPE_DECIMAL. 86 | * 87 | * @todo figure out how $scale works into the equation. 88 | * 89 | * @param int $precision Precision 90 | * @param int $scale Scale 91 | * 92 | * @return int 93 | */ 94 | function SQLSRV_SQLTYPE_DECIMAL($precision, $scale) 95 | { 96 | return sqlshim::SQLTYPE_DECIMAL($precision, $scale); 97 | } 98 | 99 | /** 100 | * SQLSRV_SQLTYPE_NCHAR. 101 | * 102 | * @param int $charCount 103 | * 104 | * @return int 105 | */ 106 | function SQLSRV_SQLTYPE_NCHAR($charCount) 107 | { 108 | return sqlshim::SQLTYPE_NCHAR($charCount); 109 | } 110 | 111 | /** 112 | * SQLSRV_SQLTYPE_NUMERIC. 113 | * 114 | * @todo figure out how $scale works into the equation. 115 | * 116 | * @param int $precision Precision 117 | * @param int $scale Scale 118 | * 119 | * @return int 120 | */ 121 | function SQLSRV_SQLTYPE_NUMERIC($precision, $scale) 122 | { 123 | return sqlshim::SQLTYPE_NUMERIC($precision, $scale); 124 | } 125 | 126 | /** 127 | * SQLSRV_SQLTYPE_NVARCHAR. 128 | * 129 | * @param int $charCount 130 | * 131 | * @return int 132 | */ 133 | function SQLSRV_SQLTYPE_NVARCHAR($charCount) 134 | { 135 | return sqlshim::SQLTYPE_NVARCHAR($charCount); 136 | } 137 | 138 | /** 139 | * SQLSRV_SQLTYPE_VARBINARY. 140 | * 141 | * @param int $byteCount 142 | * 143 | * @return int 144 | */ 145 | function SQLSRV_SQLTYPE_VARBINARY($byteCount) 146 | { 147 | return sqlshim::SQLTYPE_VARBINARY($byteCount); 148 | } 149 | 150 | /** 151 | * SQLSRV_SQLTYPE_VARCHAR. 152 | * 153 | * @param int $charCount 154 | * 155 | * @return int 156 | */ 157 | function SQLSRV_SQLTYPE_VARCHAR($charCount) 158 | { 159 | return sqlshim::SQLTYPE_VARCHAR($charCount); 160 | } 161 | 162 | /** 163 | * Begins a database transaction. 164 | * 165 | * @param object $conn 166 | * 167 | * @return bool 168 | */ 169 | function sqlsrv_begin_transaction($conn) 170 | { 171 | return sqlshim::begin_transaction($conn); 172 | } 173 | 174 | /** 175 | * Cancels a statement. 176 | * 177 | * @param object $stmt 178 | * 179 | * @return bool 180 | */ 181 | function sqlsrv_cancel($stmt) 182 | { 183 | return sqlshim::cancel($stmt); 184 | } 185 | 186 | /** 187 | * Returns information about the client and specified connection. 188 | * 189 | * @param object $conn 190 | * 191 | * @return string[] 192 | */ 193 | function sqlsrv_client_info($conn) 194 | { 195 | return sqlshim::client_info($conn); 196 | } 197 | 198 | /** 199 | * Closes an open connection and releases resourses associated with the connection. 200 | * 201 | * @param object $conn 202 | * 203 | * @return bool 204 | */ 205 | function sqlsrv_close($conn) 206 | { 207 | return sqlshim::close($conn); 208 | } 209 | 210 | /** 211 | * Commits a transaction that was begun with sqlsrv_begin_transaction(). 212 | * 213 | * @param object $conn 214 | * 215 | * @return bool 216 | */ 217 | function sqlsrv_commit($conn) 218 | { 219 | return sqlshim::commit($conn); 220 | } 221 | 222 | /** 223 | * Changes the driver error handling and logging configurations. 224 | * 225 | * @param string $setting 226 | * @param mixed $value 227 | * 228 | * @return bool 229 | */ 230 | function sqlsrv_configure($setting, $value) 231 | { 232 | return sqlshim::configure($setting, $value); 233 | } 234 | 235 | /** 236 | * Opens a connection to a Microsoft SQL Server database. 237 | * 238 | * @param string $serverName 239 | * @param array $connectionInfo 240 | * 241 | * @return \PDO 242 | */ 243 | function sqlsrv_connect($serverName, $connectionInfo) 244 | { 245 | return sqlshim::connect($serverName, $connectionInfo); 246 | } 247 | 248 | /** 249 | * Returns error and warning information about the last SQLSRV operation performed. 250 | * 251 | * @param int $errorsOrWarnings 252 | * 253 | * @return array[]|null 254 | */ 255 | function sqlsrv_errors($errorsOrWarnings = SQLSRV_ERR_ALL) 256 | { 257 | return sqlshim::errors($errorsOrWarnings); 258 | } 259 | 260 | /** 261 | * Executes a statement prepared with sqlsrv_prepare(). 262 | * 263 | * @param object $stmt 264 | * 265 | * @return bool 266 | */ 267 | function sqlsrv_execute($stmt) 268 | { 269 | return sqlshim::execute($stmt); 270 | } 271 | 272 | /** 273 | * Returns a row as an array. 274 | * 275 | * @param \PDOStatement $stmt 276 | * @param int $fetchType 277 | * @param int $row 278 | * @param int $offset 279 | * 280 | * @return array|null 281 | */ 282 | function sqlsrv_fetch_array($stmt, $fetchType = SQLSRV_FETCH_BOTH, $row = SQLSRV_SCROLL_NEXT, $offset = 0) 283 | { 284 | return sqlshim::fetch_array($stmt, $fetchType, $row, $offset); 285 | } 286 | 287 | /** 288 | * Retrieves the next row of data in a result set as an object. 289 | * 290 | * @param \PDOStatement $stmt 291 | * @param string $className 292 | * @param array $ctorParams 293 | * @param int $row 294 | * @param int $offset 295 | * 296 | * @return object|null|false 297 | */ 298 | function sqlsrv_fetch_object( 299 | $stmt, 300 | $className = 'stdClass', 301 | $ctorParams = [], 302 | $row = SQLSRV_SCROLL_NEXT, 303 | $offset = 0 304 | ) { 305 | return sqlshim::fetch_object($stmt, $className, $ctorParams, $row, $offset); 306 | } 307 | 308 | /** 309 | * Makes the next row in a result set available for reading. 310 | * 311 | * @param \PDOStatement $stmt 312 | * @param int $row 313 | * @param int $offset 314 | * 315 | * @return bool|null 316 | */ 317 | function sqlsrv_fetch($stmt, $row = SQLSRV_SCROLL_NEXT, $offset = 0) 318 | { 319 | return sqlshim::fetch($stmt, $row, $offset); 320 | } 321 | 322 | /** 323 | * Retrieves metadata for the fields of a statement prepared by sqlsrv_prepare() or sqlsrv_query(). 324 | * sqlsrv_field_metadata() can be called on a statement before or after statement execution. 325 | * 326 | * @param \PDOStatement $stmt The statment resource for which metadata is returned. 327 | * 328 | * @return array[]|false Returns an array of arrays on success. Otherwise, FALSE is returned. 329 | */ 330 | function sqlsrv_field_metadata($stmt) 331 | { 332 | return sqlshim::field_metadata($stmt); 333 | } 334 | 335 | /** 336 | * Frees all resources for the specified statement. 337 | * 338 | * @param \PDOStatement $stmt 339 | * 340 | * @return bool 341 | */ 342 | function sqlsrv_free_stmt($stmt) 343 | { 344 | return sqlshim::free_stmt($stmt); 345 | } 346 | 347 | /** 348 | * Returns the value of the specified configuration setting. 349 | * 350 | * @param string $setting 351 | * 352 | * @return mixed 353 | */ 354 | function sqlsrv_get_config($setting) 355 | { 356 | return sqlshim::get_config($setting); 357 | } 358 | 359 | /** 360 | * Gets field data from the currently selected row. 361 | * 362 | * @param \PDOStatement $stmt 363 | * @param int $fieldIndex 364 | * @param int $getAsType 365 | * 366 | * @return mixed 367 | */ 368 | function sqlsrv_get_field($stmt, $fieldIndex = 0, $getAsType = null) 369 | { 370 | return sqlshim::get_field($stmt, $fieldIndex = 0, $getAsType); 371 | } 372 | 373 | /** 374 | * Indicates whether the specified statement has rows. 375 | * 376 | * @param \PDOStatement $stmt 377 | * 378 | * @return bool 379 | */ 380 | function sqlsrv_has_rows($stmt) 381 | { 382 | return sqlshim::has_rows($stmt); 383 | } 384 | 385 | /** 386 | * Makes the next result of the specified statement active. 387 | * 388 | * @param \PDOStatement $stmt 389 | * 390 | * @return bool|null 391 | */ 392 | function sqlsrv_next_result($stmt) 393 | { 394 | return sqlshim::next_result($stmt); 395 | } 396 | 397 | /** 398 | * Retrieves the number of fields (columns) on a statement. 399 | * 400 | * @param \PDOStatement $stmt 401 | * 402 | * @return int|false 403 | */ 404 | function sqlsrv_num_fields($stmt) 405 | { 406 | return sqlshim::num_fields($stmt); 407 | } 408 | 409 | /** 410 | * Retrieves the number of rows in a result set. 411 | * 412 | * @param \PDOStatement $stmt 413 | * 414 | * @return int|false 415 | */ 416 | function sqlsrv_num_rows($stmt) 417 | { 418 | return sqlshim::num_rows($stmt); 419 | } 420 | 421 | /** 422 | * Prepares a query for execution. 423 | * 424 | * @param \PDO $conn 425 | * @param string $sql 426 | * @param array $params 427 | * @param array $options 428 | * 429 | * @return \PDOStatement|false 430 | */ 431 | function sqlsrv_prepare($conn, $sql, $params = [], $options = []) 432 | { 433 | return sqlshim::prepare($conn, $sql, $params, $options); 434 | } 435 | 436 | /** 437 | * Prepares and executes a query. 438 | * 439 | * @param \PDO $conn 440 | * @param string $sql 441 | * @param array $params 442 | * @param array $options 443 | * 444 | * @return \PDOStatement|false 445 | */ 446 | function sqlsrv_query($conn, $sql, $params = [], $options = []) 447 | { 448 | return sqlshim::query($conn, $sql, $params, $options); 449 | } 450 | 451 | /** 452 | * Rolls back a transaction that was begun with sqlsrv_begin_transaction(). 453 | * 454 | * @param \PDO $conn 455 | * 456 | * @return bool 457 | */ 458 | function sqlsrv_rollback($conn) 459 | { 460 | return sqlshim::rollback($conn); 461 | } 462 | 463 | /** 464 | * Returns the number of rows modified by the last INSERT, UPDATE, or DELETE query executed. 465 | * 466 | * @param \PDOStatement $stmt 467 | * 468 | * @return int|false 469 | */ 470 | function sqlsrv_rows_affected($stmt) 471 | { 472 | return sqlshim::rows_affected($stmt); 473 | } 474 | 475 | /** 476 | * Sends data from parameter streams to the server. 477 | * 478 | * @param \PDOStatement $stmt 479 | * 480 | * @return bool 481 | */ 482 | function sqlsrv_send_stream_data($stmt) 483 | { 484 | return sqlshim::send_stream_data($stmt); 485 | } 486 | 487 | /** 488 | * Returns information about the server. 489 | * 490 | * @param \PDO $conn 491 | * 492 | * @return string[] 493 | */ 494 | function sqlsrv_server_info($conn) 495 | { 496 | return sqlshim::server_info($conn); 497 | } 498 | 499 | return true; 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /src/sqlshim.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT 11 | * @link https://github.com/radsectors/sqlshim 12 | */ 13 | namespace radsectors; 14 | 15 | // phpcs:disable PSR1.Methods.CamelCapsMethodName, Squiz.Classes.ValidClassName 16 | 17 | /** 18 | * PHP sqlsrv functions for Linux/OS X. 19 | */ 20 | final class sqlshim 21 | { 22 | private static $config; 23 | private static $sqlsrvconf; 24 | private static $cstrdefaults; 25 | private static $tabcursor; 26 | private static $tabfetch; 27 | private static $tabscroll; 28 | private static $tablogsys; 29 | private static $client_info; 30 | private static $errors = []; 31 | private static $init = false; 32 | 33 | // const SQL_INT_MAX = 2147483648; 34 | 35 | private function __construct() 36 | { 37 | // can't call me. 38 | } 39 | 40 | /** 41 | * Initialize the sqlshimmage. 42 | * Called by globals.php 43 | * 44 | * @param array $config 45 | * 46 | * @return bool Returns whether sqlshim can have its globals defined. 47 | */ 48 | public static function init() 49 | { 50 | $loadable = !extension_loaded('sqlsrv') && !function_exists('sqlsrv_connect'); 51 | 52 | if (self::$init) { 53 | return $loadable; 54 | } 55 | 56 | self::$config = (object)[ 57 | 'prefix' => 'dblib', 58 | 'driver' => 'sqlshim', 59 | 'version' => '7.2', 60 | 'autotype_fields' => false, 61 | ]; 62 | 63 | self::$sqlsrvconf = (object)[ 64 | 'clientbuffermaxkbsize' => 10240, 65 | 'logseverity' => self::LOG_SEVERITY_ERROR, 66 | 'logsubsystems' => self::LOG_SYSTEM_OFF, 67 | 'warningsreturnaserrors' => 1, 68 | ]; 69 | 70 | self::$cstrdefaults = [ 71 | 'applicationintent' => 'ReadWrite', 72 | 'characterset' => self::ENC_CHAR, 73 | 'connectionpooling' => 1, 74 | 'encrypt' => 0, 75 | 'failover_partner' => null, 76 | 'logintimeout' => null, 77 | 'multipleactiveresultsets' => 1, 78 | 'multisubnetfailover' => 'No', 79 | 'quoteid' => 1, 80 | 'returndatesasstrings' => 0, 81 | 'traceon' => 0, 82 | 'transactionisolation' => self::TXN_READ_COMMITTED, 83 | 'trustedservercertificate' => 0, 84 | ]; 85 | 86 | self::$tabcursor = [ 87 | self::CURSOR_FORWARD => \PDO::CURSOR_FWDONLY, 88 | self::CURSOR_STATIC => \PDO::CURSOR_FWDONLY, // ??? 89 | self::CURSOR_DYNAMIC => \PDO::CURSOR_SCROLL, // ??? 90 | self::CURSOR_KEYSET => \PDO::CURSOR_FWDONLY, // ??? 91 | self::CURSOR_CLIENT_BUFFERED => \PDO::CURSOR_FWDONLY, // ??? 92 | ]; 93 | self::$tabfetch = [ 94 | self::FETCH_NUMERIC => \PDO::FETCH_NUM, 95 | self::FETCH_ASSOC => \PDO::FETCH_ASSOC, 96 | self::FETCH_BOTH => \PDO::FETCH_BOTH, 97 | ]; 98 | self::$tabscroll = [ 99 | self::SCROLL_NEXT => \PDO::FETCH_ORI_NEXT, 100 | self::SCROLL_FIRST => \PDO::FETCH_ORI_FIRST, 101 | self::SCROLL_LAST => \PDO::FETCH_ORI_LAST, 102 | self::SCROLL_PRIOR => \PDO::FETCH_ORI_PRIOR, 103 | self::SCROLL_ABSOLUTE => \PDO::FETCH_ORI_ABS, 104 | self::SCROLL_RELATIVE => \PDO::FETCH_ORI_REL, 105 | ]; 106 | self::$tablogsys = [ 107 | self::LOG_SYSTEM_ALL => \PDO::ERRMODE_EXCEPTION, 108 | self::LOG_SYSTEM_OFF => \PDO::ERRMODE_SILENT, 109 | self::LOG_SYSTEM_INIT => \PDO::ERRMODE_EXCEPTION, 110 | self::LOG_SYSTEM_CONN => \PDO::ERRMODE_EXCEPTION, 111 | self::LOG_SYSTEM_STMT => \PDO::ERRMODE_EXCEPTION, 112 | self::LOG_SYSTEM_UTIL => \PDO::ERRMODE_EXCEPTION, 113 | ]; 114 | 115 | self::$client_info = [ 116 | 'odbc' => [ 117 | 'DriverDLLName' => function () { 118 | return 'Deprecated'; 119 | }, 120 | 'DriverODBCVer' => function () { 121 | return 'Deprecated'; 122 | }, 123 | 'DriverVer' => function () { 124 | return 'Deprecated'; 125 | }, 126 | 'ExtensionVer' => function () { 127 | return phpversion('pdo_odbc'); 128 | }, 129 | ], 130 | 'dblib' => [ 131 | 'DriverDLLName' => function () { 132 | return 'pdo_dblib.so'; 133 | }, 134 | 'DriverODBCVer' => function () { 135 | return 'N/A'; 136 | }, 137 | 'DriverVer' => function () { 138 | return (new \ReflectionExtension('pdo_dblib'))->getVersion(); 139 | }, 140 | 'ExtensionVer' => function () { 141 | return 'radsectors\sqlshim '.self::getVersion().''; 142 | }, 143 | ], 144 | ]; 145 | 146 | self::$init = true; 147 | 148 | return $loadable; 149 | } 150 | 151 | /** 152 | * @internal Gets sqlshim version number. 153 | * 154 | * @return string sqlshim version number. 155 | */ 156 | private static function getVersion() 157 | { 158 | return 'alpha'; // TODO: get this from git tag if possible. 159 | } 160 | 161 | /** 162 | * @internal Collects error info to internal error log 163 | * 164 | * @param mixed $e Error Info 165 | */ 166 | private static function logErr($e) 167 | { 168 | if (is_array($e) && count($e) > 2) { 169 | self::$errors[] = [ 170 | 0 => $e[0], 171 | 'SQLSTATE' => $e[0], 172 | 1 => $e[1], 173 | 'code' => $e[1], 174 | 2 => $e[2] ?: '', 175 | 'message' => $e[2] ?: '', 176 | ]; 177 | 178 | return true; 179 | } 180 | 181 | is_a($e, 'PDOException') && ( 182 | (!empty($e->errorInfo) && self::logErr($e->errorInfo)) || 183 | (bool) preg_replace_callback( 184 | "/SQLSTATE\[(\d+)\] .*: (\d+) (.*)/", 185 | function ($matches) { 186 | if (count($matches)) { 187 | self::logErr(array_slice($matches, 1)); 188 | 189 | return '1'; 190 | } 191 | 192 | return '0'; 193 | }, 194 | $e->getMessage() 195 | ) 196 | ) || 197 | self::logErr(['SQLSHIM', -0, 'Unknown sqlshim error.']); 198 | } 199 | 200 | private static function calcSize($c, $max = 8000) 201 | { 202 | $c = intval($c); 203 | $r = 8387584; 204 | if ($c == -1) { 205 | $r += 512; 206 | } elseif ($c > 0 && $c <= $max) { 207 | $r = $c * 512; 208 | } 209 | 210 | return $r; 211 | } 212 | 213 | private static function calcSizeStr($en) 214 | { 215 | $en = strval($en); 216 | $r = 524288; 217 | if ($en == 'binary') { 218 | $r += 1; 219 | } elseif ($en == 'char') { 220 | $r += 1.5; 221 | } 222 | 223 | return intval($r * 512); 224 | } 225 | 226 | private static function calcPrecScale($prec, $scale) 227 | { 228 | $max_precision = 38; 229 | $scale = 8389120; 230 | $noscale = 2139095040; 231 | $invalid = -1; 232 | 233 | $p = is_numeric($prec) ? intval($prec) : $invalid; 234 | $s = is_numeric($scale) ? intval($scale) : $invalid; 235 | if ($s == -257) { 236 | $s = $invalid; 237 | } 238 | if ($s < -256) { 239 | $s = $s % 256; 240 | } 241 | 242 | if (($s == $invalid && ($p < 0 || $p > $max_precision))) { 243 | $c = 0; 244 | $b = $s; 245 | } else { 246 | $c = $s * $scale; 247 | $b = $p - $s; 248 | 249 | if ($p > $max_precision) { 250 | $p = $invalid; 251 | } 252 | if ($p < 0) { 253 | $p = $invalid; 254 | $c = ($s + 1) * $scale; 255 | $b = -$s - 2; 256 | } 257 | if ($s > $p) { 258 | $s = $invalid; 259 | $c = $noscale; 260 | $b = $p; 261 | } 262 | if ($p == $invalid && $s == $invalid) { 263 | $c = $scale + $noscale; 264 | $b = -2; 265 | } 266 | } 267 | 268 | return intval($c + ($b * 512)); 269 | } 270 | 271 | /** 272 | * typifyRow. 273 | * 274 | * @param object|array $row Database record to be typed. 275 | * 276 | * @return $object|array Returns the typed record. 277 | */ 278 | private static function typifyRow($row) 279 | { 280 | foreach ($row as &$value) { 281 | $value = self::typifyField($value); 282 | } 283 | 284 | return $row; 285 | } 286 | 287 | /** 288 | * typifyField. 289 | * TODO: typifyField() 7-year-old comment http://php.net/manual/en/ref.pdo-dblib.php#89827 may be on to something 290 | * 291 | * @param mixed $val The value for which the type is to be guessed. 292 | * 293 | * @return mixed Returns the typed value. 294 | */ 295 | private static function typifyField($val) 296 | { 297 | if (self::$config->autotype_fields) { 298 | if (is_numeric($val)) { 299 | if (is_float($val)) { 300 | return floatval($val); 301 | } 302 | if (is_int($val)) { 303 | return intval($val); 304 | } 305 | } 306 | } 307 | // always try to convert dates because reasons. 308 | if (preg_match('/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]).*/', $val)) { 309 | try { 310 | return new \DateTime($val); 311 | } catch (\Exception $e) { 312 | /* continue... */ 313 | } 314 | } 315 | 316 | return $val; 317 | } 318 | 319 | /* 320 | * sqlsrv constants 321 | */ 322 | const FETCH_NUMERIC = 1; 323 | const FETCH_ASSOC = 2; 324 | const FETCH_BOTH = 3; 325 | const ERR_ERRORS = 0; 326 | const ERR_WARNINGS = 1; 327 | const ERR_ALL = 2; 328 | 329 | const LOG_SYSTEM_ALL = -1; 330 | const LOG_SYSTEM_CONN = 2; 331 | const LOG_SYSTEM_INIT = 1; 332 | const LOG_SYSTEM_OFF = 0; 333 | const LOG_SYSTEM_STMT = 4; 334 | const LOG_SYSTEM_UTIL = 8; 335 | 336 | const LOG_SEVERITY_ALL = -1; 337 | const LOG_SEVERITY_ERROR = 1; 338 | const LOG_SEVERITY_NOTICE = 4; 339 | const LOG_SEVERITY_WARNING = 2; 340 | 341 | const NULLABLE_YES = 1; 342 | const NULLABLE_NO = 0; 343 | const NULLABLE_UNKNOWN = 2; 344 | 345 | const PARAM_IN = 1; 346 | const PARAM_INOUT = 2; 347 | const PARAM_OUT = 4; 348 | 349 | const PHPTYPE_INT = 2; 350 | const PHPTYPE_DATETIME = 5; 351 | const PHPTYPE_FLOAT = 3; 352 | const PHPTYPE_NULL = 1; 353 | 354 | const ENC_BINARY = 'binary'; 355 | const ENC_CHAR = 'char'; 356 | 357 | const SQLTYPE_BIGINT = -5; 358 | const SQLTYPE_BIT = -7; 359 | const SQLTYPE_DATE = 5211; 360 | const SQLTYPE_DATETIME = 25177693; 361 | const SQLTYPE_DATETIME2 = 58734173; 362 | const SQLTYPE_DATETIMEOFFSET = 58738021; 363 | const SQLTYPE_FLOAT = 6; 364 | const SQLTYPE_IMAGE = -4; 365 | const SQLTYPE_INT = 4; 366 | const SQLTYPE_MONEY = 33564163; 367 | const SQLTYPE_NTEXT = -10; 368 | const SQLTYPE_REAL = 7; 369 | const SQLTYPE_SMALLDATETIME = 8285; 370 | const SQLTYPE_SMALLINT = 5; 371 | const SQLTYPE_SMALLMONEY = 33559555; 372 | const SQLTYPE_TEXT = -1; // -1 means unlimited 373 | const SQLTYPE_TIME = 58728806; 374 | const SQLTYPE_TIMESTAMP = 4606; 375 | const SQLTYPE_TINYINT = -6; 376 | const SQLTYPE_UNIQUEIDENTIFIER = -11; 377 | const SQLTYPE_UDT = -151; 378 | const SQLTYPE_XML = -152; 379 | 380 | const TXN_READ_UNCOMMITTED = 1; 381 | const TXN_READ_COMMITTED = 2; 382 | const TXN_REPEATABLE_READ = 4; 383 | const TXN_SNAPSHOT = 32; 384 | const TXN_SERIALIZABLE = 8; 385 | 386 | const CURSOR_FORWARD = 'forward'; 387 | const CURSOR_STATIC = 'static'; 388 | const CURSOR_DYNAMIC = 'dynamic'; 389 | const CURSOR_KEYSET = 'keyset'; 390 | const CURSOR_CLIENT_BUFFERED = 'buffered'; 391 | 392 | const SCROLL_NEXT = 1; 393 | const SCROLL_FIRST = 2; 394 | const SCROLL_LAST = 3; 395 | const SCROLL_PRIOR = 4; 396 | const SCROLL_ABSOLUTE = 5; 397 | const SCROLL_RELATIVE = 6; 398 | 399 | public static function PHPTYPE_STREAM($encoding) 400 | { 401 | return self::calcSizeStr($encoding) + 67108870; 402 | } 403 | 404 | public static function PHPTYPE_STRING($encoding) 405 | { 406 | return self::calcSizeStr($encoding) + 67108868; 407 | } 408 | 409 | public static function SQLTYPE_BINARY($byteCount) 410 | { 411 | return self::calcSize($byteCount) + 2139095550; 412 | } 413 | 414 | public static function SQLTYPE_CHAR($charCount) 415 | { 416 | return self::calcSize($charCount) + 2139095041; 417 | } 418 | 419 | public static function SQLTYPE_DECIMAL($precision, $scale) 420 | { 421 | return self::calcPrecScale($precision, $scale) + 3; 422 | } 423 | 424 | public static function SQLTYPE_NCHAR($charCount) 425 | { 426 | return self::calcSize($charCount, 4000) + 2139095544; 427 | } 428 | 429 | public static function SQLTYPE_NUMERIC($precision, $scale) 430 | { 431 | return self::calcPrecScale($precision, $scale) + 2; 432 | } 433 | 434 | public static function SQLTYPE_NVARCHAR($charCount) 435 | { 436 | return self::calcSize($charCount, 4000) + 2139095543; 437 | } 438 | 439 | public static function SQLTYPE_VARBINARY($byteCount) 440 | { 441 | return self::calcSize($byteCount) + 2139095549; 442 | } 443 | 444 | public static function SQLTYPE_VARCHAR($charCount) 445 | { 446 | return self::calcSize($charCount) + 2139095052; 447 | } 448 | 449 | public static function begin_transaction(\PDO $conn) 450 | { 451 | return $conn->beginTransaction(); 452 | } 453 | 454 | public static function cancel(\PDOStatement $stmt) 455 | { 456 | // no PDO equivalent for this API 457 | return true; 458 | } 459 | 460 | public static function client_info(\PDO $conn) 461 | { 462 | $return = []; 463 | $info = self::$client_info[self::$config->prefix]; 464 | foreach ($info as $i => $call) { 465 | $return[$i] = $call(); 466 | } 467 | 468 | return $return; 469 | } 470 | 471 | public static function close(\PDO $conn) 472 | { 473 | return $conn = null; 474 | } 475 | 476 | public static function commit(\PDO $conn) 477 | { 478 | $conn->commit(); 479 | } 480 | 481 | public static function config($config = []) 482 | { 483 | // process options 484 | foreach ($config as $opt => $val) { 485 | $opt = strtolower($opt); 486 | if (is_string($val)) { 487 | $val = strtolower($val); 488 | } 489 | switch ($opt) { 490 | case 'prefix': 491 | switch ($val) { 492 | case 'sybase': 493 | case 'mssql': 494 | $val = 'dblib'; 495 | // no break 496 | case 'dblib': 497 | case 'odbc': 498 | self::$config->$opt = $val; 499 | break; 500 | default: 501 | break; 502 | } 503 | break; 504 | case 'driver': 505 | case 'version': 506 | self::$config->$opt = $val; 507 | break; 508 | case 'autotype_fields': 509 | case 'warningsreturnaserrors': 510 | self::$config->$opt = (bool) $val; 511 | // no break 512 | default: 513 | self::logErr(["IMSSP", -14, "An invalid parameter was passed to sqlsrv_configure."]); 514 | break; 515 | } 516 | } 517 | } 518 | 519 | public static function configure($setting, $value) 520 | { 521 | $setting = strtolower($setting); 522 | if (property_exists(self::$sqlsrvconf, $setting)) { 523 | switch ($setting) { 524 | case 'clientbuffermaxkbsize': 525 | case 'logseverity': 526 | return true; 527 | break; 528 | case 'logsubsystems': 529 | if (isset(self::$tablogsys[$value])) { 530 | self::$sqlsrvconf->$setting = $value; 531 | return true; 532 | } 533 | break; 534 | case 'warningsreturnaserrors': 535 | self::$sqlsrvconf->$setting = !!$value; 536 | return true; 537 | break; 538 | default: 539 | break; 540 | } 541 | } 542 | // $logseverity = [ 543 | // self::LOG_SEVERITY_ALL, 544 | // self::LOG_SEVERITY_ERROR, 545 | // self::LOG_SEVERITY_WARNING, 546 | // self::LOG_SEVERITY_NOTICE, 547 | // ]; 548 | 549 | return false; 550 | } 551 | 552 | public static function connect($serverName, $connectionInfo) 553 | { 554 | // strip tcp: prefix 555 | $serverName = str_replace('tcp:', '', $serverName); 556 | // lowercase all keys 557 | $connectionInfo = array_change_key_case($connectionInfo); 558 | // default port (1433) 559 | $port = isset($connectionInfo['port']) ? $connectionInfo['port'] : '1433'; 560 | // inline port will take precedent over $connectionInfo['port'] 561 | list( 562 | $connectionInfo['servername'], 563 | $connectionInfo['port'] 564 | ) = explode(',', "$serverName,$port", 3); 565 | 566 | $cstrparams = [ 567 | 'dblib' => [ 568 | 'version' => 'version', 569 | 'servername' => 'host', 570 | 'database' => 'dbname', 571 | 'port' => 'port', 572 | 'characterset' => 'charset', 573 | 'encrypt' => 'secure', // not used (http://php.net/manual/en/ref.pdo-dblib.connection.php) 574 | ], 575 | 'odbc' => [ 576 | 'version' => 'tds_version', 577 | 'servername' => 'server', 578 | 'database' => 'database', 579 | 'port' => 'port', 580 | 'characterset' => 'clientcharset', 581 | 'driver' => 'driver', 582 | ], 583 | ]; 584 | $cstr = self::$config->prefix.':'; 585 | foreach ($cstrparams[self::$config->prefix] as $i => $par) { 586 | if (empty($par)) { 587 | continue; 588 | } 589 | $i = strtolower($i); 590 | if (isset($connectionInfo[$i])) { 591 | $cstr .= "$par=$connectionInfo[$i];"; 592 | } elseif (isset(self::$config->$i)) { 593 | $cstr .= "$par=".self::$config->$i.';'; 594 | } 595 | } 596 | 597 | try { 598 | $conn = new \PDO($cstr, $connectionInfo['uid'], $connectionInfo['pwd']); 599 | } catch (\PDOException $e) { 600 | self::logErr($e); 601 | 602 | return false; 603 | } 604 | 605 | $conn->prepare('SET ANSI_WARNINGS ON')->execute(); 606 | $conn->prepare('SET ANSI_PADDING ON')->execute(); 607 | $conn->prepare('SET ANSI_NULLS ON')->execute(); 608 | $conn->prepare('SET QUOTED_IDENTIFIER ON')->execute(); 609 | $conn->prepare('SET CONCAT_NULL_YIELDS_NULL ON')->execute(); 610 | 611 | $conn->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); 612 | $conn->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_NATURAL); 613 | $conn->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false); // this doesn't do shit. everything comes out string. 614 | $conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 615 | $conn->setAttribute(\PDO::ATTR_ORACLE_NULLS, \PDO::NULL_NATURAL); 616 | // $conn->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); 617 | 618 | return $conn; 619 | } 620 | 621 | public static function errors($errorsOrWarnings = self::ERR_ALL) 622 | { 623 | // TODO: errors() figure out if we can differentiate between errors and warnings. 624 | return self::$errors; 625 | } 626 | 627 | public static function execute(\PDOStatement $stmt) 628 | { 629 | return $stmt->execute(); 630 | } 631 | 632 | // phpcs:ignore 633 | public static function fetch_array( 634 | \PDOStatement $stmt, 635 | $fetchType = self::FETCH_BOTH, 636 | $row = self::SCROLL_NEXT, 637 | $offset = 0 638 | ) { 639 | try { 640 | $array = $stmt->fetch(self::$tabfetch[$fetchType], self::$tabscroll[$row], $offset); 641 | if (is_array($array)) { 642 | return self::typifyRow($array); 643 | } 644 | } catch (\PDOException $e) { 645 | self::logErr($e); 646 | 647 | return false; 648 | } 649 | 650 | return; // fetch with no errors (null) 651 | } 652 | 653 | public static function fetch_object( 654 | \PDOStatement $stmt, 655 | $className = 'stdClass', 656 | $ctorParams = [], 657 | $row = self::SCROLL_NEXT, 658 | $offset = 0 659 | ) { 660 | try { 661 | $object = $stmt->fetch( 662 | \PDO::FETCH_ASSOC, 663 | self::$tabscroll[$row], 664 | $offset 665 | ); 666 | // $object = $stmt->fetchObject( 667 | // $className, 668 | // $ctorParams 669 | // ); 670 | if (is_array($object)) { 671 | $object = (object) $object; 672 | } 673 | if (is_object($object)) { 674 | return self::typifyRow($object); 675 | } 676 | } catch (\PDOException $e) { 677 | self::logErr($e); 678 | 679 | return false; 680 | } 681 | 682 | return; // fetch with no more records (null) 683 | } 684 | 685 | public static function fetch(\PDOStatement $stmt, $row, $offset) 686 | { 687 | try { 688 | $array = $stmt->fetch(\PDO::FETCH_NUM, self::$tabscroll[$row], $offset); 689 | if (is_array($array)) { 690 | return self::typifyRow($array); 691 | } 692 | } catch (\PDOException $e) { 693 | self::logErr($e); 694 | 695 | return false; 696 | } 697 | 698 | return; 699 | } 700 | 701 | public static function field_metadata(\PDOStatement $stmt) 702 | { 703 | // NOTE: field_metadata() - PDOStatement->getColumnMeta() is an "EXPERIMENTAL" function. 704 | // And its request not supported by driver. 705 | // HACK: 7-year-old comment http://php.net/manual/en/ref.pdo-dblib.php#89827 may be on to something 706 | return false; 707 | // $metadata = []; 708 | // for ( $i=0; $i<$stmt->columnCount(); $i++ ) 709 | // { 710 | // $metadata[] = $stmt->getColumnMeta($i); 711 | // } 712 | // return $metadata; 713 | } 714 | 715 | public static function free_stmt(\PDOStatement $stmt) 716 | { 717 | return $stmt->closeCursor(); 718 | } 719 | 720 | public static function get_config($setting) 721 | { 722 | $setting = strtolower($setting); 723 | if (isset(self::$sqlsrvconf->$setting)) { 724 | return self::$sqlsrvconf->$setting; 725 | } 726 | self::logErr(["IMSSP", -14, "An invalid parameter was passed to sqlsrv_get_config."]); 727 | 728 | return false; 729 | } 730 | 731 | public static function get_field(\PDOStatement $stmt, $fieldIndex = 0, $getAsType = null) 732 | { 733 | $value = $stmt->fetchColumn($fieldIndex); 734 | // NOTE: AFAIK, there's no way to get any field to come out as anything but a string. 735 | // So for now, we'll assume string. 736 | // TODO:get_field() - test to see what happens when asking for a non-convertable value 737 | switch ($getAsType) { 738 | case self::PHPTYPE_INT: 739 | return (int)$value; 740 | case self::PHPTYPE_FLOAT: 741 | return (float)$value; 742 | case self::PHPTYPE_DATETIME: 743 | // TODO:get_field() - test this especially. 744 | return date_create($value) || new DateTime(); 745 | case self::PHPTYPE_STRING(self::ENC_BINARY): 746 | return base64_encode($value); 747 | case self::PHPTYPE_STRING(self::ENC_CHAR): 748 | return "$value"; 749 | case self::PHPTYPE_STREAM(self::ENC_BINARY): 750 | $value = base64_encode($value); 751 | return fopen("data://base64,$value", 'r+'); 752 | case self::PHPTYPE_STREAM(self::ENC_CHAR): 753 | return fopen("data://text/plain,$value", 'r+'); 754 | case self::PHPTYPE_NULL: 755 | return null; 756 | default: 757 | } 758 | 759 | return $value; 760 | } 761 | 762 | public static function has_rows(\PDOStatement $stmt) 763 | { 764 | // REVIEW: has_rows() - this doesn't work unless $stmt->rowCount() works. 765 | // might just need: [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL] option set 766 | return (bool) $stmt->rowCount(); 767 | } 768 | 769 | public static function next_result(\PDOStatement $stmt) 770 | { 771 | return $stmt->nextRowset(); 772 | } 773 | 774 | public static function num_fields(\PDOStatement $stmt) 775 | { 776 | return $stmt->columnCount(); 777 | } 778 | 779 | public static function num_rows(\PDOStatement $stmt) 780 | { 781 | // REVIEW: num_rows() - $stmt->rowCount DOES NOT work for SELECTs in MSSQL PDO. 782 | $sql = $stmt->queryString; 783 | if (stripos($sql, 'select') >= 0) { 784 | $sql = preg_replace('/SELECT .* FROM/', 'SELECT COUNT(*) AS count FROM', $sql); 785 | $row = $stmt->conn->query($sql)->fetch(\PDO::FETCH_NUM); 786 | if (is_array($row) && count($row)) { 787 | return reset($row); 788 | } 789 | } 790 | 791 | return false; 792 | } 793 | 794 | public static function prepare(\PDO $conn, $sql, $params = [], $options = []) 795 | { 796 | // translate options 797 | $options = array_intersect_key($options, array_flip([ 798 | 'QueryTimeout', 799 | 'SendStreamParamsAtExec', 800 | 'Scrollable', 801 | ])); 802 | foreach ($options as $opt => $val) { 803 | switch ($opt) { 804 | case 'QueryTimeout': 805 | if (is_numeric($val)) { 806 | $conn->setAttribute(\PDO::SQLSRV_ATTR_QUERY_TIMEOUT, intval($val)); 807 | } 808 | break; 809 | case 'SendStreamParamsAtExec': 810 | // TODO: prepare() - figure out what this is and what to do with it 811 | break; 812 | case 'Scrollable': 813 | if (isset(self::$tabcursor[$val])) { 814 | $options[\PDO::ATTR_CURSOR] = self::$tabcursor[$val]; 815 | } 816 | break; 817 | default: 818 | break; 819 | } 820 | } 821 | 822 | $sql = stripslashes($sql); // REVIEW: purpose? 823 | 824 | if (!is_array($params)) { 825 | $params = []; 826 | } 827 | $count = mb_substr_count($sql, '?'); 828 | $params = array_slice(array_pad($params, $count, null), 0, $count, false); 829 | 830 | try { 831 | $stmt = $conn->prepare($sql, $options); 832 | foreach ($params as $i => $val) { 833 | if (is_array($val)) { 834 | $arr = $val; 835 | // TODO: try to support more fully 836 | // QUESTION: need type table? 837 | $val = reset($arr); 838 | $dir = next($arr); 839 | $ptype = next($arr); 840 | $stype = next($arr); 841 | } 842 | $bound = $stmt->bindValue($i + 1, $val); 843 | } 844 | $stmt->conn = $conn; // for ref 845 | return $stmt; 846 | } catch (\PDOException $e) { 847 | self::logErr($e->errorInfo); 848 | 849 | return false; 850 | } 851 | } 852 | 853 | public static function query(\PDO $conn, $sql, $params = [], $options = []) 854 | { 855 | $stmt = self::prepare($conn, $sql, $params, $options); 856 | 857 | try { 858 | if (self::execute($stmt)) { 859 | return $stmt; 860 | } else { 861 | self::logErr($stmt->errorInfo()); 862 | 863 | return $stmt; 864 | } 865 | } catch (\PDOException $e) { 866 | self::logErr($e->errorInfo); 867 | } 868 | 869 | return false; 870 | } 871 | 872 | public static function rollback(\PDO $conn) 873 | { 874 | return $conn->rollBack(); 875 | } 876 | 877 | public static function rows_affected(\PDOStatement $stmt) 878 | { 879 | return $stmt->rowCount(); 880 | } 881 | 882 | public static function send_stream_data(\PDOStatement $stmt) 883 | { 884 | // TODO: send_stream_data() - what is this? 885 | } 886 | 887 | public static function server_info(\PDO $conn) 888 | { 889 | $stmt = self::query( 890 | $conn, 891 | "SELECT 892 | DB_NAME() AS CurrentDatabase, 893 | @@VERSION AS SQLServerVersion, 894 | @@SERVERNAME AS SQLServerName 895 | ;" 896 | ); 897 | if ($stmt !== false) { 898 | $info = self::fetch_array($stmt, self::FETCH_ASSOC); 899 | $info['SQLServerVersion'] = preg_replace('/.* (\d+\.\d+\.\d+)\.\d+.*/s', '$1', $info['SQLServerVersion']); 900 | return $info; 901 | } 902 | return false; 903 | } 904 | } 905 | --------------------------------------------------------------------------------