File "Repository.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository.php
File size: 108.57 KB
MIME-type: text/x-php
Charset: utf-8
<?php
use Tribe\Traits\With_Meta_Updates_Handling;
use Tribe\Traits\With_Post_Attribute_Detection;
use Tribe__Utils__Array as Arr;
abstract class Tribe__Repository
implements Tribe__Repository__Interface {
use With_Meta_Updates_Handling;
use With_Post_Attribute_Detection;
const MAX_NUMBER_OF_POSTS_PER_PAGE = 99999999999;
/**
* The context of the current query.
*
* @since 5.2.0
*
* @var string|null
*/
protected ?string $request_context = null;
/**
* @var array An array of keys that cannot be updated on this repository.
*/
protected static $blocked_keys = [
'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 = [
'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',
'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',
'menu_order',
'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',
'meta_regexp_or_like',
'meta_equals_regexp_or_like',
'meta_not_regexp_or_like',
'meta_not_equals_regexp_or_like',
'order',
'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',
'term_in',
'term_not_in',
'term_and',
];
/**
* @var array An array of default arguments that will be applied to all queries.
*/
protected static $common_args = [
'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 = [
'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',
'order',
];
/**
* @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 = [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ];
/**
* @var array A map of SQL comparison operators to their human-readable counterpart.
*/
protected static $comparison_operators = [
'=' => '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',
];
/**
* A counter to keep track, on the class level, of the aliases generated for the terms table
* while building multi queries.
*
* @var int
*/
protected static $alias_counter = 1;
/**
* @var string
*/
protected $filter_name = 'default';
/**
* @var array The post IDs that will be updated.
*/
protected $ids = [];
/**
* @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 = [];
/**
* @var array A list of taxonomies this repository will recognize.
*/
protected $taxonomies = [];
/**
* @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 = [
'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 = [
'post_date' => 'post_date_gmt',
];
/**
* @var array
*/
protected $default_args = [ 'post_type' => 'post' ];
/**
* @var array An array of query modifying callbacks populated while applying
* the filters.
*/
protected $query_modifiers = [];
/**
* @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 = [
'meta_query' => [ 'relation' => 'AND' ],
'tax_query' => [ 'relation' => 'AND' ],
'date_query' => [ 'relation' => 'AND' ],
];
/**
* @var array An array of query arguments that support 'relation'.
*/
protected $relation_query_args = [
'meta_query',
'tax_query',
'date_query',
];
/**
* @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 = [];
/**
* @var string|null The current filter being applied.
*/
protected $current_filter;
/**
* @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 = [];
/**
* @var array A map of schema slugs and their meta keys to be queried.
*/
protected $simple_meta_schema = [];
/**
* @var array A map of schema slugs and their taxonomies to be queried.
*/
protected $simple_tax_schema = [];
/**
* @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;
/**
* A map relating aliases to their real update field name.
*
* E.g. the `title` alias might be an alias of `post_title` in update/save operations.
* This is done to allow using set-like methods with human-readable names.
* Extending classes should pre-fill this with default aliases.
*
* @var array
*/
protected $update_fields_aliases = [
'title' => 'post_title',
'content' => 'post_content',
'description' => 'post_content',
'slug' => 'post_name',
'excerpt' => 'post_excerpt',
'status' => 'post_status',
'parent' => 'post_parent',
'author' => 'post_author',
'date' => 'post_date',
'date_gmt' => 'post_date_gmt',
'date_utc' => 'post_date_gmt',
'tag' => 'post_tag',
'image' => '_thumbnail_id',
];
/**
* The default create args that will be used by the repository
* to create posts of the managed type.
*
* @var
*/
protected $create_args;
/**
* Indicates the current display context if any.
* Extending classes can support and use this property to know the
* display context.
*
* @var string
*/
protected $display_context = 'default';
/**
* Indicates the current render context if any.
* Extending classes can support and use this property to know the
* render context.
*
* @var string
*/
protected $render_context = 'default';
/**
* The query last built from the repository instance.
*
* @var WP_Query|null
*/
protected $last_built_query;
/**
* The hash of the last built query.
*
* @var string
*/
protected $last_built_hash = '';
/**
* Tribe__Repository constructor.
*
* @since 4.7.19
*/
public function __construct() {
$this->filter_query = new Tribe__Repository__Query_Filters();
$this->default_args = array_merge( [ 'posts_per_page' => - 1 ], $this->default_args );
$post_types = (array) Tribe__Utils__Array::get( $this->default_args, 'post_type', [] );
$this->taxonomies = get_taxonomies( [ 'object_type' => $post_types ], 'names' );
/**
* Allow plugins to init their classes and setup hooks at the initial setup of a repository.
*
* @since 4.9.5
*
* @param Tribe__Repository $this This repository instance
*
*/
do_action( "tribe_repository_{$this->filter_name}_init", $this );
}
/**
* {@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 = null ) {
$call_args = func_get_args();
return call_user_func_array( [ $this, 'by' ], $call_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();
// The request property will be set during the `get_posts` method and empty before it.
if ( ! empty( $query->request ) ) {
return (int) $query->post_count;
}
$original_fields_value = $query->get( 'fields', '' );
$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( "tribe_repository_{$this->filter_name}_pre_count_posts", $query );
$ids = $query->get_posts();
$query->set( 'fields', $original_fields_value );
return is_array( $ids ) ? count( $ids ) : 0;
}
/**
* {@inheritdoc}
*/
public function build_query( $use_query_builder = true ) {
$query = null;
if ( array_key_exists( 'void_query', $this->query_args ) && false !== $this->query_args['void_query'] ) {
$this->void_query = true;
}
// We'll let the query builder decide if the query has to be rebuilt or not.
if ( $use_query_builder && null !== $this->query_builder ) {
$query = $this->build_query_with_builder();
}
if ( null !== $this->last_built_query && $this->last_built_hash === $this->hash() ) {
return $this->last_built_query;
}
if ( null === $query ) {
$query = $this->build_query_internally();
}
/**
* Fires after the query has been built and before it's returned.
*
* @since 4.9.5
*
* @param WP_Query $query The built query.
* @param array $query_args An array of query arguments used to build the query.
* @param Tribe__Repository $this This repository instance.
* @param bool $use_query_builder Whether a query builder was used to build this query or not.
* @param Tribe__Repository__Interface $query_builder The query builder in use, if any.
*/
do_action( "tribe_repository_{$this->filter_name}_query",
$query,
$this,
$use_query_builder,
$this->query_builder
);
$this->last_built_query = $query;
$this->last_built_hash = $this->hash();
return $query;
}
/**
* {@inheritdoc}
*/
public function found() {
if ( $this->void_query ) {
return 0;
}
$query = $this->build_query();
$original_no_found_rows_value = $query->get( 'no_found_rows' );
// The request property will be set during the `get_posts` method and empty before it.
if ( ! empty( $query->request ) && ( false === (boolean) $original_no_found_rows_value || ! $this->skip_found_rows ) ) {
return (int) $query->found_posts;
}
$original_fields_value = $query->get( 'fields' );
$query->set( 'fields', 'ids' );
$query->set( 'no_found_rows', false );
/**
* Filters the query object by reference before counting found posts.
*
* @since 4.7.19
*
* @param WP_Query $query
*/
do_action( "tribe_repository_{$this->filter_name}_pre_found_posts", $query );
$query->get_posts();
$query->set( 'fields', $original_fields_value );
$query->set( 'no_found_rows', $original_no_found_rows_value );
return (int) $query->found_posts;
}
/**
* {@inheritdoc}
*/
public function all( $return_generator = false, int $batch_size = 50 ) {
if ( $return_generator ) {
return $this->get_all_generator( $batch_size );
}
if ( $this->void_query ) {
return [];
}
$query = $this->build_query();
// The request property will be set during the `get_posts` method and empty before it.
if ( ! empty( $query->request ) ) {
return array_map( [ $this, 'format_item' ], $query->posts );
}
$original_fields_value = $query->get( 'fields', '' );
$return_ids = 'ids' === $original_fields_value;
/**
* 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( "tribe_repository_{$this->filter_name}_pre_get_posts", $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.
*/
$formatted = $return_ids
? $results
: array_filter( array_map( [ $this, 'format_item' ], $results ) );
// Reset the fields if required.
$query->set( 'fields', $original_fields_value );
return $formatted;
}
/**
* {@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, [ 'ASC', 'DESC' ], true ) ) {
return $this;
}
$this->query_args['order'] = $order;
return $this;
}
/**
* {@inheritdoc}
*/
public function order_by( $order_by, $order = 'DESC' ) {
$this->query_args['orderby'] = $order_by;
// Based on `WP_Query->parse_orderby` we should ignore the global order passed, and use the value on for each item in array.
if ( ! is_array( $order_by ) ) {
$this->query_args['order'] = $order;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function fields( $fields ) {
$this->query_args['fields'] = $fields;
return $this;
}
/**
* {@inheritdoc}
*/
public function permission( $permission ) {
if ( ! in_array( $permission, [ 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();
$original_fields_value = $query->get( 'fields', '' );
$return_id = 'ids' === $original_fields_value;
// The request property will be set during the `get_posts` method and empty before it.
if ( ! empty( $query->request ) ) {
$ids = $this->get_ids();
if ( empty( $ids ) ) {
return null;
}
return $return_id ? reset( $ids ) : $this->format_item( reset( $ids ) );
}
$query->set( 'fields', 'ids' );
/**
* Filters the query object by reference before getting the first post from the query.
*
* @since 4.14.8
*
* @param WP_Query $query The WP_Query object before get_posts() is called.
*/
do_action( "tribe_repository_{$this->filter_name}_pre_first_post", $query );
$ids = $query->get_posts();
$query->set( 'fields', $original_fields_value );
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 ) {
$formatted = null === $this->formatter
? get_post( $id )
: $this->formatter->format_item( $id );
/**
* Filters a single formatted result.
*
* @since 4.9.11
*
* @param mixed|WP_Post $formatted The formatted post result, usually a post object.
* @param int $id The formatted post ID.
* @param Tribe__Repository__Interface $this The current repository object.
*/
$formatted = apply_filters( "tribe_repository_{$this->filter_name}_format_item", $formatted, $id, $this );
return $formatted;
}
/**
* {@inheritdoc}
*/
public function last() {
$query = $this->build_query();
$original_fields_value = $query->get( 'fields', '' );
$return_id = 'ids' === $original_fields_value;
// The request property will be set during the `get_posts` method and empty before it.
if ( ! empty( $query->request ) ) {
$ids = $this->get_ids();
if ( empty( $ids ) ) {
return null;
}
return $return_id ? end( $ids ) : $this->format_item( end( $ids ) );
}
$query->set( 'fields', 'ids' );
/**
* Filters the query object by reference before getting the last post from the query.
*
* @since 4.14.8
*
* @param WP_Query $query The WP_Query object before get_posts() is called.
*/
do_action( "tribe_repository_{$this->filter_name}_pre_last_post", $query );
$ids = $query->get_posts();
$query->set( 'fields', $original_fields_value );
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( [
$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_ids = 'ids' === $query->get( 'fields', '' );
$i = absint( $n ) - 1;
$ids = $this->get_ids();
if ( empty( $ids[ $i ] ) ) {
return null;
}
return $return_ids ? $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 = null ) {
$call_args = func_get_args();
$application = Tribe__Utils__Array::get( $this->get_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 );
$schema_entry = call_user_func_array( $application, $args_without_key );
/**
* Filters the applied modifier schema entry response.
*
* @since 4.9.5
*
* @param Tribe__Repository $this This repository instance
*
* @param mixed $schema_entry A scalar value or a callable.
*/
return apply_filters( "tribe_repository_{$this->filter_name}_apply_modifier_schema_entry", $schema_entry, $this );
}
/**
* {@inheritdoc}
*/
public function take( $n ) {
$query = $this->build_query();
$return_ids = 'ids' === $query->get( 'fields', '' );
$matching_ids = $this->get_ids();
if ( empty( $matching_ids ) ) {
return [];
}
$spliced = array_splice( $matching_ids, 0, $n );
return $return_ids ? $spliced : array_map( [ $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();
}
/**
* Filters posts by simple meta schema value.
*
* @since 4.9.5
*
* @param mixed $value Meta value.
*/
public function filter_by_simple_meta_schema( $value ) {
$filter = $this->get_current_filter();
if ( ! array_key_exists( $filter, $this->simple_meta_schema ) ) {
return;
}
$simple_meta = $this->simple_meta_schema[ $filter ];
$by = Tribe__Utils__Array::get( $simple_meta, 'by', 'meta_regexp_or_like' );
$this->by( $by, $simple_meta['meta_key'], $value );
}
/**
* Filters posts by simple tax schema value.
*
* @since 4.9.5
*
* @param int|string|array $value Term value(s).
*/
public function filter_by_simple_tax_schema( $value ) {
$filter = $this->get_current_filter();
if ( ! array_key_exists( $filter, $this->simple_tax_schema ) ) {
return;
}
$simple_tax = $this->simple_tax_schema[ $filter ];
$by = Tribe__Utils__Array::get( $simple_tax, 'by', 'term_in' );
$this->by( $by, $simple_tax['taxonomy'], $value );
}
/**
* {@inheritdoc}
*/
public function by( $key, $value = null ) {
if ( $this->void_query || ( 'void_query' === $key && false !== $value ) ) {
$this->void_query = true;
// No point in doing more computations if the query is void.
return $this;
}
$call_args = func_get_args();
$this->current_filters[ $key ] = array_slice( $call_args, 1 );
try {
// Set current filter as which one we are running.
$this->current_filter = $key;
$query_modifier = $this->modify_query( $key, $call_args );
// Set current filter as no longer active, we aren't running it anymore.
$this->current_filter = null;
/**
* 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` or `meta_query` entries should merge into one.
*/
$this->query_args = Arr::merge_recursive_query_vars( $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 ( 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( [ $this, 'apply_default_modifier' ], $call_args );
} elseif ( 2 === count( $call_args ) ) {
// Pass query argument $key with the single value argument.
$query_modifier = [
$key => $call_args[1],
];
} else {
// More than two $call_args were sent (key, value), assume it was meant for a filter that was not defined yet.
throw Tribe__Repository__Usage_Error::because_the_read_filter_is_not_defined( $key, $this );
}
} else {
$query_modifier = call_user_func_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->get_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 = [
'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;
}
/**
* {@inheritdoc}
*/
public function save( $return_promise = false ) {
$to_update = $this->get_ids();
if ( empty( $to_update ) ) {
return $return_promise ? new Tribe__Promise() : [];
}
$exit = [];
$postarrs = [];
foreach ( $to_update as $id ) {
$postarrs[ $id ] = $this->filter_postarr_for_update( $this->build_postarr( $id ), $id );
}
// If any `filter_postarr_for_update` call returned a falsy value then drop it.
$postarrs = array_filter( $postarrs );
if (
$this->is_background_update_active( $to_update )
&& count( $to_update ) > $this->get_background_update_threshold( $to_update )
) {
return $this->async_update( $postarrs, true );
}
$update_callback = $this->get_update_callback( $to_update, false );
foreach ( $postarrs as $id => $postarr ) {
$this_exit = $update_callback( $postarr );
$exit[ $id ] = $id === $this_exit ? true : $this_exit;
}
return $return_promise ? new Tribe__Promise : $exit;
}
/**
* {@inheritdoc}
*/
public function get_ids( $return_generator = false, int $batch_size = 50 ) {
if ( $return_generator ) {
return $this->get_ids_generator( $batch_size );
}
if ( $this->void_query ) {
return [];
}
try {
/** @var WP_Query $query */
$query = $this->get_query();
// The request property will be set during the `get_posts` method and empty before it.
if ( empty( $query->request ) ) {
$query->set( 'fields', 'ids' );
/**
* Filters the query object by reference before getting the post IDs from the query.
*
* @since 4.14.8
*
* @param WP_Query $query The WP_Query object before get_posts() is called.
*/
do_action( "tribe_repository_{$this->filter_name}_pre_get_ids_for_posts", $query );
return $query->get_posts();
}
return array_map(
static function ( $post ) {
if ( is_int( $post ) ) {
return $post;
}
$post_arr = (array) $post;
return Arr::get( $post_arr, 'ID', Arr::get( $post_arr, 'id', 0 ) );
},
$query->posts
);
} catch ( Tribe__Repository__Void_Query_Exception $e ) {
/*
* Extending classes might use this method to run sub-queries
* and signal a void query; let's return an empty array.
*/
return [];
}
}
/**
* {@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_updated( $key ) {
return ! in_array( $key, self::$blocked_keys, 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;
}
/**
* {@inheritdoc}
*/
public function set_args( array $update_map ) {
foreach ( $update_map as $key => $value ) {
$this->set( $key, $value );
}
return $this;
}
/**
* Sets the args to be updated during save process.
*
* @param string $key Argument key.
* @param mixed $value Argument value.
*
* @return $this
* @throws Tribe__Repository__Usage_Error
*
*/
public function set( $key, $value ) {
if ( ! is_string( $key ) ) {
throw Tribe__Repository__Usage_Error::because_update_key_should_be_a_string( $this );
}
$this->updates[ $key ] = $value;
return $this;
}
/**
* Sets the create args the repository will use to create posts.
*
* @since 4.9.5
*
* @param string|int $image The path to an image file, an image URL, or an attachment post ID.
*
* @return $this
*/
public function set_featured_image( $image ) {
if ( '' === $image || false === $image ) {
$thumbnail_id = false;
} elseif ( 0 === $image || null === $image ) {
$thumbnail_id = '';
} else {
$thumbnail_id = tribe_upload_image( $image );
}
if ( false === $thumbnail_id ) {
return $this;
}
return $this->set( '_thumbnail_id', $thumbnail_id );
}
/**
* {@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;
return $this;
}
/**
* 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 or more `meta_keys` relating the queried post type(s)
* to another post type.
* @param string $compare The SQL comparison 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, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
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;
}
/**
* Filters the query to only return posts that are related, via a meta key, to posts
* that satisfy a condition.
*
* @since 4.10.3
*
* @param string|array $meta_keys One or more `meta_keys` relating the queried post type(s)
* to another post type.
* @param string $compare The SQL comparison operator.
* @param string $meta_field One (a column in the `postmeta` table) that should match
* the comparison criteria; required if the comparison operator is not `EXISTS`
* or
* `NOT EXISTS`.
* @param string|array $meta_values One or more values the post field(s) should be compared to;
* required if the comparison operator is not `EXISTS` or `NOT EXISTS`.
* @param boolean $or_not_exists Whether or not to also include a clause to check if value IS NULL.
* Example with this as true: `value = X OR value IS NULL`.
*
* @return $this
* @throws Tribe__Repository__Usage_Error If the comparison operator requires and no value provided.
*
*/
public function where_meta_related_by_meta( $meta_keys, $compare, $meta_field = null, $meta_values = null, $or_not_exists = false ) {
$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
if ( empty( $meta_field ) || empty( $meta_values ) ) {
throw Tribe__Repository__Usage_Error::because_this_comparison_operator_requires_fields_and_values( $meta_keys, $compare, $this );
}
}
$meta_field = esc_sql( $meta_field );
/** @var wpdb $wpdb */
global $wpdb;
$pm = $this->sql_slug( 'post_meta_related_post_meta', $compare, $meta_keys );
$pmm = $this->sql_slug( 'meta_post_meta_related_post_meta', $compare, $meta_keys );
$this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} {$pm} ON {$pm}.post_id = {$wpdb->posts}.ID" );
$this->filter_query->join( "
LEFT JOIN {$wpdb->postmeta} {$pmm}
ON {$pmm}.post_id = {$pm}.meta_value
AND {$pmm}.meta_key = '{$meta_field}'
" );
$keys_in = $this->prepare_interval( $meta_keys );
if ( 'EXISTS' === $compare ) {
$this->filter_query->where( "
{$pm}.meta_key IN {$keys_in}
AND {$pmm}.meta_id IS NOT NULL
" );
} elseif ( 'NOT EXISTS' === $compare ) {
$this->filter_query->where( "
{$pm}.meta_key IN {$keys_in}
AND {$pmm}.meta_id IS NULL
" );
} else {
if ( in_array( $compare, static::$multi_value_keys, true ) ) {
$meta_values = $this->prepare_interval( $meta_values );
} else {
$meta_values = $this->prepare_value( $meta_values );
}
$clause = "{$pmm}.meta_value {$compare} {$meta_values}";
if ( $or_not_exists ) {
$clause = "
(
{$clause}
OR {$pmm}.meta_id IS NULL
)
";
}
$this->filter_query->where( "
{$pm}.meta_key IN {$keys_in}
AND {$clause}
" );
}
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 ) {
$all_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 ( $all_callbacks as $c ) {
call_user_func_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;
}
/**
* Adds an entry to the repository filter schema.
*
* @since 4.9.5
*
* @param string $key The filter key, the one that will be used in `by` and `where`
* calls.
* @param callable $callback The function that should be called to apply this filter.
*/
public function add_schema_entry( $key, $callback ) {
$this->set_schema( $key, $callback );
}
/**
* Adds a simple meta entry to the repository filter schema.
*
* @since 4.9.5
*
* @param string $key The filter key, the one that will be used in `by` and `where` calls.
* @param string|array $meta_key The meta key(s) to use for the meta lookup.
* @param string|null $by The ->by() lookup to use (defaults to meta_regexp_or_like).
*/
public function add_simple_meta_schema_entry( $key, $meta_key, $by = null ) {
$this->set_schema( $key, [ $this, 'filter_by_simple_meta_schema' ] );
$this->simple_meta_schema[ $key ] = [
'meta_key' => $meta_key,
'by' => $by,
];
}
/**
* Adds a simple taxonomy entry to the repository filter schema.
*
* @since 4.9.5
*
* @param string $key The filter key, the one that will be used in `by` and `where` calls.
* @param string|array $taxonomy The taxonomy/taxonomies to use for the tax lookup.
* @param string|null $by The ->by() lookup to use (defaults to term_in).
*/
public function add_simple_tax_schema_entry( $key, $taxonomy, $by = null ) {
$this->set_schema( $key, [ $this, 'filter_by_simple_tax_schema' ] );
$this->simple_tax_schema[ $key ] = [
'taxonomy' => $taxonomy,
'by' => $by,
];
}
/**
* {@inheritdoc}
*/
public function add_update_field_alias( $alias, $field_name ) {
$this->update_fields_aliases[ $alias ] = $field_name;
}
/**
* 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 = [];
$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 = [ $key => $value ];
break;
case 'ID':
case 'id':
$args = [ 'p' => $value ];
break;
case 'search':
if ( '' !== $value ) {
$args = [ '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 = [ '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 'menu_order':
$args = [ 'menu_order' => $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':
// Check if Regexp is fenced.
if ( tribe_is_regex( $arg_1 ) ) {
// Unfence the Regexp.
$arg_1 = tribe_unfenced_regex( $arg_1 );
}
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'REGEXP' );
break;
case 'meta_not_regexp':
case 'meta_not_equals_regexp':
// Check if Regexp is fenced.
if ( tribe_is_regex( $arg_1 ) ) {
// Unfence the Regexp.
$arg_1 = tribe_unfenced_regex( $arg_1 );
}
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT REGEXP' );
break;
case 'meta_regexp_or_like':
case 'meta_equals_regexp_or_like':
$compare = 'LIKE';
// Check if Regexp is fenced (the only way for Regexp to be supported in this context).
if ( tribe_is_regex( $arg_1 ) ) {
$compare = 'REGEXP';
// Unfence the Regexp.
$arg_1 = tribe_unfenced_regex( $arg_1 );
}
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, $compare );
break;
case 'meta_not_regexp_or_like':
case 'meta_not_equals_regexp_or_like':
$compare = 'NOT LIKE';
// Check if Regexp is fenced (the only way for Regexp to be supported in this context).
if ( tribe_is_regex( $arg_1 ) ) {
$compare = 'NOT REGEXP';
// Unfence the Regexp.
$arg_1 = tribe_unfenced_regex( $arg_1 );
}
$args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, $compare );
break;
case 'order':
// Only allow ASC and DESC for order.
$args = [ 'order' => strtoupper( trim( $value ) ) === 'DESC' ? 'DESC' : 'ASC' ];
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;
case 'term_in':
$arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false );
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'IN' );
break;
case 'term_not_in':
$arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false );
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT IN' );
break;
case 'term_and':
$arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false );
$args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', '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, [ 'post_date_gmt', 'post_modified_gmt' ], true )
? '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 [
'date_query' => [
'relation' => 'AND',
$array_key => [
'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, [ 'post_date_gmt', 'post_modified_gmt' ], true )
? '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 [
'date_query' => [
'relation' => 'AND',
$array_key => [
'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 = [
'meta_query' => [
$array_key => [
'key' => $meta_keys[0],
'compare' => strtoupper( $compare ),
],
],
];
if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
$args['meta_query'][ $array_key ]['value'] = $meta_value;
}
if ( is_string( $type_or_format ) && 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" );
} elseif ( '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, [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
$array_key = $this->sql_slug( $taxonomy, $operator );
} else {
$array_key = $this->sql_slug( $taxonomy, $field, $operator );
}
return [
'tax_query' => [
$array_key => [
'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'.
* @param string $operator The operator the interval is being prepared for;
* defaults to `IN`.
*
* @return string
*/
public function prepare_interval( $values, $format = '%s', $operator = 'IN' ) {
$values = Tribe__Utils__Array::list_to_array( $values );
$prepared = [];
foreach ( $values as $value ) {
$prepared[] = $this->prepare_value( $value, $format );
}
return in_array( $operator, [ 'BETWEEN', 'NOT BETWEEN' ] )
? sprintf( '%s AND %s', $prepared[0], $prepared[1] )
: sprintf( '(%s)', 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 by_not_related_to( $by_meta_keys, $keys = null, $values = null ) {
/** @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 NOT 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 )
)" );
return $this;
}
/**
* {@inheritdoc}
*/
public function has_filter( $key, $value = null ) {
$args = func_get_args();
$values = array_slice( $args, 1 );
if ( null === $value ) {
// We just want to check if a filter is applied.
return array_key_exists( $key, $this->current_filters );
}
// We check if the filter exists and the arguments match; inline to prevent "Undefined index" errors.
return array_key_exists( $key, $this->current_filters ) && array_slice(
$this->current_filters[ $key ],
0,
min( count( $this->current_filters[ $key ] ), count( $values ) )
) === $values;
}
/**
* {@inheritdoc}
*/
public function get_current_filter() {
return $this->current_filter;
}
/**
* Returns a map relating comparison operators to their "pretty" name.
*
* @since 4.9.5
*
* @return array
*/
public static function get_comparison_operators() {
return self::$comparison_operators;
}
/**
* {@inheritdoc}
*/
public function delete( $return_promise = false ) {
/**
* Filters the delete operation, allowing third party operations to take over.
*
* @since 5.2.1
*
* @param int[]|null $pre_check The overwritten delete values or null if not handled externally.
* @param self $this This repository instance.
*/
$pre_check = apply_filters( "tribe_repository_{$this->filter_name}_before_delete", null, $this );
if ( $pre_check !== null ) {
return $return_promise ? new Tribe__Promise() : $pre_check;
}
$to_delete = $this->get_ids();
if ( empty( $to_delete ) ) {
return $return_promise ? new Tribe__Promise() : [];
}
/**
* Filters the post delete operation allowing third party code to bail out of
* the process completely.
*
* @since 4.9.5
*
* @param array|null $deleted An array containing the the IDs of the deleted posts.
* @param array $to_delete The items to delete.
*/
$deleted = apply_filters( "tribe_repository_{$this->filter_name}_delete", null, $to_delete );
if ( null !== $deleted ) {
return $deleted;
}
if (
$this->is_background_delete_active( $to_delete )
&& count( $to_delete ) > $this->get_background_delete_threshold( $to_delete )
) {
return $this->async_delete( $to_delete, $return_promise );
}
$delete_callback = $this->get_delete_callback( $to_delete );
foreach ( $to_delete as $id ) {
$done = $delete_callback( $id );
if ( empty( $done ) ) {
tribe( 'logger' )->log(
__( 'Could not delete post with ID ' . $id, 'tribe-common' ),
Tribe__Log::WARNING,
$this->filter_name
);
continue;
}
$deleted[] = $id;
}
return $return_promise ? new Tribe__Promise() : $deleted;
}
/**
* Whether background delete is activated for the repository or not.
*
* @since 4.9.5
*
* @param array $to_delete An array of post IDs to delete.
*
* @return bool Whether background delete is activated for the repository or not.
*/
protected function is_background_delete_active( $to_delete ) {
/**
* Whether background, asynchronous, deletion of posts is active or not for all repositories.
*
* If active then if the number of posts to delete is over the threshold, defined
* by the `tribe_repository_delete_background_threshold` filter, then the deletion will happen
* in background in other requests.
*
* @since 4.9.5
*
* @param bool $background_active Whether background deletion is active or not.
* @param array $to_delete The array of post IDs to delete.
*/
$background_active = (bool) apply_filters( 'tribe_repository_delete_background_activated', true, $to_delete );
/**
* Whether background, asynchronous, deletion of posts is active or not for this specific repository.
*
* If active then if the number of posts to delete is over the threshold, defined
* by the `tribe_repository_delete_background_threshold` filter, then the deletion will happen
* in background in other requests.
*
* @since 4.9.5
*
* @param bool $background_active Whether background deletion is active or not.
* @param array $to_delete The array of post IDs to delete.
*/
$background_active = (bool) apply_filters(
"tribe_repository_{$this->filter_name}_delete_background_activated",
$background_active,
$to_delete
);
return $background_active;
}
/**
* Returns the threshold above which posts will be deleted in background.
*
* @since 4.9.5
*
* @param array $to_delete An array of post IDs to delete.
*
* @return int The threshold above which posts will be deleted in background.
*/
protected function get_background_delete_threshold( $to_delete ) {
/**
* The number of posts above which the deletion will happen in background.
*
* This filter will be ignored if background delete is deactivated with the `tribe_repository_delete_background_activated`
* or `tribe_repository_{$this->filter_name}_delete_background_activated` filter.
*
* @since 4.9.5
*
* @param int The threshold over which posts will be deleted in background.
* @param array $to_delete The post IDs to delete.
*/
$background_threshold = (int) apply_filters( 'tribe_repository_delete_background_threshold', 20, $to_delete );
/**
* The number of posts above which the deletion will happen in background.
*
* This filter will be ignored if background delete is deactivated with the `tribe_repository_delete_background_activated`
* or `tribe_repository_{$this->filter_name}_delete_background_activated` filter.
*
* @since 4.9.5
*
* @param int The threshold over which posts will be deleted in background.
* @param array $to_delete The post IDs to delete.
*/
$background_threshold = (int) apply_filters(
"tribe_repository_{$this->filter_name}_delete_background_threshold",
$background_threshold,
$to_delete
);
return $background_threshold;
}
/**
* Whether background update is activated for the repository or not.
*
* @since 4.9.5
*
* @param array $to_update An array of post IDs to update.
*
* @return bool Whether background update is activated for the repository or not.
*/
protected function is_background_update_active( $to_update ) {
/**
* Whether background, asynchronous, update of posts is active or not for all repositories.
*
* If active then if the number of posts to update is over the threshold, defined
* by the `tribe_repository_update_background_threshold` filter, then the update will happen
* in background in other requests.
*
* @since 4.9.5
*
* @param bool $background_active Whether background update is active or not.
* @param array $to_update The array of post IDs to update.
*/
$background_active = (bool) apply_filters( 'tribe_repository_update_background_activated', true, $to_update );
/**
* Whether background, asynchronous, update of posts is active or not for this specific repository.
*
* If active then if the number of posts to update is over the threshold, defined
* by the `tribe_repository_update_background_threshold` filter, then the update will happen
* in background in other requests.
*
* @since 4.9.5
*
* @param bool $background_active Whether background update is active or not.
* @param array $to_update The array of post IDs to update.
*/
$background_active = (bool) apply_filters(
"tribe_repository_{$this->filter_name}_update_background_activated",
$background_active,
$to_update
);
return $background_active;
}
/**
* Returns the threshold above which posts will be updated in background.
*
* @since 4.9.5
*
* @param array $to_update An array of post IDs to update.
*
* @return int The threshold above which posts will be updated in background.
*/
protected function get_background_update_threshold( $to_update ) {
/**
* The number of posts above which the update will happen in background.
*
* This filter will be ignored if background update is deactivated with the `tribe_repository_update_background_activated`
* or `tribe_repository_{$this->filter_name}_update_background_activated` filter.
*
* @since 4.9.5
*
* @param int The threshold over which posts will be updated in background.
* @param array $to_update The post IDs to update.
*/
$background_threshold = (int) apply_filters( 'tribe_repository_update_background_threshold', 20, $to_update );
/**
* The number of posts above which the update will happen in background.
*
* This filter will be ignored if background update is deactivated with the `tribe_repository_update_background_activated`
* or `tribe_repository_{$this->filter_name}_update_background_activated` filter.
*
* @since 4.9.5
*
* @param int The threshold over which posts will be updated in background.
* @param array $to_update The post IDs to update.
*/
$background_threshold = (int) apply_filters(
"tribe_repository_{$this->filter_name}_update_background_threshold",
$background_threshold,
$to_update
);
return $background_threshold;
}
/**
* {@inheritdoc}
*/
public function async_delete( array $to_delete, $return_promise = true ) {
$promise = new Tribe__Promise( $this->get_delete_callback( $to_delete, true ), $to_delete );
if ( ! $return_promise ) {
// Dispatch it immediately and return the IDs that will be deleted.
$promise->save()->dispatch();
return $to_delete;
}
// Return the promise and let the client do the dispatching.
return $promise;
}
/**
* Returns the delete callback function or method to use to delete posts.
*
* @since 4.9.5
*
* @param int|array $to_delete The post ID to delete or an array of post IDs to delete.
* @param bool $background Whether the callback will be used in background delete operations or not.
*
* @return callable The callback to use.
*/
protected function get_delete_callback( $to_delete, $background = false ) {
/**
* Filters the callback that all repositories should use to delete posts.
*
* @since 4.9.5
*
* @param callable $callback The callback that should be used to delete each post; defaults
* to `wp_delete_post`; falsy return values will be interpreted as
* failures to delete.
* @param array|int $to_delete An array of post IDs to delete.
* @param bool $background Whether the delete operation will happen in background or not.
*/
$callback = apply_filters( 'tribe_repository_delete_callback', 'wp_delete_post', (array) $to_delete, (bool) $background );
/**
* Filters the callback that all repositories should use to delete posts.
*
* @since 4.9.5
*
* @param callable $callback The callback that should be used to delete each post; defaults
* to `wp_delete_post`; falsy return values will be interpreted as
* failures to delete.
* @param array|int $to_delete An array of post IDs to delete.
* @param bool $background Whether the delete operation will happen in background or not.
*/
$callback = apply_filters(
"tribe_repository_{$this->filter_name}_delete_callback",
$callback,
(array) $to_delete,
(bool) $background
);
return $callback;
}
/**
* {@inheritdoc}
*/
public function get_filter_name() {
return $this->filter_name;
}
/**
* Returns the update callback function or method to use to update posts.
*
* @since 4.9.5
*
* @param int|array $to_update The post ID to update or an array of post IDs to update.
* @param bool $background Whether the callback will be used in background update operations or not.
*
* @return callable The callback to use.
*/
protected function get_update_callback( $to_update, $background = false ) {
/**
* Filters the callback that all repositories should use to update posts.
*
* @since 4.9.5
*
* @param callable $callback The callback that should be used to update each post; defaults
* to `wp_update_post`; falsy return values will be interpreted as
* failures to update.
* @param array|int $to_update An array of post IDs to update.
* @param bool $background Whether the update operation will happen in background or not.
*/
$callback = apply_filters( 'tribe_repository_update_callback', 'wp_update_post', (array) $to_update, (bool) $background );
/**
* Filters the callback that all repositories should use to update posts.
*
* @since 4.9.5
*
* @param callable $callback The callback that should be used to update each post; defaults
* to `wp_update_post`; falsy return values will be interpreted as
* failures to update.
* @param array|int $to_update An array of post IDs to update.
* @param bool $background Whether the update operation will happen in background or not.
*/
$callback = apply_filters(
"tribe_repository_{$this->filter_name}_update_callback",
$callback,
(array) $to_update,
(bool) $background
);
return $callback;
}
/**
* {@inheritdoc}
*/
public function async_update( array $to_update, $return_promise = true ) {
$promise = new Tribe__Promise( $this->get_update_callback( $to_update, true ), $to_update );
if ( ! $return_promise ) {
// Dispatch it immediately and return the IDs that will be deleted.
$promise->save()->dispatch();
return $to_update;
}
// Return the promise and let the client do the dispatching.
return $promise;
}
/**
* {@inheritdoc}
*/
public function get_update_fields_aliases() {
return $this->update_fields_aliases;
}
/**
* {@inheritdoc}
*/
public function set_update_fields_aliases( array $update_fields_aliases ) {
$this->update_fields_aliases = $update_fields_aliases;
}
/**
* {@inheritdoc}
*/
public function filter_postarr_for_update( array $postarr, $post_id ) {
/**
* Filters the post array that will be used for an update.
*
* @since 4.9.5
*
* @param array $postarr The post array that will be sent to the update callback.
* @param int The post ID if set.
*/
return apply_filters( "tribe_repository_{$this->filter_name}_update_postarr", $postarr, $post_id );
}
/**
* A utility method to cast any PHP error into an exception proper.
*
* Usage: `set_error_handler( array( $repository, 'cast_error_to_exception' ) );
*
* @since 4.9.5
*
* @param int $code The error code.
* @param string $message The error message.
*/
public function cast_error_to_exception( $code, $message ) {
throw new RuntimeException( $message, $code );
}
/**
* {@inheritdoc}
*/
public function create() {
$postarr = $this->filter_postarr_for_create( array_merge( $this->build_postarr(), $this->create_args ) );
// During the filtering allow extending classes or filters to prevent the create completely.
if ( false === ( bool ) $postarr ) {
return false;
}
$created = call_user_func( $this->get_create_callback( $postarr ), $postarr );
$post = $this->format_item( $created );
return $post instanceof WP_Post && $post->ID === $created ? $post : false;
}
/**
* {@inheritdoc}
*/
public function filter_postarr_for_create( array $postarr ) {
/**
* Filters the post array that will be used for the creation of a post
* of the type managed by the repository.
*
* @since 4.9.5
*
* @param array $postarr The post array that will be sent to the create callback.
*/
return apply_filters( "tribe_repository_{$this->filter_name}_update_postarr", $postarr );
}
/**
* {@inheritdoc}
*/
public function build_postarr( $id = null ) {
$postarr = [
'tax_input' => [],
'meta_input' => [],
];
/*
* The check is lax here by design: we leave space for the client code
* to use this method to build post arrays; when this is used by the
* repository the integrity of `$id` is granted.
*/
$is_update = null !== $id && is_numeric( $id );
// But still let's provide values that make sense.
if ( $is_update ) {
$postarr['ID'] = (int) $id;
}
foreach ( $this->updates as $key => $value ) {
if (
$value instanceof Closure ||
( is_array( $value ) && is_callable( $value ) )
) {
$value = $value( $id, $key, $this );
}
// Allow fields to be aliased
$key = Tribe__Utils__Array::get( $this->update_fields_aliases, $key, $key );
if ( ! $this->can_be_updated( $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 ) ) {
$taxonomy = get_taxonomy( $key );
if ( $taxonomy instanceof WP_Taxonomy ) {
$postarr['tax_input'][ $key ] = Tribe__Utils__Array::list_to_array( $value );
}
} else {
// it's a custom field
$postarr['meta_input'][ $key ] = $value;
}
}
return $postarr;
}
/**
* Returns the create callback function or method to use to create posts.
*
* @since 4.9.5
*
* @param array $postarr The post array that will be used for the creation.
*
* @return callable The callback to use.
*/
protected function get_create_callback( array $postarr ) {
/**
* Filters the callback that all repositories should use to create posts.
*
* @since 4.9.5
*
* @param callable $callback The callback that should be used to create posts; defaults
* to `wp_insert_post`; non numeric and existing post ID return
* values will be interpreted as failures to create the post.
* @param array $postarr The post array that will be used for the creation.
*/
$callback = apply_filters( 'tribe_repository_create_callback', 'wp_insert_post', $postarr );
/**
* Filters the callback that all repositories should use to create posts.
*
* @since 4.9.5
*
* @param callable $callback The callback that should be used to create posts; defaults
* to `wp_insert_post`; non numeric and existing post ID return
* values will be interpreted as failures to create the post.
* @param array $postarr The post array that will be used for the creation.
*/
$callback = apply_filters(
"tribe_repository_{$this->filter_name}_create_callback",
$callback,
$postarr
);
return $callback;
}
/**
* Returns the create args the repository will use to create posts.
*
* @since 4.9.5
*
* @return array The create args the repository will use to create posts.
*/
public function get_create_args() {
return $this->create_args;
}
/**
* Sets the create args the repository will use to create posts.
*
* @since 4.9.5
*
* @param array $create_args The create args the repository will use to create posts.
*/
public function set_create_args( array $create_args ) {
$this->create_args = $create_args;
}
/**
* Returns a value trying to fetch it from an array first and then
* reading it from the meta.
*
* @since 4.9.5
*
* @param array $postarr The array to look into.
* @param string $key The key to retrieve.
* @param int|null $post_id The post ID to fetch the value for.
* @param mixed $default The default value to return if nothing was found.
*
* @return mixed The found value if any.
*/
protected function get_from_postarr_or_meta( array $postarr, $key, $post_id = null, $default = null ) {
$default_value = get_post_meta( $post_id, $key, true );
if ( '' === $default_value || null === $post_id ) {
$default_value = $default;
}
return Tribe__Utils__Array::get( $postarr['meta_input'], $key, $default_value );
}
/**
* {@inheritdoc}
*/
public function set_display_context( $context = 'default' ) {
$this->display_context = $context;
return $this;
}
/**
* {@inheritdoc}
*/
public function set_render_context( $context = 'default' ) {
$this->render_context = $context;
return $this;
}
/**
* {@inheritdoc}
*/
public function get_query_for_posts( array $posts ) {
$posts = array_filter( array_map( 'get_post', $posts ) );
$query = new WP_Query();
// Let's make it look like the posts are the result of a query using `post__in`.
$query->set( 'post__in', wp_list_pluck( $posts, 'ID' ) );
$query->found_posts = count( $posts );
$query->posts = $posts;
$query->post_count = count( $posts );
$query->current_post = - 1;
return $query;
}
/**
* {@inheritdoc}
*/
public function pluck( $field ) {
$list = new WP_List_Util( $this->all() );
return $list->pluck( $field );
}
/**
* {@inheritdoc}
*/
public function filter( $args = [], $operator = 'AND' ) {
$list = new WP_List_Util( $this->all() );
return $list->filter( $args, $operator );
}
/**
* {@inheritdoc}
*/
public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false ) {
$list = new WP_List_Util( $this->all() );
return $list->sort( $orderby, $order, $preserve_keys );
}
/**
* {@inheritdoc}
*/
public function collect() {
return new Tribe__Utils__Post_Collection( $this->all() );
}
/**
* Builds the ORM query with the query builder.
*
* Allow classes extending or decorating the repository to act before
* the query is built or replace its building completely.
*
* @since 4.9.5
*
* @return WP_Query|null A built query object or `null` if the builder failed or bailed.
*/
protected function build_query_with_builder() {
$built = $this->query_builder->build_query();
$built->builder = $this->query_builder;
if ( null !== $built ) {
$query = $built;
}
return $query;
}
/**
* Builds the ORM query internally, without a query builder.
*
* @since 4.9.5
*
* @return WP_Query The built query object.
*/
protected function build_query_internally() {
$query = new WP_Query();
$query->builder = $this;
$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 = [ 'publish' ];
if ( current_user_can( 'read_private_posts' ) ) {
$default_post_status[] = 'private';
}
$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( "tribe_repository_{$this->filter_name}_query_args", $query_args, $query, $this );
/**
* Provides a last-ditch effort to override the filtered offset.
*
* This should only be used if doing creating pagination for performance purposes.
*
* @since 4.11.0
*
* @param null|int $filtered_offset Offset parameter setting.
* @param array $query_args List of query arguments.
*/
$filtered_offset = apply_filters( 'tribe_repository_query_arg_offset_override', null, $query_args );
if ( $filtered_offset || isset( $query_args['offset'] ) ) {
$per_page = (int) Tribe__Utils__Array::get( $query_args, 'posts_per_page', get_option( 'posts_per_page' ) );
if ( $filtered_offset ) {
$query_args['offset'] = $filtered_offset;
} elseif ( isset( $query_args['offset'] ) ) {
$offset = absint( $query_args['offset'] );
$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;
/**
* 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'] );
}
$query_args['posts_per_page'] = $per_page === - 1 ? self::MAX_NUMBER_OF_POSTS_PER_PAGE : $per_page;
}
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 ) && method_exists( $arg, '__invoke' ) ) {
// __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 hash( array $settings = [], WP_Query $query = null ) {
return md5( json_encode( $this->get_hash_data( $settings, $query ) ) );
}
/**
* {@inheritDoc}
*/
public function get_hash_data( array $settings, WP_Query $query = null ) {
$filters = $this->current_filters;
$query_vars = null !== $query
? $query->query
: array_merge( $this->default_args, $this->query_args );
if ( isset( $settings['exclude'] ) ) {
$filters = array_diff_key(
$filters,
array_combine( $settings['exclude'], $settings['exclude'] )
);
$query_vars = array_diff_key(
$query_vars,
array_combine( $settings['exclude'], $settings['exclude'] )
);
}
if ( isset( $settings['include'] ) ) {
$filters = array_intersect_key(
$filters,
array_combine( $settings['include'], $settings['include'] )
);
$query_vars = array_intersect_key(
$query_vars,
array_combine( $settings['include'], $settings['include'] )
);
}
Tribe__Utils__Array::recursive_ksort( $filters );
Tribe__Utils__Array::recursive_ksort( $query_vars );
return [ 'filters' => $filters, 'query_vars' => $query_vars ];
}
/**
* {@inheritDoc}
*/
public function get_last_built_query() {
return $this->last_built_query;
}
/**
* Checks a SQL relation is valid.
*
* Allowed values are 'OR' and 'AND'.
*
* @since 4.9.6
*
* @param string $relation The relation to check.
*
* @throws Tribe__Repository__Usage_Error If the relation is not a valid one.
*/
protected function validate_relation( $relation ) {
if ( ! in_array( $relation, [ 'OR', 'AND' ], true ) ) {
throw Tribe__Repository__Usage_Error::because_this_relation_is_not_valid( $relation );
}
}
/**
* Sanitizes and prepares string to be used in a LIKE comparison.
*
* If no leading and trailing `%` was found it will be added at the start and end of the string.
*
* @since 4.9.6
*
* @param string|array $value The string to prepare or an array of strings to prepare.
*
* @return string|array The sanitized string, or strings.
*/
protected function prepare_like_string( $value ) {
$original_value = $value;
$values = (array) $value;
$prepared = [];
$pattern = '/^(?<pre>%{0,1})(?<string>.*?)(?<post>%{0,1})$/u';
global $wpdb;
foreach ( $values as $v ) {
preg_match( $pattern, $v, $matches );
$pre = $matches['pre'] ?: '';
$post = $matches['post'] ?: '';
$string = $wpdb->esc_like( $matches['string'] );
if ( '' === $pre && '' === $post ) {
// If the string does not contain any starting and ending placeholder we'll add all combinations.
$prepared[] = '%' . $string;
$prepared[] = $string . '%';
$prepared[] = $string;
$pre = $post = '%';
}
$prepared[] = $pre . $string . $post;
}
return is_array( $original_value ) ? $prepared : reset( $prepared );
}
/**
* Builds the WHERE clause for a set of fields.
*
* This method is table-agnostic. While flexible it will also require some care to be used.
*
* @since 4.9.6
*
* @param string|array $fields One or more fields to build the clause for.
* @param string $compare The comparison operator to use to build the
* @param string|array $values One or more values to build the WHERE clause for.
* @param string $value_format The format, a `$wpdb::prepare()` compatible one, to use to format the values.
* @param string $where_relation The relation to apply between each WHERE fragment.
* @param string $value_relation The relation to apply between each value fragment.
*
* @return string The built WHERE clause.
*
* @throws Tribe__Repository__Usage_Error If the relations are not valid or another WHERE building issue happens.
*/
protected function build_fields_where_clause(
$fields,
$compare,
$values,
$value_format = '%s',
$where_relation = 'OR',
$value_relation = 'OR'
) {
$this->validate_relation( $where_relation );
$this->validate_relation( $value_relation );
global $wpdb;
$fields_where_clauses = [];
$fields = (array) $fields;
$values = (array) $values;
foreach ( $fields as $field ) {
$value_clauses = [];
foreach ( $values as $compare_value ) {
if ( ! is_array( $compare_value ) || count( $compare_value ) === 1 ) {
$value_clauses[] = $wpdb->prepare(
"({$field} {$compare} {$value_format})",
$compare_value
);
} else {
$value_format = implode(
',',
array_fill( 0, count( $compare_value ), $value_format )
);
$value_clauses[] = $wpdb->prepare(
"({$field} {$compare} ({$value_format}))",
$compare_value
);
}
}
$fields_where_clauses[] = '(' . implode( " {$value_relation} ", $value_clauses ) . ')';
}
$fields_where = $wpdb->remove_placeholder_escape(
implode( " {$where_relation} ", $fields_where_clauses )
);
return $fields_where;
}
/**
* Returns the term IDs of terms matching a criteria, the match is made on the terms slug and name.
*
* This should be used to break-down a query and fetch term IDs, to then use in a "lighter" join, later.
*
* @since 4.9.6
*
* @param string|array $taxonomy The taxonomy, or taxonomies, to fetch the terms for.
* @param string $compare The comparison operator to use, e.g. 'LIKE' or '=>'.
* @param string|array $value An array of values to compare the terms slug or names with.
* @param string $relation The relation, either 'OR' or 'AND', to apply to the matching.
* @param string $format The format, a `$wpdb::prepare()` supported one, to use to format the values for
* the query.
*
* @return array An array of term IDs matching the query, if any.
*/
protected function fetch_taxonomy_terms_matches( $taxonomy, $compare, $value, $relation = 'OR', $format = '%s' ) {
global $wpdb;
$taxonomies = (array) $taxonomy;
$values = (array) $value;
$compare_target = count( $values ) > 1
? '(' . $this->filter_query->create_interval_of_strings( $values ) . ')'
: $wpdb->prepare( $format, reset( $values ) );
$taxonomies_interval = $this->filter_query->create_interval_of_strings( $taxonomies );
$query = "SELECT tt.term_taxonomy_id FROM {$wpdb->terms} AS t
INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id
WHERE tt.taxonomy IN ({$taxonomies_interval}) AND
( t.slug {$compare} {$compare_target} {$relation} t.name {$compare} {$compare_target} )";
return $wpdb->get_col( $wpdb->remove_placeholder_escape( $query ) );
}
/**
* {@inheritDoc}
*/
public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' ) {
$compare = strtoupper( trim( $compare ) );
// Check each value is compatible with the comparison operator.
$values = (array) $value;
foreach ( $values as $v ) {
$this->validate_operator_and_values( $compare, 'where_multi', $v );
}
global $wpdb;
if ( in_array( $compare, [ 'LIKE', 'NOT LIKE' ], true ) ) {
$values = $this->prepare_like_string( $values );
}
$where_relation = strtoupper( trim( $where_relation ) );
$this->validate_relation( $where_relation );
$value_relation = strtoupper( trim( $value_relation ) );
$this->validate_relation( $value_relation );
$post_fields = [];
$taxonomies = [];
foreach ( $fields as $field ) {
if ( $this->is_a_post_field( $field ) ) {
$post_fields[] = $field;
} elseif ( array_key_exists( $field, $this->simple_tax_schema ) ) {
// Handle simple tax schema aliases.
$schema = $this->simple_tax_schema[ $field ]['taxonomy'];
if ( ! is_array( $schema ) ) {
$taxonomies[] = $schema;
continue;
}
// If doing an AND where relation, pass all taxonomies in to be grouped with OR.
if ( 'AND' === $where_relation ) {
$this->where_multi( $schema, $compare, $value, 'OR', $value_relation );
continue;
}
foreach ( $schema as $taxonomy ) {
$taxonomies[] = $taxonomy;
}
} elseif ( array_key_exists( $field, $this->simple_meta_schema ) ) {
// Handle simple meta schema aliases.
$schema = $this->simple_meta_schema[ $field ]['meta_key'];
if ( ! is_array( $schema ) ) {
$custom_fields[] = $schema;
continue;
}
// If doing an AND where relation, pass all meta keys in to be grouped with OR.
if ( 'AND' === $where_relation ) {
$this->where_multi( $schema, $compare, $value, 'OR', $value_relation );
continue;
}
foreach ( $schema as $meta_key ) {
$custom_fields[] = $meta_key;
}
} elseif ( $this->is_a_taxonomy( $field ) ) {
$taxonomies[] = $field;
} else {
$custom_fields[] = $field;
}
}
$value_formats = [];
foreach ( $values as $v ) {
$value_format = '%d';
if ( is_string( $v ) ) {
$value_format = '%s';
} elseif ( (int) $v !== (float) $v ) {
$value_format = '%f';
}
$value_formats[] = $value_format;
}
// If the value formats differ then treat all of them as strings.
if ( count( array_unique( $value_formats ) ) > 1 ) {
$value_format = '%s';
} else {
$value_format = reset( $value_formats );
}
$where = [];
if ( ! empty( $post_fields ) ) {
$post_fields = array_map( static function ( $post_field ) use ( $wpdb ) {
return "{$wpdb->posts}.$post_field";
}, $post_fields );
$post_fields_where = $this->build_fields_where_clause(
$post_fields,
$compare,
$values,
$value_format,
$where_relation,
$value_relation
);
$wheres[] = $post_fields_where;
}
if ( ! empty( $taxonomies ) ) {
$all_matching_term_ids = [];
$taxonomy_values = $values;
if ( in_array( $compare, [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
// We can use multiple values in the same query.
$taxonomy_values = [ $values ];
}
foreach ( $taxonomy_values as $taxonomy_value ) {
$matching_term_ids = $this->fetch_taxonomy_terms_matches(
$taxonomies,
$compare,
$taxonomy_value,
$where_relation,
$value_format
);
if ( empty( $matching_term_ids ) ) {
if ( 'AND' === $value_relation ) {
// No reason to waste any more time.
$this->void_query = true;
return $this;
}
continue;
}
$all_matching_term_ids[] = $matching_term_ids;
}
$intersection = count( $all_matching_term_ids ) > 1
? array_intersect( ...$all_matching_term_ids )
: reset( $all_matching_term_ids );
if ( 'AND' === $where_relation && 0 === count( $intersection ) ) {
// Let's not waste any more time.
$this->void_query = true;
return $this;
}
$merge = count( $all_matching_term_ids ) > 1
? array_unique( array_merge( ...$all_matching_term_ids ) )
: (array) reset( $all_matching_term_ids );
$matching_term_ids = $where_relation === 'OR' ? array_filter( $merge ) : array_filter( $intersection );
if ( 'AND' === $where_relation || ! empty( $matching_term_ids ) ) {
// Let's not add WHERE and JOIN clauses if there is nothing to add.
$tt_alias = 'tribe_tt_' . self::$alias_counter ++;
$this->filter_query->join(
"JOIN {$wpdb->term_relationships} {$tt_alias} ON {$wpdb->posts}.ID = {$tt_alias}.object_id"
);
$matching_term_ids_interval = implode( ',', $matching_term_ids );
$wheres[] = "{$tt_alias}.term_taxonomy_id IN ({$matching_term_ids_interval})";
}
}
if ( ! empty( $custom_fields ) ) {
$meta_alias = 'tribe_meta_' . self::$alias_counter ++;
$custom_fields = array_map( static function ( $custom_field ) use ( $wpdb, $meta_alias ) {
return $wpdb->prepare(
"{$meta_alias}.meta_key = %s AND {$meta_alias}.meta_value",
$custom_field
);
}, $custom_fields );
$meta_where = $this->build_fields_where_clause(
$custom_fields,
$compare,
$values,
$value_format,
$where_relation,
$value_relation
);
$this->filter_query->join(
"JOIN {$wpdb->postmeta} {$meta_alias} ON {$wpdb->posts}.ID = {$meta_alias}.post_id"
);
$wheres[] = $meta_where;
}
$this->filter_query->where( implode( " {$where_relation} ", $wheres ) );
return $this;
}
/**
* {@inheritDoc}
*/
public function set_query( WP_Query $query ) {
if (
$this->last_built_query instanceof WP_Query
&& ! empty( $this->last_built_query->request )
) {
throw Tribe__Repository__Usage_Error::because_query_cannot_be_set_after_it_ran();
}
$this->last_built_query = $query;
$this->last_built_hash = $this->hash();
return $this;
}
/**
* {@inheritDoc}
*/
public function set_found_rows( $found_rows ) {
$this->skip_found_rows = ! $found_rows;
return $this;
}
/**
* Flush current filters and query information.
*
* @since 4.9.10
*
* @return self
*/
public function flush() {
$this->current_query = null;
$this->current_filters = [];
$this->current_filter = null;
$this->last_built_query = null;
$this->last_built_hash = '';
return $this;
}
/**
* {@inheritDoc}
*/
public function next() {
$next = clone $this;
$current_page = isset( $this->query_args['paged'] )
? (int) $this->query_args['paged']
: 1;
$next->page( $current_page + 1 );
// Let's try to avoid running a query if we already know if a next page will yield any result or not.
$query_ran = ! empty( $this->last_built_query ) && ! empty( $this->last_built_query->request );
if ( $query_ran && ( false === (bool) $this->last_built_query->get( 'no_found_rows' ) ) ) {
$found = $this->last_built_query->found_posts;
$posts_per_page = $this->last_built_query->get( 'posts_per_page' );
$this_is_last_page = ( $current_page * $posts_per_page ) >= $found;
if ( $this_is_last_page ) {
$next->void_query = true;
}
}
$next->last_built_query = null;
return $next;
}
/**
* {@inheritDoc}
*/
public function prev() {
$prev = clone $this;
$current_page = isset( $this->query_args['paged'] )
? (int) $this->query_args['paged']
: 1;
if ( $current_page === 1 ) {
$prev->void_query = true;
return $prev;
}
// If we're on page 1 we know there will be previous posts.
$prev->page( $current_page - 1 );
$prev->last_built_query = null;
return $prev;
}
/**
* {@inheritDoc}
*/
public function void_query( $void_query = true ) {
$this->void_query = (bool) $void_query;
return $this;
}
/**
* {@inheritDoc}
*/
public function get_last_sql(): ?string {
return $this->last_built_query instanceof WP_Query ?
$this->last_built_query->request
: null;
}
/**
* Returns a Generator that will yield all the IDs requested avoiding unbound queries.
*
* @since 5.2.0
*
* @return Generator<int> The Generator that will yield all the IDs requested.
*
* @throws Tribe__Repository__Usage_Error If the batch size is less than 1.
*/
protected function get_ids_generator( int $batch_size = 50 ): Generator {
if ( $batch_size < 1 ) {
throw new Tribe__Repository__Usage_Error( 'Batch size must be greater than 0.' );
}
// Assume the LIMIT and OFFSET will not be set by filtering the query, but by using the query builder API.
$query_limit = $this->query_args['posts_per_page'] ?? null;
$query_offset = $this->query_args['offset'] ?? 0;
$batch_size = max( 1, $batch_size );
if ( $query_limit > 0 && $query_limit < $batch_size ) {
// There's already a limit set, let's use it.
yield from $this->get_ids();
return;
}
// Set the query limit to the batch size.
$this->query_args['posts_per_page'] = $batch_size;
// Run the query a first time and get its SQL and the first batch of results.
$results = $this->get_ids();
if ( ! $results ) {
// If there are no results in the first batch, there are no results at all.
return;
}
$sql = $this->last_built_query->request;
// Assume the structure would end with "LIMIT ..." and cut it there.
$limit_last_occurrence = strrpos( $sql, 'LIMIT' );
$sql_template = substr( $sql, 0, $limit_last_occurrence );
$sql_template .= " LIMIT %d OFFSET %d";
// Initialize the offset to the query offset.
$offset = $query_offset;
$fetched = count( $results );
global $wpdb;
do {
foreach ( $results as $result ) {
yield $result;
}
if (
( $query_limit && $fetched === $query_limit )
|| count( $results ) < $batch_size
) {
// Done, save a query.
break;
}
/*
* Prepare the next query:
* - the LIMIT is either the batch size or the remaining posts to fetch.
* - the OFFSET is the current offset plus the number of posts fetched.
*/
$offset += $batch_size;
$limit = $query_limit ?
min( $query_limit - $fetched, $batch_size )
: $batch_size;
$sql = sprintf( $sql_template, $limit, $offset );
$results = $wpdb->get_col( $sql );
$fetched += count( $results );
} while ( $results );
}
/**
* Returns a Generator that will yield all the posts requested avoiding unbound queries.
*
* @since 5.2.0
*
* @return Generator<WP_Post|mixed> The Generator that will yield all the posts requested,
* formatted according to the repository's `format_item` method.
*
* @throws Tribe__Repository__Usage_Error If the batch size is less than 1.
*/
protected function get_all_generator( int $batch_size = 50 ): Generator {
// Get all the IDs.
foreach ( $this->get_ids_generator( $batch_size ) as $id ) {
yield $this->format_item( $id );
}
}
/**
* Get the schema.
*
* @since 6.8.0
*
* @return array
*/
protected function get_schema(): array {
/**
* Filters the schema for the repository takes into account the filter name.
*
* @since 6.8.0
*
* @param array $schema The schema.
* @param Tribe__Repository $repository The repository.
*/
$schema = apply_filters( "tec_repository_schema_{$this->filter_name}", $this->schema, $this );
/**
* Filters the schema for the repository.
*
* @since 6.8.0
*
* @param array $schema The schema.
* @param Tribe__Repository $repository The repository.
*/
return apply_filters( 'tec_repository_schema', $schema, $this );
}
/**
* Set the schema.
*
* @since 6.8.0
*
* @param string $key The key.
* @param callable $callback The callback.
*/
protected function set_schema( string $key, callable $callback ): void {
$this->schema[ $key ] = $callback;
// Trigger the hooks.
$this->get_schema();
}
/**
* {@inheritDoc}
*/
public function first_id(): ?int {
$this->fields( 'ids' );
$this->per_page( 1 );
$first_id = $this->first();
return $first_id ? (int) $first_id : null;
}
/**
* {@inheritDoc}
*/
public function get_request_context(): ?string {
return $this->request_context;
}
/**
* {@inheritDoc}
*/
public function set_request_context( string $context = null ): self {
$this->request_context = $context;
return $this;
}
/**
* Maps the query results to an array using a callback.
*
* @since 5.2.0
*
* @param callable $mapper The callback to use to map the results. The callback should have signature
* `function ( mixed $item, string|int $key, array<mixed> $items ): mixed`.
*
* @return array<mixed> The mapped results.
*/
public function map( callable $mapper ): array {
$items = ( $this->query_args['fields'] ?? null ) === 'ids' ?
$this->get_ids( true )
: $this->all( true );
$mapped = [];
foreach ( $items as $k => $item ) {
$mapped[] = $mapper( $item, $k, $items );
}
return $mapped;
}
}