Search code examples
phpmysqlisql-update

"Build" a prepared SQL statement based on user-inputs with PHP


This question is a bit hard to phrase, but basically I have a form. It's an account information form where a person can change the information associated with their account. The catch is that the entire form gets submitted with each input being optional (Leave input empty for no-change) . So for example with this form you can change your city, address, and phone. However a person can choose to only fill out city, press submit and just that info gets updated, all other info remains the same.

This might make more sense with the code in hand, here is the PHP I am using to accomplish this task; There are 4 lines with comments that contain code, these were left in since they were an attempt at answering my own question; they may help explain what I'm trying to achieve:

PHP

// SQL HELPER
function prepared_Query($con, $sql, $params, $types = ""){
    $types = $types ?: str_repeat("s", count($params));
    $stmt   = $con -> prepare($sql);
    $stmt -> bind_param($types, ...$params);
    $stmt -> execute();
    return $stmt;
}

// TRIM ALL POST VARS
function trim_Val(&$val){
    $val = trim($val);
}
array_filter($_POST, 'trim_Val');

// SANITIZE ALL INPUTS 
$filter = ['filter' => FILTER_SANITIZE_STRING, 'flags' => FILTER_FLAG_ENCODE_HIGH | FILTER_FLAG_ENCODE_LOW];
$inputs = ['prov', 'city', 'addr', 'code', 'phone', 'phone_alt'];
$keys       = array_fill_keys($inputs, $filter);
$values    = filter_input_array(INPUT_POST, $keys);
unset($filter, $inputs, $keys); // cleanup

$sqlA   = [];
foreach($values as $key=>$val){
    if($val){
        $$key   = $val;
        //$sqlA[]   = "meta_".$key." = ?";
    }
}

//$sqlA = implode(", ", $sqlA);
//$sql  = "UPDATE _MAIN_meta SET ".$sqlA." WHERE meta_user = ?;";
//$stmt = prepared_Query($con, $sql, [$$key, $uid]);

if(!empty($prov)){
    $sql    = "UPDATE _MAIN_meta SET meta_prov = ? WHERE meta_user = ?;";
    $stmt   = prepared_Query($con, $sql, [$prov, $uid]);
    $stmt   -> close();
    echo "Updated Province";
}
if(!empty($city)){
    $sql    = "UPDATE _MAIN_meta SET meta_city = ? WHERE meta_user = ?;";
    $stmt   = prepared_Query($con, $sql, [$city, $uid]);
    $stmt   -> close();
    echo "Updated City";
}
if(!empty($addr)){
    $sql    = "UPDATE _MAIN_meta SET meta_addr = ? WHERE meta_user = ?;";
    $stmt   = prepared_Query($con, $sql, [$addr, $uid]);
    $stmt   -> close();
    echo "Updated Address";
}
if(!empty($code)){
    $sql    = "UPDATE _MAIN_meta SET meta_code = ? WHERE meta_user = ?;";
    $stmt   = prepared_Query($con, $sql, [$code, $uid]);
    $stmt   -> close();
    echo "Updated Postal Code";
}
if(!empty($phone)){
    $sql    = "UPDATE _MAIN_meta SET meta_phone = ? WHERE meta_user = ?;";
    $stmt   = prepared_Query($con, $sql, [$phone, $uid]);
    $stmt   -> close();
    echo "Updated Phone";
}
if(!empty($phone_alt)){
    $sql    = "UPDATE _MAIN_meta SET meta_phone_alt = ? WHERE meta_user = ?;";
    $stmt   = prepared_Query($con, $sql, [$phone_alt, $uid]);
    $stmt   -> close();
    echo "Updated Alt. Phone";
}
$con -> close();

As you can probably tell I repeat that last function over and over again, and it seems silly to run that $stmt repeatedly when it could be accomplished with a single execute. From what I've learnt in my time with PHP and programming fundamentals, this is very WET code, where T is no longer twice but 6x too much. In an attempt to solve this issue I created an array, then foreach input that was set; add the column name to that array. Once finished implode the array using a comma separator. Finally create the SQL statement using that imploded array.

This outputs a dynamically built SQL statement based on whatever inputs are set. So if City and Address contain data the statement would look like this:

$sql = "UPDATE _MAIN_meta SET meta_city = ?, meta_addr = ? WHERE meta_user = ?;";

Now I have this working up to this point, depending on what inputs I've filled out the final output of the SQL statement is exactly what I want it to be.

The problem...

...I have is when it comes to binding that statement to variable-Variables. I don't know if its even possible to get ALL of the set var-vars into the parameters of this prepared_Query() function, when I have no idea which ones are set.

Possibly my logic is flawed, but I was so close to getting this to work I have to ask if there is a way I can accomplish this, or if it even makes sense doing it this way. It makes sense to me, but I've gone down some pretty nasty blackholes with PHP before. I'm sure there is someone out there with a bit more common sense than I.. when it comes to doing something like this.

So my question is, how can I bind variable Variables to a prepared SQL statement when each one is optional. Is there any way to not have to write a separate statement for each input? Or a shorter/faster way to do this?

These SO question are similar but do not answer the question I have:

dynamically generating an SQL statement based on user form input

Dynamic SQL SELECT Statement with PHP based on user options

There seems to be a few other questions out there phrased like this, but I could not find a clear example of someone trying to solve this same sort of problem. This is what makes me think my logic is somehow flawed.

Can anyone point me in the right direction, please?

Note: I think I've pasted everything relevant to the question, let me know if something is not clear.


Solution

  • Both other answers are almost good.

    The answer from @sh4dowb is good for constructing SELECT queries where could be different conditions that are hard to automatize. But for the UPDATE query it's no use to pick every clause by hand, a loop would be a much cleaner solution.

    The approach demonstrated by @ooa is better but they forgot to create the actual update clauses. Besides, it is using these despicable variable variables.

    But the right solution is almost there. We will use my example for PDO that accomplishes the same thing, How to create a prepared statement for UPDATE query:

    // the list of allowed field names
    $allowed = ['prov', 'city', 'addr', 'code', 'phone', 'phone_alt'];
    // initialize an array with values:
    $params = [];
    // initialize a string for `fieldname` = ? pairs
    $setStr = "";
    // loop over source data array
    foreach ($allowed as $key)
    {
        if (!empty($_POST[$key]))
        {
            $setStr .= "`$key` = ?,";
            $params[] = $_POST[$key];
        }
    }
    $setStr = rtrim($setStr, ","); // removing the trailing comma
    $params[] = $_POST['id']; // adding value for id in WHERE clause
    
    $sql = "UPDATE _MAIN_meta SET $setStr WHERE main_user = ?";
    $db->prepare($sql)->execute([$params]);
    

    And for goodness sake, forget about variable variables. There is absolutely no use case for them, yet they can do a considerable harm. $_POST array is an excellent source for your data, nothing else is needed.