How to use both fetch() and fetchAll() in a PDO wrapper class? - php

How can I alter my PDO wrapper class, so that if I expect a single row result with my query it uses fetch() and if it expects multiple results it uses fetchAll().
Right now, if I have only one result I still have to loop through the result array and that seem pretty unpracticable to me.
Query in the model:
public function doccEdit() {
$id = mysql_real_escape_string($_GET['id']);
$this->result = $GLOBALS['db']->select("creditcards", "id = ?", $id);
print_r($this->result);
}
In the wrapper class:
public function run($sql, $bind="") {
$this->sql = trim($sql);
$this->bind = $this->cleanup($bind);
$this->error = "";
try {
$pdostmt = $this->prepare($this->sql);
if($pdostmt->execute($this->bind) !== false) {
if(preg_match("/^(" . implode("|", array("select", "describe", "pragma")) . ") /i", $this->sql))
return $pdostmt->fetchall(PDO::FETCH_OBJ);
elseif(preg_match("/^(" . implode("|", array("delete", "insert", "update")) . ") /i", $this->sql))
return $pdostmt->rowCount();
}
} catch (PDOException $e) {
$this->error = $e->getMessage();
$this->debug();
return false;
}
}

DON'T TRY to automate everything
The less magic in your code, the easier support and less painful troubles.
Don't try to stuff all the logic into one single method. It's a class! You can create as many methods as you need.
When you need rowCount() - select it explicitly! It's not that hard.
But when you stumble upon this code after couple months, you will know what does this value mean.
When you need single row - use a method to get a single row.
When you need many rows - use a method to get many rows.
It is simple and extremely unambiguous!
When you turn back to your code after 2 months, you will have absolutely no idea, what did you expected. So - always write it explicitly.
Here is an excerpt from my mysqli wrapper class to give you an idea:
public function query()
{
return $this->rawQuery($this->prepareQuery(func_get_args()));
}
/**
* Helper function to get scalar value right out of query and optional arguments
*
* Examples:
* $name = $db->getOne("SELECT name FROM table WHERE id=1");
* $name = $db->getOne("SELECT name FROM table WHERE id=?i", $id);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return string|FALSE either first column of the first row of resultset or FALSE if none found
*/
public function getOne()
{
$query = $this->prepareQuery(func_get_args());
if ($res = $this->rawQuery($query))
{
$row = $this->fetch($res);
if (is_array($row)) {
return reset($row);
}
$this->free($res);
}
return FALSE;
}
/**
* Helper function to get single row right out of query and optional arguments
*
* Examples:
* $data = $db->getRow("SELECT * FROM table WHERE id=1");
* $data = $db->getOne("SELECT * FROM table WHERE id=?i", $id);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return array|FALSE either associative array contains first row of resultset or FALSE if none found
*/
public function getRow()
{
$query = $this->prepareQuery(func_get_args());
if ($res = $this->rawQuery($query)) {
$ret = $this->fetch($res);
$this->free($res);
return $ret;
}
return FALSE;
}
/**
* Helper function to get single column right out of query and optional arguments
*
* Examples:
* $ids = $db->getCol("SELECT id FROM table WHERE cat=1");
* $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return array|FALSE either enumerated array of first fields of all rows of resultset or FALSE if none found
*/
public function getCol()
{
$ret = array();
$query = $this->prepareQuery(func_get_args());
if ( $res = $this->rawQuery($query) )
{
while($row = $this->fetch($res))
{
$ret[] = reset($row);
}
$this->free($res);
}
return $ret;
}
/**
* Helper function to get all the rows of resultset right out of query and optional arguments
*
* Examples:
* $data = $db->getAll("SELECT * FROM table");
* $data = $db->getAll("SELECT * FROM table LIMIT ?i,?i", $start, $rows);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return array enumerated 2d array contains the resultset. Empty if no rows found.
*/
public function getAll()
{
$ret = array();
$query = $this->prepareQuery(func_get_args());
if ( $res = $this->rawQuery($query) )
{
while($row = $this->fetch($res))
{
$ret[] = $row;
}
$this->free($res);
}
return $ret;
}
Look - from the function name you can always tell which result to expect:
$name = $db->getOne('SELECT name FROM table WHERE id = ?i',$_GET['id']);
$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);
Don't be fooled by such a pitfall like number of returned rows.
There could be honest one row in the resultset which you intend to populate with fetchAll. So, it will return single-dimensional array instead of multi-dimensional and you will have plenty of video effects on your page

Since you didn't mark an answer as accepted. I thought I'd answer you question. I found this while looking for the answer myself. I agree with "Your Common Sense" in that they should be two separate functions. However, in direct answer to your question, this is what I had (PDO example rather than mysqli):
function select($sql,$params=NULL,$fetchType=NULL){
try{
$qry = $this->db->prepare($sql);
$qry->execute($params);
if($qry->rowCount() > 1){
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetchAll(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetchAll(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetchAll(PDO::FETCH_ASSOC);
}
}
else{
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetch(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetch(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetch(PDO::FETCH_ASSOC);
}
}
if($results){
return $results;
}else{
return NULL;
}
}
catch(PDOException $err){
$this->logError($err);
}
}
However I found that if I queried all rows in a table but there was only one row in the table it would return a 1-d array instead of a 2-d array. My code to handle the result wouldn't work for both types of arrays. I could handle that each time but I found it just easier to, as stated above, separate them into different functions so if I knew there would be only one answer I could call the appropriate function. This is what I have now:
function select($sql,$params=NULL,$fetchType=NULL){
try{
$qry = $this->db->prepare($sql);
$qry->execute($params);
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetch(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetch(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetch(PDO::FETCH_ASSOC);
}
if($results){
return $results;
}else{
return NULL;
}
}
catch(PDOException $err){
$this->logError($err);
}
}
function selectAll($sql,$params=NULL,$fetchType=NULL){
try{
$qry = $this->db->prepare($sql);
$qry->execute($params);
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetchAll(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetchAll(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetchAll(PDO::FETCH_ASSOC);
}
if($results){
return $results;
}else{
return NULL;
}
}
catch(PDOException $err){
$this->logError($err);
}
}

Related

CakePHP3: How to change an association strategy on-the-fly?

I would like to change an association strategy (hasMany) on the fly to "in" (default) to "select". Because this will correct the result for this situation:
"Get all publishers and only the first five books":
$publishersTable = TableRegistry::getTableLocator()->get('Publishers');
$publishersTable->getAssociation('Books')->setStrategy('select');
$query = $publishersTable->find()
->contain(['Books'=> function(Query $q){
return $q->limit(5);
}]);
Unfortunately, Cake still using "in" to run the query and not "separated queries" and the result is only 5 publishers (and not all publishers with the first 5 books).
Is it possible to change the strategy on-the-fly?
Thanks in advance !
A hasMany association will always use a single separate query, never multiple separate queries. The difference between the select and subquery strategies is that one will directly compare against an array of primary keys, and the other against a joined subquery that will match the selected parent records.
What you are trying is to select the greatest-n-per-group, that's not possible with the built in association loaders, and it can be a little tricky depending on the DBMS that you are using, check for example How to limit contained associations per record/group? for an example for MySQL < 8.x using a custom association and loader.
For DBMS that do support it, look into window functions. Here's an example of a loader that uses native window functions, it should be possible to simply replace the one in the linked example with it, but keep in mind that it's not really tested or anything, I just had it laying around from some experiments:
namespace App\ORM\Association\Loader;
use Cake\Database\Expression\OrderByExpression;
use Cake\ORM\Association\Loader\SelectLoader;
class GroupLimitedSelectLoader extends SelectLoader
{
/**
* The group limit.
*
* #var int
*/
protected $limit;
/**
* The target table.
*
* #var \Cake\ORM\Table
*/
protected $target;
/**
* {#inheritdoc}
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->limit = $options['limit'];
$this->target = $options['target'];
}
/**
* {#inheritdoc}
*/
protected function _defaultOptions()
{
return parent::_defaultOptions() + [
'limit' => $this->limit,
];
}
/**
* {#inheritdoc}
*/
protected function _buildQuery($options)
{
$key = $this->_linkField($options);
$keys = (array)$key;
$filter = $options['keys'];
$finder = $this->finder;
if (!isset($options['fields'])) {
$options['fields'] = [];
}
/* #var \Cake\ORM\Query $query */
$query = $finder();
if (isset($options['finder'])) {
list($finderName, $opts) = $this->_extractFinder($options['finder']);
$query = $query->find($finderName, $opts);
}
$rowNumberParts = ['ROW_NUMBER() OVER (PARTITION BY'];
for ($i = 0; $i < count($keys); $i ++) {
$rowNumberParts[] = $query->identifier($keys[$i]);
if ($i < count($keys) - 1) {
$rowNumberParts[] = ',';
}
}
$rowNumberParts[] = new OrderByExpression($options['sort']);
$rowNumberParts[] = ')';
$rowNumberField = $query
->newExpr()
->add($rowNumberParts)
->setConjunction('');
$rowNumberSubQuery = $this->target
->query()
->select(['__row_number' => $rowNumberField])
->where($options['conditions']);
$columns = $this->target->getSchema()->columns();
$rowNumberSubQuery->select(array_combine($columns, $columns));
$rowNumberSubQuery = $this->_addFilteringCondition($rowNumberSubQuery, $key, $filter);
$fetchQuery = $query
->select($options['fields'])
->from([$this->targetAlias => $rowNumberSubQuery])
->where([$this->targetAlias . '.__row_number <=' => $options['limit']])
->eagerLoaded(true)
->enableHydration($options['query']->isHydrationEnabled());
if (!empty($options['contain'])) {
$fetchQuery->contain($options['contain']);
}
if (!empty($options['queryBuilder'])) {
$fetchQuery = $options['queryBuilder']($fetchQuery);
}
$this->_assertFieldsPresent($fetchQuery, $keys);
return $fetchQuery;
}
}
Thanks #ndm but I found another shorter solution:
$publishersTable->find()
->formatResults(function ($results) use ($publishersTable) {
return $results->map(function ($row) use ($publishersTable) {
$row['books'] = $publishersTable->Books->find()
->where(['publisher_id'=>$row['id']])
->limit(5)
->toArray();
return $row;
});
});

How make a Dynamic bindValue()?

Okay I have a function called sendQuery which sends your query.
I know how to do it with BindParams, but I can't really think of a way to make it work with bind values inside a execute.
This is the code:
public function sendQuery($query, array $value, $do_fetch)
{
$query_process = $this->db->prepare($query);
if(!$query_process->execute($binds))
{
throw new excpetion ("An error has occured!");
}
$this->insert = $this->db->lastInsertId();
$this->row_count = $query_process->rowCount();
if($fetch == true)
{
return $query_process->fetchAll();
}
}
As you see, it executes with $binds,
Works like (WHERE user = ?), but I want to send queries like this:
(WHERE user = :user) instead of a ' ? ', and multiple of them.
How do I do so?
You have to do exactly the same.
Just get rid of useless code and use consistent variable naming
public function sendQuery($query, array $binds, $do_fetch)
{
$stm = $this->db->prepare($query);
$stm->execute($binds);
$this->insert = $this->db->lastInsertId();
$this->row_count = $stm->rowCount();
if($do_fetch)
{
return $stm->fetchAll();
}
}
$sql = "SELECT * FROM t WHERE c1=:name AND c2=:age";
$param = array ("name" => $name,"age" => $age);
$data = $db->sendQuery($sql, $data, 1);
However, instead of just single function I would create a set of them:
query() to run non-select queries
getOne() preforms select and returns scalar value
getRow() returns a row
getAll returns all rows
it could be extremely handy

How to represent graph datastructure using PHP array

Recently, I was given a small task of using graph data structure as core to make a web application. I started out with an idea of simple path optimization problem, which can be completed in few days. The problem is that I am not able decide the correct framework for this task. Using just PHP was the only thing i could think of given the time constraint.
So, how can I represent a graph data structure using PHP's custom data structure( array).
Furthermore,can you suggest some other frameworks on which i can work for this task.
You can use the array to keep an adjacency list.
PHP's array has two uses: it can be a list of objects, or an associative array, which associates one object with another. You can also use an associative array as a poor man's set, by keeping the using the data from the set as keys in the associative array. Since it is usual to associate data to vertices, and edges, we can actually make use of this in a natural way. Following is an example of an undirected graph class.
<?php
/**
* Undirected graph implementation.
*/
class Graph
{
/**
* Adds an undirected edge between $u and $v in the graph.
*
* $u,$v can be anything.
*
* Edge (u,v) and (v,u) are the same.
*
* $data is the data to be associated with this edge.
* If the edge (u,v) already exists, nothing will happen (the
* new data will not be assigned).
*/
public function add_edge($u,$v,$data=null)
{
assert($this->sanity_check());
assert($u != $v);
if ($this->has_edge($u,$v))
return;
//If u or v don't exist, create them.
if (!$this->has_vertex($u))
$this->add_vertex($u);
if (!$this->has_vertex($v))
$this->add_vertex($v);
//Some sanity.
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list));
//Associate (u,v) with data.
$this->adjacency_list[$u][$v] = $data;
//Associate (v,u) with data.
$this->adjacency_list[$v][$u] = $data;
//We just added two edges
$this->edge_count += 2;
assert($this->has_edge($u,$v));
assert($this->sanity_check());
}
public function has_edge($u,$v)
{
assert($this->sanity_check());
//If u or v do not exist, they surely do not make up an edge.
if (!$this->has_vertex($u))
return false;
if (!$this->has_vertex($v))
return false;
//some extra sanity.
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list));
//This is the return value; if v is a neighbor of u, then its true.
$result = array_key_exists($v,$this->adjacency_list[$u]);
//Make sure that iff v is a neighbor of u, then u is a neighbor of v
assert($result == array_key_exists($u,$this->adjacency_list[$v]));
return $result;
}
/**
* Remove (u,v) and return data.
*/
public function remove_edge($u,$v)
{
assert($this->sanity_check());
if (!$this->has_edge($u,$v))
return null;
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list[$u]));
assert(array_key_exists($u,$this->adjacency_list[$v]));
//remember data.
$data = $this->adjacency_list[$u][$v];
unset($this->adjacency_list[$u][$v]);
unset($this->adjacency_list[$v][$u]);
//We just removed two edges.
$this->edge_count -= 2;
assert($this->sanity_check());
return $data;
}
//Return data associated with (u,v)
public function get_edge_data($u,$v)
{
assert($this->sanity_check());
//If no such edge, no data.
if (!$this->has_edge($u,$v))
return null;
//some sanity.
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list[$u]));
return $this->adjacency_list[$u][$v];
}
/**
* Add a vertex. Vertex must not exist, assertion failure otherwise.
*/
public function add_vertex($u,$data=null)
{
assert(!$this->has_vertex($u));
//Associate data.
$this->vertex_data[$u] = $data;
//Create empty neighbor array.
$this->adjacency_list[$u] = array();
assert($this->has_vertex($u));
assert($this->sanity_check());
}
public function has_vertex($u)
{
assert($this->sanity_check());
assert(array_key_exists($u,$this->vertex_data) == array_key_exists($u,$this->adjacency_list));
return array_key_exists($u,$this->vertex_data);
}
//Returns data associated with vertex, null if vertex does not exist.
public function get_vertex_data($u)
{
assert($this->sanity_check());
if (!array_key_exists($u,$this->vertex_data))
return null;
return $this->vertex_data[$u];
}
//Count the neighbors of a vertex.
public function count_vertex_edges($u)
{
assert($this->sanity_check());
if (!$this->has_vertex($u))
return 0;
//some sanity.
assert (array_key_exists($u,$this->adjacency_list));
return count($this->adjacency_list[$u]);
}
/**
* Return an array of neighbor vertices of u.
* If $with_data == true, then it will return an associative array, like so:
* {neighbor => data}.
*/
public function get_edge_vertices($u,$with_data=false)
{
assert($this->sanity_check());
if (!array_key_exists($u,$this->adjacency_list))
return array();
$result = array();
if ($with_data) {
foreach( $this->adjacency_list[$u] as $v=>$data)
{
$result[$v] = $data;
}
} else {
foreach( $this->adjacency_list[$u] as $v=>$data)
{
array_push($result, $v);
}
}
return $result;
}
//Removes a vertex if it exists, and returns its data, null otherwise.
public function remove_vertex($u)
{
assert($this->sanity_check());
//If the vertex does not exist,
if (!$this->has_vertex($u)){
//Sanity.
assert(!array_key_exists($u,$this->vertex_data));
assert(!array_key_exists($u,$this->adjacency_list));
return null;
}
//We need to remove all edges that this vertex belongs to.
foreach ($this->get_edge_vertices($u) as $v)
{
$this->remove_edge($u,$v);
}
//After removing all such edges, u should have no neighbors.
assert($this->count_vertex_edges($u) == 0);
//sanity.
assert(array_key_exists($u,$this->vertex_data));
assert(array_key_exists($u,$this->adjacency_list));
//remember the data.
$data = $this->vertex_data[$u];
//remove the vertex from the data array.
unset($this->vertex_data[$u]);
//remove the vertex from the adjacency list.
unset($this->adjacency_list[$u]);
assert($this->sanity_check());
return $data;
}
public function get_vertex_count()
{
assert($this->sanity_check());
return count($this->vertex_data);
}
public function get_edge_count()
{
assert($this->sanity_check());
//edge_count counts both (u,v) and (v,u)
return $this->edge_count/2;
}
public function get_vertex_list($with_data=false)
{
$result = array();
if ($with_data)
foreach ($this->vertex_data as $u=>$data)
$result[$u]=$data;
else
foreach ($this->vertex_data as $u=>$data)
array_push($result,$u);
return $result;
}
public function edge_list_str_array($ordered=true)
{
$result_strings = array();
foreach($this->vertex_data as $u=>$udata)
{
foreach($this->adjacency_list[$u] as $v=>$uv_data)
{
if (!$ordered || ($u < $v))
array_push($result_strings, '('.$u.','.$v.')');
}
}
return $result_strings;
}
public function sanity_check()
{
if (count($this->vertex_data) != count($this->adjacency_list))
return false;
$edge_count = 0;
foreach ($this->vertex_data as $v=>$data)
{
if (!array_key_exists($v,$this->adjacency_list))
return false;
$edge_count += count($this->adjacency_list[$v]);
}
if ($edge_count != $this->edge_count)
return false;
if (($this->edge_count % 2) != 0)
return false;
return true;
}
/**
* This keeps an array that associates vertices with their neighbors like so:
*
* {<vertex> => {<neighbor> => <edge data>}}
*
* Thus, each $adjacency_list[$u] = array( $v1 => $u_v1_edge_data, $v2 => $u_v2_edge_data ...)
*
* The edge data can be null.
*/
private $adjacency_list = array();
/**
* This associates each vertex with its data.
*
* {<vertex> => <data>}
*
* Thus each $vertex_data[$u] = $u_data
*/
private $vertex_data = array();
/**
* This keeps tracks of the edge count so we can retrieve the count in constant time,
* instead of recounting. In truth this counts both (u,v) and (v,u), so the actual count
* is $edge_count/2.
*/
private $edge_count = 0;
}
$G = new Graph();
for ($i=0; $i<5; ++$i)
{
$G->add_vertex($i);
}
for ($i=5; $i<10; ++$i)
{
$G->add_edge($i,$i-5);
}
print 'V: {'.join(', ',$G->get_vertex_list())."}\n";
print 'E: {'.join(', ',$G->edge_list_str_array())."}\n";
$G->remove_vertex(1);
print 'V: {'.join(', ',$G->get_vertex_list())."}\n";
print 'E: {'.join(', ',$G->edge_list_str_array())."}\n";
$G->remove_vertex(1);
print 'V: {'.join(', ',$G->get_vertex_list())."}\n";
print 'E: {'.join(', ',$G->edge_list_str_array())."}\n";
?>

returning an array in Mysqli prepared statement

I have function in my database class that returns the id of the people that are from the a specific country such as spain. But for some reason I only get one value, but there are many people with the same country. here is the function:
Class DbAb {
private $db;
public function sameCountry($country) {
$query = "SELECT id FROM users WHERE country = ? ";
$stmt = $this->db->prepare($query);
$stmt->bind_param("s", $country);
if ($stmt->execute()) {
$stmt->bind_result($sameCountry);
$stmt->fetch();
return $sameCountry;
}
return false;
}
}
$sameC = new DbAb();
$samePeople = $sameC->samecountry("spain");
print_r($samePeople);
Does anyone know how to return an array of results? I have tried to define the variable as an array but still doesn't work...
The bind_result($var) + fetch() inserts a single row into the $var variable.
If you want to return an array of ids from your method, you need to first create an empty array, then for each row, insert into it.
eg. replace this:
$stmt->bind_result($sameCountry);
$stmt->fetch();
return $sameCountry;
with this:
$arr = array();
$stmt->bind_result($id);
while ( $stmt->fetch() ) {
$arr[] = $id;
}
return $arr;

php pdo prepare repetitive variables

While writing a pdo statement, is it possible to repeat the value of a variable? I mean:
$query = "UPDATE users SET firstname = :name WHERE firstname = :name";
$stmt = $dbh -> prepare($query);
$stmt -> execute(array(":name" => "Jackie"));
Please note that I repeat the ":name" nameholder whereas I provide the value only once. How can I make this work?
The simple answer is: You can't. PDO uses an abstraction for prepared statements which has some limitations. Unfortunately this is one, you have to work-around using something like
$query = "UPDATE users SET firstname = :name1 WHERE firstname = :name2";
$stmt = $dbh -> prepare($query);
$stmt -> execute(array(":name1" => "Jackie", ":name2" => "Jackie"));
In certain cases, such as emulated prepared statements with some versions of the PDO/MySQL driver, repeated named parameters are supported; however, this shouldn't be relied upon, as it's brittle (it can make upgrades require more work, for example).
If you want to support multiple appearances of a named parameter, you can always extend PDO and PDOStatement (by classical inheritance or by composition), or just PDOStatement and set your class as the statement class by setting the PDO::ATTR_STATEMENT_CLASS attribute. The extended PDOStatement (or PDO::prepare) could extract the named parameters, look for repeats and automatically generate replacements. It would also record these duplicates. The bind and execute methods, when passed a named parameter, would test whether the parameter is repeated and bind the value to each replacement parameter.
Note: the following example is untested and likely has bugs (some related to statement parsing are noted in code comments).
class PDO_multiNamed extends PDO {
function prepare($stmt) {
$params = array_count_values($this->_extractNamedParams());
# get just named parameters that are repeated
$repeated = array_filter($params, function ($count) { return $count > 1; });
# start suffixes at 0
$suffixes = array_map(function ($x) {return 0;}, $repeated);
/* Replace repeated named parameters. Doesn't properly parse statement,
* so may replacement portions of the string that it shouldn't. Proper
* implementation left as an exercise for the reader.
*
* $param only contains identifier characters, so no need to escape it
*/
$stmt = preg_replace_callback(
'/(?:' . implode('|', array_keys($repeated)) . ')(?=\W)/',
function ($matches) use (&$suffixes) {
return $matches[0] . '_' . $suffixes[$matches[0]]++;
}, $stmt);
$this->prepare($stmt,
array(
PDO::ATTR_STATEMENT_CLASS => array('PDOStatement_multiNamed', array($repeated)))
);
}
protected function _extractNamedParams() {
/* Not actually sufficient to parse named parameters, but it's a start.
* Proper implementation left as an exercise.
*/
preg_match_all('/:\w+/', $stmt, $params);
return $params[0];
}
}
class PDOStatement_multiNamed extends PDOStatement {
protected $_namedRepeats;
function __construct($repeated) {
# PDOStatement::__construct doesn't like to be called.
//parent::__construct();
$this->_namedRepeats = $repeated;
}
/* 0 may not be an appropriate default for $length, but an examination of
* ext/pdo/pdo_stmt.c suggests it should work. Alternatively, leave off the
* last two arguments and rely on PHP's implicit variadic function feature.
*/
function bindParam($param, &$var, $data_type=PDO::PARAM_STR, $length=0, $driver_options=array()) {
return $this->_bind(__FUNCTION__, $param, func_get_args());
}
function bindValue($param, $var, $data_type=PDO::PARAM_STR) {
return $this->_bind(__FUNCTION__, $param, func_get_args());
}
function execute($input_parameters=NULL) {
if ($input_parameters) {
$params = array();
# could be replaced by array_map_concat, if it existed
foreach ($input_parameters as $name => $val) {
if (isset($this->_namedRepeats[$param])) {
for ($i=0; $i < $this->_namedRepeats[$param], ++$i) {
$params["{$name}_{$i}"] = $val;
}
} else {
$params[$name] = $val;
}
}
return parent::execute($params);
} else {
return parent::execute();
}
}
protected function _bind($method, $param, $args) {
if (isset($this->_namedRepeats[$param])) {
$result = TRUE;
for ($i=0; $i < $this->_namedRepeats[$param], ++$i) {
$args[0] = "{$param}_{$i}";
# should this return early if the call fails?
$result &= call_user_func_array("parent::$method", $args);
}
return $result;
} else {
return call_user_func_array("parent::$method", $args);
}
}
}
In my case this error appeared when I switched from dblib freedts to sqlsrv PDO driver. Dblib driver handled duplicate parameters names with no errors. I have quite complicated dynamic queries with lots of unions and a lot of duplicated params so I used following helper as a workaround:
function prepareMsSqlQueryParams($query, $params): array
{
$paramsCount = [];
$newParams = [];
$pattern = '/(:' . implode('|:', array_keys($params)) . ')/';
$query = preg_replace_callback($pattern, function ($matches) use ($params, &$newParams, &$paramsCount) {
$key = ltrim($matches[0], ':');
if (isset($paramsCount[$key])) {
$paramsCount[$key]++;
$newParams[$key . $paramsCount[$key]] = $params[$key];
return $matches[0] . $paramsCount[$key];
} else {
$newParams[$key] = $params[$key];
$paramsCount[$key] = 0;
return $matches[0];
}
}, $query);
return [$query, $newParams];
}
Then you can use it this way:
$query = "UPDATE users SET firstname = :name WHERE firstname = :name";
$params = [":name" => "Jackie"];
// It will return "UPDATE users SET firstname = :name WHERE firstname = :name1"; with appropriate parameters array
list($query, $params) = prepareMsSqlQueryParams($query, $params);
$stmt = $dbh->prepare($query);
$stmt->execute(params);

Categories