Safe Way To Loop PDO Statement - php

Question:
Loop through a prepared PDO statement, check for duplicates, if no duplicates execute the query?
Current PDO Statement:
$STH = $DBH->prepare("INSERT INTO inbox (id,efrom,subject,msg,eread,date) VALUES ('',:efrom,:esubject,:emsg,:eread,:edate)");
$STH->bindParam(':efrom', $inbox_from[0]);
$STH->bindParam(':esubject', $inbox_subject[0]);
$STH->bindParam(':emsg', $inbox_msg[0]);
$STH->bindParam(':eread', $inbox_read[0]);
$STH->bindParam(':edate', $inbox_date[0]);
Why there needs to be a loop:
I need the arrays $inbox_* to be incremented then queried until it gets to the end of the array.
Example:
$inbox_from('hello','how','are','you');
someloop(somecondition) {
//output would be:
$STH->bindParam(':efrom', $inbox_from[0]);
$STH->bindParam(':efrom', $inbox_from[1]);
$STH->bindParam(':efrom', $inbox_from[2]);
$STH->bindParam(':efrom', $inbox_from[3]);
//It ends at [3] index because its the end of the array.
}
$STH = execute();
//So now it executes and should have put the 4 array indexes into different efrom columns. So column id 1 has 'hello' and id 4 has 'you'.
Maybe:
for($i = 0; //Something to check if array ended; ++$i) {
//Someway to Properly bind the arrays and execute?
}
or some use of a While Loop?
Hope I explained it my best.
Whats the best practice to use here?

Use numbered parameters instead of named parameters, and build the query and parameters dynamically.
$sql = "INSERT INTO inbox (efrom,subject,msg,eread,date) VALUES ";
// array_fill will create an array of N "(?, ?, ?, ?, ?)" strings
// implode will then join them together with comma separators
$sql .= implode(', ', array_fill(0, count($inbox_from), "(?, ?, ?, ?, ?)"));
$STH = $DBH->prepare($sql);
$params = array();
// Populate the $params array with all the input values
foreach ($inbox_from as $i => $from) {
$params[] = $from;
$params[] = $inbox_subject[$i];
$params[] = $inbox_msg[$i];
$params[] = $inbox_read[$i];
$params[] = $inbox_date[$i];
}
$STH->execute($params);
You can leave the id field out of the column list, and it will be filled in automatically using auto-increment.
To remove duplicate messages, you can do:
$check_stmt = $DBH->prepare("SELECT COUNT(*) AS count FROM inbox WHERE msg = :msg");
$check_stmt->bindParam(':msg', $msg);
$messages_seen = array();
foreach ($inbox_msg as $i => $msg) {
// Check if the message is already in the DB
$check_stmt->execute();
$first_row = $check_stmt->fetch(PDO::FETCH_OBJ);
$check_stmt->fetchAll(); // Fetch the rest of the query to get in sync
if ($first_row->count > 0) {
$messages_seen[$msg] = true; // Remember that we already saw this message
} elseif (!isset($messages_seen[$msg])) // If we haven't already seen this message
$params[] = $inbox_from[$i];
$params[] = $inbox_subject[$i];
$params[] = $msg;
$params[] = $inbox_read[$i];
$params[] = $inbox_date[$i];
$messages_seen[$msg] = true; // Remember that we added this message
}
}
$sql = "INSERT INTO inbox (efrom,subject,msg,eread,date) VALUES ";
// There's 1 (...) group for every 5 parameters, so divide the length of $params by 5 to know how many of them to put in the SQL
$sql .= implode(', ', array_fill(0, count($params)/5, "(?, ?, ?, ?, ?)"));
$STH = $DBH->prepare($sql);
$STH->execute($params);
When adding an index on a TEXT datatype, you have to specify the number of bytes of the text to store in the index. So it should be something like:
CREATE INDEX ix_msg ON inbox (msg(200));

You can create one big query if you are using Insert to same table
INSERT INTO table (columns) VALUES (....), (....)...., (...)
Its better than calling to sql for each row. Faster too

Related

PDO Generate placeholders for binding from array and single value for a multiple INSERT statement

I am trying to generate a single insert statement that will insert multiple rows. I have and array of values, which is what I am wanting to insert into a table that all use the same userkey.
I have tried using a named PDO parameter and binding to that, then passing in the role array during execute but that doesn't work. So I moved on to placeholders, but I can't get that to work either.
I call my function like addUsersRoles(1, [100,101,102]);
And looking at the generated SQL I get:
INSERT user_roles (userkey, roleid) VALUES (?,?),(?,?),(?,?)
Which I think is the correct format for inserting multiple records.
Based up on that, what I am trying to generate is:
INSERT user_roles (userkey, roleid) VALUES (1,100),(1,101),(1,102)
How can I combine the power of PDO's binding to a SQL statement in this manner?
public function addUsersRoles($userkey, $roles = []){
$in = str_repeat('?,', count($roles) - 1) . '?';
$base_user_sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$sql = $base_user_sql;
foreach ($roles as $role) {
//$sql .= "(:USERKEY, $in),"; // Didn't Work
$sql .= "($in),";
}
//Remove trailing comma
$sql = rtrim($sql, ',');
$db = static::getDB();
$stmt = $db->prepare($sql);
//$stmt->bindValue(':USERKEY', $userkey, PDO::PARAM_STR);
return $stmt->execute($roles);
}
You can use placeholders as well. Look at the following example:
public function addUsersRoles(string $userKey, array $roles = []): bool
{
$values = [];
$inputParameters = [':user_key' => $userKey];
foreach ($roles as $index => $role) {
$rolePlaceholder = ':roleid' . $index;
$values[] = sprintf('(:user_key, %s)', $rolePlaceholder);
$inputParameters[$rolePlaceholder] = $role;
}
$sql = 'INSERT INTO user_roles (user_key, roleid) VALUES ';
$sql .= implode(', ', $values);
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($inputParameters);
}
This code will generate a query like this:
INSERT INTO user_roles (user_key, roleid) VALUES (:user_key, :roleid0), (:user_key, :roleid1), (:user_key, :roleid2), (:user_key, :roleid3), (:user_key, :roleid4);
And the $inputParameters will be like this:
[
':user_key' => 'some user key',
':roleid0' => 1,
':roleid1' => 2,
]
You shouldn't use count($roles) when making $in. It's always just ?, ?. You just need the count of roles when repeating that for all the rows. You can use array_fill to create an array of (?, ?) strings, and then implode to put commas between them.
You also need to insert create an array with alternating keys and roles, and use that as the parameters when executing.
public function addUsersRoles($userkey, $roles = []){
$values = implode(',', array_fill(0, count($roles), '(?, ?)'));
$base_user_sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$sql = $base_user_sql . $values;
$keys_and_roles = [];
foreach ($roles as $role) {
$keys_and_roles[] = $userkey;
$keys_and_roles[] = $role;
}
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($keys_and_roles);
}
I've simplified it a bit, but this splits into two parts that are done together. The first is to generate the SQL...
INSERT user_roles (userkey, roleid) VALUES (?,?),(?,?),(?,?)
This code just loops through the roles and adds (?,?), for each one.
The second part is building up the bind data. As the SQL needs a list of the data in the order userkey, roleid pairs, as it's building the SQL, it also adds these values to a data array at the same time.
So the main code comes out as...
public function addUsersRoles($userkey, $roles = []){
$sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$binds = [];
foreach ( $roles as $role ) {
$sql .= "(?,?),";
$binds[] = $userkey;
$binds[] = $role;
}
//Remove trailing comma
$sql = rtrim($sql, ',');
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($binds)
}
(Although I haven't been able to test the execute part).
You should also make sure any errors are being dealt with as well.

Incorrect integer value: 'SELECT LAST_INSERT_ID()[0]' for column 'po_trans_id' at row 1

trying to insert multiple input and get the last id of other table and insert in into this table for foreign key.
tried to remove from the loop and tried to use foor loop
if($result){
$j = 0;
foreach($_POST as $val){
$po_trans_id = "SELECT LAST_INSERT_ID()[$j]";
$po_qty = $_POST['po_qty'][$j];
$po_unit = $_POST['po_unit'][$j];
$po_description = $_POST['po_description'][$j];
$po_unit_price = $_POST['po_unit_price'][$j];
$po_total_amount = $_POST['po_total_amount'][$j];
$payment_terms = $_POST['paymentTerms'][$j];
$user = $_SESSION["username"][$j];
$query = "INSERT INTO request_po (po_trans_id,po_qty,po_unit,po_description,po_unit_price,po_total_amount,totalPrice,user) VALUES ('$po_trans_id' , '$po_qty' , '$po_unit' , '$po_description' , '$po_unit_price' , '$po_total_amount' , '$totalPrice' , '$user')";
$j++;
$result = mysqli_multi_query($link, $query) or die(mysqli_error($link));
}
id like to insert my last id to other table for relational database.
LAST INSERT_ID() is valid SQL. LAST_INSERT_ID()[0] is not, that's PHP notation and has no place in SQL.
What you want is available as insert_id through mysqli itself. You must ensure that each command completed correctly before proceeding or you will potentially create a mess in your database that's difficult to unwind.
To fix this, keep in mind the following:
DO NOT use mysqli_multi_query. This command does not support placeholders and cannot be secured properly.
What you want is to convert this to proper mysqli with prepared statements:
<?php
if ($result) {
$j = 0;
$count = count($_POST['po_qty']);
// Use insert_id property
$po_trans_id = $link->insert_id;
$stmt = $link->prepare("INSERT INTO request_po (po_trans_id,po_qty,po_unit,po_description,po_unit_price,po_total_amount,totalPrice,user) VALUES (? , ?, ?, ?, ?, ?, ?, ?)");
for ($j = 0; $j < $count; $j++) {
$stmt->bind_param('sssssss',
$po_trans_id,
$_POST['po_qty'][$j],
$_POST['po_unit'][$j],
$_POST['po_description'][$j],
$_POST['po_unit_price'][$j],
$_POST['po_total_amount'][$j],
$_POST['paymentTerms'][$j],
$_SESSION["username"][$j]
);
$stmt->execute();
}
}
?>
Where that statement is prepared once and run many times. If you enable exceptions then you can avoid the or die(...) anti-pattern as well.

Insert multple rows with one insert

So I have three tables in a MySQL database:
order(id, etc..),
product(id, title, etc..)
orderproduct(productFK, orderFK)
Now I want to be able to insert an order with one order-id and (in some cases) multiple product-ids for orders containing more than one product:
order 1: orderid = 1, productids = 1
order 2: orderid = 2, productids = 2, 3
this while using prepared statements, like:
$stmt = $mysqli->prepare("INSERT INTO orderproduct (orderFK, productFK)
VALUES (?, ?)");
$result = $stmt->bind_param('ss', $orderid, $productid);
if($stmt->execute() == false) {
$flag = false;
}
$stmt->close();
One obvious solution is to loop the insert query but is there another way to do this without having to call the database multiple times?
This is working (hardcoded) but still, I can't figure out how to fill the bind_param dynamically..
$strings = "";
$values = "";
foreach ($params['products'] as $product) {
$strings .= 'ss';
$values .= "(?, ?),";
}
$values = substr($values, 0, -1);
$productid = array(1, 2);
$stmt = $mysqli->prepare("INSERT INTO orderproduct (orderFK, productFK)
VALUES " . $values);
$result = $stmt->bind_param($strings, $orderid, $productid[0], $orderid, $productid[1]);
if($stmt->execute() == false) {
$flag = false;
}
$stmt->close();
If you are on php 5.6+ you can use argument unpacking ... to bind your variables:
$args = [
$arg1,
$arg2,
$arg3,
$arg4,
];
$result = $stmt->bind_param($strings, ...$args);
An alternative would be to use PDO where you can send an array of arguments to bind to the execute() method.
Mysql will happily process something like:
INSERT INTO mytable (col1, col2)
VALUES (c1a, c2a)
,(C1b, c2b)
,(C1c, c2c)
...
(Up to max_allowed_packet) however expressing the bind_param() call cleanly (in a way which is easy to debug) will be rather difficult. Invoking with call_user_func_array() means you can simply pass a single array as the argument, but you still need to map your 2 dimensional data set to a one dimensional array.
Previously I've used the procedural mysql api to do multiple inserts resulting in a significant speed up of inserts and better balanced indexes.

MySQL prepared statement with bulk insert

I have a module which allows admins to add some users (maybe multiple) to a group.
PHP
$user_id = $_POST["user_id"]; //this can be an array
$group_id = $_POST["group_id"];
$sql = "INSERT IGNORE INTO users_groups (user_id, group_id)
VALUES ($user_id[0],$group_id)";
for ( $i = 1; $i < count($user_id); $i++)
{
$sql .= ", ($user_id[$i],$group_id)"
}
As you can see the sql-query depends on how much users the admin has selected.
How can I use this code with prepared statements because the post-variables come from a user (SQL injections...)
UPDATE
I have two selectboxes:
one for the user selection (multiple selection is possible -> array): $_POST["user_id"]
one for the group selection (only one selection is possible): $_POST["group_id"]
And now I want a prepared SQL statement for inserting user_id and group_id to the many-to-many-table (users_groups). The problem is that the number of values which have to be inserted can change (depending how much users the admin has selected in the selectbox).
I want to change the prepared query depending on how much users the admin has selected.
For example:
the admin selected two users -> sql: INSERT IGNORE INTO users_groups (user_id, group_id) VALUES ($user_id[0],$group_id), ($user_id[1],$group_id)
the admin selected four users -> sql: INSERT IGNORE INTO users_groups (user_id, group_id) VALUES ($user_id[0],$group_id), ($user_id[1],$group_id), ($user_id[2],$group_id), ($user_id[3],$group_id)
My question: How can I do this automaticaly and with prepared sql statements because I dont want to have like 10 times if(count($user_id) == number) {...?
UPDATE 2
If I would do this manually the code would look like this:
$sql = $db->prepare("INSERT IGNORE INTO users_groups (user_id, group_id) VALUES (?, ?)");
$sql->bind_param('ii', $user_id[0], $group_id);
UPDATE 3
To check whether there are only integers in the post variables:
$user_id = filter_input_array(INPUT_POST, 'user_id', FILTER_SANITIZE_NUMBER_INT);
$user_id = abs($user_id);
$group_id = filter_input(INPUT_POST, 'group_id', FILTER_SANITIZE_NUMBER_INT);
$group_id = abs($group_id);
To create the SQL statement, you can use this to generate as many placeholders as you will need for all of your user_id values.
$user_ids = $_POST["user_id"]; //this can be an array
$group_id = $_POST["group_id"];
$placeholders = implode(', ', array_fill(0, count($user_ids), "(?, ?)"));
$sql = "INSERT IGNORE INTO users_groups (user_id, group_id) VALUES $placeholders";
Then to bind the values, if you are using pdo, something like this should work:
$stmt = $pdo->prepare($sql);
$i = 1;
foreach ($user_ids as $user_id) {
$stmt->bindValue($i++, $user_id);
$stmt->bindValue($i++, $group_id);
}
Or if you are using mysqli, you could try this approach using reflection (taken from a user note in the php docs here) the note does state that this needs PHP 5.3+, which hopefully you do have.
$stmt = $mysqli->prepare($sql);
$values[] = str_repeat('ii', count($user_ids));
foreach ($user_ids as $user_id) {
$values[] = $user_id;
$values[] = $group_id;
}
$ref = new ReflectionClass('mysqli_stmt');
$method = $ref->getMethod("bind_param");
$method->invokeArgs($stmt, $values);
$stmt->execute();
Or, because I'm stubborn and the thing from the php docs user note doesn't seem to work, which after reading more I don't see how it could work, considering the php doc for ReflectionMethod::invokeArgs specifically says
Note: If the function has arguments that need to be references, then they must be references in the passed argument list.
then using call_user_func_array could possibly work.
$stmt = $mysqli->prepare($sql);
$type = str_repeat('ii', count($user_ids));
foreach ($user_ids as $user_id) {
$values[] = $user_id;
$values[] = $group_id;
}
foreach ($values as $key => $value) {
$value_references[$key] = &$values[$key];
}
call_user_func_array('mysqli_stmt_bind_param',
array_merge(array($stmt, $type), $value_references));
But what a pain. I really like pdo.

PDOStatement->prepare and PDOStatement->bindParam() combination not working [duplicate]

This question already has answers here:
Can PHP PDO Statements accept the table or column name as parameter?
(8 answers)
Closed 8 years ago.
I have some code that should loop through values and change entries in a table. The 5 values of the variables $change_val, $column, and $id all echo out correctly, so I assume there is something wrong with my usage of bindParam (but I am not sure what it is).
$connection = new PDO("mysql:host=localhost;dbname=logbook", $username, $password);
$perform_edit = $connection->prepare("UPDATE contacts SET :column = :value WHERE name_id = :name_id");
[Definition of Arrays]
for ($i = 1; $i <= 5; $i++) {
if (!empty($_POST[ $change_array[$i]])) {
$change_val = $_POST[$change_array[$i]];
$column = $column_array[$i];
$id = $_POST["name_id_ref"];
$perform_edit->bindParam(":column", $column, PDO::PARAM_STR);
$perform_edit->bindParam(":value", $_POST[$change_array[$i]], PDO::PARAM_STR);
$perform_edit->bindParam(":name_id", $_POST["name_id_ref"], PDO::PARAM_INT);
$perform_edit->execute();
}
}
The $_POST statement is there because the value I want is actually passed from another file. When I place appropriate echo statements within the loop, though, they all print out their correct value.
I've also tried bindValue, but that did not work either. I see no errors and things at least compile smoothly—just not as they should. Nothing in the table is changed.
What's wrong here?
You cannot use place holders for table or column names it would defeat the purpose of preparing a statement ahead of time if the structure of that statement changed.
You would need to pre-build your prepare statement with the correct column names, whether you name them by hand, string replacement, or implode a list of column names.
I don't have an environment to test on right now but something like:
//Some random values and DB column names
$arrLocation = array ('Victoria','Washington','Toronto','Halifax','Vancouver');
$arrName = array ('Sue', 'Bob', 'Marley', 'Tim', 'Fae');
$arrColumn = array (1 => 'name', 2 => 'age', 3 => 'location');
/* Build column & named placeholders
* $strSet = '`name` = :name, `age` = :age, `location` = :location';
*/
$strSet = '';
foreach ($arrColumn as $column) {
$strSet .= "`$column` = :$column, ";
}
$strSet = rtrim($strSet, ', ');
$connection = new PDO($dsn, $user, $pass);
/*
* Prepared statement then evaluates to:
* UPDATE `table` SET `name` = :name, `age` = :age, `location` = :location
* WHERE `id` = :id;
*/
$stmt = $connection->prepare("UPDATE `table` SET $strSet WHERE `id` = :id;");
$arrChange = array (
1 => $arrName[(rand(0, count($arrName)-1))],
2 => rand(0, 30),
3 => $arrLocation[(rand(0, count($arrLocation)-1))]
);
$idToUpdate = 1;
$stmt->bindParam(':id', $idToUpdate, PDO::PARAM_INT);
foreach($arrChange as $key=>$value) {
$stmt->bindValue(":$arrColumn[$key]", $value);
}
$stmt->execute();

Categories