| Current File : /home/bwalansa/www/wp-content.backup/plugins/the-events-calendar/common/src/Tribe/Repository.php |
<?php
abstract class Tribe__Repository
implements Tribe__Repository__Interface {
/**
* @var array An array of keys that cannot be updated on this repository.
*/
protected static $blocked_keys = array(
'ID',
'post_type',
'post_modified',
'post_modified_gmt',
'guid',
'comment_count',
);
/**
* @var array A list of the default filters supported and implemented by the repository.
*/
protected static $default_modifiers = array(
'p',
'author',
'author_name',
'author__in',
'author__not_in',
'has_password',
'post_password',
'cat',
'category__and',
'category__in',
'category__not_in',
'category_name',
'comment_count',
'comment_status',
'menu_order',
'title',
'title_like',
'name',
'post_name__in',
'ping_status',
'post__in',
'post__not_in',
'post_parent',
'post_parent__in',
'post_parent__not_in',
'post_mime_type',
's',
'search',
'tag',
'tag__and',
'tag__in',
'tag__not_in',
'tag_id',
'tag_slug__and',
'tag_slug__in',
'ID',
'id',
'date',
'after_date',
'before_date',
'date_gmt',
'after_date_gmt',
'before_date_gmt',
'post_title',
'post_content',
'post_excerpt',
'post_status',
'to_ping',
'post_modified',
'post_modified_gmt',
'post_content_filtered',
'guid',
'perm',
'meta',
'meta_equals',
'meta_not_equals',
'meta_gt',
'meta_greater_than',
'meta_gte',
'meta_greater_than_or_equal',
'meta_like',
'meta_not_like',
'meta_lt',
'meta_less_than',
'meta_lte',
'meta_less_than_or_equal',
'meta_in',
'meta_not_in',
'meta_between',
'meta_not_between',
'meta_exists',
'meta_not_exists',
'meta_regexp',
'meta_equals_regexp',
'meta_not_regexp',
'meta_not_equals_regexp',
'taxonomy_exists',
'taxonomy_not_exists',
'term_id_in',
'term_id_not_in',
'term_id_and',
'term_name_in',
'term_name_not_in',
'term_name_and',
'term_slug_in',
'term_slug_not_in',
'term_slug_and',
);
/**
* @var array An array of default arguments that will be applied to all queries.
*/
protected static $common_args = array(
'post_type' => 'post',
'suppress_filters' => false,
'posts_per_page' => - 1,
);
/**
* @var array A list of query modifiers that will trigger a overriding merge, thus
* replacing previous values, when set multiple times.
*/
protected static $replacing_modifiers = array(
'p',
'author',
'author_name',
'author__in',
'author__not_in',
'has_password',
'post_password',
'cat',
'category__and',
'category__in',
'category__not_in',
'category_name',
'comment_count',
'comment_status',
'menu_order',
'title',
'title_like',
'name',
'post_name__in',
'ping_status',
'post__in',
'post__not_in',
'post_parent',
'post_parent__in',
'post_parent__not_in',
'post_mime_type',
's',
'search',
'tag',
'tag__and',
'tag__in',
'tag__not_in',
'tag_id',
'tag_slug__and',
'tag_slug__in',
'ID',
'id',
'date',
'after_date',
'before_date',
'date_gmt',
'after_date_gmt',
'before_date_gmt',
'post_title',
'post_content',
'post_excerpt',
'post_status',
'to_ping',
'post_modified',
'post_modified_gmt',
'post_content_filtered',
'guid',
'perm',
);
/**
* @var int
*/
protected static $meta_alias = 0;
/**
* @var array A list of keys that denote the value to check should be cast to array.
*/
protected static $multi_value_keys = array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' );
/**
* @var array A map of SQL comparison operators to their human-readable counterpart.
*/
protected static $comparison_operators = array(
'=' => 'equals',
'!=' => 'not-equals',
'>' => 'gt',
'>=' => 'gte',
'<' => 'lt',
'<=' => 'lte',
'LIKE' => 'like',
'NOT LIKE' => 'not-like',
'IN' => 'in',
'NOT IN' => 'not-in',
'BETWEEN' => 'between',
'NOT BETWEEN' => 'not-between',
'EXISTS' => 'exists',
'NOT EXISTS' => 'not-exists',
'REGEXP' => 'regexp',
'NOT REGEXP' => 'not-regexp',
);
/**
* @var string
*/
protected $filter_name = 'default';
/**
* @var array The post IDs that will be updated.
*/
protected $ids = array();
/**
* @var bool Whether the post IDs to update have already been fetched or not.
*/
protected $has_ids = false;
/**
* @var array The updates that will be saved to the database.
*/
protected $updates = array();
/**
* @var array A list of taxonomies this repository will recognize.
*/
protected $taxonomies = array();
/**
* @var array A map detailing which fields should be converted from a
* GMT time and date to a local one.
*/
protected $to_local_time_map = array(
'post_date_gmt' => 'post_date',
);
/**
* @var array A map detailing which fields should be converted from a
* localized time and date to a GMT one.
*/
protected $to_gmt_map = array(
'post_date' => 'post_date_gmt',
);
/**
* @var array
*/
protected $default_args = array( 'post_type' => 'post' );
/**
* @var array An array of query modifying callbacks populated while applying
* the filters.
*/
protected $query_modifiers = array();
/**
* @var bool Whether the current query is void or not.
*/
protected $void_query = false;
/**
* @var array An array of query arguments that will be populated while applying
* filters.
*/
protected $query_args = array(
'meta_query' => array( 'relation' => 'AND' ),
'tax_query' => array( 'relation' => 'AND' ),
);
/**
* @var WP_Query The current query object built and modified by the instance.
*/
protected $current_query;
/**
* @var array An associative array of the filters that will be applied and the used values.
*/
protected $current_filters = array();
/**
* @var Tribe__Repository__Query_Filters
*/
public $filter_query;
/**
* @var string The filter that should be used to get a post by its primary key.
*/
protected $primary_key = 'p';
/**
* @var array A map of callbacks in the shape [ <slug> => <callback|primitive> ]
*/
protected $schema = array();
/**
* @var Tribe__Repository__Interface
*/
protected $main_repository;
/**
* @var Tribe__Repository__Formatter_Interface
*/
protected $formatter;
/**
* @var bool
*/
protected $skip_found_rows = true;
/**
* @var Tribe__Repository__Interface
*/
protected $query_builder;
/**
* Tribe__Repository constructor.
*
* @since 4.7.19
*/
public function __construct() {
$this->filter_query = new Tribe__Repository__Query_Filters();
$this->default_args = array_merge( array( 'posts_per_page' => - 1 ), $this->default_args );
$post_types = (array) Tribe__Utils__Array::get( $this->default_args, 'post_type', array() );
$this->taxonomies = get_taxonomies( array( 'object_type' => $post_types ), 'names' );
}
/**
* {@inheritdoc}
*/
public function get_default_args() {
return $this->default_args;
}
/**
* {@inheritdoc}
*/
public function set_default_args( array $default_args ) {
$this->default_args = $default_args;
}
/**
* Returns the value of a protected property.
*
* @since 4.7.19
*
* @param string $name
*
* @return mixed|null
* @throws Tribe__Repository__Usage_Error If trying to access a non defined property.
*/
public function __get( $name ) {
if ( ! property_exists( $this, $name ) ) {
throw Tribe__Repository__Usage_Error::because_property_is_not_defined( $name, $this );
}
return $this->{$name};
}
/**
* Magic method to set protected properties.
*
* @since 4.7.19
*
* @param string $name
* @param mixed $value
*
* @throws Tribe__Repository__Usage_Error As properties have to be set extending
* the class, using setter methods or via constructor injection
*/
public function __set( $name, $value ) {
throw Tribe__Repository__Usage_Error::because_properties_should_be_set_correctly( $name, $this );
}
/**
* Whether the class has a property with the specific name or not.
*
* @since 4.7.19
*
* @param string $name
*
* @return bool
*/
public function __isset( $name ) {
return property_exists( $this, $name ) && isset( $this->{$name} );
}
/**
* {@inheritdoc}
*/
public function where( $key, $value ) {
return call_user_func_array( array( $this, 'by' ), func_get_args() );
}
/**
* {@inheritdoc}
*/
public function page( $page ) {
$this->query_args['paged'] = absint( $page );
return $this;
}
/**
* {@inheritdoc}
*/
public function per_page( $per_page ) {
// we allow for `-1` here
$this->query_args['posts_per_page'] = $per_page;
return $this;
}
/**
* {@inheritdoc}
*/
public function count() {
if ( $this->void_query ) {
return 0;
}
$query = $this->build_query();
$query->set( 'fields', 'ids' );
/**
* Filters the query object by reference before counting found posts in the current page.
*
* @since 4.7.19
*
* @param WP_Query $query
*/
do_action_ref_array( "{$this->filter_name}_pre_count_posts", array( &$query ) );
$ids = $query->get_posts();
return is_array( $ids ) ? count( $ids ) : 0;
}
/**
* {@inheritdoc}
*/
public function build_query() {
/**
* Allow classes extending or decorating the repository to act before
* the query is built or replace its building completely.
*/
if ( null !== $this->query_builder ) {
$built = $this->query_builder->build_query();
if ( null !== $built ) {
return $built;
}
}
$query = new WP_Query();
$this->filter_query->set_query( $query );
/**
* Here we merge, not recursively, to allow user-set query arguments
* to override the default ones.
*/
$query_args = array_merge( $this->default_args, $this->query_args );
$default_post_status = current_user_can( 'read_private_posts' ) ? 'any' : '';
$query_args['post_status'] = Tribe__Utils__Array::get( $query_args, 'post_status', $default_post_status );
/**
* Filters the query arguments that will be used to fetch the posts.
*
* @param array $query_args An array of the query arguments the query will be
* initialized with.
* @param WP_Query $query The query object, the query arguments have not been parsed yet.
* @param $this $this This repository instance
*/
$query_args = apply_filters( "{$this->filter_name}_query_args", $query_args, $query, $this );
if ( isset( $query_args['offset'] ) ) {
$offset = absint( $query_args['offset'] );
$per_page = (int) Tribe__Utils__Array::get( $query_args, 'posts_per_page', get_option( 'posts_per_page' ) );
$page = (int) Tribe__Utils__Array::get( $query_args, 'paged', 1 );
$real_offset = $per_page === - 1 ? $offset : ( $per_page * ( $page - 1 ) ) + $offset;
$query_args['offset'] = $real_offset;
$query_args['posts_per_page'] = $per_page === - 1 ? 99999999999 : $per_page;
/**
* Unset the `offset` query argument to avoid applying it multiple times when this method
* is used, on the same repository, more than once.
*/
unset( $this->query_args['offset'] );
}
foreach ( $query_args as $key => $value ) {
$query->set( $key, $value );
}
/**
* Here process the previously set query modifiers passing them the
* query object before it executes.
* The query modifiers should modify the query by reference.
*/
foreach ( $this->query_modifiers as $arg ) {
if ( is_object( $arg ) ) {
// __invoke, assume changes are made by reference
$arg( $query );
} elseif ( is_callable( $arg ) ) {
// assume changes are made by reference
$arg( $query );
}
}
return $query;
}
/**
* {@inheritdoc}
*/
public function found() {
if ( $this->void_query ) {
return 0;
}
$query = $this->build_query();
$query->set( 'fields', 'ids' );
/**
* Filters the query object by reference before counting found posts.
*
* @since 4.7.19
*
* @param WP_Query $query
*/
do_action_ref_array( "{$this->filter_name}_pre_found_posts", array( &$query ) );
$query->get_posts();
return (int) $query->found_posts;
}
/**
* {@inheritdoc}
*/
public function all() {
if ( $this->void_query ) {
return array();
}
$query = $this->build_query();
$return_ids = 'ids' === $query->get( 'fields', '' );
/**
* Do not skip counting the rows if we have some filtering to do on
* `found_posts`.
*/
$query->set( 'no_found_rows', $this->skip_found_rows );
// we'll let the class build the items later
$query->set( 'fields', 'ids' );
/**
* Filters the query object by reference before getting the posts.
*
* @since 4.7.19
*
* @param WP_Query $query
*/
do_action_ref_array( "{$this->filter_name}_pre_get_posts", array( &$query ) );
$results = $query->get_posts();
/**
* Allow extending classes to customize the return value.
* Since we are filtering the array returning empty values while formatting
* the item will exclude it from the return values.
*/
return $return_ids
? $results
: array_filter( array_map( array( $this, 'format_item' ), $results ) );
}
/**
* {@inheritdoc}
*/
public function offset( $offset, $increment = false ) {
/**
* The `offset` argument will only be used when `posts_per_page` is not -1
* and will ignore pagination.
* So we filter to apply a real SQL OFFSET; we also leave in place the `offset`
* query var to have a fallback should the LIMIT cause proving difficult to filter.
*/
$this->query_args['offset'] = $increment
? absint( $offset ) + (int) Tribe__Utils__Array::get( $this->query_args, 'offset', 0 )
: absint( $offset );
return $this;
}
/**
* {@inheritdoc}
*/
public function order( $order = 'ASC' ) {
$order = strtoupper( $order );
if ( ! in_array( $order, array( 'ASC', 'DESC' ) ) ) {
return $this;
}
$this->query_args['order'] = $order;
return $this;
}
/**
* {@inheritdoc}
*/
public function order_by( $order_by ) {
$this->query_args['orderby'] = $order_by;
return $this;
}
/**
* {@inheritdoc}
*/
public function fields( $fields ) {
$this->query_args['fields'] = $fields;
return $this;
}
/**
* {@inheritdoc}
*/
public function permission( $permission ) {
if ( ! in_array( $permission, array( self::PERMISSION_READABLE, self::PERMISSION_EDITABLE ), true ) ) {
return $this;
}
$this->query_args['perm'] = $permission;
return $this;
}
/**
* {@inheritdoc}
*/
public function in( $post_ids ) {
$this->add_args( 'post__in', $post_ids );
return $this;
}
/**
* Merges arguments into a query arg.
*
* @since 4.7.19
*
* @param string $key
* @param array|int $value
*/
protected function add_args( $key, $value ) {
$this->query_args[ $key ] = (array) $value;
}
/**
* {@inheritdoc}
*/
public function not_in( $post_ids ) {
$this->add_args( 'post__not_in', $post_ids );
return $this;
}
/**
* {@inheritdoc}
*/
public function parent( $post_id ) {
$this->add_args( 'post_parent__in', $post_id );
return $this;
}
/**
* {@inheritdoc}
*/
public function parent_in( $post_ids ) {
$this->add_args( 'post_parent__in', $post_ids );
return $this;
}
/**
* {@inheritdoc}
*/
public function parent_not_in( $post_ids ) {
$this->add_args( 'post_parent__not_in', $post_ids );
return $this;
}
/**
* {@inheritdoc}
*/
public function search( $search ) {
$this->query_args['s'] = $search;
return $this;
}
/**
* {@inheritdoc}
*/
public function first() {
$query = $this->build_query();
$return_id = 'ids' === $query->get( 'fields', '' );
$query->set( 'fields', 'ids' );
$ids = $query->get_posts();
if ( empty( $ids ) ) {
return null;
}
return $return_id ? reset( $ids ) : $this->format_item( reset( $ids ) );
}
/**
* Formats a post handled by the repository to the expected
* format.
*
* Extending classes should use this method to format return values to the expected format.
*
* @since 4.7.19
*
* @param int|WP_Post $id
*
* @return WP_Post
*/
protected function format_item( $id ) {
return null === $this->formatter
? get_post( $id )
: $this->formatter->format_item( $id );
}
/**
* {@inheritdoc}
*/
public function last() {
$query = $this->build_query();
$return_id = 'ids' === $query->get( 'fields', '' );
$query->set( 'fields', 'ids' );
$ids = $query->get_posts();
if ( empty( $ids ) ) {
return null;
}
return $return_id ? end( $ids ) : $this->format_item( end( $ids ) );
}
/**
* {@inheritdoc}
*/
public function nth( $n ) {
$per_page = (int) Tribe__Utils__Array::get_in_any( array(
$this->query_args,
$this->default_args,
), 'posts_per_page', get_option( 'posts_per_page' ) );
if ( - 1 != $per_page && $n > $per_page ) {
return null;
}
$query = $this->build_query();
$return_id = 'ids' === $query->get( 'fields', '' );
$i = absint( $n ) - 1;
$query->set( 'fields', 'ids' );
$ids = $query->get_posts();
if ( empty( $ids[ $i ] ) ) {
return null;
}
return $return_id ? $ids[ $i ] : $this->format_item( $ids[ $i ] );
}
/**
* Applies and returns a schema entry.
*
* @since 4.7.19
*
* @param string $key
* @param mixed $value
* @param mixed ...$args Additional arguments for the application.
*
* @return mixed A scalar value or a callable.
*/
public function apply_modifier( $key, $value ) {
$call_args = func_get_args();
$application = Tribe__Utils__Array::get( $this->schema, $key, null );
/**
* Return primitives, including `null`, as they are.
*/
if ( ! is_callable( $application ) ) {
return $application;
}
/**
* Allow for callbacks to fire immediately and return more complex values.
* This also means that callbacks meant to run on the next step, the one
* where args are applied, will need to be "wrapped" in callbacks themselves.
* The `$key` is removed from the args to get the value first and avoid
* unused args.
*/
$args_without_key = array_splice( $call_args, 1 );
return call_user_func_array( $application, $args_without_key );
}
/**
* {@inheritdoc}
*/
public function take( $n ) {
$query = $this->build_query();
$return_id = 'ids' === $query->get( 'fields', '' );
$query->set( 'fields', 'ids' );
$matching_ids = $query->get_posts();
if ( empty( $matching_ids ) ) {
return array();
}
$spliced = array_splice( $matching_ids, 0, $n );
return $return_id ? $spliced : array_map( array( $this, 'format_item' ), $spliced );
}
/**
* Fetches a single instance of the post type handled by the repository.
*
* Similarly to the `get_post` function permissions are not taken into account when returning
* an instance by its primary key; extending classes can refine this behaviour to suit.
*
* @param mixed $primary_key
*
* @return WP_Post|null|mixed
*/
public function by_primary_key( $primary_key ) {
return $this->by( $this->primary_key, $primary_key )->first();
}
/**
* {@inheritdoc}
*/
public function by( $key, $value ) {
if ( $this->void_query ) {
// No point in doing more computations if the query is void.
return $this;
}
$call_args = func_get_args();
$this->current_filters[ $key ] = $value;
try {
$query_modifier = $this->modify_query( $key, $call_args );
/**
* Here we allow the repository to call one of its own methods and return `null`.
* A repository might have a `where` or `by` that is just building
* a more complex query using a base `where` or `by`.
*/
if ( null === $query_modifier ) {
return $this;
}
/**
* Primitives are just merged in.
* Since we are using `array_merge_recursive` we expect them to be arrays.
*/
if ( ! ( is_object( $query_modifier ) || is_callable( $query_modifier ) ) ) {
if ( ! is_array( $query_modifier ) ) {
throw new InvalidArgumentException( 'Query modifier should be an array!' );
}
$replace_modifiers = in_array( $key, $this->replacing_modifiers(), true );
if ( $replace_modifiers ) {
/**
* We do a merge to make sure new values will override and replace the old
* ones.
*/
$this->query_args = array_merge( $this->query_args, $query_modifier );
} else {
/**
* We do a recursive merge to allow "stacking" of same kind of queries;
* e.g. two or more `tax_query`.
*/
$this->query_args = array_merge_recursive( $this->query_args, $query_modifier );
}
} else {
/**
* If we get back something that is not an array then we add it to
* the stack of query modifying callbacks we'll call on the query
* after building it.
*/
$this->query_modifiers[] = $query_modifier;
}
} catch ( Tribe__Repository__Void_Query_Exception $e ) {
/**
* We allow for the `apply` method to orderly fail to micro-optimize.
* If applying one parameter would yield no results then let's immediately bail.
* Schema should throw t
* his Exception if a light-weight on the filters would already
* deem a query as yielding nothing.
*/
$this->void_query = true;
return $this;
}
/**
* Catching other type of exceptions is something the client code should handle!
*/
return $this;
}
/**
* Returns the query modifier for a key.
*
* @since 4.7.19
*
* @param string $key
* @param array $call_args
*
* @return mixed
*
* @throws Tribe__Repository__Usage_Error If the required filter is not defined by the class.
* @throws Tribe__Repository__Void_Query_Exception To signal the query would yield no results.
*/
protected function modify_query( $key, $call_args ) {
if ( ! $this->schema_has_modifier_for( $key ) ) {
if ( $this->has_default_modifier( $key ) ) {
// let's use the default filters normalizing the key first
$call_args[0] = $this->normalize_key( $key );
$query_modifier = call_user_func_array( array( $this, 'apply_default_modifier' ), $call_args );
} else {
throw Tribe__Repository__Usage_Error::because_the_read_filter_is_not_defined( $key, $this );
}
} else {
$query_modifier = call_user_func_array( array( $this, 'apply_modifier' ), $call_args );
}
return $query_modifier;
}
/**
* Whether the current schema defines an application for the key or not.
*
* @since 4.7.19
*
* @param $key
*
* @return bool
*/
protected function schema_has_modifier_for( $key ) {
return isset( $this->schema[ $key ] );
}
/**
* Whether a filter defined and handled by the repository exists or not.
*
* @since 4.7.19
*
* @param string $key
*
* @return bool
*/
protected function has_default_modifier( $key ) {
$normalized_key = $this->normalize_key( $key );
return in_array( $normalized_key, self::$default_modifiers, true );
}
/**
* Normalizes the filter key to allow broad matching of the `by` filters.
*
* @since 4.7.19
*
* E.g. `by( 'id', 23 )` is the same as `by( 'ID', 23 ).
* E.g. `by( 'parent', 23 )` is the same as `by( `post_parent`, 23 )`
*
* @param string $key
*
* @return string The normalized filter key
*/
protected function normalize_key( $key ) {
// `ID` to `id`
$normalized = strtolower( $key );
$post_prefixed = array(
'password',
'name__in',
'_in',
'_not_in',
'parent',
'parent__in',
'parent__not_in',
'mime_type',
'content',
'excerpt',
'status',
'modified',
'modified_gmt',
'content_filtered',
);
if ( in_array( $key, $post_prefixed, true ) ) {
$normalized = 'post_' . $key;
}
return $normalized;
}
/**
* Returns a list of modifiers that, when applied multiple times,
* will replace the previous value.
*
* This behaviour is in opposition to "stackable" modifiers that will,
* instead, be composed and stacked.
*
* @since 4.7.19
*
* @return array
*/
protected function replacing_modifiers() {
return self::$replacing_modifiers;
}
/**
* Batch filter application method.
*
* This is the same as calling `where` multiple times with different arguments.
*
* @since 4.7.19
*
* @param array $args An associative array of arguments to filter
* the posts by in the shape [ <key>, <value> ].
*
* @return Tribe__Repository__Read_Interface|Tribe__Repository__Update_Interface
*/
public function where_args( array $args ) {
return $this->by_args( $args );
}
/**
* {@inheritdoc}
*/
public function by_args( array $args ) {
foreach ( $args as $key => $value ) {
$this->by( $key, $value );
}
return $this;
}
/**
* Commits the updates to the selected post IDs to the database.
*
* @since 4.7.19
*
* @param bool $sync Whether to apply the updates in a synchronous process
* or in an asynchronous one.
*
* @return array A list of the post IDs that have been (synchronous) or will
* be (asynchronous) updated. When running in sync mode the return
* value will be a map in the shape [ <id> => <update_result> ] where
* `true` indicates a correct update.
*
* @throws Tribe__Repository__Usage_Error If trying to update a field that cannot be
* updated.
*/
public function save( $sync = true ) {
$ids = $this->get_ids();
if ( empty( $ids ) ) {
return array();
}
$exit = array();
$postarrs = array();
foreach ( $ids as $id ) {
$postarr = array(
'ID' => $id,
'tax_input' => array(),
'meta_input' => array(),
);
foreach ( $this->updates as $key => $value ) {
if ( is_callable( $value ) ) {
$value = $value( $id, $key, $this );
}
if ( ! $this->can_be_udpated( $key ) ) {
throw Tribe__Repository__Usage_Error::because_this_field_cannot_be_updated( $key, $this );
}
if ( $this->is_a_post_field( $key ) ) {
if ( $this->requires_converted_date( $key ) ) {
$this->update_postarr_dates( $key, $value, $postarr );
} else {
$postarr[ $key ] = $value;
}
} elseif ( $this->is_a_taxonomy( $key ) ) {
$postarr['tax_input'][ $key ] = $value;
} else {
// it's a custom field
$postarr['meta_input'][ $key ] = $value;
}
}
$postarrs[ $id ] = $postarr;
}
// @todo actually implement async
foreach ( $postarrs as $id => $postarr ) {
$this_exit = wp_update_post( $postarr );
$exit[ $id ] = $id === $this_exit ? true : $this_exit;
}
return $exit;
}
/**
* Gets the post IDs that should be updated.
*
* @since 4.7.19
*
* @return array An array containing the post IDs to update.
*/
protected function get_ids() {
/** @var WP_Query $query */
$query = $this->get_query();
$query->set( 'fields', 'ids' );
return $query->get_posts();
}
/**
* {@inheritdoc}
*/
public function get_query() {
return $this->build_query();
}
/**
* Whether the current key can be updated by this repository or not.
*
* @since 4.7.19
*
* @param string $key
*
* @return bool
*/
protected function can_be_udpated( $key ) {
return ! in_array( $key, self::$blocked_keys, true );
}
/**
* Whether the key is a field of the posts table or not.
*
* @since 4.7.19
*
* @param string $key
*
* @return bool
*/
protected function is_a_post_field( $key ) {
return in_array( $key, array(
'ID',
'post_author',
'post_date',
'post_date_gmt',
'post_content',
'post_title',
'post_excerpt',
'post_status',
'comment_status',
'ping_status',
'post_password',
'post_name',
'to_ping',
'pinged',
'post_modified',
'post_modified_gmt',
'post_content_filtered',
'post_parent',
'guid',
'menu_order',
'post_type',
'post_mime_type',
'comment_count',
), true );
}
/**
* Whether the current key is a date one requiring a converted key pair too or not.
*
* @param string $key
*
* @return bool
*/
protected function requires_converted_date( $key ) {
return array_key_exists( $key, $this->to_local_time_map ) || array_key_exists( $key, $this->to_gmt_map );
}
/**
* Updates the update post payload to add dates that should be provided in GMT
* and localized version.
*
* @since 4.7.19
*
* @param string $key
* @param string|int $value
* @param array $postarr
*/
protected function update_postarr_dates( $key, $value, array &$postarr ) {
if ( array_key_exists( $key, $this->to_gmt_map ) ) {
$postarr[ $this->to_gmt_map[ $key ] ] = Tribe__Timezones::to_tz( $value, 'UTC' );
} elseif ( array_key_exists( $key, $this->to_local_time_map ) ) {
$postarr[ $this->to_local_time_map[ $key ] ] = Tribe__Timezones::to_tz( $value, Tribe__Timezones::wp_timezone_string() );
}
$postarr[ $key ] = $value;
}
/**
* Whether the current key identifies one of the supported taxonomies or not.
*
* @since 4.7.19
*
* @param string $key
*
* @return bool
*/
protected function is_a_taxonomy( $key ) {
return in_array( $key, $this->taxonomies, true );
}
/**
* {@inheritdoc}
*/
public function set_args( array $update_map ) {
foreach ( $update_map as $key => $value ) {
$this->set( $key, $value );
}
return $this;
}
/**
* {@inheritdoc}
*/
public function set( $key, $value ) {
if ( ! is_string( $key ) ) {
throw Tribe__Repository__Usage_Error::because_udpate_key_should_be_a_string( $this );
}
$this->updates[ $key ] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function filter_name( $filter_name ) {
$this->filter_name = trim( $filter_name );
return $this;
}
/**
* {@inheritdoc}
*/
public function set_formatter( Tribe__Repository__Formatter_Interface $formatter ) {
$this->formatter = $formatter;
}
/**
* Filters the query to only return posts that are related, via a meta key, to posts
* that satisfy a condition.
*
* @param string|array $meta_keys One ore more `meta_keys` relating the queried post type(s)
* to another post type.
* @param string $compare The SQL compoarison operator.
* @param string $field One (a column in the `posts` table) that should match
* the comparison criteria; required if the comparison operator is not `EXISTS` or
* `NOT EXISTS`.
* @param string|array $values One or more values the post field(s) should be compared to;
* required if the comparison operator is not `EXISTS` or `NOT EXISTS`.
*
* @return $this
* @throws Tribe__Repository__Usage_Error If the comparison operator requires
*/
public function where_meta_related_by( $meta_keys, $compare, $field = null, $values = null ) {
$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
if ( ! in_array( $compare, array( 'EXISTS', 'NOT EXISTS' ) ) ) {
if ( empty( $field ) || empty( $values ) ) {
throw Tribe__Repository__Usage_Error::because_this_comparison_operator_requires_fields_and_values( $meta_keys, $compare, $this );
}
$field = esc_sql( $field );
}
/** @var wpdb $wpdb */
global $wpdb;
$p = $this->sql_slug( 'meta_related_post', $compare, $meta_keys );
$pm = $this->sql_slug( 'meta_related_post_meta', $compare, $meta_keys );
$this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} {$pm} ON {$wpdb->posts}.ID = {$pm}.post_id" );
$this->filter_query->join( "LEFT JOIN {$wpdb->posts} {$p} ON {$pm}.meta_value = {$p}.ID" );
$keys_in = $this->prepare_interval( $meta_keys );
if ( 'EXISTS' === $compare ) {
$this->filter_query->where( "{$pm}.meta_key IN {$keys_in} AND {$pm}.meta_id IS NOT NULL" );
} elseif ( 'NOT EXISTS' === $compare ) {
$this->filter_query->where( "{$pm}.meta_id IS NULL" );
} else {
if ( in_array( $compare, self::$multi_value_keys, true ) ) {
$values = $this->prepare_interval( $values );
} else {
$values = $this->prepare_value( $values );
}
$this->filter_query->where( "{$pm}.meta_key IN {$keys_in} AND {$p}.{$field} {$compare} {$values}" );
}
return $this;
}
/**
* Builds a fenced group of WHERE clauses that will be used with OR logic.
*
* Mind that this is a lower level implementation of WHERE logic that requires
* each callback method to add, at least, one WHERE clause using the repository
* own `where_clause` method.
*
* @param array $callbacks One or more WHERE callbacks that will be called
* this repository. The callbacks have the shape
* [ <method>, <...args>]
*
* @return $this
* @throws Tribe__Repository__Usage_Error If one of the callback methods does
* not add any WHERE clause.
*
* @see Tribe__Repository::where_clause()
* @see Tribe__Repository__Query_Filters::where()
*/
public function where_or( $callbacks ) {
$callbacks = func_get_args();
$buffered = $this->filter_query->get_buffered_where_clauses( true );
$this->filter_query->buffer_where_clauses( true );
$buffered_count = count( $buffered );
foreach ( $callbacks as $c ) {
call_user_func_array( array( $this, $c[0] ), array_slice( $c, 1 ) );
if ( $buffered_count === count( $this->filter_query->get_buffered_where_clauses() ) ) {
throw Tribe__Repository__Usage_Error::because_where_or_should_only_be_used_with_methods_that_add_where_clauses( $c, $this );
}
$buffered_count ++;
}
$buffered = $this->filter_query->get_buffered_where_clauses( true );
$fenced = sprintf( '( %s )', implode( ' OR ', $buffered ) );
$this->where_clause( $fenced );
return $this;
}
/**
* Returns modified query arguments after applying a default filter.
*
* @since 4.7.19
*
* @param string $key
* @param mixed $value
*
* @return array
* @throws Tribe__Repository__Usage_Error If a filter is called with wrong arguments.
*/
protected function apply_default_modifier( $key, $value ) {
$args = array();
$call_args = func_get_args();
$arg_1 = isset( $call_args[2] ) ? $call_args[2] : null;
$arg_2 = isset( $call_args[3] ) ? $call_args[3] : null;
/** @var wpdb $wpdb */
global $wpdb;
switch ( $key ) {
default:
// leverage built-in WP_Query filters
$args = array( $key => $value );
break;
case 'ID':
case 'id':
$args = array( 'p' => $value );
break;
case 'search':
$args = array( 's' => $value );
break;
case 'post_status':
$this->query_args['post_status'] = (array) $value;
break;
case 'date':
case 'after_date':
$args = $this->get_posts_after( $value, 'post_date' );
break;
case 'before_date':
$args = $this->get_posts_before( $value, 'post_date' );
break;
case 'date_gmt':
case 'after_date_gmt':
$args = $this->get_posts_after( $value, 'post_date_gmt' );
break;
case 'before_date_gmt':
$args = $this->get_posts_before( $value, 'post_date_gmt' );
break;
case 'title_like':
$this->filter_query->to_get_posts_with_title_like( $value );
break;
case 'post_content':
$this->filter_query->to_get_posts_with_content_like( $value );
break;
case 'post_excerpt':
$this->filter_query->to_get_posts_with_excerpt_like( $value );
break;
case 'to_ping':
$this->filter_query->to_get_posts_to_ping( $value );
$args = array( 'to_ping' => $value );
break;
case 'post_modified':
$args = $this->get_posts_after( $value, 'post_modified' );
break;
case 'post_modified_gmt':
$args = $this->get_posts_after( $value, 'post_modified_gmt' );
break;
case 'post_content_filtered':
$this->filter_query->to_get_posts_with_filtered_content_like( $value );
break;
case 'guid':
$this->filter_query->to_get_posts_with_guid_like( $value );
break;
case 'meta':
case 'meta_equals':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '=', $format = $arg_2 );
break;
case 'meta_not_equals':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '!=', $format = $arg_2 );
break;
case 'meta_gt':
case 'meta_greater_than':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '>', $format = $arg_2 );
break;
case 'meta_gte':
case 'meta_greater_than_or_equal':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '>=', $format = $arg_2 );
break;
case 'meta_like':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'LIKE' );
break;
case 'meta_not_like':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT LIKE' );
break;
case 'meta_lt':
case 'meta_less_than':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '<', $format = $arg_2 );
break;
case 'meta_lte':
case 'meta_less_than_or_equal':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '<=', $format = $arg_2 );
break;
case 'meta_in':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'IN', $format = $arg_2 );
break;
case 'meta_not_in':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT IN', $format = $arg_2 );
break;
case 'meta_between':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'BETWEEN', $format = $arg_2 );
break;
case 'meta_not_between':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT BETWEEN', $format = $arg_2 );
break;
case 'meta_exists':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'EXISTS' );
break;
case 'meta_not_exists':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT EXISTS' );
break;
case 'meta_regexp':
case 'meta_equals_regexp':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'REGEXP' );
break;
case 'meta_not_regexp':
case 'meta_not_equals_regexp':
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT REGEXP' );
break;
case 'taxonomy_exists':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'EXISTS' );
break;
case 'taxonomy_not_exists':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT EXISTS' );
break;
case 'term_id_in':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'IN' );
break;
case 'term_id_not_in':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT IN' );
break;
case 'term_id_and':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'AND' );
break;
case 'term_name_in':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'IN' );
break;
case 'term_name_not_in':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'NOT IN' );
break;
case 'term_name_and':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'AND' );
break;
case 'term_slug_in':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'IN' );
break;
case 'term_slug_not_in':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'NOT IN' );
break;
case 'term_slug_and':
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'AND' );
break;
}
return $args;
}
/**
* Builds a date query entry to get posts after a date.
*
* @since 4.7.19
*
* @param string $value
* @param string $column
*
* @return array
*/
protected function get_posts_after( $value, $column = 'post_date' ) {
$timezone = in_array( $column, array( 'post_date_gmt', 'post_modified_gmt' ) )
? 'UTC'
: Tribe__Timezones::generate_timezone_string_from_utc_offset( Tribe__Timezones::wp_timezone_string() );
if ( is_numeric( $value ) ) {
$value = "@{$value}";
}
$date = new DateTime( $value, new DateTimeZone( $timezone ) );
$array_key = sprintf( '%s-after', $column );
return array(
'date_query' => array(
'relation' => 'AND',
$array_key => array(
'inclusive' => true,
'column' => $column,
'after' => $date->format( 'Y-m-d H:i:s' ),
),
),
);
}
/**
* Builds a date query entry to get posts before a date.
*
* @since 4.7.19
*
* @param string $value
* @param string $column
*
* @return array
*/
protected function get_posts_before( $value, $column = 'post_date' ) {
$timezone = in_array( $column, array( 'post_date_gmt', 'post_modified_gmt' ) )
? 'UTC'
: Tribe__Timezones::generate_timezone_string_from_utc_offset( Tribe__Timezones::wp_timezone_string() );
if ( is_numeric( $value ) ) {
$value = "@{$value}";
}
$date = new DateTime( $value, new DateTimeZone( $timezone ) );
$array_key = sprintf( '%s-before', $column );
return array(
'date_query' => array(
'relation' => 'AND',
$array_key => array(
'inclusive' => true,
'column' => $column,
'before' => $date->format( 'Y-m-d H:i:s' ),
),
),
);
}
/**
* Builds a meta query entry.
*
* @since 4.7.19
*
* @param string $meta_key
* @param string|array $meta_value
* @param string $compare
* @param string $type_or_format The type of value to compare
*
* @return array|null
* @throws Tribe__Repository__Usage_Error If trying to compare multiple values with a single
* comparison operator.
*/
protected function build_meta_query( $meta_key, $meta_value = 'value', $compare = '=', $type_or_format = null ) {
$meta_keys = Tribe__Utils__Array::list_to_array( $meta_key );
$postfix = Tribe__Utils__Array::get( self::$comparison_operators, $compare, '' );
if ( count( $meta_keys ) === 1 ) {
$array_key = $this->sql_slug( $meta_keys[0], $postfix );
$args = array(
'meta_query' => array(
$array_key => array(
'key' => $meta_keys[0],
'compare' => strtoupper( $compare ),
),
),
);
if ( ! in_array( $compare, array( 'EXISTS', 'NOT EXISTS' ) ) ) {
$args['meta_query'][ $array_key ]['value'] = $meta_value;
}
if ( 0 === strpos( $type_or_format, '%' ) ) {
throw Tribe__Repository__Usage_Error::because_the_type_is_a_wpdb_prepare_format( $meta_key, $type_or_format, $this );
}
if ( null !== $type_or_format ) {
$args['meta_query'][ $array_key ]['type'] = $type_or_format;
}
return $args;
}
if ( null === $type_or_format ) {
$type_or_format = '%s';
} elseif ( 0 !== strpos( $type_or_format, '%' ) ) {
throw Tribe__Repository__Usage_Error::because_the_format_is_not_a_wpdb_prepare_one( $meta_key, $type_or_format, $this );
}
/** @var wpdb $wpdb */
global $wpdb;
// Build custom WHERE and JOINS to reduce the JOIN clauses
$pm_alias = $this->sql_slug( 'meta', $postfix, ++ self::$meta_alias );
$meta_keys_in = sprintf( "('%s')", implode( "','", array_map( 'esc_sql', $meta_keys ) ) );
$this->validate_operator_and_values( $compare, $meta_keys, $meta_value );
if ( in_array( $compare, self::$multi_value_keys, true ) ) {
$meta_values = $this->prepare_interval( Tribe__Utils__Array::list_to_array( $meta_value ), $type_or_format );
} else {
$meta_values = $this->prepare_value( $meta_value, $type_or_format );
}
$this->filter_query->join( "JOIN {$wpdb->postmeta} {$pm_alias} ON {$wpdb->posts}.ID = {$pm_alias}.post_id" );
if ( 'EXISTS' === $compare ) {
$this->filter_query->where( "{$pm_alias}.meta_key IN {$meta_keys_in} AND {$pm_alias}.meta_id IS NOT NULL" );
} else if ( 'NOT EXISTS' === $compare ) {
$this->filter_query->where( "{$pm_alias}.meta_key NOT IN {$meta_keys_in} AND {$pm_alias}.meta_id IS NOT NULL" );
} else {
$this->filter_query->where( "{$pm_alias}.meta_key IN {$meta_keys_in} AND {$pm_alias}.meta_value {$compare} {$meta_values}" );
}
}
/**
* Generates a SQL friendly slug from the provided, variadic, fragments.
*
* @since 4.7.19
*
* @param ...string $frag
*
* @return string
*/
protected function sql_slug( $frag ) {
$frags = func_get_args();
foreach ( $frags as &$frag ) {
if ( is_string( $frag ) ) {
Tribe__Utils__Array::get( self::$comparison_operators, $frag, $frag );
} elseif ( is_array( $frag ) ) {
$frag = implode( '_', $frag );
}
}
$frags = array_filter( $frags );
return strtolower( str_replace( '-', '_', sanitize_title( implode( '_', $frags ) ) ) );
}
/**
* Builds a taxonomy query entry.
*
* @since 4.7.19
*
* @param string $taxonomy
* @param int|string|array $terms
* @param string $field
* @param string $operator
*
* @return array
*/
protected function build_tax_query( $taxonomy, $terms, $field, $operator ) {
if ( in_array( $operator, array( 'EXISTS', 'NOT EXISTS' ) ) ) {
$array_key = $this->sql_slug( $taxonomy, $operator );
} else {
$array_key = $this->sql_slug( $taxonomy, $field, $operator );
}
return array(
'tax_query' => array(
$array_key => array(
'taxonomy' => $taxonomy,
'field' => $field,
'terms' => $terms,
'operator' => strtoupper( $operator ),
),
),
);
}
/**
* {@inheritdoc}
*/
public function join_clause( $join ) {
$this->filter_query->join( $join );
}
/**
* {@inheritdoc}
*/
public function where_clause( $where ) {
$this->filter_query->where( $where );
}
/**
* {@inheritdoc}
*/
public function set_query_builder( $query_builder ) {
$this->query_builder = $query_builder;
}
/**
* Builds and escapes an interval of strings.
*
* The return string includes opening and closing braces.
*
* @since 4.7.19
*
* @param string|array $values One or more values to use to build
* the interval.
* @param string $format The format that should be used to escape
* the values; default to '%s'.
*
* @return string
*/
public function prepare_interval( $values, $format = '%s' ) {
/** @var wpdb $wpdb */
global $wpdb;
$values = Tribe__Utils__Array::list_to_array( $values );
$prepared = array();
foreach ( $values as $value ) {
$prepared[] = $this->prepare_value( $value, $format );
}
return sprintf( '(' . $format . ')', implode( ',', $prepared ) );
}
/**
* Prepares a single value to be used in a SQL query.
*
* @since 4.7.19
*
* @param mixed $value
* @param string $format
*
* @return string
*/
public function prepare_value( $value, $format = '%s' ) {
/** @var wpdb $wpdb */
global $wpdb;
return $wpdb->prepare( $format, $value );
}
/**
* Validates that a comparison operator is used with the correct type of values.
*
* This is just a wrap to signal this kind of code error not in bad SQL error but
* with a visible exception.
*
* @since 4.7.19
*
* @param string $compare A SQL comparison operator
* @param string|array $meta_key
* @param mixed $meta_value
*
* @throws Tribe__Repository__Usage_Error
*/
protected function validate_operator_and_values( $compare, $meta_key, $meta_value ) {
if ( is_array( $meta_value ) && ! in_array( $compare, self::$multi_value_keys, true ) ) {
throw Tribe__Repository__Usage_Error::because_single_value_comparisons_should_be_used_with_one_value(
$meta_key,
$meta_value,
$compare,
$this
);
}
}
/**
* {@inheritdoc}
*/
public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null ) {
$min = $this->prepare_value( $min, '%d' );
/** @var wpdb $wpdb */
global $wpdb;
$by_meta_keys = $this->prepare_interval( $by_meta_keys );
$join = '';
$and_where = '';
if ( ! empty( $keys ) || ! empty( $values ) ) {
$join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
}
if ( ! empty( $keys ) ) {
$keys = $this->prepare_interval( $keys );
$and_where .= "\nAND pm2.meta_key IN {$keys}\n";
}
if ( ! empty( $values ) ) {
$values = $this->prepare_interval( $values );
$and_where .= "\nAND pm2.meta_value IN {$values}\n";
}
$this->where_clause( "{$wpdb->posts}.ID IN (
SELECT pm1.meta_value
FROM {$wpdb->postmeta} pm1 {$join}
WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
GROUP BY( pm1.meta_value )
HAVING COUNT(DISTINCT pm1.post_id) >= {$min}
)" );
return $this;
}
/**
* {@inheritdoc}
*/
public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null ) {
$max = $this->prepare_value( $max, '%d' );
/** @var wpdb $wpdb */
global $wpdb;
$join = '';
$and_where = '';
if ( ! empty( $keys ) || ! empty( $values ) ) {
$join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
}
if ( ! empty( $keys ) ) {
$keys = $this->prepare_interval( $keys );
$and_where .= "\nAND pm2.meta_key IN {$keys}\n";
}
if ( ! empty( $values ) ) {
$values = $this->prepare_interval( $values );
$and_where .= "\nAND pm2.meta_value IN {$values}\n";
}
$by_meta_keys = $this->prepare_interval( $by_meta_keys );
$this->where_clause( "{$wpdb->posts}.ID IN (
SELECT pm1.meta_value
FROM {$wpdb->postmeta} pm1 {$join}
WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
GROUP BY( pm1.meta_value )
HAVING COUNT(DISTINCT pm1.post_id) <= {$max}
)" );
return $this;
}
/**
* {@inheritdoc}
*/
public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null ) {
$min = $this->prepare_value( $min, '%d' );
$max = $this->prepare_value( $max, '%d' );
/** @var wpdb $wpdb */
global $wpdb;
$by_meta_keys = $this->prepare_interval( $by_meta_keys );
$join = '';
$and_where = '';
if ( ! empty( $keys ) || ! empty( $values ) ) {
$join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n";
}
if ( ! empty( $keys ) ) {
$keys = $this->prepare_interval( $keys );
$and_where .= "\nAND pm2.meta_key IN {$keys}\n";
}
if ( ! empty( $values ) ) {
$values = $this->prepare_interval( $values );
$and_where .= "\nAND pm2.meta_value IN {$values}\n";
}
$this->where_clause( "{$wpdb->posts}.ID IN (
SELECT pm1.meta_value
FROM {$wpdb->postmeta} pm1 {$join}
WHERE pm1.meta_key IN {$by_meta_keys} {$and_where}
GROUP BY( pm1.meta_value )
HAVING COUNT(DISTINCT pm1.post_id) BETWEEN {$min} AND {$max}
)" );
return $this;
}
/**
* {@inheritdoc}
*/
public function has_filter( $key, $value = null ) {
return null === $value
? array_key_exists( $key, $this->current_filters )
: array_key_exists( $key, $this->current_filters ) && $this->current_filters[ $key ] == $value;
}
}