PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / faq.php

faq.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/faq.php

180 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * FAQ resolution for products.
4 *
5 * A product's rendered FAQ is the merge of:
6 * 1. its own inline Q&A (product meta _storeengine_product_faqs)
7 * 2. groups whose rules match its categories (group meta _storeengine_faq_category_ids)
8 * 3. groups whose rules match its brands (_storeengine_faq_brand_ids)
9 * 4. groups whose rules target it directly (_storeengine_faq_product_ids)
10 * 5. groups flagged store-wide (_storeengine_faq_apply_all)
11 *
12 * @package StoreEngine\Classes
13 */
14
15 namespace StoreEngine\Classes;
16
17 use StoreEngine\Utils\Helper;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 class Faq {
24
25 const BRAND_TAXONOMY = 'storeengine_product_brand';
26
27 /**
28 * Resolve the grouped FAQ list for a product.
29 *
30 * @param int $product_id Product id.
31 *
32 * @return array<int,array{id:int,title:string,items:array<int,array{question:string,answer:string}>}>
33 */
34 public static function get_product_faqs( int $product_id ): array {
35 $cache_key = 'storeengine_faq_' . self::cache_version() . '_' . $product_id;
36 $cached = wp_cache_get( $cache_key, 'storeengine_faqs' );
37
38 if ( false !== $cached ) {
39 return $cached;
40 }
41
42 $product = Helper::get_product( $product_id );
43
44 if ( ! $product ) {
45 return [];
46 }
47
48 $groups = [];
49
50 // 1. Inline product Q&A -> a title-less leading group.
51 $inline = self::clean_items( $product->get_faqs() );
52 if ( ! empty( $inline ) ) {
53 $groups[] = [ 'id' => 0, 'title' => '', 'items' => $inline ];
54 }
55
56 // In "product only" mode the group library is disabled, so groups never
57 // render on the storefront even if a product still carries saved
58 // attachments/rules. Only the product's own inline Q&A shows.
59 if ( 'product_only' === Helper::get_settings( 'faq_mode', 'global' ) ) {
60 $groups = apply_filters( 'storeengine/product_faq_groups', $groups, $product_id );
61 wp_cache_set( $cache_key, $groups, 'storeengine_faqs', HOUR_IN_SECONDS );
62
63 return $groups;
64 }
65
66 // Groups matched by their display rules (all / category / brand / product).
67 $category_id = wp_get_post_terms( $product_id, Helper::PRODUCT_CATEGORY_TAXONOMY, [ 'fields' => 'ids' ] );
68 $category_id = is_wp_error( $category_id ) ? [] : array_map( 'absint', $category_id );
69
70 $brand_ids = [];
71 if ( taxonomy_exists( self::BRAND_TAXONOMY ) ) {
72 $brand_ids = wp_get_post_terms( $product_id, self::BRAND_TAXONOMY, [ 'fields' => 'ids' ] );
73 $brand_ids = is_wp_error( $brand_ids ) ? [] : array_map( 'absint', $brand_ids );
74 }
75
76 foreach ( self::get_all_groups() as $group ) {
77 $apply_all = (bool) get_post_meta( $group->ID, '_storeengine_faq_apply_all', true );
78 $group_cats = array_map( 'absint', (array) get_post_meta( $group->ID, '_storeengine_faq_category_ids', true ) );
79 $group_brands = array_map( 'absint', (array) get_post_meta( $group->ID, '_storeengine_faq_brand_ids', true ) );
80 $group_prods = array_map( 'absint', (array) get_post_meta( $group->ID, '_storeengine_faq_product_ids', true ) );
81
82 $matches = $apply_all
83 || in_array( $product_id, $group_prods, true )
84 || array_intersect( $category_id, $group_cats )
85 || array_intersect( $brand_ids, $group_brands );
86
87 if ( ! $matches ) {
88 continue;
89 }
90
91 $items = self::clean_items( get_post_meta( $group->ID, '_storeengine_faq_items', true ) );
92 if ( empty( $items ) ) {
93 continue;
94 }
95
96 $groups[] = [
97 'id' => (int) $group->ID,
98 'title' => $group->post_title,
99 'items' => $items,
100 ];
101 }
102
103 /**
104 * Filter a product's resolved FAQ groups.
105 *
106 * @param array $groups Resolved groups.
107 * @param int $product_id Product id.
108 */
109 $groups = apply_filters( 'storeengine/product_faq_groups', $groups, $product_id );
110
111 wp_cache_set( $cache_key, $groups, 'storeengine_faqs', HOUR_IN_SECONDS );
112
113 return $groups;
114 }
115
116 /**
117 * All published FAQ groups, ordered by menu order then title.
118 *
119 * @return \WP_Post[]
120 */
121 protected static function get_all_groups(): array {
122 static $groups = null;
123
124 if ( null === $groups ) {
125 $groups = get_posts( [
126 'post_type' => Helper::FAQ_POST_TYPE,
127 'post_status' => 'publish',
128 'posts_per_page' => 500,
129 'orderby' => [ 'menu_order' => 'ASC', 'title' => 'ASC' ],
130 'suppress_filters' => false,
131 ] );
132 }
133
134 return $groups;
135 }
136
137 /**
138 * Normalise a stored items array — drop empty rows, coerce shape.
139 *
140 * @param mixed $items Raw meta value.
141 *
142 * @return array<int,array{question:string,answer:string}>
143 */
144 protected static function clean_items( $items ): array {
145 if ( ! is_array( $items ) ) {
146 return [];
147 }
148
149 $clean = [];
150 foreach ( $items as $item ) {
151 $question = trim( (string) ( $item['question'] ?? '' ) );
152 $answer = (string) ( $item['answer'] ?? '' );
153
154 if ( '' === $question && '' === trim( $answer ) ) {
155 continue;
156 }
157
158 $clean[] = [ 'question' => $question, 'answer' => $answer ];
159 }
160
161 return $clean;
162 }
163
164 /**
165 * Monotonic cache version — folded into every cache key so a single option
166 * bump invalidates all resolved FAQs at once, even under a persistent object
167 * cache (where flushing a single group can be a no-op).
168 */
169 protected static function cache_version(): int {
170 return (int) get_option( 'storeengine_faq_cache_ver', 1 );
171 }
172
173 /**
174 * Bust the resolved-FAQ cache. Called broadly on any group/product save.
175 */
176 public static function flush_cache() {
177 update_option( 'storeengine_faq_cache_ver', self::cache_version() + 1, false );
178 }
179 }
180