prepare(): values as %s/%d, the table name * as %i. The table name is assembled here from $wpdb->prefix and a fixed * suffix, so it is never user input, but it is still bound rather than * interpolated, because "it cannot be user input" is an argument about today's * callers and %i is a guarantee about every future one (docs/08, docs/09). * * Methods assign the global to a local named $wpdb. The name is deliberate: * WPCS's prepared-SQL sniffs only follow a variable called $wpdb, so any other * name turns a correct query into an unreviewable one. */ abstract class AbstractRepository { /** * Table suffix without the `fxlc_` prefix, e.g. `forms`, `leads`. */ abstract protected function suffix(): string; final protected function db(): wpdb { global $wpdb; return $wpdb; } /** * The real table name, for binding to a %i placeholder. */ final protected function table(): string { return $this->db()->prefix . 'fxlc_' . $this->suffix(); } /** * Clamp pagination to safe bounds and return a [limit, offset] pair. * * @return array{0:int,1:int} */ final protected function limit_offset( int $page, int $per_page ): array { $per_page = max( 1, min( 100, $per_page ) ); $page = max( 1, $page ); return [ $per_page, ( $page - 1 ) * $per_page ]; } /** * Encode a value to JSON for longtext columns. * * @param array $value */ final protected function encode( array $value ): string { $json = wp_json_encode( $value ); return false === $json ? '[]' : $json; } /** * Decode a JSON longtext column back to an array. * * @return array */ final protected function decode( mixed $json ): array { if ( ! is_string( $json ) || '' === $json ) { return []; } $decoded = json_decode( $json, true ); return is_array( $decoded ) ? $decoded : []; } }