MySQLi准备了语句与查询

It seems like every question on this topic is the difference between mysqli prepared statements and straight mysqli queries in PHP, but nothing on what to do when prepared statements don't meet your needs.

A prepared statement is certainly the way to go when performing a simple query:

$stmt = $connection->prepare("SELECT * FROM my_table WHERE id = ?");

But what about when things get more complicated? From the PHP manual:

However, [markers] are not allowed for identifiers (such as table or column names), in the select list that names the columns to be returned by a SELECT statement, or to specify both operands of a binary operator such as the = equal sign.

This becomes a problem with complicated queries that do need to specify both operands of a binary operator, or some of the other restrictions that mysqli_prepare has.

In my case, I need to perform some queries to return results for blog entries (This is a simplified example, my connection variable is actually a private property of a blog class, but you get the idea):

$query = $connection->query("SELECT * FROM my_table WHERE $field = '$search'");

In this example, the $field variable is what column to search by, and the $search variable is what to search for. This type of query is not possible with prepared statements.

I've done a lot of careful planning for functions such as these, and since I know that there are only X amount of columns to search by, I use conditionals to check that $field is equal to one of those columns, and mysqli_real_escape_string to escape any possible quote characters. But is this good practice? Based on what I've read and answers here on SO, you should always use prepared statements, but I have never seen complicated queries in those examples. Is there a better way to prevent SQL injection, a more advanced way to use prepared statements, or should I just stick to very careful validation here?

Yes and no: It is necessary to check the $field variable agains a white-list - that is the only way to prevent sql injection - but there is no point in using mysqli_real_escape_string on the $field variable. If your column name is a reserved word or starts for example with a number, you should quote it in backticks but that is it.

You should still use a prepared statement for the $search variable, although here mysqli_real_escape_string would also do (instead of a prepared statement, not both).

Regarding:

$query = $connection->query("SELECT * FROM my_table WHERE $field = '$search'");

This type of query is not possible with prepared statements.

It is possible to achieve this using prepared statements. As long as the input doesn't come from the client, it is (somewhat) safe to use concatenation on strings when you perform a query, for example:

'SELECT * FROM `my_table` WHERE `'.$field.'` = ?'

Note that I've included the ? marker for my prepared statement, and it won't break my query as long as $field is a valid column name.

However, it's a bad practice to do so, instead you should implement a switch or an if … else block if you want to perform your query on different column or table names. Example:

switch($columnId) {
    case 0:
        $q = 'SELECT * FROM `my_table` WHERE `column0` = ?';
        break;
    case 1:
        $q = 'SELECT * FROM `my_table` WHERE `column1` = ?';
    …
}

Update:

Or use a whitelist with your column names, as hakre suggests:

$columns = ['column0', 'column1', 'column2'];
$columnId = $_GET['column'];

if ( isset($columns[$columnId]) ) $field = $columnId;
else throw new Exception('Column not defined');

$q = 'SELECT * FROM `my_table` WHERE `'.$columns[$columnId].'` = ?';

Or

if ( isset($columns[$columnId]) ) $field = $columns[$columnId];
else throw new Exception('Column not defined');

$q = 'SELECT * FROM `my_table` WHERE `'.$field.'` = ?';

In this case I used a numeric index, you can use a key o whatever serves you better.

But if the input comes from the user you should always use prepared statements. It's either that or manually scape your strings.