I am very new to PHP Object Orientated Programming so was wondering if I could get some good advice on a database object I created.
I call the class db and include my class into every page load and initiate the database object using $db = new db. I then call the method inside this for each action I may need (building a menu from the database, getting login information etc.) with different parameters depending on what it is i want to do.
It takes its first parameter as the query with the ? symbol as replacements for values I want to bind, the second parameter is the values to bind to it in an array that is then looped through inside the prepared_statement method and the third parameter is the type ( FETCH_ARRAY returns an array of a SELECT statements rows, NUM_ROWS returns the amount of rows affected and INSERT returns the last inserted ID).
An example of how I would call this function is below:
$db->prepared_execute( "SELECT * FROM whatever WHERE ? = ? ", array( 'password', 'letmein' ), NUM_ROWS );
The second and third parameters are optional for if there are no parameters to be bound or no return is needed.
As I am new to OOP I am finding it hard to get my head around exactly when to use correctly and what are public, private, static functions/variables and design patterns (Singleton etc.).
I've read many tutorials to get as far as I hav but I feel now I need to take it here to get further answers or advice on where to go next with OOP and with this class I've built.
It works as it is which for me is a good starting place except for any error handling which I will add in next but I want to make sure I am not making any obvious design errors here.
The code for the class is below:
class db {
var $pdo;
public function __construct() {
$this->pdo = new PDO('mysql:dbname=' . DB_NAME . ';host=' . DB_HOST . ';charset=utf8', DB_USER, DB_PASS);
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function prepared_execute( $query, $bind_values = null, $type = null ) {
$preparedStatement = $this->pdo->prepare( $query );
if( $bind_values ) {
$i = 1;
foreach( $bind_values as $bind_value ) {
$preparedStatement->bindValue($i, $bind_value);
$i++;
} }
$preparedStatement->execute();
if( $type == FETCH_ARRAY ) { return $preparedStatement->fetchAll(); }
elseif( $type == NUM_ROWS ) { return $preparedStatement->rowCount(); }
elseif( $type == INSERT ) { return $this->pdo->lastInsertId(); }
else{ return true; }
}
Your code is a bit outdated. You should use one of the visibility keywords instead of var to declare your properties. In this case you probably want to use protected so that it cant be modified from outside the class but so that any future sub classes can modify it internally. Youll also probably want to add a getter in-case you need to work with PDO directly (which you will - see my final statements below my class example).
Its bad to hard code you PDO connection information in the class. You should pass these in as parameters same as you would if using PDO directly. I would also add the ability to pass in a pre-configured PDO instance as well.
While not required, its a good idea to conform to PSR-0 through PSR-2; Sepcifically in your case im speaking about Class and method naming which should both be camelCase and the first char of the class should be capital. Related to this your code formatting is also ugly particularly your block statements... If thats jsut an issue with copy and paste then ignore that comment.
So overall i would refactor your code to look something like this:
class Db {
protected $pdo;
public function __construct($dsn, $user, $pass, $options = array()) {
if($dsn instanceof PDO) {
// support passing in a PDO instance directly
$this->pdo = $dsn;
} else {
if(is_array($dsn)) {
// array format
if(!empty($options)) {
$dsn['options'] = $options;
}
$dsn = $this->buildDsn($options);
} else {
// string DSN but we need to append connection string options
if(!empty($options)) {
$dsn = $this->buildDsn(array('dsn' => $dsn, 'options' => $options));
}
}
// otherwise just use the string dsn
// ans create PDO
$this->pdo = new PDO($dsn, $user, $pass);
}
// set PDO attributes
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function getConnection()
{
return $this->pdo;
}
protected function buildDsn($options) {
if($isDbParts = isset($options['dbname'], $options['hostname']) || !($isDsn = isset($option['dsn']))) {
throw new Exception('A dsn OR dbname and hostname are required');
}
if($isDsn === true) {
$dsn = $options['dsn'];
} else if {
$format = '%s:dbname=%s;host=%s';
$driver = isset($options['dbtype']) ? $options['dbtype'] : 'mysql';
$dsn = sprintf($format, $options['dbtype'], $options['dbname'], $options['host']);
}
if(isset($options['options'])) {
$opts = array();
foreach($options['options'] as $name => $value) {
$opts[] = $name . '=' . $value;
}
if(!empty($opts)) {
$dsn .= ';' . implode(';', $opts);
}
}
return $dsn;
}
public function preparedExecute( $query, $bind_values = null, $type = null ) {
$preparedStatement = $this->pdo->prepare( $query );
if( $bind_values ) {
$i = 1;
foreach( $bind_values as $bind_value ) {
$preparedStatement->bindValue($i, $bind_value);
$i++;
}
}
$preparedStatement->execute();
if( $type == FETCH_ARRAY ) {
return $preparedStatement->fetchAll();
}
elseif( $type == NUM_ROWS ) {
return $preparedStatement->rowCount();
}
elseif( $type == INSERT ) {
return $this->pdo->lastInsertId();
}
else {
return true;
}
}
}
Lastly, unless this is just for educational purposes I wouldnt do this. There are a ton of different queries that have varying part assemblies which arent considered here so at some point this is not going to support what you need it to do. Instead i would use Doctrine DBAL, Zend_Db or something similar that is going to support greater query complexity through its API. In short, dont reinvent the wheel.
I have developed something similar and it can helps you.
public function select($sql, $array = array(), $fetchMode = PDO::FETCH_ASSOC){
$stmt = $this->prepare($sql);
foreach ($array as $key => $value){
$stmt->bindValue("$key", $value);
}
$stmt->execute();
return $stmt->fetchAll();
}
public function insert($table, $data){
ksort($data);
$fieldNames = implode('`,`', array_keys($data));
$fieldValues = ':' .implode(', :', array_keys($data));
$sql = "INSERT INTO $table (`$fieldNames`) VALUES ($fieldValues)";
$stmt = $this->prepare($sql);
foreach ($data as $key => $value){
$stmt->bindValue(":$key", $value);
}
$stmt->execute();
}
Related
I'm a new in web programming and would ask the advice for code below.
I have the code in class Database. There is it. As you see there's a connect to database with mysqli. And this code work.
function __construct() {
$this->conn = $this->connectDB();
}
function connectDB() {
$conn = mysqli_connect($this->host,$this->user,$this->password,$this->database);
return $conn;
}
function runBaseQuery($query) {
$result = mysqli_query($this->conn,$query);
while($row=mysqli_fetch_assoc($result)) {
$resultset[] = $row;
}
if(!empty($resultset))
return $resultset;
}
function runQuery($query, $param_type, $param_value_array) {
$sql = $this->conn->prepare($query);
$this->bindQueryParams($sql, $param_type, $param_value_array);
$sql->execute();
$result = $sql->get_result();
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$resultset[] = $row;
}
}
if(!empty($resultset)) {
return $resultset;
}
}
function bindQueryParams($sql, $param_type, $param_value_array) {
$param_value_reference[] = & $param_type;
for($i=0; $i<count($param_value_array); $i++) {
$param_value_reference[] = & $param_value_array[$i];
}
call_user_func_array(array(
$sql,
'bind_param'
), $param_value_reference);
}
function insert($query, $param_type, $param_value_array) {
$sql = $this->conn->prepare($query);
$this->bindQueryParams($sql, $param_type, $param_value_array);
$sql->execute();
}
function update($query, $param_type, $param_value_array) {
$sql = $this->conn->prepare($query);
$this->bindQueryParams($sql, $param_type, $param_value_array);
$sql->execute();
}
I have to write this class in PDO. I've done it, but something is wrong. I try to connect my Database and get the error
Fatal error: Uncaught TypeError: PDO::__construct() expects parameter 4
class DB {
private $host = "";
private $user = "";
private $password = "";
private $database = "";
private $pdo;
function __construct() {
$this->pdo = $this->connectDB();
}
function connectDB() {
try
{
$pdo = new PDO($this->host,$this->user,$this->password,$this->database);
}
catch (PDOException $e) {
print "Error!: " . $e->getMessage();
}
die();
}
function runBaseQuery($query)
{
$result = $pdo->query($query);
while ($row = $pdo->fetch(PDO::FETCH_ASSOC)) {
$resultset[] = $row;
}
if (!empty($resultset))
return $resultset;
}
function runQuery($query, $param_type, $param_value_array) {
$sql = $pdo->prepare($query);
$pdo->execute($sql, $param_type, $param_value_array);
$result = $pdo->fetchAll(PDO::FETCH_ASSOC);
if ($result->num_rows > 0) {
while ($row = $pdo->fetchAll(PDO::FETCH_ASSOC)) {
$resultset[] = $row;
}
}
if(!empty($resultset)) {
return $resultset;
}
}
function bindQueryParams($sql, $param_type, $param_value_array) {
$param_value_reference[] = & $param_type;
for($i=0; $i<count($param_value_array); $i++) {
$param_value_reference[] = & $param_value_array[$i];
}
call_user_func_array(array(
$sql,
'bind_param'
), $param_value_reference);
}
function insert($query, $param_type, $param_value_array) {
$sql = $pdo->prepare($query);
$pdo->execute($sql, $param_type, $param_value_array);
}
function update($query, $param_type, $param_value_array) {
$sql = $pdo->prepare($query);
$pdo->execute($sql, $param_type, $param_value_array);
}
}
But my the new code don't work. Where is the problem?
Your new class has multiple problems. The one you are asking about can be solved by understanding how to connect properly with PDO and what DSN is. For this I have to refer you to this awesome article https://phpdelusions.net/pdo#dsn
Take this DSN for example:
mysql:host=localhost;dbname=test;port=3306;charset=utf8mb4
driver^ ^ colon ^param=value pair ^semicolon
You start by specifying which DB driver you would like to use: mysql:. After this comes a list of key-value pairs separated by a semicolon. The order should be host, DB name, and charset. You should specify all of these values.
Your DSN is your first argument to PDO::__construct(), the second and third is username and password respectively. The third one is an array of options.
Your options array should contain at least two values. You need to enable error reporting and switch off emulated prepared statements. These are the recommended settings.
Your connection should look at least similar to this:
$options = [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_EMULATE_PREPARES => false,
];
$this->pdo = new PDO('mysql:host='.$this->host.';dbname='.$this->database.';charset=utf8mb4', $this->user, $this->password, $options);
However, please take note, that the constructor will be called once, and you do not need to put these values in private properties. They can simply be constants or hardcoded in the constructor. You will never need to reuse this values after opening connection.
Your second main mistake is that you are referring in a lot of places to $pdo, but you should be using $this->pdo.
Other notes:
die(); is going to end your whole script. Do not use it.
Do not catch exceptions, just to print out the error message. It defeats the whole purpose of the exceptions.
The method bindQueryParams() seems completely unnecessary. I would recommend you remove it.
runQuery is riddled with mistakes. $pdo->execute() takes in only one argument, which should be the array of values to be bound. There is no need for param type like in mysqli. The while loop is redundant and incorrect.
num_rows does not exist in PDO, and is not needed at all.
None of these method provide any benefit over just plain PDO.
Conclusion:
The class you have written is completely unnecessary and only makes your code more difficult to understand and maintain. While it might have made more sense with mysqli, PDO is simpler to use and does not need to be wrapped in such class. Please read the article linked at the beginning carefully, it will help you a lot.
The PDO connection does not take a 4th parameter for the database name. The DB name is passed in with the host name. So change:
$pdo = new PDO($this->host,$this->user,$this->password,$this->database);
to:
$pdo = new PDO($this->host . ';dbname=' . $this->database, $this->user, $this->password);
For more information see https://www.php.net/manual/en/pdo.connections.php
Additionally, it is unclear what $this->host contains but for PDO if that is just a host path you need to pass in the driver you are using as well, so you might even need to add
mysql:host=
to the start of this. With mysqli this is not required because the only RDBMS mysqli supports is mysql.
So potentially complete answer:
$pdo = new PDO('mysql:host=' . $this->host . ';dbname=' . $this->database, $this->user, $this->password);
I'm trying to write a bit of a controller to do some CRUD with PDO and have this issue where nothing happens and doesn't throw. I am pretty sure i am binding correctly but after dumping out, I can't see any issues with this and the table doesn't update.
Can someone please have a look?.
public function update($query, $array)
{
try
{
$stm = $this->conn->prepare($query);
foreach($array AS $key=>$value)
{
if(gettype($value) == "integer")
{
$stm->bindParam($key,$value,PDO::PARAM_INT);
}
if(gettype($value) == "string")
{
$stm->bindParam($key,$value,PDO::PARAM_STR);
}
}
$stm->execute();
return ($stm->rowCount()<=0) ? FALSE : $stm->rowCount();
}
catch(Exception $e)
{
echo 'Error with the query on line: ' . __LINE__ . ' in file: ' . __FILE__;
}
}
$test = new SQL('127.0.0.1', 'test', '*************', 'mike_test');
$pull = $test->update('UPDATE names SET name=:name WHERE id=:id;',[':name'=>'James',':id'=>2]);
The problem is the use of bindParam() which binds to a variable, and since you bind each parameter to the same variable ($value), you essentially use the last value for all parameters.
Solution: use bindValue().
I can't see any issues with this and the table doesn't update.
Then the ID value doesn't match anything in the table, as it did update on my test system.
You just made you code too bloated. But there is a rule: the more code you write, the more errors you introduce. So make this function this way
public function update($query, $array)
{
$stm = $this->conn->prepare($query);
$stm->execute($array);
return $stm->rowCount();
}
I'm having a bit of trouble with the OOP CRUD method. My POST method is not retrieving or posting data to the DB. And I'm not sure where to look since it does not give any errors to display.
The logic above the form:
$id = $_GET['id'];
//Add Board
$b = new Board();
$userID = $_SESSION['id'];
$boards= $b->loadBoards($userID);
if(isset($_POST['addBoard'])){
try{
$sB = new Board();
$postID = 61;
$boardID = 1;
$sBoard = $sB->savePostToBoard($postID, $boardID);
} catch (Exception $e) {
$error = $e->getMessage();
}
}
This is the form:
<form method="post">
<div class="btn-group" data-toggle="buttons">
<?php foreach($boards as $key) : ?>
<label class="btn btn-primary active">
<input type="radio" name="option[]"value="
<?php echo $key['boardID'];?>">
<?php echo $key['boardTitle']; ?></label>
<?php endforeach ?>
<input class="btn btn-danger"type="submit" value="Toevoegen"
id="addBoard" name="addBoard">
</div>
</form>
And the class function:
public function getConnection() {
$conn = Db::getInstance();
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
return $conn;
}
private function getInputParameterDataType($value) {
$dataType = PDO::PARAM_STR;
if (is_int($value)) {
$dataType = PDO::PARAM_INT;
} elseif (is_bool($value)) {
$dataType = PDO::PARAM_BOOL;
}
return $dataType;
}
public function savePostToBoard($postID, $boardID)
{
$sql ="UPDATE board SET postID=:". $postID . " WHERE boardID=:boardID";
$statement = $this->getConnection()->prepare($sql);
$statement->bindValue(":boardID",$boardID, $this-
>getInputParameterDataType($boardID));
$statement->bindValue(":postID", $postid);
return $statement->execute();
}
Any feedback is highly appreciated, thanks for taking the time.
Kind regards
*) All your radio buttons have the same id attribute. It should be unique.
*) Give the id "addBoard" to the submit button.
*) Why do you use POST and GET?
$boardID = $_POST['option'];
$postID = $_GET['id'];
*) You missed the $ sign in statement->bindValue(":boardID",boardID);!
*) Posted data type should correspond to db data type. Use a third parameter in bindValue() to define the corresponding data type. And use a function:
private function getInputParameterDataType($value) {
$dataType = PDO::PARAM_STR;
if (is_int($value)) {
$dataType = PDO::PARAM_INT;
} elseif (is_bool($value)) {
$dataType = PDO::PARAM_BOOL;
}
return $dataType;
}
And then call like this:
$statement->bindValue(":boardID",$boardID, $this->getInputParameterDataType($boardID));
*) PDO::prepare() can throw a PDOException OR the value FALSE. So you should handle both cases. I wrote an answer about this: My answer for exception handling of prepare() & execute().
*) Have you made some changes until now? Works everything, or not yet?
Ok, I'll study it now.
Solutions for the new code:
*) Don't use semicolon ";" at the end of sql statements.
*) Update should have this form:
UPDATE [table-name] SET [col1]=:[col1],[col2]=:[col2] WHERE [PK-name]=:[PK-name]
*) For readability: pass the sql statement in a variable and use points to delimit used variables in it. Like:
$sql = "UPDATE board SET postID=:" . $postid . " WHERE boardID=:boardID"
$statement = $conn->prepare($sql);
*) As #teresko reminded you: you didn't pass $boardID to savePostToBoard():
$sBoard = $sB->savePostToBoard($postID, $boardID);
public function savePostToBoard($postID, $boardID) {...}
*) Use either $postID or $postid overall. Right now you are using both forms. Make a choice.
It should work now. Let me know.
Some recommandations:
*) Let your methods do only one thing (if possible). In your question, the connection creation doesn't belong in the method. If you call 20 methods which require a connection, then you have to write the same connection creation code in each of them. In your case, better define a getConnection() method in the Board class.
public function getConnection() {
$conn = Db::getInstance();
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
return $conn;
}
And now you have only this in your savePostOnBoard() by calling it:
public function savePostToBoard($postID) {
$statement = $this->getConnection()->prepare(...);
//...
}
*) In general, it's better to use the constructor for passing the variables used later by the methods. That's the role of the constructor: to initialize, e.g to give initial values to the object properties on creation. Example:
class Auto {
private $color;
private $doors;
private $airConditioning;
public __function __construct($color = 'blue', $doors = 3, $airConditioning = true) {
$this->color = $color;
$this->doors = $doors;
$this->airConditioning = $airConditioning;
}
}
$myAuto = new Auto('red', 4, false);
$hisAuto = new Auto('yellow', 8, true);
Oh, and always give easy-to-follow names to variables, functions, classes, etc. In your case, the upper phrase and this one would apply, for example, like this:
$board = new Board($boardID);
$boardUpdated = $board->update($postID);
See? Nicer names, more logical (following our real-world perception) arrangement of the arguments.
*) I would also recommend you to split your code in methods. This way you achieve a better reusability of code pieces and an elegant, easy-to-follow structure. Maybe something like this in analogy with your code:
public function savePostToBoard($postID, $boardID) {
$sql = "UPDATE board SET postID=:" . $postID . " WHERE boardID=:boardID";
$bindings = array(
':postID' => $postID,
':boardID' => $boardID
);
$statement = $this->execute($sql, $bindings);
return $statement->rowCount();
}
protected function execute($sql, array $bindings = array()) {
$statement = $this->prepareStatement($sql);
$this->bindInputParameters($statement, $bindings);
$this->executePreparedStatement($statement);
return $statement;
}
private function prepareStatement($sql) {
$statement = $this->getConnection()->prepare($sql);
return $statement;
}
private function bindInputParameters($statement, $bindings) {
foreach ($bindings as $key => $value) {
$statement->bindValue(
$this->getInputParameterName($key)
, $value
, $this->getInputParameterDataType($value)
);
}
return $this;
}
private function getInputParameterName($key) {
if (is_int($key)) {
return $key + 1;
}
$trimmed = ltrim($key, ':');
return ':' . $trimmed;
}
private function getInputParameterDataType($value) {
$dataType = PDO::PARAM_STR;
if (is_int($value)) {
$dataType = PDO::PARAM_INT;
} elseif (is_bool($value)) {
$dataType = PDO::PARAM_BOOL;
}
return $dataType;
}
private function executePreparedStatement($statement) {
$statement->execute();
return $this;
}
Good luck with your project!
You really should use these in your PDO instantiation:
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Then you would have seen the syntax error in your query:
for some reason you have VALUES (:postID;) (notice the semicolon).
Also, you are not actually passing $boardID to the savePostToBoard() method at any point. You should add a second prarameter in that method.
As for your general application structure, you really should avoid using singletons to share the database connection and you should separate the domain logic from the persistence logic. Reading this post might be beneficial for you.
My SQL Statement works in phpMyAdmin, but when I use PHP to run it from my webpage, it does nothing.
My code is as below, which always returns true. I have solved this problem, but the primary problem is that the code does not delete the row.
// Delete Area
public function deleteArea($product_area_id){
$this->db->query("
DELETE
FROM product_area
WHERE product_area_id = :product_area_id
LIMIT 1
");
//bind
$this->db->bind(':product_area_id', $product_area_id);
//Execute
if($this->db->execute()){
return true;
} else {
return false;
}
}
My database class:
public function bind($param, $value, $type = null) {
if (is_null ( $type )) {
switch (true) {
case is_int ( $value ) :
$type = PDO::PARAM_INT;
break;
case is_bool ( $value ) :
$type = PDO::PARAM_BOOL;
break;
case is_null ( $value ) :
$type = PDO::PARAM_NULL;
break;
default :
$type = PDO::PARAM_STR;
}
}
$this->stmt->bindValue ( $param, $value, $type );
}
public function query($query) {
$this->stmt = $this->dbh->prepare($query);
}
public function execute(){
return $this->stmt->execute();
}
You're mixing PDO with the mysql_ functions. They do NOT work interoperably. What you need here is rowCount
$this->db->query("
DELETE
FROM product_area
WHERE product_area_id = :product_area_id
LIMIT 1
");
//bind
$this->db->bind(':product_area_id', $product_area_id);
//Execute
$this->db->execute();
if(this->db->rowCount() > 0){
A comment quote from the op (the last comment at the moment):
The primary problem is, it will not delete. even though the sql
statment is correct when tested directly in phpmyadmin. the secondary
problem is, it will also say true. even though it didn't delete.
Machavity solved the secondary problem. the primary remains. -
codenoob
TLDR: // Note: PDOStatement::execute .... Returns TRUE on success or FALSE on failure.
So that is why it returns 1 all the time for you. See the below for 2 files to test this with a change using rowCount() off of a PDOStatement object.
You said in comments that the primary problem remains. I have no problem using the below after sort of hand crafting a database class, because you did not offer one (you mention one). Note the use of try/catch blocks.
Frankly we don't know if you had any exceptions or how you are handling them, or whether or not you have error reporting activated. The below should survive any testing, assuming the pdo object can successfully return the rowCount() value.
Schema for testing:
create table product_area
( product_area_id int primary key,
theName varchar(100) not null
);
-- blockA begin
truncate product_area;
insert product_area (product_area_id,theName) values
(1,'Houston zone B'),(2,'Houston zone BH'),(20,'Houston zone Z');
-- blockA end
Test File:
For the test file, there are only a few lines of code near the top for testing in the section called "Mini test area"
<?php
// pdo_del_test_20160703.php
// related to answering http://stackoverflow.com/questions/38061597 at the moment
error_reporting(E_ALL);
ini_set("display_errors", 1);
include "myPDO_DB.php"; // Database class. The class name = myPDO_DB
// Mini test area BEGIN:
$a1=new aClass();
$ret=$a1->deleteArea(2);
echo "retValue=".$ret."<br>";
// Mini test area ... END
class aClass {
private $db=null;
public function __construct(){
echo "in constructor1<br>";
$this->db=new myPDO_DB();
echo "in constructor2<br>";
//$this->db=null;
echo "in constructor3<br>";
}
public function deleteArea($product_area_id){
$this->db->query("
DELETE
FROM product_area
WHERE product_area_id = :product_area_id
LIMIT 1
");
// Note: PDOStatement::execute .... Returns TRUE on success or FALSE on failure.
//
// so on a Delete call, it just says sure, OK, done with that (if no exception)
// It doesn't give feedback natively as to whether or not a row was actually deleted
//
//bind
$this->db->bind(':product_area_id', $product_area_id);
//Execute
// Don't forget to run schema `blockA` before testing (so that data is there)
if ($this->db->execute()) { // if this function returns anything other than TRUE you have exception problems
$pdo_rowcount=$this->db->stmt->rowCount(); // see http://php.net/manual/en/pdostatement.rowcount.php
// Depending on your systems ability to get pdo.rowCount() to work, consider the following
// Please see http://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_row-count
$this->db->query("select row_count()"); // from mysql "Information Functions". Based on prior mysql call.
$mysql_rowcount=$this->db->execute();
// but note, row_count() in mysql is not exactly a solution for the majority of systems out there.
// http://www.heidisql.com/forum.php?t=9791
// on mysql 5.5 and my 5.6.24, it returns 0
// so depending on your system, you will choose between $pdo_rowcount and $mysql_rowcount
return $pdo_rowcount; // your best bet, bust test it on your setup.
}
else {
// do something
return 0; // you would have already endured an exception though
}
}
}
?>
Database class:
<?php
// myPDO_DB.php
//
error_reporting(E_ALL);
ini_set("display_errors", 1);
include "db_connect_info.php"; // brings in the "defines" .. Shoot for a secure o/s vault in this file.
class myPDO_DB {
// will grab the stub from http://culttt.com/2012/10/01/roll-your-own-pdo-php-class/
//
// and then build your class into it (because you did not provide it)
//
// and then further improve it with try/catch blocks that were lacking
//
private $host = DB_HOST; // these were brought in with the include above. File not shown.
private $user = DB_USER;
private $pass = DB_PASS;
private $dbname = DB_NAME;
private $dbh;
public $stmt; // was made public to get into rowCount(); .... change this for your needs
private $error;
public function __construct(){
// Set DSN
$dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->dbname;
// Set options
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false
);
// Create a new PDO instanace
try{
$this->dbh = new PDO($dsn, $this->user, $this->pass, $options);
echo "Connect Ok<br>";
}
catch(PDOException $e){
$this->error = $e->getMessage();
}
}
public function bind($param, $value, $type = null) {
try {
if (is_null ( $type )) {
switch (true) {
case is_int ( $value ) :
$type = PDO::PARAM_INT;
break;
case is_bool ( $value ) :
$type = PDO::PARAM_BOOL;
break;
case is_null ( $value ) :
$type = PDO::PARAM_NULL;
break;
default :
$type = PDO::PARAM_STR;
}
}
$this->stmt->bindValue ( $param, $value, $type );
}
catch(PDOException $e){
$this->error = $e->getMessage();
throw $e;
}
}
public function query($query) {
try {
$this->stmt = $this->dbh->prepare($query);
}
catch(PDOException $e){
$this->error = $e->getMessage();
throw $e;
}
}
public function execute(){
try {
return $this->stmt->execute();
}
catch(PDOException $e){
$this->error = $e->getMessage();
throw $e;
}
}
}
Sorry I derped. the value I passed in was $Product_area_id and the one I used is $product_area_id. god.
I am still working on my own database class with pdo:
class Database {
private $databaseConnection;
public function __construct($path = "", $dbUsername = "", $dbPassword = ""){
$parts = explode('.',$path);
$documentType = array_pop($parts);
if(($path == "") || ((strcmp($documentType, "sq3") !== 0) && (strcmp($documentType, "sqlite") !== 0))) {
throw new OwnException("The Database must bee .sq3 or .sqlite and Path must be stated");
}
$this->databaseConnection = new PDO('sqlite:' . $path, $dbUsername, $dbPassword);
$this->databaseConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->databaseConnection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$this->databaseConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
self::query('CREATE TABLE IF NOT EXISTS User(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(40) NOT NULL UNIQUE,
numberoflogins INTEGER DEFAULT 0,
bannedstatus BOOLEAN DEFAULT FALSE,
dateofjoining TIME
)');//password field coming soon
//self::query('CREATE TABLE...');
//self::query('CREATE TABLE...');
}
private function query($sql, $params = NULL){
$pdoStatement = $this->databaseConnection->prepare($sql);
$pdoStatement->execute(array_values((array) $params));
return $pdoStatement;
}
public function getObjects($objectTable, $searchedForAttribute, $attributeValue){
$pdoStatement = $this->databaseConnection->prepare("SELECT * FROM $objectTable WHERE $searchedForAttribute = ?");
$pdoStatement->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, $objectTable);
$pdoStatement->execute(array($attributeValue));
$resultObjects = array();
while($resultObject = $pdoStatement->fetch()){
array_push($resultObjects, $resultObject);
}
if(empty($resultObjects)){
return false;
}
return $resultObjects;
}
public function getObject($objectTable, $searchedForAttribute, $attributeValue){
//...returns only the first Object from getObjects()
}
public function insertObject($object){
$objectTable = get_class($object);
$objectData = $object->getAttributes();
return $this->query("INSERT INTO $objectTable("
. join(',', array_keys($objectData)) . ")VALUES("
. str_repeat('?,', count($objectData)-1). '?)', $objectData);
}
public function updateAttribute($objectTable, $setData, $searchedAttribute, $searchedAttributeValue){
...
}
public function updateObject($object){
...
}
public function attributeRemoveObject($objectTable, $searchedForAttribute, $attributeValue){
...
}
public function __destruct(){
unset($this->databaseConnection);
}
}
as you can see there is still no data validation for the functions (and no exception handling, work in progress) such like getObjects() so the variables $objectTable, $searchedForAttribute and $attributeValue going direct into the query. This means no protection against SQL injections.
So I thought it would be quite helpful if I use a static function to validate data before inserting into query:
public static function validate($unsafeData){
//validate $unsafeData
return $safeData
}
Because I want to have the ability to search for usernames with similar names and stuff bin2hex() and hex2bin() is a bad choice and for some attributes like the username it is easy to find some starting points for the validation. For instance I would search for empty space, ', " and =...
But how should I validate the content of a forumpost which contains a lot of signs used for SQL queries to? I mean it could also be a a post about sql itself.
I saw a lot of examples for SQL Injections but all of them missing the point that the main manipulation could also be in the content box.
So how does a forum prevent SQL Injections and Errors referring to the content of a post ?
Your weakest point is here:
public function insertObject($object){
$objectTable = get_class($object);
$objectData = $object->getAttributes();
return $this->query("INSERT INTO $objectTable("
. join(',', array_keys($objectData)) . ")VALUES("
. str_repeat('?,', count($objectData)-1). '?)', $objectData);
}
The best way to avoid SQL injection is using PDO::bindParam. It doesn't matter if a string field contains valid SQL as long as you use prepared queries and bound parameters:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pquery = $pdo->prepare(
'INSERT INTO table(column1, column2) VALUES(:column1, :column2)');
// PDO::PARAM_INT, PDO::PARAM_STR, PDO::PARAM_BOOL, etc.
$pquery->bindValue(':column1', $column1, PDO::PARAM_INT); // error if $column1 isn't integer value
$pquery->bindValue(':column2', $column2, PDO::PARAM_STR); // string are sanitized
$pquery->execute();
For an arbitrary object you have to use some sort of metadata to select the correct PDO::PARAM_X value (default is PDO::PARAM_STR):
<?php
class User
{
public $username = 'foo\'; DROP TABLE User; --';
public $email = 'bar#gmail.com';
public $age = 500;
}
function getColumnType()
{
return PDO::PARAM_STR; // just for demo
}
$object = new User;
$ref = new ReflectionObject($object);
$table = $ref->getShortName(); // to avoid FQN
$properties = $ref->getProperties(ReflectionProperty::IS_PUBLIC);
$params = []; $columns = [];
foreach ($properties as $property) {
$params[] = ':'.($columns[] = $property->getName());
}
// in memory db for demo
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('create table User(id INTEGER PRIMARY_KEY, username VARCHAR(250) NOT NULL,email VARCHAR(250) NOT NULL,age INT)');
// your answer
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pquery = $pdo->prepare("INSERT INTO $table(".implode(',', $columns).") VALUES(".implode(',', $params).")");
foreach ($properties as $property) {
$paramName = ':'.$property->getName();
$paramValue = $property->getValue($object);
$paramType = getColumnType($object, $property); // return PDO::PARAM_X
$pquery->bindValue($paramName, $paramValue, $paramType);
}
$pquery->execute();
// check
$all = $pdo->prepare('select * from User');
$all->execute();
var_dump($all->fetchAll(PDO::FETCH_CLASS, 'User'));
Output:
array(1) {
[0] =>
class User#10 (4) {
public $id =>
NULL
public $username =>
string(25) "foo'; DROP TABLE User; --"
public $email =>
string(13) "bar#gmail.com"
public $age =>
string(3) "500"
}
}
You must implement getColumnType to get the correct column data type, for example, parsing annotated comments. But at this point you better use some ORM like Eloquent or Doctrine.