How to Late bind with T-SQL and the Statement Object? - php

I am looking for a long term solution to accommodate T-SQL queries that need to use late binding. Late binding syntax with MS-SQL is a pain and is unique.
When it comes to T-SQL late binding we need to DECLARE and SET, like
DECLARE #id int
SET #id = 1
SELECT * FROM users
WHERE user_id = #id;
Now Zend Framework 2 builds queries like
$select = new Sql\Select();
$select->from(['users']);
$select->columns(['*']);
$select->where->equalTo('user_id', '#id');
// Create the statement object
$stmt = $sql->prepareStatementForSqlObject();
$stmt->prepare();
$stmt->execute(['#id' => 1]);
Now in ZF2's Statement object's prepare() method I can prefix my DECLARE and SET to the already existing $sql, like so:
public function prepare($sql = null, array $options = [])
{
if ($this->isPrepared) {
throw new Exception\RuntimeException('Already prepared');
}
$sql = ($sql) ?: $this->sql;
$options = ($options) ?: $this->prepareOptions;
$pRef = &$this->parameterReferences;
// change substr_count($sql, '?') <-> TO |-> substr_count($sql, '#')
for ($position = 0, $count = substr_count($sql, '?'); $position < $count; $position++) {
if (!isset($this->prepareParams[$position])) {
$pRef[$position] = ['', SQLSRV_PARAM_IN, null, null];
} else {
$pRef[$position] = &$this->prepareParams[$position];
}
}
// <!-- Beg of custom stuff. I can grab the $sql string, that we built up here, with $this->sql, and prefix it with the T-SQL required DECLARE and SET. -->
// We can get #id and 1 through the execute()'s call and the parameter Container.
$prependSQL = "DECLARE #id int";
$prependSQL .= "SET #id = 1";
$prependSQL .= $this->sql;
$this->sql = $prependSQL;
$sql = $this->sql;
// <!-- End of custom stuff -->
$this->resource = sqlsrv_prepare($this->sqlsrv, $sql, $pRef, $options);
$this->isPrepared = true;
return $this;
}
Is this how I should be doing this? I feel like I am undermining ZF2's DB abstraction layer. Any direction or input would be great on how I am suppose to do this.

Related

Array to string conversion notice in PHP

I am trying to display records using PDO LIKE query, but I am getting this error message can I know how to solve this.
This is my code:
$rs = new JSONRecordSet();
$searchbooksSQL = "SELECT Title FROM l_stock WHERE Title LIKE ?";
$params = array("%$term%");
echo $rs->getRecordSet($searchbooksSQL, $params);
This is the getRecordSet code:
class R_RecordSet {
function getRecordSet($sql, $params = null) {
if (is_array($params)) {
$this->stmt = $this->db->prepare($sql);
// execute the statement passing in the named placeholder and the value it'll have
$this->stmt->execute($params);
} else {
$this->stmt = $this->db->query($sql);
}
return $this->stmt;
}
}
class JSONRecordSet extends R_RecordSet {
function getRecordSet($sql, $elementName = "ResultSet", $params = null) {
$stmt = parent::getRecordSet($sql, $params);
$recordSet = $stmt->fetchAll(PDO::FETCH_ASSOC);
$nRecords = count($recordSet);
if ($nRecords == 0) {
$status = 'error';
$message = json_encode(array("text" => "No records found"));
$result = '[]';
} else {
$status = 'ok';
$message = json_encode(array("text" => ""));
$result = json_encode($recordSet);
}
return "{\"status\": \"$status\", \"message\":$message, \"$elementName\" :{\"RowCount\": $nRecords ,\"Result\": $result}}";
}
}
The error message i am getting is "Notice: Array to string conversion"
getRecordSet() is defined as:
function getRecordSet($sql, $elementName = "ResultSet", $params = null) {
however, you are calling it as:
echo $rs->getRecordSet($searchbooksSQL, $params);
You will need to modify your code to pass in an appropriate $elementName parameter. (The default is probably reasonable.)
echo $rs->getRecordSet($searchbooksSQL, 'ResultSet', $params);
Additionally, you should probably use json_encode() to generate the final result from JSONRecordSet::getRecordSet(), rather than building it up with string concatenation. It will make the code easier to read and understand.
Also, your two implementations of getRecordSet() are incompatible with each other, according to the Liskov Substitution Principle due to the change in the semantics of the input parameters, and is likely what led you to the parameter mismatch at your call site in the first place. You probably want to re-order JSONRecordSet::getRecordSet() parameters to:
function getRecordSet($sql, $params = null, $elementName = 'ResultSet') {

PHP / MySQLi on Windows: inserts into a MySQL innoDB Table increases the AUTO_INCREMENT column by 2

When I am doing a simple insert via the PHP mysqli API with prepared statements on Windows within a PHP process, the defined AUTO_INCREMENT column is increased by 2 instead of 1:
INSERT INTO `table` (`name`) VALUES (?)
It get increased by 1 when doing multiple inserts (one by one in separate transactions) within one PHP process.
It always get increased by 1 when I use the same SQL query via phpmyadmin.
There are no other INSERT or UPDATE statements before or after the mentioned INSERT. Only a SHOW and some SELECT statements before.
I cannot find the cause for this problem. What can be the causes for such a behaviour?
Here the main code parts:
<?php
class DB
{
private function __construct($host, $username, $password, $schema, $port, $socket)
{
if(is_null(self::$DB))
{
self::$DB = new \MySQLi((string) $host, (string) $username, (string) $password, (string) $schema, (int) $port, (string) $socket);
self::$DB->set_charset('utf8');
}
}
// [...]
public function __destruct()
{
if(!is_null(self::$DB))
self::$DB->close();
}
// [...]
public static function connect($host = '', $username = '', $password = '', $schema = '', $port = 0, $socket = '')
{
if(is_null(self::$instance))
{
$MD = new \MySQLi_driver();
$MD->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;
// [...]
self::$instance = new self($host, $username, $password, $schema, $port, $socket);
}
return self::$instance;
}
// [...]
public static function __callStatic($name, $args)
{
self::connect();
switch(true)
{
case in_array($name, array('insert', 'select', 'update', 'delete', 'show', 'describe', 'explain')):
$query = isset($args[0]) ? (string) $args[0] : '';
$vals = isset($args[1]) ? $args[1] : array();
$select = isset($args[2]) ? trim((string) $args[2]) : 'array';
$empty = isset($args[3]) ? $args[3] : array();
$types = isset($args[4]) ? trim((string) $args[4]) : '';
return self::dml(in_array($name, array('show', 'describe', 'explain')) ? 'select' : $name, $query, $vals, $select, $empty, $types);
break;
}
//[...]
}
// [...]
public static function dml($type, $query, $vals = array(), $select = 'array', $empty = array(), $types = '')
{
// [...]
if(!empty($vals) || mb_strpos($query,'?') !== false)
{
if(!$stmt = self::$DB->prepare($query))
throw new DBException('Failed to prepare statement '.htmlspecialchars($query).PHP_EOL.self::$DB->error.' ('.self::$DB->sqlstate.').');
$args = array();
if(empty($types))
{
foreach($vals as &$val)
{
$t = gettype($val);
if($t == 'string' || $t == 'NULL')
$types.= 's';
elseif($t == 'integer' || $t == 'boolean')
$types.= 'i';
elseif($t == 'double' || $t == 'float')
$types.= 'd';
else
throw new DBException('Its not possible to automatically assign a value of type '.$t.'. Please specify the corresponding types manually.');
$args[] = $val;
}
}
else
{
foreach($vals as &$val)
$args[] = $val;
}
array_unshift($args, $types);
$RC = new \ReflectionClass($stmt);
$RM = $RC->getMethod('bind_param');
if(!$RM->invokeArgs($stmt, $args))
throw new DBException('Failed to bind params.'.PHP_EOL.self::$DB->error.' ('.self::$DB->sqlstate.').');
if(!$stmt->execute())
throw new DBException('Failed to execute Statement.'.PHP_EOL.self::$DB->error.' ('.self::$DB->sqlstate.').');
if($type == 'select')
{
// [...]
}
else
{
$return = $type == 'insert' && self::$DB->insert_id > 0 ? self::$DB->insert_id : self::$DB->affected_rows;
$stmt->close();
return $return;
}
}
else
{
// [...]
}
}
}
?>
AND:
echo DB::insert("INSERT INTO `table` (`name`) VALUES (?)", ["test"]);
The Query-Log shows 2 inserts. This causes the additional increment. But, when i put an echo into the corresponding DB::dml()-method, its only outputted once, thus called once. The mysql-query-log:
151026 12:54:49 3 Connect XXX#localhost on XXX
3 Query SET NAMES utf8
3 Query SELECT DATABASE() AS `schema`
3 Query SHOW GRANTS FOR CURRENT_USER
3 Prepare INSERT INTO `table` (`name`) VALUES (?)
3 Execute INSERT INTO `table` (`name`) VALUES ('testinsert22')
3 Close stmt
3 Quit
4 Connect XXX#localhost on XXX
4 Query SET NAMES utf8
4 Query SELECT DATABASE() AS `schema`
4 Query SHOW GRANTS FOR CURRENT_USER
4 Prepare INSERT INTO `table` (`name`) VALUES (?)
4 Execute INSERT INTO `table` (`name`) VALUES ('testinsert22')
4 Close stmt
4 Quit
Ok. I finally got it.
The problem is: I redirect all requests to my index.php and do all the rest with PHP. I tested some classes behaviour with simple outputs. One of the tests lead to the problem above.
I checked the apache access log and noticed repeated favicon.ico requests. These requests - automatically produced by major browsers (see also How to prevent favicon.ico requests?) - also got to the index.php - and looks like some redirect to the browser?
normally this is all handled in my application framework, but i disabled it while i tested.

workaround for mysqlnd missing driver

My hosting provider has PHP 5.3 without mysqlnd installed, and thus it returns an error when I call mysqli_stmt_get_result()
is there any workaround to achieve the same functionality without having to install mysqlnd?
I don't want to change 100's of functions, if there's an easier method, any help will be appreciated :)
I went through a lot of trouble on a server where mysqlnd wasn't available, and had a lot of headaches.
If you don't have mysqlnd installed/loaded whatever, you will get an undefined reference when trying to call mysqli_stmt_get_result().
I wrote my own mysqli_stmt_get_result() and a mysqli_result_fetch_array() to go with it.
<?php
class iimysqli_result
{
public $stmt, $nCols;
}
function iimysqli_stmt_get_result($stmt)
{
/** EXPLANATION:
* We are creating a fake "result" structure to enable us to have
* source-level equivalent syntax to a query executed via
* mysqli_query().
*
* $stmt = mysqli_prepare($conn, "");
* mysqli_bind_param($stmt, "types", ...);
*
* $param1 = 0;
* $param2 = 'foo';
* $param3 = 'bar';
* mysqli_execute($stmt);
* $result _mysqli_stmt_get_result($stmt);
* [ $arr = _mysqli_result_fetch_array($result);
* || $assoc = _mysqli_result_fetch_assoc($result); ]
* mysqli_stmt_close($stmt);
* mysqli_close($conn);
*
* At the source level, there is no difference between this and mysqlnd.
**/
$metadata = mysqli_stmt_result_metadata($stmt);
$ret = new iimysqli_result;
if (!$ret) return NULL;
$ret->nCols = mysqli_num_fields($metadata);
$ret->stmt = $stmt;
mysqli_free_result($metadata);
return $ret;
}
function iimysqli_result_fetch_array(&$result)
{
$ret = array();
$code = "return mysqli_stmt_bind_result(\$result->stmt ";
for ($i=0; $i<$result->nCols; $i++)
{
$ret[$i] = NULL;
$code .= ", \$ret['" .$i ."']";
};
$code .= ");";
if (!eval($code)) { return NULL; };
// This should advance the "$stmt" cursor.
if (!mysqli_stmt_fetch($result->stmt)) { return NULL; };
// Return the array we built.
return $ret;
}
?>
This related question has another workaround to get data without installing mysqlnd. It puts the data into an associative array.
Mysqli - Bind results to an Array
The solution from Daan is very nice! For this, i have to say thank you!
The point is, that i had, based on this, developed a class which works with mysqlnd, when it's there and Daan's alternative when it's missing.
I have built in fetchAssoc, fetchAllAssoc/Array, num_rows as methods. It's still in development and part of youphptube. This maybe also helps Ehab (whish for fetchAssoc).
https://github.com/DanielnetoDotCom/YouPHPTube/blob/master/objects/mysql_dal.php
Usage:
$sql = "SELECT * FROM users WHERE id = ?";
$res = sqlDAL::readSql($sql,"i",array($users_id));
$fullData = sqlDAL::fetchAllAssoc($res);
// $data = sqlDAL::fetchAssoc($res); $data2 = sqlDAL::fetchAssoc($res); // or like this
// $countedRows = sqlDAL::num_rows($res); // or like this
sqlDAL::close($res);
I would made this as a sub-post to Daan's answer, but my reputation is too small.

Class returns integer instead of object while trying to get column data

Just getting into OOP, and changing from mysql queries to PDO. I am trying to create a class that will return the column names and meta data for a table. This is so I can output copy/paste data for all the tables I use. I have used such a tool, based on the mysql extension, for ages and it spits out variants such as complete SELECT/INSERT/UPDATE queries. Amongst other things I now want to add DECLARE listings for Stored Procedures - so getting meta data like type and length are essential. With about 150 tables across two schemas, such automation is essential.
With uncertainty about the reliability of getColumnMeta I hunted for code and found what looked good in a Sitepoint answer. I have attempted to wrap it in a class and mimic its original context but I am simply getting a number 1 when I try to echo or print_r the response. I have also had 'not an object' error messages while trying solutions.
This is the calling code
$db_host="localhost";
$db_username='root';
$db_pass='';
$db_name='mydatabase';
try{
$db= new PDO('mysql:host='.$db_host.';dbname='.$db_name,$db_username,$db_pass, array(PDO::ATTR_PERSISTENT=>false));
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
}
catch(PDOException $e){echo "Error: ".$e->getMessage()."<br />"; die(); }
include 'ColMetaData.php'; //the file containing the class for getting a column listing for each table
$coldat= new supplyColumnMeta($db);
$tablemet=$coldat->getColumnMeta('groups'); // a manual insertion of a table name for testing
echo $tablemet;
And this is the class that sits in the include file
class supplyColumnMeta{
public function __construct($db){
$this->db=$db;
}
/**
* Automatically get column metadata
*/
public function getColumnMeta($table)
{$this->tableName=$table;
// Clear any previous column/field info
$this->_fields = array();
$this->_fieldMeta = array();
$this->_primaryKey = NULL;
// Automatically retrieve column information if column info not specified
if(count($this->_fields) == 0 || count($this->_fieldMeta) == 0)
{
// Fetch all columns and store in $this->fields
$columns = $this->db->query("SHOW COLUMNS FROM " . $this->tableName, PDO::FETCH_ASSOC);
foreach($columns as $key => $col)
{
// Insert into fields array
$colname = $col['Field'];
$this->_fields[$colname] = $col;
if($col['Key'] == "PRI" && empty($this->_primaryKey)) {
$this->_primaryKey = $colname;
}
// Set field types
$colType = $this->parseColumnType($col['Type']);
$this->_fieldMeta[$colname] = $colType;
}
}
return true;
}
protected function parseColumnType($colType)
{
$colInfo = array();
$colParts = explode(" ", $colType);
if($fparen = strpos($colParts[0], "("))
{
$colInfo['type'] = substr($colParts[0], 0, $fparen);
$colInfo['pdoType'] = '';
$colInfo['length'] = str_replace(")", "", substr($colParts[0], $fparen+1));
$colInfo['attributes'] = isset($colParts[1]) ? $colParts[1] : NULL;
}
else
{
$colInfo['type'] = $colParts[0];
}
// PDO Bind types
$pdoType = '';
foreach($this->_pdoBindTypes as $pKey => $pType)
{
if(strpos(' '.strtolower($colInfo['type']).' ', $pKey)) {
$colInfo['pdoType'] = $pType;
break;
} else {
$colInfo['pdoType'] = PDO::PARAM_STR;
}
}
return $colInfo;
}
/**
* Will attempt to bind columns with datatypes based on parts of the column type name
* Any part of the name below will be picked up and converted unless otherwise sepcified
* Example: 'VARCHAR' columns have 'CHAR' in them, so 'char' => PDO::PARAM_STR will convert
* all columns of that type to be bound as PDO::PARAM_STR
* If there is no specification for a column type, column will be bound as PDO::PARAM_STR
*/
protected $_pdoBindTypes = array(
'char' => PDO::PARAM_STR,
'int' => PDO::PARAM_INT,
'bool' => PDO::PARAM_BOOL,
'date' => PDO::PARAM_STR,
'time' => PDO::PARAM_INT,
'text' => PDO::PARAM_STR,
'blob' => PDO::PARAM_LOB,
'binary' => PDO::PARAM_LOB
);
}
Here's the problem:
public function getColumnMeta($table)
{
$this->tableName=$table;
// Clear any previous column/field info
$this->_fields = array();
$this->_fieldMeta = array();
$this->_primaryKey = NULL;
// Automatically retrieve column information if column info not specified
if(count($this->_fields) == 0 || count($this->_fieldMeta) == 0)
{
// Fetch all columns and store in $this->fields
$columns = $this->db->query("SHOW COLUMNS FROM " . $this->tableName, PDO::FETCH_ASSOC);
foreach($columns as $key => $col)
{
// Insert into fields array
$colname = $col['Field'];
$this->_fields[$colname] = $col;
if($col['Key'] == "PRI" && empty($this->_primaryKey)) {
$this->_primaryKey = $colname;
}
// Set field types
$colType = $this->parseColumnType($col['Type']);
$this->_fieldMeta[$colname] = $colType;
}
}
return true;//<<--- not returning an object/array!
}
Your getColumnMeta method returns a boolean, true. The string representation of this value is, of course, 1. If you want this method to return all of the meta-data, change the return statement to something like:
return array(
'fields' => $this->_fields,
'meta' => $this->_fieldMeta,
'primary' => $this->_primaryKey
);
There are some other issues with your code, too, but seeing as this is not codereview.stackexchange, I'm not going to go into too much details. I will say this, though: Please, try to follow the coding standards that most major players adhere to: these standards can be found here: PHP-FIG.
Oh, and if you want to show the meta-data, don't echo them, but rather var_dump or print_r them, seeing as you return an array or an object.
Or at least echo json_encode($instance->getColumnMeta($table)); to get a correct string representation of the returned value(s).

ZF2 custom db query using SUM

Can someone give me a hint what I am doing wrong?
public function getPaymentSumByTypeAndProject($project_id,$type) {
$type = (int) $type;
$project_id = (int) $project_id;
$rowset = $this->tableGateway->select(array('total_amount' => new Expression('SUM(payment.amount)')))->where(array('type' => $type, 'project_id' => $project_id));
$row = $rowset->toArray();
if (!$row) {
throw new \Exception("Busted :/");
}
return $rowset;
}
I want to make the same query:
SELECT SUM(amount) FROM payment WHERE type='$type' AND project_id ='$project_id';
Edit:
I made small progress, i have figured out how to sum whole column
public function getPaymentSumByTypeAndProject($project_id, $type) {
$type = (int) $type;
$project_id = (int) $project_id;
$resultSet = $this->tableGateway->select(function (Select $select) {
$select->columns(array(new \Zend\Db\Sql\Expression('SUM(amount) as amount')))->where('type="0"');
});
return $resultSet;
Maybe someone could help me to figure out how to add condition: "WHERE type='$type' AND project_id='$project_id'" ?
I know this is an old question, but I came across it and figured I'd throw in my two cents:
public function getPaymentSumByTypeAndProject($project_id, $type) {
// This TableGateway is already setup for the table 'payment'
// So we can skip the ->from('payment')
$sql = $this->tableGateway->getSql();
// We'll follow the regular order of SQL ( SELECT, FROM, WHERE )
// So the query is easier to understand
$select = $sql->select()
// Use an alias as key in the columns array instead of
// in the expression itself
->columns(array('amount' => new \Zend\Db\Sql\Expression('SUM(amount)')))
// Type casting the variables as integer can take place
// here ( it even tells us a little about the table structure )
->where(array('type' => (int)$type, 'project_id' => (int)$project_id));
// Use selectWith as a shortcut to get a resultSet for the above select
return $this->tableGateway->selectWith($select);
}
Also, an adapter can be retrieved from a table gateway like this:
$adapter = $this->tableGateway->getAdapter();
But you don't really need it anymore when you select using the above mentioned method.
Ok now this is working, tell me is this how it;s should be done?
public function getPaymentSumByTypeAndProject($project_id, $type) {
$type = (int) $type;
$project_id = (int) $project_id;
$adapter = $this->tableGateway->adapter;
$sql = new Sql($adapter);
$select = $sql->select();
$select->from('payment');
$select->where(array('type'=>$type,'project_id'=>$project_id));
$select->columns(array(new \Zend\Db\Sql\Expression('SUM(amount) as amount')));
$selectString = $sql->getSqlStringForSqlObject($select);
$resultSet = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);
return $resultSet;
}
use This one
$select = $this->getSql()->select()
->columns(array('amount' => new \Zend\Db\Sql\Expression('SUM(amount)')))
->where("type ='$type'")
->where("project_id ='$project_id'");
$resultSet = $this->selectWith($select);
$row = $resultSet->current();
// echo $select->getSqlString();die; //check query use this line
if(!$row){
return False;
}
return $row->amount;
try to make like that instead of (int) $type
intval($type);
and
intval($project_id);
and in your sql
change your variables to
'".$type."'
AND
'".$project_id."'

Categories