felixarntz/plugin-lib

View on GitHub
src/db-objects/query.php

Summary

Maintainability
F
5 days
Test Coverage
<?php
/**
 * Query class
 *
 * @package Leaves_And_Love\Plugin_Lib
 * @since 1.0.0
 */

namespace Leaves_And_Love\Plugin_Lib\DB_Objects;

use WP_Meta_Query;
use WP_Date_Query;

if ( ! class_exists( 'Leaves_And_Love\Plugin_Lib\DB_Objects\Query' ) ) :

    /**
     * Base class for a query
     *
     * This class represents a general query.
     *
     * @since 1.0.0
     *
     * @property-read string     $request
     * @property-read array      $request_args
     * @property-read array      $sql_clauses
     * @property-read array      $query_vars
     * @property-read array      $query_var_defaults
     * @property-read Collection $results
     */
    abstract class Query {
        /**
         * The manager instance for the query.
         *
         * @since 1.0.0
         * @var Manager
         */
        protected $manager;

        /**
         * SQL for database query.
         *
         * Contains placeholders that need to be filled with `$request_args`.
         *
         * @since 1.0.0
         * @var string
         */
        protected $request = '';

        /**
         * Arguments that need to be escaped in the database query.
         *
         * @since 1.0.0
         * @var array
         */
        protected $request_args = array();

        /**
         * SQL query clauses.
         *
         * @since 1.0.0
         * @var array
         */
        protected $sql_clauses = array(
            'select'  => '',
            'from'    => '',
            'where'   => array(),
            'groupby' => '',
            'orderby' => '',
            'limits'  => '',
        );

        /**
         * Query vars set by the user.
         *
         * @since 1.0.0
         * @var array
         */
        protected $query_vars = array();

        /**
         * Default values for query vars.
         *
         * @since 1.0.0
         * @var array
         */
        protected $query_var_defaults = array();

        /**
         * The results for the query.
         *
         * @since 1.0.0
         * @var Collection
         */
        protected $results;

        /**
         * Metadata query container.
         *
         * @since 1.0.0
         * @var WP_Meta_Query
         */
        private $meta_query;

        /**
         * Date query container.
         *
         * @since 1.0.0
         * @var WP_Date_Query
         */
        private $date_query;

        /**
         * Metadata query clauses.
         *
         * @since 1.0.0
         * @var array
         */
        private $meta_query_clauses = array();

        /**
         * Constructor.
         *
         * Sets the manager instance and assigns the defaults.
         *
         * @since 1.0.0
         *
         * @param Manager $manager The manager instance for the model query.
         */
        public function __construct( $manager ) {
            $this->manager = $manager;

            $primary_property = $this->manager->get_primary_property();

            $this->query_var_defaults = array(
                'fields'        => 'objects',
                'number'        => 10,
                'offset'        => 0,
                'no_found_rows' => null,
                'orderby'       => array( $primary_property => 'ASC' ),
                'include'       => '',
                'exclude'       => '',
                'update_cache'  => true,
            );

            if ( method_exists( $this->manager, 'get_slug_property' ) ) {
                $this->query_var_defaults[ $this->manager->get_slug_property() ] = '';
            }

            if ( method_exists( $this->manager, 'get_title_property' ) ) {
                $this->query_var_defaults[ $this->manager->get_title_property() ] = '';
            }

            if ( method_exists( $this->manager, 'get_type_property' ) ) {
                $this->query_var_defaults[ $this->manager->get_type_property() ] = '';
            }

            if ( method_exists( $this->manager, 'get_status_property' ) ) {
                $this->query_var_defaults[ $this->manager->get_status_property() ] = '';
            }

            if ( method_exists( $this->manager, 'get_author_property' ) ) {
                $this->query_var_defaults[ $this->manager->get_author_property() ] = '';
            }

            $search_fields = $this->get_search_fields();
            if ( ! empty( $search_fields ) ) {
                $this->query_var_defaults['search'] = '';
            }

            if ( method_exists( $this->manager, 'get_date_property' ) ) {
                $this->query_var_defaults['date_query'] = null;
            }

            if ( method_exists( $this->manager, 'get_meta_type' ) ) {
                $this->query_var_defaults['meta_key']   = ''; // phpcs:ignore WordPress.DB.SlowDBQuery
                $this->query_var_defaults['meta_value'] = ''; // phpcs:ignore WordPress.DB.SlowDBQuery
                $this->query_var_defaults['meta_query'] = ''; // phpcs:ignore WordPress.DB.SlowDBQuery

                if ( method_exists( $this->manager, 'update_meta_cache' ) ) {
                    $this->query_var_defaults['update_meta_cache'] = true;
                }
            }
        }

        /**
         * Magic isset-er.
         *
         * @since 1.0.0
         *
         * @param string $property Property to check for.
         * @return bool True if property is set, false otherwise.
         */
        public function __isset( $property ) {
            switch ( $property ) {
                case 'request':
                case 'request_args':
                case 'query_vars':
                case 'query_var_defaults':
                case 'results':
                    return true;
                case 'date_query':
                    if ( method_exists( $this->manager, 'get_date_property' ) ) {
                        return true;
                    }
                    return false;
                case 'meta_query':
                case 'meta_query_clauses':
                    if ( method_exists( $this->manager, 'get_meta_type' ) ) {
                        return true;
                    }
                    return false;
            }

            return false;
        }

        /**
         * Magic getter.
         *
         * @since 1.0.0
         *
         * @param string $property Property to get.
         * @return mixed Property value.
         */
        public function __get( $property ) {
            switch ( $property ) {
                case 'request':
                case 'request_args':
                case 'query_vars':
                case 'query_var_defaults':
                case 'results':
                    return $this->$property;
                case 'date_query':
                    if ( method_exists( $this->manager, 'get_date_property' ) ) {
                        return $this->$property;
                    }
                    return null;
                case 'meta_query':
                case 'meta_query_clauses':
                    if ( method_exists( $this->manager, 'get_meta_type' ) ) {
                        return $this->$property;
                    }
                    return null;
            }

            return null;
        }

        /**
         * Sets up the query for retrieving models.
         *
         * @since 1.0.0
         *
         * @param string|array $query {
         *      Array or query string of model query arguments.
         *
         *      @type string       $fields        Fields to return. Accepts 'ids' (returns a collection of model
         *                                        IDs) or 'objects' (returns a collection of full model objects).
         *                                        Default 'objects'.
         *      @type int          $number        Maximum number of models to retrieve. Default -1 (no limit).
         *      @type int          $offset        Number of models to offset the query. Used to build the LIMIT clause.
         *                                        Default 0.
         *      @type bool         $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default depends
         *                                        on the $number parameter: If unlimited, default is true, otherwise
         *                                        default is false.
         *      @type array        $orderby       Array of orderby => order pairs. Accepted orderby key is 'id'.
         *                                        The orderby values must be either 'ASC' or 'DESC'. Default
         *                                        array( 'id' => 'ASC' ).
         * }
         * @return Leaves_And_Love\Plugin_Lib\DB_Objects\Collection Collection of models.
         */
        public function query( $query ) {
            $this->parse_query( $query );

            return $this->get_results();
        }

        /**
         * Parses arguments passed to the model query with default query arguments.
         *
         * @since 1.0.0
         *
         * @see Query::query()
         *
         * @param string|array $query Array or query string of model query arguments. See
         *                            {@see Query::query()}.
         */
        protected function parse_query( $query ) {
            $this->query_vars = wp_parse_args( $query, $this->query_var_defaults );

            $this->query_vars['number'] = intval( $this->query_vars['number'] );
            if ( $this->query_vars['number'] < 0 ) {
                $this->query_vars['number'] = 0;
            }

            $this->query_vars['offset'] = absint( $this->query_vars['offset'] );

            if ( null === $this->query_vars['no_found_rows'] ) {
                if ( 0 === $this->query_vars['number'] ) {
                    $this->query_vars['no_found_rows'] = true;
                } else {
                    $this->query_vars['no_found_rows'] = false;
                }
            }

            if ( method_exists( $this->manager, 'get_date_property' ) && ! empty( $this->query_vars['date_query'] ) && is_array( $this->query_vars['date_query'] ) ) {
                $full_table_name = $this->manager->db()->get_table( $this->manager->get_table_name(), 'full' );

                $this->date_query         = new WP_Date_Query( $this->query_vars['date_query'], $full_table_name . '.' . $this->manager->get_date_property() );
                $this->date_query->column = $full_table_name . '.' . $this->manager->get_date_property();
            }

            if ( method_exists( $this->manager, 'get_meta_type' ) ) {
                $this->meta_query = new WP_Meta_Query();
                $this->meta_query->parse_query_vars( $this->query_vars );

                if ( ! empty( $this->meta_query->queries ) ) {
                    $prefix = $this->manager->db()->get_prefix();
                    $name   = $this->manager->get_meta_type();

                    $table_name = $this->manager->get_table_name();

                    $this->meta_query_clauses = $this->meta_query->get_sql( $prefix . $name, "%{$table_name}%", 'id', $this );
                }
            }
        }

        /**
         * Retrieves a list of models matching the query vars.
         *
         * @since 1.0.0
         *
         * @return Collection Collection of models.
         */
        protected function get_results() {
            $key          = $this->get_cache_key( $this->query_vars );
            $last_changed = $this->manager->get_from_cache( 'last_changed' );
            if ( ! $last_changed ) {
                $last_changed = microtime();
                $this->manager->set_in_cache( 'last_changed', $last_changed );
            }

            $cache_key   = "get_results:$key:$last_changed";
            $cache_value = $this->manager->get_from_cache( $cache_key );

            if ( false === $cache_value ) {
                $total     = 0;
                $model_ids = $this->query_results();
                if ( $model_ids && ! $this->query_vars['no_found_rows'] ) {
                    $total_models_query = 'SELECT FOUND_ROWS()';
                    $total              = (int) $this->manager->db()->get_var( $total_models_query );
                }

                $cache_value = array(
                    'model_ids' => $model_ids,
                    'total'     => $total,
                );

                $this->manager->add_to_cache( $cache_key, $cache_value );
            } else {
                $model_ids = $cache_value['model_ids'];
                $total     = $cache_value['total'];
            }

            if ( $this->query_vars['update_cache'] ) {
                $non_cached_ids = $this->get_non_cached_ids( $model_ids );
                if ( ! empty( $non_cached_ids ) ) {
                    $this->update_cache( $non_cached_ids );

                    if ( isset( $this->query_vars['update_meta_cache'] ) && $this->query_vars['update_meta_cache'] ) {
                        $this->manager->update_meta_cache( $non_cached_ids );
                    }
                }
            }

            return $this->create_collection( $model_ids, $total, $this->query_vars['fields'] );
        }

        /**
         * Gets the query cache key for a given set of query vars.
         *
         * @since 1.0.0
         *
         * @param array $query_vars Array of model query arguments.
         * @return string Cache key to use for the query.
         */
        protected function get_cache_key( $query_vars ) {
            $args = wp_array_slice_assoc( $query_vars, array_keys( $this->query_var_defaults ) );

            // The following arguments do not affect the actual query.
            $non_key_args = array( 'fields', 'update_cache', 'update_meta_cache' );
            foreach ( $non_key_args as $arg ) {
                if ( isset( $args[ $arg ] ) ) {
                    unset( $args[ $arg ] );
                }
            }

            return md5( serialize( $args ) ); // phpcs:ignore
        }

        /**
         * Retrieves IDs that are not already present in the cache.
         *
         * @since 1.0.0
         *
         * @param array $model_ids The model IDs to check for.
         * @return array List of IDs not present in the cache.
         */
        protected function get_non_cached_ids( $model_ids ) {
            $clean = array();

            foreach ( $model_ids as $id ) {
                $id = (int) $id;
                if ( false === $this->manager->get_from_cache( $id ) ) {
                    $clean[] = $id;
                }
            }

            return $clean;
        }

        /**
         * Updates the cache for given models.
         *
         * @since 1.0.0
         *
         * @param array $non_cached_ids Array of model IDs.
         */
        protected function update_cache( $non_cached_ids ) {
            $table_name       = $this->manager->get_table_name();
            $primary_property = $this->manager->get_primary_property();

            $fresh_models = $this->manager->db()->get_results( sprintf( "SELECT %%{$table_name}%%.* FROM %%{$table_name}%% WHERE {$primary_property} IN (%s)", join( ',', array_map( 'intval', $non_cached_ids ) ) ) );
            foreach ( (array) $fresh_models as $model ) {
                $this->manager->add_to_cache( $model->$primary_property, $model );
            }
        }

        /**
         * Creates a collection object from the query results.
         *
         * @since 1.0.0
         *
         * @param array  $model_ids The model IDs, or objects for this collection.
         * @param int    $total     Optional. The total amount of models in the collection. Default is the
         *                          number of models.
         * @param string $fields    Optional. Mode of the models passed. Default 'ids'.
         * @return Collection Collection of models.
         */
        protected function create_collection( $model_ids, $total, $fields ) {
            $model_ids = array_map( 'intval', $model_ids );

            $this->results = $this->manager->get_collection( $model_ids, $total, 'ids' );

            if ( 'objects' === $fields ) {
                $this->results->transform_into_objects();
            }

            return $this->results;
        }

        /**
         * Used internally to get a list of model IDs matching the query vars.
         *
         * @since 1.0.0
         *
         * @return array Array of model IDs.
         */
        protected function query_results() {
            $table_name = $this->manager->get_table_name();

            list( $fields, $distinct ) = $this->parse_fields();
            if ( is_bool( $distinct ) ) {
                $distinct = $distinct ? 'DISTINCT' : '';
            }

            $number = $this->query_vars['number'];
            $offset = $this->query_vars['offset'];

            $limits = null;
            if ( $number ) {
                if ( $offset ) {
                    $limits = "LIMIT $offset,$number";
                } else {
                    $limits = "LIMIT $number";
                }
            }

            $found_rows = '';
            if ( ! $this->query_vars['no_found_rows'] ) {
                $found_rows = 'SQL_CALC_FOUND_ROWS';
            }

            $orderby = $this->parse_orderby( $this->query_vars['orderby'] );

            list( $this->sql_clauses['where'], $this->request_args ) = $this->parse_where();

            $join = $this->parse_join();

            $groupby = $this->parse_groupby();

            $where = implode( ' AND ', $this->sql_clauses['where'] );

            $clauses = array(
                'fields'  => $fields,
                'join'    => $join,
                'where'   => $where,
                'orderby' => $orderby,
                'limits'  => $limits,
                'groupby' => $groupby,
            );

            $fields  = isset( $clauses['fields'] ) ? $clauses['fields'] : '';
            $join    = isset( $clauses['join'] ) ? $clauses['join'] : '';
            $where   = isset( $clauses['where'] ) ? $clauses['where'] : '';
            $orderby = isset( $clauses['orderby'] ) ? $clauses['orderby'] : '';
            $limits  = isset( $clauses['limits'] ) ? $clauses['limits'] : '';
            $groupby = isset( $clauses['groupby'] ) ? $clauses['groupby'] : '';

            if ( $where ) {
                $where = "WHERE $where";
            }

            if ( $orderby ) {
                $orderby = "ORDER BY $orderby";
            }

            if ( $groupby ) {
                $groupby = "GROUP BY $groupby";
            }

            $this->sql_clauses['select']  = "SELECT $distinct $found_rows $fields";
            $this->sql_clauses['from']    = "FROM %{$table_name}% $join";
            $this->sql_clauses['groupby'] = $groupby;
            $this->sql_clauses['orderby'] = $orderby;
            $this->sql_clauses['limits']  = $limits;

            $this->request = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";

            return $this->manager->db()->get_col( $this->request, $this->request_args );
        }

        /**
         * Parses the SQL fields value.
         *
         * @since 1.0.0
         *
         * @return array Array with the first element being the fields part of the SQL query and the second
         *               being a boolean specifying whether to use the DISTINCT keyword.
         */
        protected function parse_fields() {
            $table_name       = $this->manager->get_table_name();
            $primary_property = $this->manager->get_primary_property();

            return array( '%' . $table_name . '%.' . $primary_property, false );
        }

        /**
         * Parses the SQL join value.
         *
         * @since 1.0.0
         *
         * @return string Join value for the SQL query.
         */
        protected function parse_join() {
            if ( ! empty( $this->meta_query_clauses ) ) {
                return $this->meta_query_clauses['join'];
            }

            return '';
        }

        /**
         * Parses the SQL where clause.
         *
         * @since 1.0.0
         *
         * @return array Array with the first element being the array of SQL where clauses and the second
         *               being the array of arguments for those where clauses.
         */
        protected function parse_where() {
            $where = array();
            $args  = array();

            list( $where, $args ) = $this->parse_list_where_field( $where, $args, $this->manager->get_primary_property(), 'include', 'exclude', '%d', 'absint' );

            if ( method_exists( $this->manager, 'get_slug_property' ) ) {
                $slug_property = $this->manager->get_slug_property();

                list( $where, $args ) = $this->parse_default_where_field( $where, $args, $slug_property, $slug_property, '%s', 'sanitize_title', true );
            }

            if ( method_exists( $this->manager, 'get_title_property' ) ) {
                $title_property = $this->manager->get_title_property();

                list( $where, $args ) = $this->parse_default_where_field( $where, $args, $title_property, $title_property, '%s', null, false );
            }

            if ( method_exists( $this->manager, 'get_type_property' ) ) {
                $type_property = $this->manager->get_type_property();

                list( $where, $args ) = $this->parse_default_where_field( $where, $args, $type_property, $type_property, '%s', 'sanitize_key', true );
            }

            if ( method_exists( $this->manager, 'get_status_property' ) ) {
                $status_property = $this->manager->get_status_property();

                list( $where, $args ) = $this->parse_default_where_field( $where, $args, $status_property, $status_property, '%s', 'sanitize_key', true );
            }

            if ( method_exists( $this->manager, 'get_author_property' ) ) {
                $author_property = $this->manager->get_author_property();

                list( $where, $args ) = $this->parse_default_where_field( $where, $args, $author_property, $author_property, '%d', 'absint', false );
            }

            $search_fields = $this->get_search_fields();
            if ( ! empty( $search_fields ) && ! empty( $this->query_vars['search'] ) ) {
                $where['search'] = $this->get_search_sql( $this->query_vars['search'], $search_fields );
            }

            if ( $this->date_query ) {
                $where['date_query'] = preg_replace( '/^\s*AND\s*/', '', $this->date_query->get_sql() );
            }

            if ( ! empty( $this->meta_query_clauses ) ) {
                $where['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] ); // phpcs:ignore WordPress.DB.SlowDBQuery
            }

            return array( $where, $args );
        }

        /**
         * Parses the SQL groupby clause.
         *
         * @since 1.0.0
         *
         * @return string Groupby value for the SQL query.
         */
        protected function parse_groupby() {
            if ( ! empty( $this->meta_query_clauses ) ) {
                $table_name = $this->manager->get_table_name();

                return '%' . $table_name . '%.id';
            }

            return '';
        }

        /**
         * Parses the SQL orderby clause.
         *
         * @since 1.0.0
         *
         * @param string|array $orderby The $orderby query var.
         * @return string The orderby clause.
         */
        protected function parse_orderby( $orderby ) {
            if ( in_array( $orderby, array( 'none', array(), false ), true ) ) {
                return '';
            }

            if ( empty( $orderby ) ) {
                $table_name = $this->manager->get_table_name();

                return '%' . $table_name . '%.id ASC';
            }

            $orderby_array = array();
            foreach ( $orderby as $_orderby => $_order ) {
                if ( ! in_array( $_orderby, $this->get_valid_orderby_fields(), true ) ) {
                    continue;
                }

                $parsed_orderby = $this->parse_single_orderby( $_orderby );
                $parsed_order   = $this->parse_single_order( $_order, $_orderby );
                if ( ! empty( $parsed_order ) ) {
                    $parsed_orderby .= ' ' . $parsed_order;
                }

                $orderby_array[] = $parsed_orderby;
            }

            return implode( ', ', array_unique( $orderby_array ) );
        }

        /**
         * Parses a single $orderby element.
         *
         * @since 1.0.0
         *
         * @param string $orderby The orderby field. Must be valid.
         * @return string The parsed orderby SQL string.
         */
        protected function parse_single_orderby( $orderby ) {
            $table_name = $this->manager->get_table_name();

            if ( 'include' === $orderby ) {
                $ids = implode( ',', array_map( 'absint', $this->query_vars['include'] ) );

                return "FIELD( %{$table_name}%.id, $ids )";
            }

            if ( method_exists( $this->manager, 'get_meta_type' ) && in_array( $orderby, $this->get_meta_orderby_fields(), true ) ) {
                $meta_table = _get_meta_table( $this->manager->db()->get_prefix() . $this->manager->get_meta_type() );

                if ( $this->query_vars['meta_key'] === $orderby || 'meta_value' === $orderby ) {
                    return "$meta_table.meta_value";
                }

                if ( 'meta_value_num' === $orderby ) {
                    return "$meta_table.meta_value+0";
                }

                $meta_query_clauses = $this->meta_query->get_clauses();

                return sprintf( 'CAST(%s.meta_value AS %s)', esc_sql( $meta_query_clauses[ $orderby ]['alias'] ), esc_sql( $meta_query_clauses[ $orderby ]['cast'] ) );
            }

            return '%' . $table_name . '%.' . $orderby;
        }

        /**
         * Parses a single $order element.
         *
         * @since 1.0.0
         *
         * @param string $order   The order value. Either 'ASC' or 'DESC'.
         * @param string $orderby The orderby field. Must be valid.
         * @return string The parsed order SQL string, or empty if not necessary.
         */
        protected function parse_single_order( $order, $orderby ) {
            if ( 'include' === $orderby ) {
                return '';
            }

            return 'DESC' === strtoupper( $order ) ? 'DESC' : 'ASC';
        }

        /**
         * Parses a default field for an argument in the WHERE clause.
         *
         * This utility method can be used inside the `Leaves_And_Love\Plugin_Lib\DB_Objects\Query::parse_where()`
         * method to created clauses for individual database properties.
         *
         * Clauses created by this method support an exact match search for a specific value, or a whitelist of
         * values if $support_array is specified as true.
         *
         * @since 1.0.0
         *
         * @param array         $where             The input where array to modify.
         * @param array         $args              The input arguments array to modify.
         * @param string        $property          Name of the property in the database.
         * @param string        $query_var         Name of the query variable to check for.
         * @param string        $placeholder       Optional. Placeholder for the SQL statement. Either '%s',
         *                                         '%d' or '%f'. Default '%s'.
         * @param callable|null $sanitize_callback Optional. Callback to sanitize each value passed in the query.
         *                                         Default null.
         * @param bool          $support_array     Optional. Whether to support array values for an IN clause.
         *                                         Default false.
         * @return array Array with the first element being the array of SQL where clauses and the second
         *               being the array of arguments for those where clauses.
         */
        protected function parse_default_where_field( $where, $args, $property, $query_var, $placeholder = '%s', $sanitize_callback = null, $support_array = false ) {
            if ( $this->query_vars[ $query_var ] !== $this->query_var_defaults[ $query_var ] ) {
                $table_name = $this->manager->get_table_name();

                if ( $support_array && is_array( $this->query_vars[ $query_var ] ) ) {
                    $values = $this->query_vars[ $query_var ];
                    if ( $sanitize_callback ) {
                        $values = array_map( $sanitize_callback, $values );
                    }

                    $where[ $query_var ] = "%{$table_name}%.{$property} IN ( " . implode( ',', array_fill( 0, count( $values ), $placeholder ) ) . ' )';
                    $args                = array_merge( $args, $values );
                } else {
                    $value = $this->query_vars[ $query_var ];
                    if ( $sanitize_callback ) {
                        $value = call_user_func( $sanitize_callback, $value );
                    }

                    $where[ $query_var ] = "%{$table_name}%.{$property} = {$placeholder}";
                    $args[]              = $value;
                }
            }

            return array( $where, $args );
        }

        /**
         * Parses a whitelist or blacklist field for an argument in the WHERE clause.
         *
         * This utility method can be used inside the `Leaves_And_Love\Plugin_Lib\DB_Objects\Query::parse_where()`
         * method to created clauses for individual database properties.
         *
         * This method creates two clauses, one for the `$property . '__in'` and one for the `$property . '__not_in'`
         * query variable. Both of these must contain an array with a whitelist or blacklist respectively.
         *
         * @since 1.0.0
         *
         * @param array         $where             The input where array to modify.
         * @param array         $args              The input arguments array to modify.
         * @param string        $property          Name of the property in the database.
         * @param string        $include_query_var Name of the query variable to check for included values.
         * @param string        $exclude_query_var Name of the query variable to check for excluded values.
         * @param string        $placeholder       Optional. Placeholder for the SQL statement. Either '%s',
         *                                         '%d' or '%f'. Default '%s'.
         * @param callable|null $sanitize_callback Optional. Callback to sanitize each value passed in the query.
         *                                         Default null.
         * @return array Array with the first element being the array of SQL where clauses and the second
         *               being the array of arguments for those where clauses.
         */
        protected function parse_list_where_field( $where, $args, $property, $include_query_var, $exclude_query_var, $placeholder = '%s', $sanitize_callback = null ) {
            $table_name = $this->manager->get_table_name();

            if ( ! empty( $this->query_vars[ $include_query_var ] ) ) {
                $values = $this->query_vars[ $include_query_var ];
                if ( $sanitize_callback ) {
                    $values = array_map( $sanitize_callback, $values );
                }

                $where[ $include_query_var ] = "%{$table_name}%.{$property} IN ( " . implode( ',', array_fill( 0, count( $values ), $placeholder ) ) . ' )';
                $args                        = array_merge( $args, $values );
            }

            if ( ! empty( $this->query_vars[ $exclude_query_var ] ) ) {
                $values = $this->query_vars[ $exclude_query_var ];
                if ( $sanitize_callback ) {
                    $values = array_map( $sanitize_callback, $values );
                }

                $where[ $exclude_query_var ] = "%{$table_name}%.{$property} NOT IN ( " . implode( ',', array_fill( 0, count( $values ), $placeholder ) ) . ' )';
                $args                        = array_merge( $args, $values );
            }

            return array( $where, $args );
        }

        /**
         * Returns the fields that are valid to be used in orderby clauses.
         *
         * @since 1.0.0
         *
         * @return array Array of valid orderby fields.
         */
        public function get_valid_orderby_fields() {
            $orderby_fields = array( 'id', 'include' );

            if ( method_exists( $this->manager, 'get_slug_property' ) ) {
                $orderby_fields[] = $this->manager->get_slug_property();
            }

            if ( method_exists( $this->manager, 'get_title_property' ) ) {
                $orderby_fields[] = $this->manager->get_title_property();
            }

            if ( method_exists( $this->manager, 'get_type_property' ) ) {
                $orderby_fields[] = $this->manager->get_type_property();
            }

            if ( method_exists( $this->manager, 'get_status_property' ) ) {
                $orderby_fields[] = $this->manager->get_status_property();
            }

            if ( method_exists( $this->manager, 'get_author_property' ) ) {
                $orderby_fields[] = $this->manager->get_author_property();
            }

            if ( method_exists( $this->manager, 'get_date_property' ) ) {
                $orderby_fields[] = $this->manager->get_date_property();
                $orderby_fields   = array_merge( $orderby_fields, $this->manager->get_secondary_date_properties() );
            }

            if ( method_exists( $this->manager, 'get_meta_type' ) ) {
                $orderby_fields = array_merge( $orderby_fields, $this->get_meta_orderby_fields() );
            }

            return $orderby_fields;
        }

        /**
         * Returns the fields that are searchable.
         *
         * @since 1.0.0
         *
         * @return array Array of database column names.
         */
        public function get_search_fields() {
            $search_fields = array();

            if ( method_exists( $this->manager, 'get_title_property' ) ) {
                $search_fields[] = $this->manager->get_title_property();
            }

            if ( method_exists( $this->manager, 'get_content_property' ) ) {
                $search_fields[] = $this->manager->get_content_property();
            }

            return $search_fields;
        }

        /**
         * Used internally to generate an SQL string for searching across multiple columns.
         *
         * @since 1.0.0
         *
         * @global \wpdb $wpdb WordPress database abstraction object.
         *
         * @param string $string Search string.
         * @param array  $fields Database columns to search.
         * @return string Search SQL.
         */
        protected function get_search_sql( $string, $fields ) {
            global $wpdb;

            $table_name = $this->manager->get_table_name();

            if ( false !== strpos( $string, '*' ) ) {
                $like = '%' . implode( '%', array_map( array( $wpdb, 'esc_like' ), explode( '*', $string ) ) ) . '%';
            } else {
                $like = '%' . $wpdb->esc_like( $string ) . '%';
            }

            $searches = array();
            foreach ( $fields as $field ) {
                $searches[] = "%{$table_name}%." . $wpdb->prepare( "{$field} LIKE %s", $like ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            }

            return '(' . implode( ' OR ', $searches ) . ')';
        }

        /**
         * Returns the meta orderby fields to use in orderby clauses.
         *
         * These depend on the current meta query.
         *
         * @since 1.0.0
         *
         * @return array Array of meta orderby fields.
         */
        private function get_meta_orderby_fields() {
            if ( empty( $this->meta_query->queries ) ) {
                return array();
            }

            $meta_orderby_fields = array();

            if ( ! empty( $this->query_vars['meta_key'] ) ) {
                $meta_orderby_fields[] = $this->query_vars['meta_key'];
                $meta_orderby_fields[] = 'meta_value';
                $meta_orderby_fields[] = 'meta_value_num';
            }

            $meta_query_clauses = $this->meta_query->get_clauses();
            if ( $meta_query_clauses ) {
                $meta_orderby_fields = array_merge( $meta_orderby_fields, array_keys( $meta_query_clauses ) );
            }

            return $meta_orderby_fields;
        }
    }

endif;