/** * Retrieve the terms in a given taxonomy or list of taxonomies. * * NOTE: This is the Connections equivalent of @see get_terms() in WordPress core ../wp-includes/taxonomy.php * * Filters: * cn_get_terms_atts - The method variables. * Passes: (array) $atts, (array) $taxonomies * Return: $atts * * cn_get_terms_fields - The fields for the SELECT query clause. * Passes: (array) $select, (array) $atts, (array) $taxonomies * Return: $select * * cn_term_inclusions - Query clause which includes terms. * Passes: (string) $inclusions, (array) $atts, (array) $taxonomies * Return: $inclusions * * cn_term_exclusions - Query clause which excludes terms. * Passes: (string) $exclusions, (array) $atts, (array) $taxonomies * Return: $exclusions * * cn_term_orderby - The ORDER BY query clause. * Passes: (string) $orderBy, (array) $atts, (array) $taxonomies * Return: $orderBy * * cn_terms_clauses - An array containing the the query clause segments. * Passes: (array) $pieces, (array) $taxonomies, (array) $atts * Return: $pieces * * @access public * @since 8.1 * @since 8.5.10 Introduced 'name' and 'childless' parameters. * Introduced the 'meta_query' and 'update_meta_cache' parameters. * Converted to return a list of cnTerm_Object objects. * @static * * @global wpdb $wpdb * * @param string|array $taxonomies Taxonomy name or array of taxonomy names. * @param array $atts { * Optional. Array or string of arguments to get terms. * * @type string $get Whether to return terms regardless of ancestry or whether the terms are empty. * Accepts: 'all' | '' * Default: '' * @type array|string $orderby Field(s) to order terms by. * Use 'include' to match the 'order' of the $include param, or 'none' to skip ORDER BY. * Accepts: term_id | name | slug | term_group | parent | count | include | none * Default: 'name * @type string $order Whether to order terms in ascending or descending order. * Accepts: 'ASC' | 'DESC' * Default: 'ASC' * @type bool|int $hide_empty Whether to hide terms not assigned to any posts. * Accepts: 1|true || 0|false * Default: TRUE. * @type array|string $include Array or comma/space-separated string of term ids to include. * Default: array() * @type array|string $exclude Array or comma/space-separated string of term ids to exclude. * If $include is non-empty, $exclude is ignored. * Default empty array. * @type array|string $exclude_tree Array or comma/space-separated string of term ids to exclude * along with all of their descendant terms. If $include is * non-empty, $exclude_tree is ignored. Default empty array. * @type int|string $number Maximum number of terms to return. * Accepts: ''|0 (all) or any positive number. * Default: 0 * @type int $offset The number by which to offset the terms query. * Accepts: integers. * Default: 0 * @type string $fields Term fields to query for. * Accepts: 'all' (returns an array of complete term objects), * 'ids' (returns an array of ids), * 'id=>parent' (returns an associative array with ids as keys, parent term IDs as values), * 'names' (returns an array of term names), * 'count' (returns the number of matching terms), * 'id=>name' (returns an associative array with ids as keys, term names as values), * 'id=>slug' (returns an associative array with ids as keys, term slugs as values). * Default: 'all'. * @type string|array $name Name or array of names to return term(s) for. * Default: '' * @type string|array $slug Slug or array of slugs to return term(s) for. * Default: ''. * @type bool $hierarchical Whether to include terms that have non-empty descendants (even if $hide_empty is set to true). * Default: TRUE * @type string $search Search criteria to match terms. Will be SQL-formatted with wildcards before and after. * Default: '' * @type string $name__like Retrieve terms with criteria by which a term is LIKE $name__like. * Default: '' * @type string $description__like Retrieve terms where the description is LIKE $description__like. * Default: '' * @type int|string $parent Parent term ID to retrieve direct-child terms of. * Default: '' * @type bool $childless True to limit results to terms that have no children. * This parameter has no effect on non-hierarchical taxonomies. * Default: FALSE * @type int $child_of Term ID to retrieve child terms of. * If multiple taxonomies are passed, $child_of is ignored. * Default: 0 * @type bool $pad_counts Whether to pad the quantity of a term's children in the quantity * of each term's "count" object variable. * Default: FALSE * @type bool $update_meta_cache Whether to prime meta caches for matched terms. * Default: TRUE * @type array $meta_query Meta query clauses to limit retrieved terms by. * @see cnMeta_Query. * Default: array() * } * * @uses apply_filters() * @uses wp_parse_args() * @uses wp_parse_id_list() * @uses sanitize_title() * @uses wpdb::prepare() * @uses $wpdb::esc_like() * @uses absint() * @uses wpdb::get_results() * @uses cnTerm::filter() * @uses cnTerm::descendants() * @uses cnTerm::childrenIDs() * @uses cnTerm::padCounts() * @uses cnTerm::children() * * @return array|int|WP_Error Indexed array of cnTerm_Object objects. Will return WP_Error, if any of $taxonomies do not exist.* */ public static function getTaxonomyTerms($taxonomies = array('category'), $atts = array()) { global $wpdb; $select = array(); $where = array(); $orderBy = array(); $orderByClause = ''; /* * @TODO $taxonomies need to be checked against registered taxonomies. * Presently $taxonomies only support a string rather than array. * Additionally, category is the only supported taxonomy. */ $single_taxonomy = !is_array($taxonomies) || 1 === count($taxonomies); if (!is_array($taxonomies)) { $taxonomies = array($taxonomies); } $defaults = array('get' => '', 'orderby' => 'name', 'order' => 'ASC', 'hide_empty' => TRUE, 'include' => array(), 'exclude' => array(), 'exclude_tree' => array(), 'number' => 0, 'offset' => 0, 'fields' => 'all', 'name' => '', 'slug' => '', 'hierarchical' => TRUE, 'search' => '', 'name__like' => '', 'description__like' => '', 'parent' => '', 'childless' => FALSE, 'child_of' => 0, 'pad_counts' => FALSE, 'meta_query' => array(), 'update_meta_cache' => TRUE); /** * Filter the terms query arguments. * * @since 8.1 * * @param array $atts An array of arguments. * @param string|array $taxonomies A taxonomy or array of taxonomies. */ $atts = apply_filters('cn_get_terms_args', $atts, $taxonomies); $atts = wp_parse_args($atts, $defaults); // @TODO Implement is_taxonomy_hierarchical(). if (!$single_taxonomy || '' !== $atts['parent'] && 0 !== $atts['parent']) { $atts['hierarchical'] = FALSE; $atts['pad_counts'] = FALSE; } // 'parent' overrides 'child_of'. if (0 < intval($atts['parent'])) { $atts['child_of'] = FALSE; } if ('all' == $atts['get']) { $atts['childless'] = FALSE; $atts['child_of'] = 0; $atts['hide_empty'] = 0; $atts['hierarchical'] = FALSE; $atts['pad_counts'] = FALSE; } if ($atts['child_of']) { $hierarchy = self::childrenIDs(reset($taxonomies)); if (!isset($hierarchy[$atts['child_of']])) { return array(); } } if ($atts['parent']) { $hierarchy = self::childrenIDs(reset($taxonomies)); if (!isset($hierarchy[$atts['parent']])) { return array(); } } // $args can be whatever, only use the args defined in defaults to compute the key $filter_key = has_filter('cn_term_exclusions') ? serialize($GLOBALS['wp_filter']['cn_term_exclusions']) : ''; $key = md5(serialize(wp_array_slice_assoc($atts, array_keys($defaults))) . serialize($taxonomies) . $filter_key); $last_changed = wp_cache_get('last_changed', 'cn_terms'); if (!$last_changed) { $last_changed = microtime(); wp_cache_set('last_changed', $last_changed, 'cn_terms'); } $cache_key = "cn_get_terms:{$key}:{$last_changed}"; $cache = wp_cache_get($cache_key, 'cn_terms'); if (FALSE !== $cache) { /** * Filter the given taxonomy's terms cache. * * @since 8.1.6 * * @param array $cache Cached array of terms for the given taxonomy. * @param string|array $taxonomies A taxonomy or array of taxonomies. * @param array $args An array of arguments to get terms. */ $cache = apply_filters('cn_terms', $cache, $taxonomies, $atts); return $cache; } /* * Construct the ORDER By query clause. */ if (is_array($atts['orderby'])) { foreach ($atts['orderby'] as $i => $value) { if (!isset($order)) { $order = 'ASC'; } switch ($value) { case 'name': $orderField = 't.name'; break; case 'id': case 'term_id': $orderField = 't.term_id'; break; case 'slug': $orderField = 't.slug'; break; case 'include': $include = implode(',', wp_parse_id_list($atts['include'])); $orderField = "FIELD( t.term_id, {$include} )"; break; case 'term_group': $orderField = 't.term_group'; break; case 'none': $orderField = ''; // If an `none` order field was supplied, break out of both the switch and foreach statements. break 2; case 'parent': $orderField = 'tt.parent'; break; case 'count': $orderField = 'tt.count'; break; default: $orderField = 't.name'; break; } // Set the $order to align with $atts['orderby']. if (is_array($atts['order']) && isset($atts['order'][$i])) { $order = $atts['order'][$i]; // If an aligned $atts['order'] does not exist use the last $order set otherwise use $atts['order']. } else { $order = is_array($atts['order']) ? $order : $atts['order']; } $order = strtoupper($order); $order = in_array($order, array('ASC', 'DESC')) ? $order : 'ASC'; $orderBy[] = sprintf('%s %s', $orderField, $order); } // The @var $value will be set to the last value from the $atts['orderby'] foreach loop. // If a `none` $atts['orderby'] was found in the supplied array, no order by clause will be set. if (!empty($orderBy) && $value != 'none') { $orderByClause = 'ORDER BY ' . implode(', ', $orderBy); } } else { switch ($atts['orderby']) { case 'name': $atts['orderby'] = 't.name'; break; case 'id': case 'term_id': $atts['orderby'] = 't.term_id'; break; case 'slug': $atts['orderby'] = 't.slug'; break; case 'include': $include = implode(',', wp_parse_id_list($atts['include'])); $atts['orderby'] = "FIELD( t.term_id, {$include} )"; break; case 'term_group': $atts['orderby'] = 't.term_group'; break; case 'none': $atts['orderby'] = ''; break; case 'parent': $atts['orderby'] = 'tt.parent'; break; case 'count': $atts['orderby'] = 'tt.count'; break; default: $atts['orderby'] = 't.name'; break; } if (is_array($atts['order'])) { // $atts['orderby'] was a string but an array was passed for $atts['order'], assume the 0 index. $order = $atts['order'][0]; } else { $order = $atts['order']; } if (!empty($atts['orderby'])) { $order = strtoupper($order); $order = in_array($order, array('ASC', 'DESC')) ? $order : 'ASC'; $orderByClause = 'ORDER BY ' . sprintf('%s %s', $atts['orderby'], $order); } } /** * Filter the ORDER BY clause of the terms query. * * @since 8.1 * * @param string $orderBy ORDER BY clause of the terms query. * @param array $atts An array of terms query arguments. * @param string|array $taxonomies A taxonomy or array of taxonomies. */ $orderBy = apply_filters('cn_terms_orderby', $orderByClause, $atts, $taxonomies); /* * Start construct the WHERE query clause. */ $where[] = 'tt.taxonomy IN (\'' . implode('\', \'', $taxonomies) . '\')'; /* * Define the included terms. */ $inclusions = ''; if (!empty($atts['include'])) { $atts['exclude'] = ''; $atts['exclude_tree'] = ''; $inclusions = implode(',', wp_parse_id_list($atts['include'])); } if (!empty($inclusions)) { $inclusions = 'AND t.term_id IN ( ' . $inclusions . ' )'; } /** * Filter the terms to be included in the terms query. * * @since 8.1 * * @param string $inclusions IN clause of the terms query. * @param array $atts An array of terms query arguments. * @param string|array $taxonomies A taxonomy or array of taxonomies. */ $inclusions = apply_filters('cn_term_inclusions', $inclusions, $atts, $taxonomies); if (!empty($inclusions)) { $where[] = $inclusions; } /* * Define the excluded terms. */ $exclusions = array(); if (!empty($atts['exclude_tree'])) { $atts['exclude_tree'] = wp_parse_id_list($atts['exclude_tree']); $excluded_children = $atts['exclude_tree']; foreach ($atts['exclude_tree'] as $extrunk) { $excluded_children = array_merge($excluded_children, (array) cnTerm::getTaxonomyTerms($taxonomies[0], array('child_of' => intval($extrunk), 'fields' => 'ids', 'hide_empty' => 0))); } $exclusions = array_merge($excluded_children, $exclusions); } if (!empty($atts['exclude'])) { $exclusions = array_merge(wp_parse_id_list($atts['exclude']), $exclusions); } // 'childless' terms are those without an entry in the flattened term hierarchy. $childless = (bool) $atts['childless']; if ($childless) { foreach ($taxonomies as $_tax) { $term_hierarchy = self::childrenIDs($_tax); $exclusions = array_merge(array_keys($term_hierarchy), $exclusions); } } if (!empty($exclusions)) { $exclusions = ' AND t.term_id NOT IN (' . implode(',', array_map('intval', $exclusions)) . ')'; } else { $exclusions = ''; } /** * Filter the terms to exclude from the terms query. * * @since 8.1 * * @param string $exclusions NOT IN clause of the terms query. * @param array $atts An array of terms query arguments. * @param string|array $taxonomies A taxonomy or array of taxonomies. */ $exclusions = apply_filters('cn_term_exclusions', $exclusions, $atts, $taxonomies); if (!empty($exclusions)) { $where[] = $exclusions; } if (!empty($atts['name'])) { $names = (array) $atts['name']; foreach ($names as &$_name) { $_name = sanitize_term_field('name', $_name, 0, reset($taxonomies), 'db'); } $where[] = "AND t.name IN ('" . implode("', '", array_map('esc_sql', $names)) . "')"; } if (!empty($atts['slug'])) { if (is_array($atts['slug'])) { $slug = array_map('sanitize_title', $atts['slug']); $where[] = " AND t.slug IN ('" . implode("', '", $slug) . "')"; } else { $slug = sanitize_title($atts['slug']); $where[] = " AND t.slug = '{$slug}'"; } } if (!empty($atts['name__like'])) { $where[] = $wpdb->prepare(" AND t.name LIKE %s", '%' . $wpdb->esc_like($atts['name__like']) . '%'); } if (!empty($atts['description__like'])) { $where[] = $wpdb->prepare(" AND tt.description LIKE %s", '%' . $wpdb->esc_like($atts['description__like']) . '%'); } if ('' !== $atts['parent']) { $where[] = $wpdb->prepare('AND tt.parent = %d', $atts['parent']); } if ('count' == $atts['fields']) { $atts['hierarchical'] = FALSE; } if ($atts['hide_empty'] && !$atts['hierarchical']) { $where[] = 'AND tt.count > 0'; } // Do not limit the query results when we have to descend the family tree. if ($atts['number'] && !$atts['hierarchical'] && !$atts['child_of'] && '' === $atts['parent']) { $atts['number'] = absint($atts['number']); $atts['offset'] = absint($atts['offset']); if ($atts['offset']) { $limit = $wpdb->prepare('LIMIT %d,%d', $atts['offset'], $atts['number']); } else { $limit = $wpdb->prepare('LIMIT %d', $atts['number']); } } else { $limit = ''; } if (!empty($atts['search'])) { //$atts['search'] = like_escape( $atts['search'] ); $atts['search'] = $wpdb->esc_like($atts['search']); $where[] = $wpdb->prepare('AND ( (t.name LIKE %s) OR (t.slug LIKE %s) )', '%' . $atts['search'] . '%', '%' . $atts['search'] . '%'); } // Meta query support. $distinct = ''; $join = ''; if (!empty($atts['meta_query'])) { $meta_query = new cnMeta_Query($atts['meta_query']); $meta_clauses = $meta_query->get_sql('term', 't', 'term_id'); $distinct .= 'DISTINCT '; $join .= $meta_clauses['join']; $where[] = $meta_clauses['where']; } switch ($atts['fields']) { case 'all': $select = array('t.*', 'tt.*'); break; case 'ids': case 'id=>parent': $select = array('t.term_id', 'tt.parent', 'tt.count'); break; case 'names': $select = array('t.term_id', 'tt.parent', 'tt.count', 't.name'); break; case 'count': $orderBy = ''; //$order = ''; $select = array('COUNT(*)'); break; case 'id=>name': $select = array('t.term_id', 't.name', 'tt.count', 'tt.taxonomy'); break; case 'id=>slug': $select = array('t.term_id', 't.slug', 'tt.count', 'tt.taxonomy'); break; } /** * Filter the fields to select in the terms query. * * @since 8.1 * * @param array $select An array of fields to select for the terms query. * @param array $atts An array of term query arguments. * @param string|array $taxonomies A taxonomy or array of taxonomies. */ $fields = implode(', ', apply_filters('cn_get_terms_fields', $select, $atts, $taxonomies)); $join .= 'INNER JOIN ' . CN_TERM_TAXONOMY_TABLE . ' AS tt ON t.term_id = tt.term_id'; $pieces = array('fields', 'join', 'where', 'distinct', 'orderBy', 'orderby', 'order', 'limit'); /** * Filter the terms query SQL clauses. * * @since 8.1 * * @param array $pieces Terms query SQL clauses. * @param string|array $taxonomies A taxonomy or array of taxonomies. * @param array $atts An array of terms query arguments. */ $clauses = apply_filters('cn_terms_clauses', compact($pieces), $taxonomies, $atts); foreach ($pieces as $piece) { ${$piece} = isset($clauses[$piece]) ? $clauses[$piece] : ''; } $sql = sprintf('SELECT %1$s FROM %2$s AS t %3$s WHERE %4$s %5$s%6$s', $distinct . $fields, CN_TERMS_TABLE, $join, implode(' ', $where), $orderBy, empty($limit) ? '' : ' ' . $limit); if ('count' == $atts['fields']) { $term_count = $wpdb->get_var($sql); return absint($term_count); } $terms = $wpdb->get_results($sql); if ('all' == $atts['fields']) { foreach ($taxonomies as $taxonomy) { update_term_cache($terms, 'cn_' . $taxonomy); } } // Prime term meta cache. if ($atts['update_meta_cache']) { $term_ids = wp_list_pluck($terms, 'term_id'); cnMeta::updateCache('term', $term_ids); } if (empty($terms)) { wp_cache_add($cache_key, array(), 'cn_terms', DAY_IN_SECONDS); $terms = apply_filters('cn_terms', array(), $taxonomies, $atts); return $terms; } if ($atts['child_of']) { $children = self::childrenIDs(reset($taxonomies)); if (!empty($children)) { $terms = self::descendants($atts['child_of'], $terms, reset($taxonomies)); } } /* * @todo Add method to adjust counts based on user visibility permissions. */ // Update term counts to include children. if ($atts['pad_counts'] && 'all' == $atts['fields']) { foreach ($taxonomies as $_tax) { self::padCounts($terms, $_tax); } } // Make sure we show empty categories that have children. if ($atts['hierarchical'] && $atts['hide_empty'] && is_array($terms)) { foreach ($terms as $k => $term) { if (!$term->count) { $children = self::children($term->term_id, $term->taxonomy); if (is_array($children)) { foreach ($children as $child_id) { $child = self::filter($child_id, $term->taxonomy); if ($child->count) { continue 2; } } } // It really is empty unset($terms[$k]); } } } $_terms = array(); if ('id=>parent' == $atts['fields']) { foreach ($terms as $term) { $_terms[$term->term_id] = $term->parent; } } elseif ('ids' == $atts['fields']) { foreach ($terms as $term) { $_terms[] = $term->term_id; } } elseif ('names' == $atts['fields']) { foreach ($terms as $term) { $_terms[] = $term->name; } } elseif ('id=>name' == $atts['fields']) { foreach ($terms as $term) { $_terms[$term->term_id] = $term->name; } } elseif ('id=>slug' == $atts['fields']) { foreach ($terms as $term) { $_terms[$term->term_id] = $term->slug; } } if (!empty($_terms)) { $terms = $_terms; } if ($atts['number'] && is_array($terms) && count($terms) > $atts['number']) { $terms = array_slice($terms, $atts['offset'], $atts['number']); } wp_cache_add($cache_key, $terms, 'cn_terms', DAY_IN_SECONDS); if ('all' === $atts['fields']) { $terms = array_map(array('cnTerm', 'get'), $terms); } $terms = apply_filters('cn_terms', $terms, $taxonomies, $atts); return $terms; }