I have the following function in my PHP code:
function getFromTable(){ //$sql,$index,$index2,...,$pdo
$args = func_get_args(); // because we don't know how many $index arguments there will be
$sql = $args[0];
$indexes = [];
for ($i=1; $i <count($args)-1; $i++) {
array_push($indexes,$args[$i]);
}
$pdo = $args[count($args)-1]; //penultimate index
$ret = [];
$result = $pdo->prepare($sql);
$result->execute();
foreach($result as $row){
array_push($ret,[$row[$indexes]);
}
return $ret;
}
The purpose of the function is to return rows from a MySQL query. User can choose which indexes to return. Since I don't know the number of indexes, I use the func_get_args() argument. First argument is always the query, last argument is PDO, and all the arguments in-between are indexes. The problem arises in the foreach statement, where I push the values of the indexes into an array. I want all indexes to be in one array, however I don't know how to do it in PHP. If it was Python, I would use a one liner for loop or lambda. What are the alternatives in PHP?
Firstly let's take advantage of PHP 7.4 features like argument unpacking and short-form lambdas:
function getFromTable($sql, ...$indices) {
$pdo = array_pop($indices); // Pop the last array entry
$flippedIndices = array_flip($indices); // This is important
$result = $pdo->prepare($sql);
$result->execute();
return array_map(fn ($row) => array_intersect_key($row, $flippedIndices), $result);
}
Here's the breakdown:
We write the parameters as $sql, ...$indices so we now say that the first argument is the query and the rest of the arguments are indices. It would actually work nicer if the parameters were $sql, $pdo, ...$indices to save us from needing (2)
We pop $pdo from the rest arguments since it's last argument.
The way to get specific indices from an array is with array_intersect_key but to use this we need to pass an array with the keys we want to intersect. array_flip will achive this.
The array_map should map all rows to arrays which only have the specified keys. We use the PHP 7.4+ arrow functions feature but you can just use function ($row) use ($flippedIndices) { return array_intersect_key($row, $flippedIndices); } for the same result if you want to use this code in earlier PHP versions
Sandbox link
Related
I have recently asked some questions about security against SQL injection vulnerabilities. I decided to make a function that would do a sql query using a prepared statement so I didn't have to write out so many lines of code for every query:
function secure_sql($query, $values, $datatypes){
global $link;
$stmt = mysqli_prepare($link, $query);
foreach($values as &$value){
$value = mysqli_real_escape_string($link, $value);
mysqli_stmt_bind_param($stmt, substr($datatypes, 0), $value);
$datatypes = substr($datatypes, -(strlen($datatypes)-1));
}
mysqli_stmt_execute($stmt);
mysqli_stmt_bind_result($stmt, implode(", ", $values));
$results = mysqli_stmt_get_result($stmt);
return $results;
}
where query is the prepared query (with ?s), $values is an array of all the variables replacing the placeholder ?s (in order) and $datatypes is a string containing all the data types for the variables. $link is a database connection.
So I have two main questions.
1) It is not working. I think this must be because of implode maybe not being used correctly in this context. What would I use instead? I can't use call_user_func_array because I also need to have $stmt as an argument. I have tried using array_unshift to add $stmt to the beginning of the argument, but it doesn't work.
2) If I do get it to work, what could be done to improve it? I am still a PHP and SQL beginner.
EDIT: I have solved the problem now. Thank you all for your helpful comments and answers!
Instead of directly giving you the code I came up with, I feel that explaining it is necessary, seeing how some of your ways of thinking are rather incorrect.
Firstly, the use of mysqli_stmt_bind_param() confuses me - your second argument expression (substr($datatypes, 0)) returns a value of all the datatypes, yet you are only binding one. What I think you meant to put is:
mysqli_stmt_bind_param($stmt, $datatypes[0] /* <-- retrieves the first character */, $value);
But more importantly, you should only call mysqli_stmt_bind_param() once, which gives you some bigger difficulties... To omit the foreach-loop, how about call_user_func_array()? (Actually, it's very possible to keep $stmt as an argument):
call_user_func_array("mysqli_stmt_bind_param", array_merge(array($stmt, $datatypes), $values));
//calls mysqli_stmt_bind_param($stmt, $datatypes, $values[0], $values[1]... values[n]);
If you're confused by the thing above, look at it in this way:
call_user_func_array //a call to mysqli_stmt_bind_param, with all the appropriate parameters
(
"mysqli_stmt_bind_param", //the function to call
array_merge //an array consisting of the statement, the datatype and all the values
(
array($stmt, $datatypes), //statement and datatype-parameters
$values //all the values
)
);
This does, however, require your $values-array to consist of references, as the mysqli_stmt_bind_param expects your values to be so. If you still want to pass them as values into your function, you could add this, and later pass $ref_values into the call_user_func_array() function:
foreach ($values as &$value) $ref_values[] = &$value;
Now we come to the use of implode(), which also is incorrect. To reference from the PHP-manual:
Implode (Return Value):
Returns a string containing a string representation of all the array elements in the same order, with the glue string between each element.
mysqli_stmt_bind_result (Second-Nth Parameter):
The variable to be bound.
So what you're attempting to do here is to make a string returned by implode() a variable, which makes no sense. Although luckily, mysqli_stmt_get_result() returns an object which is fetched after execution, meaning that the bind_result-function isn't needed. So try removing that line.
In all, I would re-write the code like this:
function secure_sql($query, $values, $datatypes) {
global $link;
$stmt = mysqli_prepare($link, $query);
foreach ($values as &$value) $ref_values[] = &$value;
call_user_func_array("mysqli_stmt_bind_param", array_merge(array($stmt, $datatypes), $ref_values));
mysqli_stmt_execute($stmt);
return mysqli_stmt_get_result($stmt);
}
To me, it sounds like that should work, but if it doesn't, tell me (I might be missing something or thinking incorrectly somewhere).
To answer the second question, it all looks good, except that I wouldn't advice you to use mysqli_-functions, as they will get removed eventually (as said in the PHP-manual). If you're planning to use objects instead, most of it is similar when it comes to my changes (apart from the fact that you need to use object-properties and object-methods with them instead...), except the call_user_func_array() function. Luckily though, calling a method with it is possible as well, by specifying an array as the first parameter, consisting of the object and the method name (call_user_func_array($prepared_obj, "bind_param") ...)).
Edit: Considering how necessary it is, I made a function that does the same thing, but works on an mysqli object instead:
function secure_sql($query, $values, $datatypes) {
global $mysqli_object; //declared with $mysql_object = new mysqli(...)
$stmt = $mysqli_object->prepare($query);
foreach ($values as &$value) $ref_values[] = &$value;
call_user_func_array(array($stmt, "bind_param"), array_merge(array($datatypes), $ref_values));
$stmt->execute();
return $stmt->get_result();
}
I am using MySQLi and PHP to call a stored MySQL routine with prepared statements. It returns a result set with dozens of columns.
$stmt = $dbconnection->prepare("CALL SomebodysDbProcedure(?);");
$stmt->bind_param("s", $idvalue);
$stmt->execute();
$stmt->bind_result($col1, $col2, $col3, ...);
However, I am only interested in a subset of the output columns.
The documentation says bind_result() is required to handle the complete set of returned columns:
Note that all columns must be bound after mysqli_stmt_execute() and
prior to calling mysqli_stmt_fetch().
Do I need to add code also for those columns I'm uninterested in? If so the application will break if the MySQL stored routine result set is expanded in the future, or even columns rearranged. Is there a workaround for this?
I'm assuming that you just don't want to write out all those variables for the bind_result() function. You could use a function like below instead of the bind_result() function. Pass it your $stmt object and you'll get back an array of standard objects with the fields you want.
function getResult($stmt)
{
$valid_fields = array('title', 'date_created'); // enter field names you care about
if (is_a($stmt, 'MySQLi_STMT')) {
$result = array();
$metadata = $stmt->result_metadata();
$fields = $metadata->fetch_fields();
for (; ;)
{
$pointers = array();
$row = new \stdClass();
$pointers[] = $stmt;
foreach ($fields as $field)
{
if (in_array($field->name, $valid_fields)) {
$fieldname = $field->name;
$pointers[] = &$row->$fieldname;
}
}
call_user_func_array('mysqli_stmt_bind_result', $pointers);
if (!$stmt->fetch())
break;
$result[] = $row;
}
$metadata->free();
return $result;
}
return array();
}
The answer of Jonathan Mayhak guided me in the right direction. On PHP bind_result page, nieprzeklinaj provides a function called fetch(). It works; use it like this:
$stmt = $conn->prepare("CALL SomebodysDbProcedure(?);");
$stmt->bind_param("s", $idvalue);
$stmt->execute();
$sw = (array)(fetch($stmt));
$s = $sw[0]; // Get first row
$dateCreated = $s['date_created']; // Get field date_created
Edit: Unfortunately successive calls within the same PHP file don't seem to work with this method.
Try using fetch_fields php method:
array mysqli_fetch_fields ( mysqli_result $result )
http://php.net/manual/en/mysqli-result.fetch-fields.php
Here is my somewhat broken strategy
If it is a query with no input ( always the same ), just use query();
$results = $this->database_top->query( $query );
If a single row is returned and there is input, do the prep (not shown here) and use
$results = $pdoStatement->fetch(PDO::FETCH_ASSOC);
If multiple rows are returned and there is input,do the prep (not shown here) and use:
$results = $pdoStatement->fetchAll();
Problem I'm facing is that I need the first method to return an array or array of arrays like the second and third.
Prep looks like this FYI
$this->database_top->quote($query); // quote it
$pdoStatement = $this->database_top->prepare($query); // prepare it
$results = $pdoStatement->execute($parameterArray); // execute it
How can I modify my code so that all 3 methods return arrays or array of arrays?
Iterating over query()
$result_array = $this->DatabaseObject->_pdoQuery( 'multiple', 'tweet_model' );
foreach( $result_array as $array_1d )
{
$array_2d[]=$array_1d;
}
query does not return results, so I'm not sure what you meant on the first example. query returns a PDOStatement. It's basically the same as:
$qry = $db->query("...");
//equiv:
$qry = $db->prepare("...");
$qry->execute();
As vicTROLLA noted, the simplest solution is to just use:
$results = $stmt->fetch(PDO::FETCH_ASSOC);
However, if you need to process the results as you go for some reason, you could always loop over them manually.
$stmt = ...;
$results = array();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$results[] = $row;
}
//use or return $results
$results will thus always be an array with 0 or more arrays inside of it.
I find it useful to build arrays where the array key is the primary key of the record:
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$results[$row['id']] = $row;
}
Also, I suspect that you've misunderstood the purpose and functionality of quote. It is used for escaping strings that you are going to interpolate into a query, not for magically escaping all values in a query (hence $db->quote($query) makes no sense).
Even worse is that drivers are not required to support quote. (Though it does at least return false if there's no support.)
prepare is massively preferred over quote.
In all of those cases, you can do
$arrayOfAssocArrays = $pdoStatement->fetchAll(PDO::FETCH_ASSOC);
I already asked this but I dont know how to fix it please help, my $obj->obtainID($a); i returns nothing for the second index.
Description:
I got an $arrayDirectoy variable which contains just 2 values, then I tried to pass that variable to a query in a for loop to retrieve the ids of each one, but It just echo me one id, and there are two ids, I dont know how to fix that, It just print me 1 id.
Example $arrayDirectory[] = {user1, user2};
it must echo 1 2 but just print me 1
for($i=0;$i<sizeof($arrayDirectory);$i++){
$res[$i] = $obj->obtainID($arrayDirectory[$i]);
echo $res[$i];
}
this is my obtainID method
public function obtainID($user){
$conexion = $this->objConexion->configuracion();
$query = "CALL sp_xxx('$user')";
$stmt = $conexion->prepare($query);
$stmt->execute();
$resultado = $stmt->fetchColumn();
return $res;
}
There are a couple issues. First, you have a syntax error -- you forgot a closing parenthesis to sizeof():
for($i=0;$i<sizeof($arrayDirectory;$i++){
Also, if you're not changing the number of elements, there's no need to call sizeof() more than once:
$count = count($arrayDirectory); // or sizeof(), doesn't matter
for($i=0; $i<$count; $i++)
Lastly, you should be calling count() or sizeof() on $arrayDirectory[0], not $arrayDirectory. This is because the array only has one element, which happens to be an object that contains two fields. Alternatively, it probably makes sense for $arrayDirectory to just be an object, not an array.
So, your final code should look like:
$arrayDirectory = {'user1', 'user2'};
$count = count($arrayDirectory); // or sizeof(), doesn't matter
for($i=0; $i<$count; $i++)
$res[$i] = $obj->obtainID($arrayDirectory[$i]);
echo $res[$i];
}
You aren't checking for errors. Try checking for
if (!$stmt->execute()) { print_r($stmt->errorInfo()); }
also, I wouldn't use fetchColumn() I'd use either fetch() or fetchAll() if you have more than one result.
wondering how i could bind the results of a PHP prepared statement into an array and then how i could go about calling them. for example this query
$q = $DBH->prepare("SELECT * FROM users WHERE username = ?");
$q->bind_param("s", $user);
$q->execute();
and this would return the results the username, email, and id. wondering if i could bind it in an array, and then store it in a variable so i could call it throughout the page?
PHP 5.3 introduced mysqli_stmt::get_result, which returns a resultset object. You can then call mysqli_result::fetch_array() or
mysqli_result::fetch_assoc(). It's only available with the native MySQL driver, though.
Rule of thumb is that when you have more than one column in the result then you should use get_result() and fetch_array() or fetch_all(). These are the functions designed to fetch the results as arrays.
The purpose of bind_result() was to bind each column separately. Each column should be bound to one variable reference. Granted it's not very useful, but it might come in handy in rare cases. Some older versions of PHP didn't even have get_result().
If you can't use get_result() and you still want to fetch multiple columns into an array, then you need to do something to dereference the values. This means giving them a new zval container. The only way I can think of doing this is to use a loop.
$data = [];
$q->bind_result($data["category_name"], $data["id"]);
while ($q->fetch()) {
$row = [];
foreach ($data as $key => $val) {
$row[$key] = $val;
}
$array[] = $row;
}
Another solution as mentioned in the comments in PHP manual is to use array_map, which internally will do the same, but in one line using an anonymous function.
while ($q->fetch()) {
$array[] = array_map(fn($a) => $a , $data);
}
Both solutions above will have the same effect as the following:
$q = $DBH->prepare("SELECT * FROM users WHERE username = ?");
$q->bind_param("s", $user);
$q->execute();
$result = $q->get_result();
$array = $result->fetch_all(MYSQLI_ASSOC);
See Call to undefined method mysqli_stmt::get_result for an example of how to use bind_result() instead of get_result() to loop through a result set and store the values from each row in a numerically-indexed array.