wpdb::prepare()publicWP 2.3.0

Allows writing an SQL query with sanitization of the parameters passed to it.

In the query string, instead of the passed parameter, you need to use a placeholder:

  • %d (integer)
  • %f (float)
  • %s (string)

Also, for each of the placeholders, a PHP variable must be specified that will replace the placeholder. When replacing, the variable will be sanitized. The syntax is similar to sprintf().

Since WP 3.5, at least 2 parameters must be passed: the query and the variable value, otherwise there will be a PHP error (User Notice).

Quotes for placeholders %s and '%s'.

Placeholders can be in quotes or without them: WHERE field = %s or WHERE field = '%s'. It is customary not to use quotes.

echo $wpdb->prepare( "foo = %s", 'a' );   // foo = 'a'
echo $wpdb->prepare( "foo = '%s'", 'a' ); // foo = 'a'
Parameter for each placeholder.

A parameter must be specified for each placeholder.

echo $wpdb->prepare( 'foo = %s AND bar = %s', 'a' );
echo $wpdb->prepare( 'foo = %1$s AND bar = %1$s', 'a' );
// in both cases we will see an error:
// User Notice: wpdb::prepare was called incorrectly.
// The query does not contain the correct number of placeholders (2)
// for the number of arguments passed (1).
Positional placeholders %1$s.

For compatibility with older versions: positional placeholders (for example, %1$s, %5s) are processed differently - they do not have quotes added, so they must be supplied with the correct quotes in the query string.

echo $wpdb->prepare( 'foo = %1$s', 'a"a' );   // foo = a\"a
echo $wpdb->prepare( 'foo = "%1$s"', 'a"a' ); // foo = "a\"a"
echo $wpdb->prepare( 'foo = %1s', 'a"a' );    // foo = a\"a
echo $wpdb->prepare( 'foo = %s', 'a"a' );     // foo = 'a\"a'
The sign %

The sign % in the query string that does not relate to a placeholder should be written as %%.

echo $wpdb->prepare( "%foo AND id = %d", 2 ); // User Notice: wpdb::prepare was called incorrectly.
echo $wpdb->prepare( "%%foo AND id = %d", 2 ); // %foo AND id = 2
% in LIKE syntax

Percentage wildcard signs % in LIKE syntax should be specified through a substitution parameter containing the full LIKE string, not directly in the query. Also see wpdb::esc_like().

$like = '%'. $wpdb->esc_like( "bar's" ) .'%end';
echo $wpdb->prepare( "foo LIKE %s", $like ); // foo LIKE '{a0d1d}bar\'s{a0d1d}end'

SQL injection

In SQL, there is a concept called "injection" (inserting SQL code into a query). This can be done when dynamic data is passed in the query. For example, a value from an input field is passed in the query, and this field can contain data that will ultimately become part of the SQL query. This way, one can inject into the query and spoil something or simply disrupt the code of the query itself. It looks like this:

$sql = "SELECT * FROM table WHERE id = '$var'";

Now, if var = 2' AND id = (DROP TABLE table2) then the resulting query will look like this:

SELECT * FROM table WHERE id = '2' AND id = (DROP TABLE table2)

Thus, one can inject into the query itself and change it. To prevent this from happening, queries with passed variables must be processed using the prepare() method:

$sql = $wpdb->prepare( "SELECT * FROM table WHERE id = %s", $var );

esc_sql()

In addition to the $wpdb->prepare() method, a query can be sanitized using the function esc_sql(). However, "prepare" is preferable because it fixes some formatting errors.

$name   = esc_sql( $name );
$status = esc_sql( $status );

$wpdb->get_var( "SELECT something FROM table WHERE foo = '$name' and status = '$status'" );

IMPORTANT! After esc_sql(), the sanitized string can only be used inside quotes '' or "". That is, it should be written as field = '$value', not field = $value, where $value = esc_sql( $value );

Method of the class: wpdb{}

No Hooks.

Returns

String|null. Sanitized query string, if there is a query to prepare.

Usage

global $wpdb;
$wpdb->prepare( $query, ...$args );
$query(string) (required)

The query string. It can use placeholders:

  • %d - number
  • %s - string
  • %f - floating point number (floating point number, since version 3.3).
...$args(string/number/array)

Variables that will be used to replace the placeholders %s %d %f in the query string.

These variables can be specified as comma-separated (like additional function parameters) or in an array:

  • $wpdb->prepare( 'query', $param1, $param2 )
  • $wpdb->prepare( 'query', [ $param1, $param2 ] ).

Examples

0

#1 Demo of sanitizing SQL query

$sql = $wpdb->prepare(
	"SELECT * FROM `table` WHERE `column` = %s AND `field` = %d OR `other_field` LIKE %s",
	[ 'foo', 1337, '%bar' ]
);

$wpdb->get_results( $sql );
$sql = $wpdb->prepare(
	"SELECT DATE_FORMAT( `field`, '%%c' ) FROM `table` WHERE `column` = %s",
	'column_val'
);

$wpdb->get_results( $sql );
0

#2 Add a custom field to post 10

In this example you may see there is no need to take care of escaping quotes and other things that can harm the query.

$metakey = "'crash' database";
$metavalue = "WordPress can 'break' the Database if the query is not escaped";

$wpdb->query( $wpdb->prepare(
	"INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) VALUES ( %d, %s, %s )",
	10, $metakey, $metavalue
) );
0

#3 Passing parameters as an array

This is the same example, only here all variables are passed in the second parameter as an array.

Passing parameters as an array can be useful when we don't know in advance the number of arguments to pass.

The function will also accept an array of un-sanitized values, though, like this:

$wpdb->prepare( 
	"SELECT id FROM $wpdb->posts WHERE id > %d AND `post_status` = %s", 
	array( $min_id, $status )
)

That can be useful in certain circumstances, like when you have a multi-dimensional array where each sub-array contains a different number of items, and so you need to build the placeholders dynamically:

foreach ( $new_status_post_id_map as $new_status => $wordcamp_ids ) {

	$ids_pholders   = implode( ', ', array_fill( 0, count( $wordcamp_ids ), '%d' ) );
	$prepare_values = array_merge( array( $new_status ), $wordcamp_ids );

	$wpdb->query( $wpdb->prepare( "
		UPDATE `$table_name`
		SET `post_status` = %s
		WHERE ID IN ( $ids_pholders )",
		$prepare_values
	) );
}

So if a sub-array has 2 items, then $wordcamp_id_placeholders will be '%d, %d', and if the next array has 4 items, then its placeholder string would be '%d, %d, %d, %d'.

0

#4 Argument swapping is not supported

You can not reuse the same argument several times in a prepare statement.

For example, this does NOT WORK but throws an error because the number of placeholders does not match the number of arguments passed:

// Does NOT work due to not enough arguments being passed.
$post_date = 'post_date';
$search_string = 'search_string';

echo $wpdb->prepare(
	'SELECT * FROM table_name WHERE `post_date` > %1$s AND `post_title` LIKE %2$s OR `post_content` LIKE %2$s',
	$post_date,
	$search_string
);

/* Result:

SELECT * FROM table_name WHERE `post_date` > post_date AND `post_title` LIKE search_string OR `post_content` LIKE search_string

PHP User Notice: Function wpdb::prepare was called incorrectly. The query does not contain the correct number of placeholders (3)
*/

Instead, you need to pass each argument individually:

$post_date = 'post_date';
$search_string = 'search_string';

echo $wpdb->prepare(
	'SELECT * FROM table_name WHERE post_date > %1$s AND post_title LIKE %2$s OR post_content LIKE %3$s',
	$post_date,
	$search_string,
	$search_string
);

/*
SELECT * FROM table_name WHERE post_date > post_date AND post_title LIKE search_string OR post_content LIKE search_string
*/

There is no string escape (tested WP 6.0):

echo $wpdb->prepare( 'post_date = %1$s', 'post" date' ); // post\" date
echo $wpdb->prepare( 'post_date = %s', 'post" date' );   // post_date = 'post\" date'
0

#5 Available placeholders

%s – string (value is escaped and wrapped in quotes)
%d – integer
%f – float
%% – % sign

LIKE Statements – use esc_like() and use placeholder % in arg-value, not inside the query

$my_domain = 'example.com';

$sql = $wpdb->prepare(
	"SELECT * FROM $wpdb->options WHERE option_value LIKE %s;",
	'%' . $wpdb->esc_like( $my_domain ) . '%'
);
0

#6 Clearing values for WHERE IN conditions

This is useful when you have an array of values that you want to pass to the IN condition of the query. The number of array elements can be different, so you have to create the placeholders dynamically:

$in_values = [ 'one', 'two' ];

$in_pholders = implode( ',', array_fill( 0, count( $in_values ), '%s' ) );

$sql = $wpdb->prepare( 
	"SELECT $wpdb->posts WHERE post_type = %s WHERE post_name IN ( $in_pholders )",
	[ 'page', ...$in_values ]
);

echo $sql;  

// SELECT wp_posts WHERE post_type = 'page' WHERE post_name IN ( 'one','two' )

Changelog

Since 2.3.0 Introduced.
Since 5.3.0 Formalized the existing and already documented ...$args parameter by updating the function signature. The second parameter was changed from $args to ...$args.
Since 6.2.0 Added %i for identifiers, e.g. table or field names. Check support via wpdb::has_cap('identifier_placeholders'). This preserves compatibility with sprintf(), as the C version uses %d and $i as a signed integer, whereas PHP only supports %d.

wpdb::prepare() code WP 6.8.3

public function prepare( $query, ...$args ) {
	if ( is_null( $query ) ) {
		return;
	}

	/*
	 * This is not meant to be foolproof -- but it will catch obviously incorrect usage.
	 *
	 * Note: str_contains() is not used here, as this file can be included
	 * directly outside of WordPress core, e.g. by HyperDB, in which case
	 * the polyfills from wp-includes/compat.php are not loaded.
	 */
	if ( false === strpos( $query, '%' ) ) {
		wp_load_translations_early();
		_doing_it_wrong(
			'wpdb::prepare',
			sprintf(
				/* translators: %s: wpdb::prepare() */
				__( 'The query argument of %s must have a placeholder.' ),
				'wpdb::prepare()'
			),
			'3.9.0'
		);
	}

	/*
	 * Specify the formatting allowed in a placeholder. The following are allowed:
	 *
	 * - Sign specifier, e.g. $+d
	 * - Numbered placeholders, e.g. %1$s
	 * - Padding specifier, including custom padding characters, e.g. %05s, %'#5s
	 * - Alignment specifier, e.g. %05-s
	 * - Precision specifier, e.g. %.2f
	 */
	$allowed_format = '(?:[1-9][0-9]*[$])?[-+0-9]*(?: |0|\'.)?[-+0-9]*(?:\.[0-9]+)?';

	/*
	 * If a %s placeholder already has quotes around it, removing the existing quotes
	 * and re-inserting them ensures the quotes are consistent.
	 *
	 * For backward compatibility, this is only applied to %s, and not to placeholders like %1$s,
	 * which are frequently used in the middle of longer strings, or as table name placeholders.
	 */
	$query = str_replace( "'%s'", '%s', $query ); // Strip any existing single quotes.
	$query = str_replace( '"%s"', '%s', $query ); // Strip any existing double quotes.

	// Escape any unescaped percents (i.e. anything unrecognised).
	$query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdfFi]))/", '%%\\1', $query );

	// Extract placeholders from the query.
	$split_query = preg_split( "/(^|[^%]|(?:%%)+)(%(?:$allowed_format)?[sdfFi])/", $query, -1, PREG_SPLIT_DELIM_CAPTURE );

	$split_query_count = count( $split_query );

	/*
	 * Split always returns with 1 value before the first placeholder (even with $query = "%s"),
	 * then 3 additional values per placeholder.
	 */
	$placeholder_count = ( ( $split_query_count - 1 ) / 3 );

	// If args were passed as an array, as in vsprintf(), move them up.
	$passed_as_array = ( isset( $args[0] ) && is_array( $args[0] ) && 1 === count( $args ) );
	if ( $passed_as_array ) {
		$args = $args[0];
	}

	$new_query       = '';
	$key             = 2; // Keys 0 and 1 in $split_query contain values before the first placeholder.
	$arg_id          = 0;
	$arg_identifiers = array();
	$arg_strings     = array();

	while ( $key < $split_query_count ) {
		$placeholder = $split_query[ $key ];

		$format = substr( $placeholder, 1, -1 );
		$type   = substr( $placeholder, -1 );

		if ( 'f' === $type && true === $this->allow_unsafe_unquoted_parameters
			/*
			 * Note: str_ends_with() is not used here, as this file can be included
			 * directly outside of WordPress core, e.g. by HyperDB, in which case
			 * the polyfills from wp-includes/compat.php are not loaded.
			 */
			&& '%' === substr( $split_query[ $key - 1 ], -1, 1 )
		) {

			/*
			 * Before WP 6.2 the "force floats to be locale-unaware" RegEx didn't
			 * convert "%%%f" to "%%%F" (note the uppercase F).
			 * This was because it didn't check to see if the leading "%" was escaped.
			 * And because the "Escape any unescaped percents" RegEx used "[sdF]" in its
			 * negative lookahead assertion, when there was an odd number of "%", it added
			 * an extra "%", to give the fully escaped "%%%%f" (not a placeholder).
			 */

			$s = $split_query[ $key - 2 ] . $split_query[ $key - 1 ];
			$k = 1;
			$l = strlen( $s );
			while ( $k <= $l && '%' === $s[ $l - $k ] ) {
				++$k;
			}

			$placeholder = '%' . ( $k % 2 ? '%' : '' ) . $format . $type;

			--$placeholder_count;

		} else {

			// Force floats to be locale-unaware.
			if ( 'f' === $type ) {
				$type        = 'F';
				$placeholder = '%' . $format . $type;
			}

			if ( 'i' === $type ) {
				$placeholder = '`%' . $format . 's`';
				// Using a simple strpos() due to previous checking (e.g. $allowed_format).
				$argnum_pos = strpos( $format, '$' );

				if ( false !== $argnum_pos ) {
					// sprintf() argnum starts at 1, $arg_id from 0.
					$arg_identifiers[] = ( ( (int) substr( $format, 0, $argnum_pos ) ) - 1 );
				} else {
					$arg_identifiers[] = $arg_id;
				}
			} elseif ( 'd' !== $type && 'F' !== $type ) {
				/*
				 * i.e. ( 's' === $type ), where 'd' and 'F' keeps $placeholder unchanged,
				 * and we ensure string escaping is used as a safe default (e.g. even if 'x').
				 */
				$argnum_pos = strpos( $format, '$' );

				if ( false !== $argnum_pos ) {
					$arg_strings[] = ( ( (int) substr( $format, 0, $argnum_pos ) ) - 1 );
				} else {
					$arg_strings[] = $arg_id;
				}

				/*
				 * Unquoted strings for backward compatibility (dangerous).
				 * First, "numbered or formatted string placeholders (eg, %1$s, %5s)".
				 * Second, if "%s" has a "%" before it, even if it's unrelated (e.g. "LIKE '%%%s%%'").
				 */
				if ( true !== $this->allow_unsafe_unquoted_parameters
					/*
					 * Note: str_ends_with() is not used here, as this file can be included
					 * directly outside of WordPress core, e.g. by HyperDB, in which case
					 * the polyfills from wp-includes/compat.php are not loaded.
					 */
					|| ( '' === $format && '%' !== substr( $split_query[ $key - 1 ], -1, 1 ) )
				) {
					$placeholder = "'%" . $format . "s'";
				}
			}
		}

		// Glue (-2), any leading characters (-1), then the new $placeholder.
		$new_query .= $split_query[ $key - 2 ] . $split_query[ $key - 1 ] . $placeholder;

		$key += 3;
		++$arg_id;
	}

	// Replace $query; and add remaining $query characters, or index 0 if there were no placeholders.
	$query = $new_query . $split_query[ $key - 2 ];

	$dual_use = array_intersect( $arg_identifiers, $arg_strings );

	if ( count( $dual_use ) > 0 ) {
		wp_load_translations_early();

		$used_placeholders = array();

		$key    = 2;
		$arg_id = 0;
		// Parse again (only used when there is an error).
		while ( $key < $split_query_count ) {
			$placeholder = $split_query[ $key ];

			$format = substr( $placeholder, 1, -1 );

			$argnum_pos = strpos( $format, '$' );

			if ( false !== $argnum_pos ) {
				$arg_pos = ( ( (int) substr( $format, 0, $argnum_pos ) ) - 1 );
			} else {
				$arg_pos = $arg_id;
			}

			$used_placeholders[ $arg_pos ][] = $placeholder;

			$key += 3;
			++$arg_id;
		}

		$conflicts = array();
		foreach ( $dual_use as $arg_pos ) {
			$conflicts[] = implode( ' and ', $used_placeholders[ $arg_pos ] );
		}

		_doing_it_wrong(
			'wpdb::prepare',
			sprintf(
				/* translators: %s: A list of placeholders found to be a problem. */
				__( 'Arguments cannot be prepared as both an Identifier and Value. Found the following conflicts: %s' ),
				implode( ', ', $conflicts )
			),
			'6.2.0'
		);

		return;
	}

	$args_count = count( $args );

	if ( $args_count !== $placeholder_count ) {
		if ( 1 === $placeholder_count && $passed_as_array ) {
			/*
			 * If the passed query only expected one argument,
			 * but the wrong number of arguments was sent as an array, bail.
			 */
			wp_load_translations_early();
			_doing_it_wrong(
				'wpdb::prepare',
				__( 'The query only expected one placeholder, but an array of multiple placeholders was sent.' ),
				'4.9.0'
			);

			return;
		} else {
			/*
			 * If we don't have the right number of placeholders,
			 * but they were passed as individual arguments,
			 * or we were expecting multiple arguments in an array, throw a warning.
			 */
			wp_load_translations_early();
			_doing_it_wrong(
				'wpdb::prepare',
				sprintf(
					/* translators: 1: Number of placeholders, 2: Number of arguments passed. */
					__( 'The query does not contain the correct number of placeholders (%1$d) for the number of arguments passed (%2$d).' ),
					$placeholder_count,
					$args_count
				),
				'4.8.3'
			);

			/*
			 * If we don't have enough arguments to match the placeholders,
			 * return an empty string to avoid a fatal error on PHP 8.
			 */
			if ( $args_count < $placeholder_count ) {
				$max_numbered_placeholder = 0;

				for ( $i = 2, $l = $split_query_count; $i < $l; $i += 3 ) {
					// Assume a leading number is for a numbered placeholder, e.g. '%3$s'.
					$argnum = (int) substr( $split_query[ $i ], 1 );

					if ( $max_numbered_placeholder < $argnum ) {
						$max_numbered_placeholder = $argnum;
					}
				}

				if ( ! $max_numbered_placeholder || $args_count < $max_numbered_placeholder ) {
					return '';
				}
			}
		}
	}

	$args_escaped = array();

	foreach ( $args as $i => $value ) {
		if ( in_array( $i, $arg_identifiers, true ) ) {
			$args_escaped[] = $this->_escape_identifier_value( $value );
		} elseif ( is_int( $value ) || is_float( $value ) ) {
			$args_escaped[] = $value;
		} else {
			if ( ! is_scalar( $value ) && ! is_null( $value ) ) {
				wp_load_translations_early();
				_doing_it_wrong(
					'wpdb::prepare',
					sprintf(
						/* translators: %s: Value type. */
						__( 'Unsupported value type (%s).' ),
						gettype( $value )
					),
					'4.8.2'
				);

				// Preserving old behavior, where values are escaped as strings.
				$value = '';
			}

			$args_escaped[] = $this->_real_escape( $value );
		}
	}

	$query = vsprintf( $query, $args_escaped );

	return $this->add_placeholder_escape( $query );
}