prepare(), with %i for table names and %s/%d/%f for values, so nothing is ever interpolated into SQL. /** * Central authority for custom table names. Repositories must resolve * table names through this class only, never by string concatenation * of user input. */ class Schema { /** * Table keys this build owns. Extensions add their own with the * ppl_afw_tables filter so Schema stays the single whitelist. */ const TABLES = array( 'affiliates', 'visits', 'referrals', 'payouts', 'notifications', 'activity_log', 'stats_daily', ); /** * Returns the prefixed table name for a whitelisted key. * * @since 1.0.0 * @param string $key Table key from self::TABLES. * @return string * @throws \InvalidArgumentException When the key is not whitelisted. */ public static function table( $key ) { global $wpdb; /** * Filters the whitelisted table keys. * * @since 1.0.0 * @param string[] $tables Table keys, without the prefix. */ $tables = (array) apply_filters( 'ppl_afw_tables', self::TABLES ); if ( ! in_array( $key, $tables, true ) ) { throw new \InvalidArgumentException( esc_html( "Unknown table key: {$key}" ) ); } return $wpdb->prefix . 'ppl_afw_' . $key; } }