This commit is contained in:
proelements
2025-11-13 15:18:34 +02:00
commit 9ac2bf2aa0
1178 changed files with 296944 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query\Data;
class Query_Constants {
public const DATA = [
'AND' => [
'separator' => [
'decoded' => '+',
'from-browser' => ' ',
'encoded' => '%2B',
],
'operator' => 'AND',
'relation' => 'AND',
],
'OR' => [
'separator' => [
'decoded' => '~',
'from-browser' => '~',
'encoded' => '%7C',
],
'operator' => 'IN',
'relation' => 'OR',
],
'NOT' => [
'separator' => [
'decoded' => '!',
'from-browser' => '!',
'encoded' => '%21',
],
'operator' => 'NOT IN',
'relation' => 'AND',
],
'DISABLED' => [
'separator' => [
'decoded' => '',
'from-browser' => '',
'encoded' => '',
],
'operator' => 'AND',
'relation' => 'AND',
],
];
}

View File

@@ -0,0 +1,10 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query\Interfaces;
use ElementorPro\Modules\LoopFilter\Query\Taxonomy_Manager;
interface Query_Interface {
public function __construct( $filter_terms, Taxonomy_Manager $taxonomy_manager );
public function get_query();
}

View File

@@ -0,0 +1,93 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query\QueryTypes;
use ElementorPro\Modules\LoopFilter\Query\Data\Query_Constants;
use ElementorPro\Modules\LoopFilter\Query\Interfaces\Query_Interface;
use ElementorPro\Modules\LoopFilter\Query\Taxonomy_Manager;
class Hierarchy_And_Query implements Query_Interface {
private $query;
private $terms;
private $taxonomy;
private $logical_join;
private $taxonomy_manager;
public function __construct( $filter_terms, $taxonomy_manager ) {
$this->query = Query_Constants::DATA;
$this->taxonomy_manager = $taxonomy_manager;
$this->terms = $filter_terms['hierarchical-terms'] ?? [];
$this->taxonomy = $filter_terms['taxonomy'];
$this->logical_join = $filter_terms['logicalJoin'];
}
/**
* @return array
*/
public function get_query() {
$query = [];
if ( empty( $this->terms ) ) {
return $query;
}
foreach ( $this->terms as $parent_term => $terms ) {
$filtered_terms = $this->filter_query_terms( $parent_term, $terms );
$query[] = $this->get_hierarchy_query( $filtered_terms );
}
return $query;
}
/**
* @param $parent_term
* @param $terms
* @return array
*/
private function filter_query_terms( $parent_term, $terms ) {
$query_terms = [];
$filters_on_parent_term = in_array( $parent_term, $terms );
$filters_on_parent_and_child_terms = count( $terms ) > 1 && $filters_on_parent_term;
$is_parent_term_only = $filters_on_parent_term && ! $filters_on_parent_and_child_terms;
$has_child_terms_only = ! $filters_on_parent_term;
// For an AND Queries exclude parent term if there are child and parent terms.
if ( $filters_on_parent_and_child_terms ) {
$query_terms = array_diff( $terms, [ $parent_term ] ); //drop parent term
}
if ( $is_parent_term_only || $has_child_terms_only ) {
$query_terms = $terms;
}
return $query_terms;
}
/**
* Create the Inner query for AND OR queries with one or more filter terms targeted at the same Widget.
* @param array $terms
* @return array
*/
private function get_hierarchy_query( $terms ) {
$inner_query = [
'taxonomy' => $this->taxonomy,
'field' => 'slug',
'terms' => [],
'operator' => $this->query['OR']['operator'],
];
foreach ( $terms as $term ) {
if ( 1 < count( $terms ) && $this->taxonomy_manager->is_parent_term_with_children( $term, $this->taxonomy ) ) {
$parent_query = $this->get_hierarchy_query( [ $term ] );
continue;
}
$inner_query['terms'] = [ urldecode( sanitize_title( $term ?? '' ) ) ]; //decode non-latin strings
$child_queries[] = $inner_query;
}
$hierarchy_query = array_merge( $parent_query ?? [], $child_queries ?? [] );
$hierarchy_query['relation'] = $this->query['OR']['relation']; //broaden search results when two child terms are selected. And relation between unrelated terms.
return $hierarchy_query;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query\QueryTypes;
use ElementorPro\Modules\LoopFilter\Query\Data\Query_Constants;
use ElementorPro\Modules\LoopFilter\Query\Interfaces\Query_Interface;
class Hierarchy_Or_Query implements Query_Interface {
private $query;
private $terms;
private $taxonomy;
private $logical_join;
public function __construct( $filter_terms, $taxonomy_manager ) {
$this->query = Query_Constants::DATA;
$this->terms = $filter_terms['hierarchical-terms'] ?? [];
$this->taxonomy = $filter_terms['taxonomy'];
$this->logical_join = $filter_terms['logicalJoin'];
}
/**
* @return array
*/
public function get_query() {
$query = [];
if ( empty( $this->terms ) ) {
return $query;
}
foreach ( $this->terms as $parent_term => $terms ) {
$filtered_terms = $this->filter_query_terms( $parent_term, $terms );
$query[] = $this->get_hierarchy_query( $filtered_terms );
}
return $query;
}
/**
* @description
* @param $parent_term
* @param $terms
* @return array
*/
private function filter_query_terms( $parent_term, $terms ) {
$query_terms = [];
$filters_on_parent_term = in_array( $parent_term, $terms );
$filters_on_parent_and_child_terms = count( $terms ) > 1 && $filters_on_parent_term;
$is_parent_term_only = $filters_on_parent_term && ! $filters_on_parent_and_child_terms;
$has_child_terms_only = ! $filters_on_parent_term;
// For an OR Queries exclude child terms if there is a parent term in the terms.
if ( $filters_on_parent_and_child_terms ) {
$query_terms = [ $parent_term ]; // drop child terms
}
if ( $is_parent_term_only || $has_child_terms_only ) {
$query_terms = $terms;
}
return $query_terms;
}
/**
* Create the Inner query for AND OR queries with one or more filter terms targeted at the same Widget.
* @param array $terms
* @return array
*/
private function get_hierarchy_query( $terms ) {
$inner_query = [
'taxonomy' => $this->taxonomy,
'field' => 'slug',
'terms' => [],
'operator' => $this->query['OR']['operator'],
];
foreach ( $terms as $term ) {
if ( 1 < count( $terms ) && $this->taxonomy_manager->is_parent_term_with_children( $term, $this->taxonomy ) ) {
$parent_query = $this->get_hierarchy_query( [ $term ] );
continue;
}
$inner_query['terms'] = [ urldecode( sanitize_title( $term ?? '' ) ) ]; //decode non-latin strings
$child_queries[] = $inner_query;
}
$hierarchy_query = array_merge( $parent_query ?? [], $child_queries ?? [] );
$hierarchy_query['relation'] = $this->query['OR']['relation']; //broaden search results when two child terms are selected. And relation between unrelated terms.
return $hierarchy_query;
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query\QueryTypes;
use ElementorPro\Modules\LoopFilter\Query\Data\Query_Constants;
use ElementorPro\Modules\LoopFilter\Query\Interfaces\Query_Interface;
class Single_Terms_Query implements Query_Interface {
private $query;
private $terms;
private $taxonomy;
private $logical_join;
private $taxonomy_manager;
public function __construct( $filter_terms, $taxonomy_manager ) {
$this->query = Query_Constants::DATA;
$this->taxonomy_manager = $taxonomy_manager;
$this->set_single_or_multiple_selection_terms( $filter_terms );
$this->taxonomy = $filter_terms['taxonomy'] ?? [];
$this->logical_join = $filter_terms['logicalJoin'];
}
/**
* Create the Inner query for AND OR queries with one or more filter terms targeted at the same Widget using terms with no parent and no children
* @return array
*/
public function get_query() {
if ( empty( $this->terms ) ) {
return [];
}
$query = [
[
'taxonomy' => $this->taxonomy,
'field' => 'slug',
'terms' => [],
'operator' => $this->get_inner_query_operator(),
],
];
foreach ( $this->terms as $term ) {
$query[0]['terms'][] = urldecode( sanitize_title( $term ?? '' ) ); //decode non-latin strings
}
return $query;
}
/**
* @param $filter_terms
* @return void
*/
private function set_single_or_multiple_selection_terms( $filter_terms ) {
// Single Selection
if ( ! empty( $filter_terms['single-term'][0] ) ) {
$this->terms = $filter_terms['single-term'];
return;
}
$this->terms = $filter_terms['parent-terms-without-children'];
}
/**
* @return string 'IN' / 'AND' (default)
*/
private function get_inner_query_operator() {
if ( $this->is_single_parent_term() ) {
return $this->query['OR']['operator']; // Allow showing posts from parent category when it's selected.
}
if ( 'DISABLED' !== $this->logical_join ) {
return $this->query[ $this->logical_join ?? 'DISABLED' ]['operator'];
}
return $this->query['AND']['operator'];
}
/**
* @return boolean
*/
private function is_single_parent_term() {
if ( empty( $this->terms ?? [] ) ) {
return false;
}
$is_parent_term = $this->taxonomy_manager->is_parent_term_with_children( $this->terms[0], $this->taxonomy );
return 1 === count( $this->terms ?? [] ) && $is_parent_term;
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query;
class Taxonomy_Manager {
private $terms_by_slug = [];
private $terms_by_id = [];
private $terms_hierarchy = [];
/**
* @param string $taxonomy default 'category'. Use taxonomy string i.e. 'product_cat'. This generates the terms_by_slug and terms_by_id arrays.
* @return void
*/
public function get_taxonomy_terms( $taxonomy = 'category' ) {
$args = [
'taxonomy' => $taxonomy,
'hide_empty' => true,
];
$terms = get_terms( $args );
if ( is_wp_error( $terms ) ) {
return;
}
$this->get_terms_by_slug_and_id( $terms );
$this->terms_hierarchy = $this->filter_child_terms_by_depth( $terms, 100 );
}
private function get_term_id( $slug, $taxonomy ) {
if ( ! isset( $this->terms_by_slug[ $taxonomy ][ $slug ] ) ) {
return -1;
}
return $this->terms_by_slug[ $taxonomy ][ $slug ]['term_id'];
}
/**
* Check if a term is a parent term.
* @param string $slug
* @param string $taxonomy
* @return bool;
*/
public function is_parent_term_without_children( $slug, $taxonomy ) {
// Empty terms do not exist in $terms_by_slug
$has_children = $this->has_children( $slug, $taxonomy );
$is_top_level_parent_term = $this->is_top_level_parent_term( $slug, $taxonomy );
return ( isset( $this->terms_by_slug[ $taxonomy ][ $slug ] ) && $is_top_level_parent_term && ! $has_children );
}
public function is_parent_term_with_children( $slug, $taxonomy ) {
// Empty terms do not exist in $terms_by_slug
return ( isset( $this->terms_by_slug[ $taxonomy ][ $slug ] ) && $this->has_children( $slug, $taxonomy ) );
}
private function has_children( $slug, $taxonomy ) {
$term_id = $this->get_term_id( $slug, $taxonomy );
return isset( $this->terms_hierarchy[ $term_id ] ) && count( $this->terms_hierarchy[ $term_id ] ) > 0;
}
private function get_children( $slug, $taxonomy ) {
$term_id = $this->get_term_id( $slug, $taxonomy );
return $this->terms_hierarchy[ $term_id ];
}
private function is_child_term( $slug, $taxonomy ) {
return ( isset( $this->terms_by_slug[ $taxonomy ][ $slug ] ) && 0 !== $this->terms_by_slug[ $taxonomy ][ $slug ]['parent'] );
}
public function is_top_level_parent_term( $slug, $taxonomy ) {
return ( isset( $this->terms_by_slug[ $taxonomy ][ $slug ] ) && 0 === $this->terms_by_slug[ $taxonomy ][ $slug ]['parent'] );
}
private function get_parent( $slug, $taxonomy ) {
return $this->terms_by_slug[ $taxonomy ][ $slug ]['parent'];
}
private function get_parent_id( $child_slug, $taxonomy ) {
if ( ! isset( $this->terms_by_slug[ $taxonomy ][ $child_slug ]['parent'] ) ) {
return -1;
}
return $this->terms_by_slug[ $taxonomy ][ $child_slug ]['parent'];
}
private function get_parent_slug( $child_slug, $taxonomy ) {
$parent_id = $this->get_parent_id( $child_slug, $taxonomy );
if ( -1 === $parent_id ) {
return 'UNDEFINED';
}
return $this->terms_by_id[ $taxonomy ][ $parent_id ]['slug'];
}
/**
* @param array $filter_terms
* @param string $taxonomy
* @return array
*/
public function get_hierarchy_of_selected_terms( $filter_terms, $taxonomy ) {
// Taxonomy Filter parameter is empty i.e. `e-filter-389c132-product_cat=`.
if ( ! empty( $filter_terms['terms'] ) && '' === $filter_terms['terms'][0] ) {
return [
'single-term' => [],
'parent-terms-without-children' => [],
'hierarchical-terms' => [],
'logicalJoin' => $filter_terms['logicalJoin'],
'taxonomy' => $taxonomy,
];
}
$parents_without_children = [];
$parents_with_children_by_parent = [];
$single_selection_term = [];
if ( 1 === count( $filter_terms['terms'] ) ) {
$single_selection_term = $filter_terms['terms'];
}
foreach ( $filter_terms['terms'] as $term ) {
if ( ! empty( $single_selection_term ) ) {
break;
}
$term = urldecode( sanitize_title( $term ) ); // decode non-latin slugs.
$is_parent_term_without_children = $this->is_parent_term_without_children( $term, $taxonomy );
$is_parent_term_with_children = $this->is_parent_term_with_children( $term, $taxonomy );
if ( $is_parent_term_without_children ) {
$parents_without_children[] = $term;
continue;
}
if ( $is_parent_term_with_children ) {
$parents_with_children_by_parent[ $term ][] = $term;
continue;
}
$parent_slug = $this->get_parent_slug( $term, $taxonomy );
if ( 'UNDEFINED' === $parent_slug ) {
continue;
}
$parents_with_children_by_parent[ $parent_slug ][] = $term;
}
return [
'single-term' => $single_selection_term,
'parent-terms-without-children' => $parents_without_children,
'hierarchical-terms' => $parents_with_children_by_parent,
'logicalJoin' => $filter_terms['logicalJoin'],
'taxonomy' => $taxonomy,
];
}
/**
* @param array $terms
* @return void
*/
private function get_terms_by_slug_and_id( $terms = [] ) {
$this->terms_by_slug = [];
foreach ( $terms as $term ) {
$slug = urldecode( $term->slug );
$this->try_set_terms_by_slug( $slug, $term );
$this->try_set_terms_by_id( $slug, $term );
}
}
/**
* @param string $slug
* @param string $term
* @return void
*/
public function try_set_terms_by_slug( $slug, $term ) {
$term_id = $term->term_id;
$taxonomy = $term->taxonomy;
if ( ! isset( $this->terms_by_slug[ $taxonomy ][ $slug ] ) ) {
$this->terms_by_slug[ $taxonomy ][ $slug ] = [
'term_id' => $term_id,
'parent' => $term->parent,
'count' => $term->count,
];
}
}
/**
* @param string $slug
* @param string $term
* @return void
*/
public function try_set_terms_by_id( $slug, $term ) {
$term_id = $term->term_id;
$taxonomy = $term->taxonomy;
if ( ! isset( $this->terms_by_id[ $taxonomy ][ $slug ] ) ) {
$this->terms_by_id[ $taxonomy ][ $term_id ] = [
'slug' => $slug,
'parent' => $term->parent,
'count' => $term->count,
];
}
}
/**
* @param \WP_Term[] $terms
* @param int $target_depth
* @return \WP_Term[]
*/
private function filter_child_terms_by_depth( $terms, $target_depth ) {
$filtered = [];
foreach ( $terms as $term ) {
$this->filter_single_term( $filtered, $terms, $term, $target_depth );
}
return $filtered;
}
/**
* @param \WP_Term[] $terms
* @param \WP_Term $current_term
* @param int $target_depth
* @return void
*/
private function filter_single_term( &$result, $terms, $current_term, $target_depth ) {
if ( 0 === $current_term->parent ) {
$result[ $current_term->parent ][ $current_term->term_id ] = $current_term;
return;
}
$item_depth = $this->calculate_depth_for_child_term_recursively( $terms, $current_term, 0 );
if ( $item_depth <= $target_depth ) {
$result[ $current_term->parent ][ $current_term->term_id ] = $current_term;
}
}
/**
* @param \WP_Term[] $terms
* @param \WP_Term $child_term
* @param int $depth
* @return int|void
*/
private function calculate_depth_for_child_term_recursively( $terms, $child_term, $depth ) {
$depth++;
foreach ( $terms as $term ) {
if ( $term->term_id !== $child_term->parent ) {
continue;
}
if ( 0 === $term->parent ) {
return $depth;
}
return $this->calculate_depth_for_child_term_recursively( $terms, $term, $depth );
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace ElementorPro\Modules\LoopFilter\Query;
use ElementorPro\Modules\LoopFilter\Query\Data\Query_Constants;
use ElementorPro\Modules\LoopFilter\Query\Interfaces\Query_Interface;
use ElementorPro\Modules\LoopFilter\Query\QueryTypes\Hierarchy_And_Query;
use ElementorPro\Modules\LoopFilter\Query\QueryTypes\Hierarchy_Or_Query;
use ElementorPro\Modules\LoopFilter\Query\QueryTypes\Single_Terms_Query;
class Taxonomy_Query_Builder {
private $query;
private $single_terms_query;
private $hierarchy_query;
private $filter_terms;
private $tax_query;
private $taxonomy_manager;
public function __construct() {
$this->query = Query_Constants::DATA;
}
public function get_merged_queries( &$tax_query, $taxonomy, $filter ) {
$this->tax_query = &$tax_query;
// Taxonomy Filter parameter is empty i.e. `e-filter-389c132-product_cat=`.
if ( ! empty( $filter['terms'] ) && '' === $filter['terms'][0] ) {
return;
}
$this->taxonomy_manager = new Taxonomy_Manager();
$this->taxonomy_manager->get_taxonomy_terms( $taxonomy );
$this->filter_terms = $this->taxonomy_manager->get_hierarchy_of_selected_terms( $filter, $taxonomy );
if ( ! empty( $this->filter_terms['single-term'] ) ) {
$this->get_single_selection_query( $tax_query );
return;
}
$this->get_multiple_selection_query( $tax_query );
}
private function get_single_selection_query( &$tax_query ) {
$this->single_terms_query = $this->get_query( new Single_Terms_Query( $this->filter_terms, $this->taxonomy_manager ) );
$this->merge_single_selection_query( $tax_query );
}
private function get_multiple_selection_query( &$tax_query ) {
$this->single_terms_query = $this->get_query( new Single_Terms_Query( $this->filter_terms, $this->taxonomy_manager ) );
if ( 'AND' === $this->filter_terms['logicalJoin'] ) {
$this->hierarchy_query = $this->get_query( new Hierarchy_And_Query( $this->filter_terms, $this->taxonomy_manager ) );
}
if ( 'OR' === $this->filter_terms['logicalJoin'] ) {
$this->hierarchy_query = $this->get_query( new Hierarchy_Or_Query( $this->filter_terms, $this->taxonomy_manager ) );
}
$this->merge_multiple_selection_query( $tax_query );
}
private function merge_single_selection_query( &$tax_query ) {
if ( empty( $this->single_terms_query ?? [] ) ) {
return;
}
$tax_query = array_merge( $tax_query, $this->single_terms_query ?? [] );
}
private function merge_multiple_selection_query( &$tax_query ) {
if ( empty( $this->single_terms_query ?? [] ) && empty( $this->hierarchy_query ?? [] ) ) {
return;
}
$tax_query = [ array_merge( $tax_query, $this->single_terms_query ?? [], $this->hierarchy_query ?? [], [ 'relation' => $this->query[ $this->filter_terms['logicalJoin'] ]['relation'] ] ) ];
}
private function get_query( Query_Interface $query_type ) {
return $query_type->get_query();
}
}