2

Ok so im new to binding, here is some code that works. I learned this format from a tutorial but i imagine there is more efficent ways to do it. In my example there is 4 names but in reality i will be doing a lot of inserts and updates in a project im working on that will have 20 or so fields. I like this approach for its clarity but obviously when your talking 20 fields or more it does take a lot of real estate. Lets look at my code first.

Here are the functions it uses:

// prepare the statement
public function query($query){
    $this->stmt = $this->dbh->prepare($query);
}

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;
        }
    }
// run the binding process
$this->stmt->bindValue($param, $value, $type);
}

// execute the prepared statement
public function execute(){
    return $this->stmt->execute();
}

and now the actual code

$database->query("
    INSERT INTO users(
        user_name,
        user_password_hash,
        user_email,
        user_activation_hash
)

    VALUES(
        :user_name,
        :user_password_hash,
        :user_email,
        :user_activation_hash
    )
");

// bind the values
$database->bind(":user_name", "$this->user_name");
$database->bind(":user_password_hash", "$this->user_password_hash");
$database->bind(":user_email", "$this->user_email");
$database->bind(":user_activation_hash", "$this->user_activation_hash");

// execute the statement and insert the values into the database
$database->execute();

it just cries out for a loop, especially since i have a habit of calling post fields, input fields, variables and placeholders the same name, not sure if that is a good or a bad thing but i find its helpful for me when dealing with large forms which i will be.

in any case i could do something like this:

$placeholder_array = array(
    "user_name"               => "\$this->user_name",
    "user_password_hash"      => "\$this->user_password_hash",
    "user_email"              => "\$this->user_email",
    "user_activation_hash"    => "\$this->user_activation_hash"
);

// well use this copy to edit the array keys and keep original for the binding
$placeholder_copy = $placeholder_array;


// turn the array into a string i.e user_name, user_password_hash....
$fields = implode (", ", array_keys($placeholder_array));

// foreach to add : placeholder prefix for binding
foreach ($placeholder_copy as $key => $value){
$placeholder_copy [':'.$key] = $value;
unset($placeholder_copy[$key]);
}

// turn the copy array which has prefix :user_name into a string 
$placeholders = implode (", ", array_keys($placeholder_copy));

$database->query("
    INSERT INTO users($fields)
    VALUES($placeholders) 
");

// bind the values
foreach ($placeholder_copy as $bind_values => $value){
    echo '$database->bind("'.$bind_values.'", "'.$value.'");' . "<br />";
}

// execute the statement and insert the values into the database
$database->execute();

i could then turn this into a function with parameters for passing in the associative array and the table name to keep my main code much cleaner.

Now imagine i am going to be doing any amount of these as the project im working on involves tons of big forms submitting data to users. I'm new to PDO and trying to grasp it so there maybe a simpler way of structuring these types of queries, i had a look on google and stackflow but i didnt really get what they were doing so i thought doing my own one would allow people to explain to me better what is going on, i would rather get this right starting my project than have to go back and change everything later. So is there a better approach or is this one ok?

Really appreciate any feedback and im glad now i took peoples advice on here and made the move to PDO.

user1547410
  • 863
  • 7
  • 27
  • 58

2 Answers2

2

No, sadly, but PDO offers no help for the matter.
Yet your own approach doesn't seem to me an efficient one either.

First of all, let me indicate that your set of functions is quite useless. I understand no such direct rewriting of API functions. PDO already doing all these things.

With raw PDO you can have even more concise code:

$stm  = $pdo->prepare("INSERT INTO users VALUES(NULL,?,?,?,?)");
$data = array($this->user_name, 
              $this->user_password_hash, 
              $this->user_email, 
              $this->user_activation_hash
);
$stm->execute($data);

Regarding your dynamical query building, I find it too much bloated yet still insecure. Some flaws are

  • there is no point in bothering with ":named" placeholders as they are supposed to be human-readable but no human supposed to read them here. Not to mention PDO will convert them back into ?s before sending to mysql
  • another objection against named placeholders - while Mysql (as well as HTTP) can let you have a space in a field name all right, a placeholder with a space will just crash your query.
  • there is no real benefit in this code from your approach of the equal names - you are bound to write them by hand a dozen times each, mind you.
  • yet if it would be used, you have no white list to check your field list against, which could be a great security breach
  • too much code
  • no support for UPDATE query which could be extremely handy as well (if you are using mysql)
  • when you put this code into function, you will slip to the same bad grounds as others, tempted by the possibility of saving you whole two words!

Here is my earlier approach for the matter:

$allowed = array(
    "user_name", "user_password_hash", "user_email", "user_activation_hash"
);
$sql = "INSERT INTO users SET ".pdoSet($allowed, $values);
$stmt = $dbh->prepare($sql);
$stmt->execute($values);

And here goes my current approach, the best of the best (though implemented using mysqli, not PDO), using custom placeholder for the array data:

$placeholder_array = array(
    "user_name"               => $this->user_name,
    "user_password_hash"      => $this->user_password_hash,
    "user_email"              => $this->user_email,
    "user_activation_hash"    => $this->user_activation_hash
);
$db->query("INSERT INTO users SET ?u", $placeholder_array);

or, in case of direct connection between form fields and SQL columns

$allowed = array(
    "user_name", "user_password_hash", "user_email", "user_activation_hash"
);
$insert = $db->filterArray($_POST,$allowed);
$db->query("INSERT INTO users SET ?u", $insert);

This way let me use INSERT IGNORE, INSERT DELAYED, INSERT.. ON DUPLICATE UPDATE, UPDATE, UPDATE with join, and innumerable other options, supporting full SQL language.

Community
  • 1
  • 1
Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
1

What I've done recently and continuing to improve, is building a helper class to simplify PDO based SQL statements in my applications.

Lets take a tour with your example. You want to insert data into a user table:

INSERT INTO users(
    user_name,
    user_password_hash,
    user_email,
    user_activation_hash
) VALUES(
    :user_name,
    :user_password_hash,
    :user_email,
    :user_activation_hash
)

For inserting a single record, my SQL is constructed like this:

function myInsertSingle($PDO, $table, $ins_array) {
    $SQL = "INSERT INTO `".$table."` (".dbFieldList($ins_array)
            .") VALUES (".dbValuePList($ins_array).")";

...continued with preparing.

  • $PDO: My PDO connection.
  • $table: The table I want to insert to.
  • $ins_array: A structured array for inserting data.

For your example, the array $ins_array will look like this:

$ins_array = array(
    "user_name"               => "your_user",
    "user_password_hash"      => "your_pw_hash",
    "user_email"              => "your@mail.xyz",
    "user_activation_hash"    => "your_other_Hash"
);

Notice the similarity to your array!

Two functions are involved. The first one gives me a list of (escaped) fieldnames.

function dbFieldList($fields) {
    $set = '';
    foreach ($fields as $field => $item) {
        $set .= "`".$field."`,";
    }   
    return rtrim($set, ',');
}

Basically from the above array example, the function returns

`user_name`, `user_password_hash`, `user_email`, `user_activation_hash`

...which is the list of the fields needed in the INSERT-statement.

The other one does something similar for the values.

function dbValuePList($fields) {
    $set = '';
    foreach ($fields as $field => $item) {
        $set .= ":".$field.",";
    }   
    return rtrim($set, ',');
}

You guessed it, the result looks like this:

:user_name, :user_password_hash, :user_email, :user_activation_hash

Now you have your SQL statement which you can prepare. To bind the values, if I understand your class correctly, just use a loop which looks like this:

foreach ($ins_array as $field => $item) {
    $PDO->bind(':'.$field,$item);
}

Now you can execute the statement.

Summary. Basically with my approach a single INSERT statement is reduced to this:

$ins_array = array(
    "user_name"               => "your_user",
    "user_password_hash"      => "your_pw_hash",
    "user_email"              => "your@mail.xyz",
    "user_activation_hash"    => "your_other_Hash"
);

myInsertSingle($PDO, 'your_table', $ins_array);

If this looks just like another piece of overhead, keep in mind, I use these functions in other SQL statements like SELECT, UPDATE, DELETE and so on.

Bjoern
  • 15,934
  • 4
  • 43
  • 48
  • These dedicated INSERT, UPDATE, DELETE functions turns out to be inflexible yet quite useless, adding no real benefit. It's better to create a custom *placeholder* for the array type, to keep SQL intact yet keep code concise. – Your Common Sense Aug 20 '13 at 06:46
  • I partially disagree, since I actually _have_ benefits in some larger applications doing it this way: (1) It drastically simplifies using common SQL statements whereever the app needs them. (2) It centralises the database related tasks, which makes general changes and additions easier. P.e., as it is, I currently count the queries and trace performance this way, which would be quite a task adding this in hundreds of scripts. In terms of conciseness I agree, but I haven't found a better and more concise way yet without loosing the benefits mentioned above. – Bjoern Aug 20 '13 at 07:23
  • You don't need no dedicated `insert()` function for all this stuff. a common `query()` is enough. See my answer for details. – Your Common Sense Aug 20 '13 at 07:28
  • Also, your code *attracts* insecurity, via table name for example. One who uses your API, would not mind to add a table name from user input, thinking it is already safe. While attempt to concatenate it into a regular query will alert him. – Your Common Sense Aug 20 '13 at 07:32
  • You're tackling the OPs question from a different perspective. He is already using an API, and wants to make his/her live easier while shortening the code needed to talk to the API, and thats what my answer aims at, no more, no less. +1 for you to have written an API yourself tackling this issue, this gives you a deeper insight into the issue at hand. – Bjoern Aug 20 '13 at 07:56
  • Yes, I am always tracking *any* question from the real life usage perspective. He is already using API which is wrong. Instead of helping him to use a wrong API, I am showing the right way. The very idea of using dedicated `insert()` function is wrong – Your Common Sense Aug 20 '13 at 08:03
  • thanks guys for both of your replies, very insightful and helpful. regarding the passing of the table name, it was mentioned that might be a security risk, should i escape that with trim like we did for the field values functions? – user1547410 Aug 21 '13 at 00:15
  • I don't think you need to trim it, but you should escape it. This avoids conflicts with possible keywords. The commenter simply stated that if it is used as an API, people might use it directly from unfiltered user input, which plainly shouldn't be done at all. – Bjoern Aug 21 '13 at 04:10