instructor_id = absint( $params['instructor_id'] ?? 0 ); $scope->category_id = absint( $params['category_id'] ?? 0 ); /** * Filter the resolved statistics scope before it shapes any query. * * Fires once per request at the single scope sanitation boundary, so a * handler here reaches every scoped query and report across all tabs * ( e.g. force a teacher role to only their own courses ). * * @param StatisticsScope $scope Resolved scope. * @param array $params Sanitized request params. * @since 4.4.2 */ $scope = apply_filters( 'learn-press/statistics/scope', $scope, $params ); // Poka-yoke: a handler returning the wrong type falls back to an unscoped scope. return $scope instanceof self ? $scope : new self(); } /** * @return bool */ public function is_empty(): bool { return 0 === $this->instructor_id && 0 === $this->category_id; } /** * Stable identity for cache keys — two scopes with the same ids share a cache entry. * * @return array [ int instructor_id, int category_id ] */ public function signature(): array { return array( $this->instructor_id, $this->category_id ); } /** * Add scope joins/where to a query whose rows already carry a course id column. * * @param LP_Filter $filter Query filter to extend. * @param string $course_id_field One of COURSE_ID_FIELDS. * @return LP_Filter * @throws InvalidArgumentException On a course id field outside the allowlist. */ public function apply( LP_Filter $filter, string $course_id_field ): LP_Filter { if ( ! in_array( $course_id_field, self::COURSE_ID_FIELDS, true ) ) { throw new InvalidArgumentException( 'Unknown course id field for statistics scope.' ); } if ( $this->is_empty() ) { return $filter; } global $wpdb; if ( $this->instructor_id > 0 ) { $filter->join[] = "INNER JOIN {$wpdb->posts} AS scope_p ON scope_p.ID = {$course_id_field}"; $filter->where[] = $wpdb->prepare( 'AND scope_p.post_author = %d', $this->instructor_id ); } if ( $this->category_id > 0 ) { $filter->join[] = "INNER JOIN {$wpdb->term_relationships} AS scope_tr ON scope_tr.object_id = {$course_id_field}"; $filter->join[] = $wpdb->prepare( "INNER JOIN {$wpdb->term_taxonomy} AS scope_tt ON scope_tt.term_taxonomy_id = scope_tr.term_taxonomy_id AND scope_tt.taxonomy = %s", LP_COURSE_CATEGORY_TAX ); $filter->where[] = $wpdb->prepare( 'AND scope_tt.term_id = %d', $this->category_id ); } return $filter; } /** * Scope an orders query that has no course id column: keep orders containing * at least one course item matching the scope. * * Uses EXISTS instead of a join so an order with several scoped items still * counts once — order-level COUNT/SUM fields stay correct without DISTINCT rewrites. * * @param LP_Filter $filter Query filter to extend. * @param string $order_id_field One of ORDER_ID_FIELDS. * @return LP_Filter * @throws InvalidArgumentException On an order id field outside the allowlist. */ public function apply_to_orders( LP_Filter $filter, string $order_id_field ): LP_Filter { if ( ! in_array( $order_id_field, self::ORDER_ID_FIELDS, true ) ) { throw new InvalidArgumentException( 'Unknown order id field for statistics scope.' ); } if ( $this->is_empty() ) { return $filter; } global $wpdb; $tb_order_items = $wpdb->prefix . 'learnpress_order_items'; $joins = ''; $conditions = ''; if ( $this->instructor_id > 0 ) { $joins .= " INNER JOIN {$wpdb->posts} AS scope_p ON scope_p.ID = scope_oi.item_id"; $conditions .= $wpdb->prepare( ' AND scope_p.post_author = %d', $this->instructor_id ); } if ( $this->category_id > 0 ) { $joins .= " INNER JOIN {$wpdb->term_relationships} AS scope_tr ON scope_tr.object_id = scope_oi.item_id"; $joins .= $wpdb->prepare( " INNER JOIN {$wpdb->term_taxonomy} AS scope_tt ON scope_tt.term_taxonomy_id = scope_tr.term_taxonomy_id AND scope_tt.taxonomy = %s", LP_COURSE_CATEGORY_TAX ); $conditions .= $wpdb->prepare( ' AND scope_tt.term_id = %d', $this->category_id ); } $filter->where[] = "AND EXISTS ( SELECT 1 FROM {$tb_order_items} AS scope_oi{$joins} WHERE scope_oi.order_id = {$order_id_field}{$conditions} )"; return $filter; } /** * Prepared "AND EXISTS(...)" conditions for raw SQL queries (subselects, * HAVING-grouped queries) where LP_Filter joins do not reach. * * EXISTS-based so it is alias-collision-free and never duplicates rows. * Returns '' when the scope is empty. * * @param string $course_id_field One of COURSE_ID_FIELDS. * @return string * @throws InvalidArgumentException On a course id field outside the allowlist. */ public function sql_conditions( string $course_id_field ): string { if ( ! in_array( $course_id_field, self::COURSE_ID_FIELDS, true ) ) { throw new InvalidArgumentException( 'Unknown course id field for statistics scope.' ); } if ( $this->is_empty() ) { return ''; } global $wpdb; $conditions = ''; if ( $this->instructor_id > 0 ) { $conditions .= $wpdb->prepare( " AND EXISTS ( SELECT 1 FROM {$wpdb->posts} AS scope_sp WHERE scope_sp.ID = {$course_id_field} AND scope_sp.post_author = %d )", $this->instructor_id ); } if ( $this->category_id > 0 ) { $conditions .= $wpdb->prepare( " AND EXISTS ( SELECT 1 FROM {$wpdb->term_relationships} AS scope_str" . " INNER JOIN {$wpdb->term_taxonomy} AS scope_stt ON scope_stt.term_taxonomy_id = scope_str.term_taxonomy_id AND scope_stt.taxonomy = %s" . " WHERE scope_str.object_id = {$course_id_field} AND scope_stt.term_id = %d )", LP_COURSE_CATEGORY_TAX, $this->category_id ); } return $conditions; } }