I am writing a custom (bespoke, for 1 client, not for publication and general consumption) plugin, that needs to access custom tables.
In general with WP,I follow the principle of using the higher level APIs where possible, so rarely have to work with \wpdb. Now that i do have a need, i am discovering how archaic it is!
Looking through the source code, it seems I can be sure that it is using mysqli (since the plugin requires php 7.1, and we also control the server) so I thought why not use that for my custom tables logic?
I wrote a tiny wrapper:
<?php
namespace PluginNamespaceHere\DB;
use mysqli;
use wpdb;
/**
* A small wrapper class that contains, and provides direct access to, the $wpdb object,
* as well as the underlying MYSQLI object, so we can do real prepared statements etc
*/
class DB
{
/** @var wpdb */
public $wpdb;
/** @var mysqli */
public $mysqli;
/**
* @param wpdb $wpdb
*/
public function __construct(wpdb $wpdb)
{
$this->wpdb = $wpdb;
//$wpdb is protected, but accessible via magic __get() wp-db.php line: 643
$this->mysqli = $wpdb->dbh;
}
/**
* Run an SQL query. If $params are provided, prepared statements are used. If $bind_types are provided, they will be
* used in the prepared statement, if not, all params will be treated as strings
*
* @param string $sql The SQL string, unprefixed table names should be wrapped in curly braces eg SELECT * FROM {posts}
* @param array $params Optional parameters for prepared statements
* @param string $bind_types Optional bind types for prepared statements, defaults to string
* @retun bool|mysqli_result
*/
public function run($sql, $params=[], $bind_types='')
{
$sql = $this->prefixTableNamesInSqlString($sql);
if(!is_array($params) || empty($params)){
return $this->mysqli->query($sql);
}
if($bind_types == ''){
$bind_types = str_repeat("s", count($params));
}
$stmt = $this->mysqli->prepare($sql);
$stmt->bind_param($bind_types, ...$params);
$stmt->execute();
return $stmt->get_result();
}
/**
* Replaces curly brace table names with their actual, prefixed name
* Eg "SELECT * from {table_name}" => "SELECT * from wp_table_name"
* @param string $sql
* @return string
*/
private function prefixTableNamesInSqlString($sql)
{
return str_replace(["{", "}"], [$this->wpdb->prefix, ""], $sql);
}
}
This would allow me to use real prepared queries, and have nice readable code like:
<?php
$sql = "
SELECT DISTINCT c.ID AS course_id, c.post_title AS course_title
FROM {posts} c
JOIN {tmsc_course_product} cp
ON c.id = cp.course_id
WHERE cp.product_id IN(?,?)
";
//Yes, i know DB::run() can return bool! Out of scope for this question
$courses = $db->run($sql, [57,4761])->fetch_all(MYSQLI_ASSOC);
My only concern is if this is going to have any knock on effects with wpdb and its dependents. The wpdb class is not exactly easy to read, and appears holds a lot of state.
$wpdb->prepare()
? If the plugin is only for a specific case, I think you can make a lot more assumptions. If it were for the public, you'd need to be aware that some people swap mysqli for other clients (e.g. postgres).wpdb::prepare()
is just a giant string manipulation routine, that does not use prepared statements at all, as far as i can tell from looking through the source and reading$wpdb->prepare()
is quite safe, otherwise we would have heard security people comlaining loudly about it - especially since it is so widely used within WP and its ecosystem.$wpdb
altogether and use PDO for your queries - as long as you know what you're doing. I have no interest in reading the full source of wpdb but know that some caching is taking place there, your custom class/extension might clash with this. If the whole project is not about optimizing the db class, I would focus on more important aspects and just use$wpdb
.wpdb::prepare()
edge cases like%
etc