MySQL Insert failing dynamically but working directly - php

I've got a MySQL table with fields a1,a2,a3,b1,...,d1,d2, each field was declared as a BOOLEAN in the CREATE statement. (I also tried TINYINT(1) but had the same problem).
Then I have this PHP function which receives data from an HTML form:
public function add($a) {
$sql = "INSERT INTO property_classification
(a1,a2,a3,b1,b2,b3,b4,b5,b6,b7,b8,c1,c2,c3,d1,d2)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
// creating the classification_id
// e.g. "a1a2a3" => ["a1","a2","a3"]
$classifications = str_split($a['classifications'], 2);
$data = array();
// compile $data array
foreach (self::$classification_fields as $classification) {
// if user array contained any classification, set to true
if (in_array($classification, $classifications)) {
$data[$classification] = "1"; // I tried `true` too
} else {
$data[$classification] = "0"; // I tried `false` here
}
}
// set type for binding PDO params
foreach ($data as $key=>$value) settype($data[$key], 'int'); // tried 'bool'
$this->db->query($sql, $data);
$a['classification_id'] = $this->db->lastInsertId();
$this->log($a['classification_id']); // Output: "0"
...
The output should be a valid ID from 1+, but the insert failed so the lastInsertId() returned 0.
I checked what $sql compiled to, it came to this:
INSERT INTO property_classification (a1,a2,a3,b1,b2,b3,b4,b5,b6,b7,b8,c1,c2,c3,d1,d2) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
I also output $data with the code: implode(",",$data); and it gave me this output:
1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0
Which was perfect because the input was "a1a2".
The only problem now is I don't understand why the query is failing all the time, because I put the two bits together like so:
INSERT INTO property_classification (a1,a2,a3,b1,b2,b3,b4,b5,b6,b7,b8,c1,c2,c3,d1,d2) VALUES(1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
Then I executed that query in MySQL Query Browser and it worked.
So why is it failing through PDO?
DBO class
function query($sql, $data) {
try {
$this->query = $this->db->prepare($sql);
if (!is_null($data) && is_array($data))
$this->query->execute($data);
else
$this->query->execute();
} catch (PDOException $e) {
array_push($this->log, $e->getMessage());
}
}

Since you're actually passing an associative array to the PDO, you can bind to named parameters. The use of ? or positional placeholders require a standard indexed array. If you're against using named params, just replace $data[$classification] = with $data[] =
Try the below.
public function add($a) {
$sql = "INSERT INTO property_classification
(a1,a2,a3,b1,b2,b3,b4,b5,b6,b7,b8,c1,c2,c3,d1,d2)
VALUES(:a1,:a2,:a3,:b1,:b2,:b3,:b4,:b5,:b6,:b7,:b8,:c1,:c2,:c3,:d1,:d2);";
// creating the classification_id
// e.g. "a1a2a3" => ["a1","a2","a3"]
$classifications = str_split($a['classifications'], 2);
$data = array();
// compile $data array
foreach (self::$classification_fields as $classification)
$data[$classification] = in_array($classification, $classifications) ? 1 : 0;
$this->db->query($sql, $data);
$a['classification_id'] = $this->db->lastInsertId();
$this->log($a['classification_id']); // Output: "0"

Related

PDO statement updates 0 values instead of null or empty

I am making a PDO update statement but it is entering 0's on empty values.
I want a empty value to enter null
I want a zero value to enter zero
Here is my code:
include 'dbconfig.php';
$id = $_POST['id'];
$data = $_POST['data'];
try {
$options = [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO("mysql:charset=utf8mb4;host=$servername;dbname=$dbname", $username, $password);
$setStr = "";
$values = array();
foreach($data as $field => $value) {
$setStr .= "$field=:$field,";
$values[$field] = $value;
}
$setStr = rtrim($setStr, ",");
$values['id'] = $id;
$stmt = $pdo->prepare("UPDATE products SET $setStr WHERE id = :id");
$stmt->execute($values);
$count = $stmt->rowCount();
if ($count > 0) {
echo json_encode(array('response'=>'success','message'=>'Product ID # '.$id." successfully updated."));
}else{
echo json_encode(array('response'=>'danger','message'=>'Product not successfully updated'));
}
}catch(PDOException $e){
echo json_encode(array('response'=>'danger','message'=>$e->getMessage()));
}
$conn = null;
What is the best way to do this? Thank you
Check for an empty string in your loop, and convert it to null.
foreach($data as $field => $value) {
$setStr .= "$field=:$field,";
$values[$field] = $value === "" ? null : $value;
}
PDOStatement::execute considers every parameter to be a string. In some databases the driver deals with this without issue, but if you want to specifically type values you need instead to use bindValue. With this typing, we can type null (or empty) values to the database-null type (instead of whatever the column default is) - see my bindEmptyAsNull function.
Also, since I'm using a trusted source for the column names, I can safely use your $setStr pattern for building the update params, but note you might not want to update a column that isn't included in the $dangerData associative array (as it is, if it isn't submitted, the column will be cleared).
<?php
//... your setup code, including $pdo
// List all the names of the columns in the products table here - you havent' provided enough info so I'm guessing
$keys=["name","cost","weight_grams"];
$dangerId = $_POST['id'];
$dangerData = $_POST['data'];
/**
* Convert null values into PARAM_NULL types, not
* #param $type Should be one of the [PDO::PARAM_*](https://www.php.net/manual/en/pdo.constants.php) constants
*/
function bindEmptyAsNull($stmt,$name,$value,$type=PDO::PARAM_STR) {
//If you want empty string to be considered null too:
//if ($value===null || $value==="") {
if ($value===null) {
//Note the column needs to support null types
$stmt->Value($name,null,PDO::PARAM_NULL);
} else {
$stmt->bindValue($name,$value,$type);
}
}
//Build the set string
$setStr='';
foreach($keys as $col) {
$setStr.=$col.'=:'.$col.',';
}
$setStr=substr($set,0,-1);//Remove the trailing comma
//Create the statement
$stmt=$pdo->prepare("UPDATE products SET $setStr WHERE id=:id");
//Bind the danger values
// NOTE: The only way you could automate this (re-read the keys array) is to also have an
// associative array of key:type mapping too
$stmt->bindValue(":id",$dangerId,PDO::PARAM_INT);
bindEmptyAsNull($stmt,":name",$dangerData["name"]);
bindEmptyAsNull($stmt,":cost",$dangerData["cost"]);
bindEmptyAsNull($stmt,":weight_grams",$dangerData["weight_grams"],PDO::PARAM_INT);
$stmt->execute();
// .. Further processing

PHP invalid JSON encoding

I'm trying to GET a JSON format back when I POST a specific ID to my database. As I get more than one result I have multiple rows, which I want to get back. I do get different arrays back, but it is not a valid JSON Format. Instead of
[{...},{...},{...}]
it comes back as
{...}{...}{...}
Therefore the [...] are missing and the arrays are not separated by commas.
My code is down below. The function "getUserBookingsKl" is defined in a different php.
//get user bookings
public function getUserBookingsKl($id) {
//sql command
$sql = "SELECT * FROM `***` WHERE `hf_id`=$id AND `alloc_to`>DATE(NOW()) AND NOT `confirmation`=0000-00-00 ORDER BY `alloc_from`";
//assign result we got from $sql to $result var
$result = $this->conn->query($sql);
// at least one result
if ($result !=null && (mysqli_num_rows($result) >= 1 ))
{
while ($row = $result->fetch_array())
{
$returArray[] = $row;
}
}
return $returArray;
}
...
...
foreach($userdb as $dataset)
{
$returnArray["group"] = $dataset["kf_id"];
$returnArray["from"] = $dataset["alloc_from"];
$returnArray["to"] = $dataset["alloc_to"];
echo json_encode($returnArray);
# return;
}
// Close connection after registration
$access->disconnect();
It looks like you're sequentially emitting the values, not pushing into an array. You need to make an array, push into it, then call json_encode on the resulting structure:
$final = [ ];
foreach ($userdb as $dataset)
{
$returnArray = [ ];
$returnArray["group"] = $dataset["kf_id"];
$returnArray["from"] = $dataset["alloc_from"];
$returnArray["to"] = $dataset["alloc_to"];
$final[] = $returnArray;
}
echo json_encode($final);
Note that it's important here to not use the same variable inside the loop each time through or you're just pushing the same array in multiple times.

PHP - How to substitute array as host parameter in prepared statement

I am able to bind values of type int, str, bool and null but I am unable to bind array type.
I have tried both functions, i.e. bindValue and bindParam but neither of them worked.
How can I accomplish this ?
// a helper function to map Sqlite data type
function getArgType($arg) {
switch (gettype($arg)) {
case 'double': return SQLITE3_FLOAT;
case 'integer': return SQLITE3_INTEGER;
case 'boolean': return SQLITE3_INTEGER;
case 'NULL': return SQLITE3_NULL;
case 'string': return SQLITE3_TEXT;
default:
throw new \InvalidArgumentException('Argument is of invalid type '.gettype($arg));
}
}
$sql = "SELECT * FROM table_name WHERE id IN (?)";
$params = [[10, 9, 6]]; // array of array
$dbpath = '/path/to/sqlite.sqlite';
$db = new SQLite3($dbPath, SQLITE3_OPEN_READONLY);
$stmt = $db->prepare($sql);
try {
foreach ($params as $index => $val) {
if (is_array($val)) {
/************* I am stuck here *************/
$ok = $stmt->bindParam($index + 1, $val);
// Using bindValue also didn't worked!
} else {
$ok = $stmt->bindValue($index + 1, $val, getArgType($val));
}
if (!$ok) {
throw new Exception("Unable to bind param: $val");
}
}
} catch (Exception $ex) {
// NO exception is thrown from bindValue() or bindParam()
$reason = "Error in binding statement. " . $ex->getMessage();
die($reason);
}
$result = $stmt->execute();
$data = [];
while ($row = $result->fetchArray($mode)) {
$data[] = $row;
}
var_dump($data);
Edit: I already tried replacing single ? with required number of question marks in param array, but then it is working only if my array has less than 1000 values! I think it's a limitation of how statements are prepared in SQLite3 in PHP.
Unfortunately this is not possible! You cannot bind an array.
The easiest solution for you problem would be the following:
Create the SQL-Query with one placeholder (?) per value in the array
Bind each value by iterating over the array.
But there are also another options (e.g. a sub-SELECT)
More information here (even if it's a Java question, it is nearly the same topic/problem because the database type doesn't matter in this case)
EDIT: Normally, the SQL Limit for bound parameters is set so 999, but you can change it if you need to.
You cannot bind arrays as a list for an IN (?) clause. Each value in the IN list must get its distinct place holder.
To make this dynamic, first determine the array of values and then dynamically build the SQL.
This would be your code:
$arrayParam = [10, 9, 6];
$placeHolders = implode(',', array_fill(0, count($arrayParam), '?'));
$sql = "SELECT * FROM table_name WHERE id IN ($placeHolders) AND name = ?";
// Merge the "array" parameter values with any other parameter values
// into one non-nested array:
$params = array_merge($arrayParam, ['myname']);
// ...
foreach ($params as $index => $val) {
// No sub arrays allowed:
$ok = $stmt->bindValue($index + 1, $val, getArgType($val));
// ... etc
I would propose a work-around in what you try to do. Pass the query result to a temporary table.
// a helper function to map Sqlite data type
function getArgType($arg) {
switch (gettype($arg)) {
case 'double': return SQLITE3_FLOAT;
case 'integer': return SQLITE3_INTEGER;
case 'boolean': return SQLITE3_INTEGER;
case 'NULL': return SQLITE3_NULL;
case 'string': return SQLITE3_TEXT;
default:
throw new \InvalidArgumentException('Argument is of invalid type '.gettype($arg));
}
}
function getTempValues() {
$sql = "SELECT * FROM `myTemp`";
$params = [$in]; // array of array
$dbpath = '/path/to/sqlite.sqlite';
$db = new SQLite3($dbPath, SQLITE3_OPEN_READONLY);
$stmt = $db->prepare($sql);
$result = $stmt->execute();
$data = [];
while ($row = $result->fetchArray($mode)) {
$data[] = $row;
}
return $data;
}
function addToTemp($in) {
$sql = "SELECT * INTO `myTemp` FROM `table_name` WHERE `id` = ?";
$params = [$in]; // array of array
$dbpath = '/path/to/sqlite.sqlite';
$db = new SQLite3($dbPath, SQLITE3_OPEN_READONLY);
$stmt = $db->prepare($sql);
if(is_array($in)) {
foreach($in as $newValue) {
addToTemp($newValue);
}
} else {
try {
foreach ($params as $index => $val) {
$ok = $stmt->bindValue($index + 1, $val, getArgType($val));
if (!$ok) {
throw new Exception("Unable to bind param: $val");
}
}
} catch (Exception $ex) {
// NO exception is thrown from bindValue() or bindParam()
$reason = "Error in binding statement. " . $ex->getMessage();
die($reason);
}
$stmt->execute();
}
return getTempValues();
}
print_r(addToTemp([10,9,6]));
(Caveat: This answer was written before the 'mysql' tag has been removed; I don't know if it addslashes works for sqlite3.)
In PHP, given $list as an array of values destined for an IN list:
$list = array(1, 2, 'abcd', 'double quote: "', "apostrophe: don't");
$ins = implode(', ', array_map(
function($a) {
return "'" . addslashes($a) . "'";
}, $list));
echo $sql = "... IN ($ins) ...";;
yields
... IN ('1', '2', 'abcd', 'double quote: \"', 'apostrophe: don\'t') ...
(Yes, this could be done with a normal for loop, without using array_map and an "anonymous function".)
Don't worry; quotes around numbers ('123') is OK for numeric columns.
You can add ? placeholder as many as number of items in your array
Select * from table_a where field in (?,?,?,?,?,?,?,.....)
If they are bigger than 1000 then split them into two or more queries.

Pulling NHL Standings from XML Table with PHP

I'm working on a project in which I pull various statistics about the NHL and inserting them into an SQL table. Presently, I'm working on the scraping phase, and have found an XML parser that I've implemented, but I cannot for the life of me figure out how to pull information from it. The table can be found here -> http://www.tsn.ca/datafiles/XML/NHL/standings.xml.
The parser supposedly generates a multi-dimmensional array, and I'm simply trying to pull all the stats from the "info-teams" section, but I have no idea how to pull that information from the array. How would I go about pulling the number of wins Montreal has? (Solely as an example for the rest of the stats)
This is what the page currently looks like -> http://mattegener.me/school/standings.php
here's the code:
<?php
$strYourXML = "http://www.tsn.ca/datafiles/XML/NHL/standings.xml";
$fh = fopen($strYourXML, 'r');
$dummy = fgets($fh);
$contents = '';
while ($line = fgets($fh)) $contents.=$line;
fclose($fh);
$objXML = new xml2Array();
$arrOutput = $objXML->parse($contents);
print_r($arrOutput[0]); //This print outs the array.
class xml2Array {
var $arrOutput = array();
var $resParser;
var $strXmlData;
function parse($strInputXML) {
$this->resParser = xml_parser_create ();
xml_set_object($this->resParser,$this);
xml_set_element_handler($this->resParser, "tagOpen", "tagClosed");
xml_set_character_data_handler($this->resParser, "tagData");
$this->strXmlData = xml_parse($this->resParser,$strInputXML );
if(!$this->strXmlData) {
die(sprintf("XML error: %s at line %d",
xml_error_string(xml_get_error_code($this->resParser)),
xml_get_current_line_number($this->resParser)));
}
xml_parser_free($this->resParser);
return $this->arrOutput;
}
function tagOpen($parser, $name, $attrs) {
$tag=array("name"=>$name,"attrs"=>$attrs);
array_push($this->arrOutput,$tag);
}
function tagData($parser, $tagData) {
if(trim($tagData)) {
if(isset($this->arrOutput[count($this->arrOutput)-1]['tagData'])) {
$this->arrOutput[count($this->arrOutput)-1]['tagData'] .= $tagData;
}
else {
$this->arrOutput[count($this->arrOutput)-1]['tagData'] = $tagData;
}
}
}
function tagClosed($parser, $name) {
$this->arrOutput[count($this->arrOutput)-2]['children'][] = $this->arrOutput[count($this- >arrOutput)-1];
array_pop($this->arrOutput);
}
}
?>
add this search function to your class and play with this code
$objXML = new xml2Array();
$arrOutput = $objXML->parse($contents);
// first param is always 0
// second is 'children' unless you need info like last updated date
// third is which statistics category you want for example
// 6 => the array you want that has wins and losses
print_r($arrOutput[0]['children'][6]);
//using the search function if key NAME is Montreal in the whole array
//result will be montreals array
$search_result = $objXML->search($arrOutput, 'NAME', 'Montreal');
//first param is always 0
//second is key name
echo $search_result[0]['WINS'];
function search($array, $key, $value)
{
$results = array();
if (is_array($array))
{
if (isset($array[$key]) && $array[$key] == $value)
$results[] = $array;
foreach ($array as $subarray)
$results = array_merge($results, $this->search($subarray, $key, $value));
}
return $results;
}
Beware
this search function is case sensitive it needs modifications like match to
a percentage the key or value changing capital M in montreal to lowercase will be empty
Here is the code I sent you working in action. Pulling the data from the same link you are using also
http://sjsharktank.com/standings.php
I have actually used the same exact XML file for my own school project. I used DOM Document. The foreach loop would get the value of each attribute of team-standing and store the values. The code will clear the contents of the table standings and then re-insert the data. I guess you could do an update statement, but this assumes you never did any data entry into the table.
try {
$db = new PDO('sqlite:../../SharksDB/SharksDB');
$db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
} catch (Exception $e) {
echo "Error: Could not connect to database. Please try again later.";
exit;
}
$query = "DELETE FROM standings";
$result = $db->query($query);
$xmlDoc = new DOMDocument();
$xmlDoc->load('http://www.tsn.ca/datafiles/XML/NHL/standings.xml');
$searchNode = $xmlDoc->getElementsByTagName( "team-standing" );
foreach ($searchNode as $searchNode) {
$teamID = $searchNode->getAttribute('id');
$name = $searchNode->getAttribute('name');
$wins = $searchNode->getAttribute('wins');
$losses = $searchNode->getAttribute('losses');
$ot = $searchNode->getAttribute('overtime');
$points = $searchNode->getAttribute('points');
$goalsFor = $searchNode->getAttribute('goalsFor');
$goalsAgainst = $searchNode->getAttribute('goalsAgainst');
$confID = $searchNode->getAttribute('conf-id');
$divID = $searchNode->getAttribute('division-id');
$query = "INSERT INTO standings ('teamid','confid','divid','name','wins','losses','otl','pts','gf','ga')
VALUES ('$teamID','$confID','$divID','$name','$wins','$losses','$ot','$points','$goalsFor','$goalsAgainst')";
$result= $db->query($query);
}

Variable amount of columns returned in mysqli prepared statement

I have a situation where a dynamic query is being generated that could select anywhere from 1 to over 300 different columns across multiple tables. It currently works fine just doing a query, however the issue I'm running into in using a prepared statement is that I do not know how to handle the fact that I don't know how many columns I will be asking for each time and therefor don't know how to process the results.
The reason I believe a bind statement will help is because once this query is run once, it will most likely (though not always) be run again with the exact same parameters.
Currently I have something like this:
$rows = array();
$this->statement = $this->db->prepare($query);
$this->statement->bind_param('i',$id);
$this->statement->execute();
$this->statement->bind_result($result);
while($this->statement->fetch())
{
$rows[] = $result;
}
I know this doesn't work as I want it to, my question is how do I get the data back out of the query. Is it possible to bring the columns back in an associative array by column name, like a standard mysqli query?
I prever not to use eval, this is my solution (similar to ashurexm):
$md = $stmt -> result_metadata();
$fields = $md -> fetch_fields();
$result = new stdClass(); // Actual result object
$params = array(); // Array of fields passed to the bind_result method
foreach($fields as $field) {
$result -> {$field -> name} = null;
$params[] = &$result -> {$field -> name};
}
call_user_func_array(array($stmt, 'bind_result'), $params);
Using VolkerK's suggestion of mysqli_statement->result_metadata() I was able to fashion together the following code that accomplishes what I'm looking for, though the performance isn't any faster than using a standard query. I get the statement->result_metadata() to build an associative array to call bind_result on. I build up a bind_result statement as a string and eval it. I know this isn't particularly safe but it is my first pass.
public function executePreparedStatement()
{
if($this->statement->execute())
{
$this->record = array();
$md = $this->statement->result_metadata();
$fields = $md->fetch_fields();
$bindResult = '$this->statement->bind_result(';
foreach($fields as $field)
{
$bindResult .= "\$this->record['" . $field->name . "'],";
}
$bindResult = substr($bindResult,0,strlen($bindResult) - 1) . ');';
eval($bindResult);
return true;
}
else
{
$this->error = $this->db->error;
return false;
}
}
...
$this->prepareStatement($query);
$this->bindParameter('i',$runId);
if($this->executePreparedStatement())
{
$report = new Report();
while($this->statement->fetch())
{
$row = $this->record;
$line = array();
foreach($row as $key => &$value)
{
array_push($line,$value);
}
$report->addLine($line);
}
return $report
}
This is the very reason why mysqli should never be used with prepared statements.
So, you have to use PDO instead, which will make your executePreparedStatement() into three lines:
function executePreparedStatement($sql,$params) {
$stm = $this->db->prepare($sql);
$stm->execute($params);
return $stm->fetchAll();
}
used like this
$sql = "SELECT * from news WHERE category=?";
$data = $this->executePreparedStatement($sql,$_GET['category']);

Categories