i am lost trying to figure out how to properly set up a prepared PDO SQL statement when updating an existing record.
my array being passed into my method is such:
array(9) { ["catName"]=> string(10) "Kato Marks" ["age"]=> string(2) "40" ["gender"]=> string(6) "Female" ["createTime"]=> string(19) "2017-10-03 03:55:39" ["coloring"]=> string(5) "Black" ["hairLength"]=> string(5) "Short" ["currentMood"]=> string(5) "rowdy" ["weight"]=> string(2) "17" ["hasCatittude"]=> string(1) "0" }
with key is the database table column, the value is the database value i wish to update
so far i have the following:
public function updateRow($data, $catID)
{
$db = self::getConnection();
try
{
$stmt = $db->prepare("UPDATE users SET " . implode('= ', array_keys($data). array_values($data)). " WHERE id='{$catID}'");
foreach($data as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
$stmt->execute();
} catch (\PDOException $e) {
return 'Adding new record failed' . $e->getMessage();
}
}
but when i print_r the statement to the screen i get the following:
PDOStatement Object ( [queryString] => UPDATE users SET WHERE id='108' )
i am unable to figure out how to properly write the prepared statement, where i dynamically set the table name to the array key, etc. any help would be greatly appreciated.
You need to update your function to something like this. You first will build your column array, implode it with ', '. Loop though and bind your variables, then bind your $catID.
public function updateRow($data, $catID)
{
$db = self::getConnection();
try
{
foreach($data as $key => $value) {
$columns[] = '`' . $key . '` = :' . $key;
}
$stmt = $db->prepare("UPDATE users SET " . implode(', ', $columns). " WHERE id=:catID");
foreach($data as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
$stmt->bindValue('catID', $catID, PDO::PARAM_INT);
$stmt->execute();
} catch (\PDOException $e) {
return 'Adding new record failed' . $e->getMessage();
}
}
From your comment, this function belongs in a Database
class. Admittedly, this class could be used on any table to update any row based on any criteria. However, looking at the function signature, we can see it is supposed to accept a $catId
parameter, implying it will be used on a very specific table, filtering rows using a specific column.
You could get around this by simply renaming the function and the parameters to more closely represent what it is actually doing but that still wouldn't solve what is, in my opinion, the core of the problem: The object has too many responsibilities.
According to the Single Responsibility Principle, a class "should have one, and only one, reason to be changed". In this case, if a column is added to the User
table, or that another column needs to be added to the WHERE
clause, only the User
class (or Entity) should be affected while the Database
class should not need any changes.
For the sake of simplicity, let's imagine a fridge
database, with a table called fruits
(I know, very imaginative).
create table fruits (
id int not null auto_increment,
name varchar(20),
color varchar(20),
weight int,
order int, /* order of appearance, say, for a list */
PRIMARY KEY (id)
);
The php representation of this table would probably look a bit like this
class Fruit {
private $id;
private $name;
private $color;
private $weight;
private $order;
public function __construct($id, $name, $color, $weight) {
$this->id = $id;
$this->name = $name;
$this->color = $color;
$this->weight = $weight;
$this->order = $order;
}
}
Great, now we have a table, and an object that can be used to represent a record of that table. Now we can add a FruitTable
class that will act as a medium between Fruit
and the Database itself.
class FruitTable {
private $columns = ['id', 'name', 'color', 'weight', 'order'];
private $table = 'fridge.fruits';
private $conn;
public function __construct($connection) {
$this->conn = $connection
}
public function findOneById($id) {
/**
* Query the DB and hydrate a Fruit instance
*/
return $fruit;
}
}
Now imagine we need to be able to change the order
field of Fruit
based on the color of the fruit (I know, this is some really weird business we're working for). No problem, we can add a function to FruitTable
to make the update.
public function updateOrderByColor($newOrder, $color) {
/* assume proper validation */
$query = $this->conn->prepare("UPDATE fruits SET order = :newOrder WHERE color = :color");
$query->bindParam(':newOrder', $newOrder);
$query->bindParam(':color', $color);
$query->execute();
}
This is a very simplistic example, but it shows how much flexibility can be gained over having a simple Database
class. If a column needs to be added, or a condition changed, there is only one place in the code you need to jump in and mess around with. If there's a problem with a query, there's only one place to look for that bug.
This example isn't perfect, FruitTable
probably shouldn't have the responsibility of dealing with the database connection directly. You can look at how Doctrine and Eloquent approach query builders, and how they manage to keep each class encapsulated from the rest.