PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / trunk
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant vtrunk
2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 All 59 releases
merchant / inc / modules / class-add-module.php

class-add-module.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant trunk, at inc/modules/class-add-module.php

470 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit; // Exit if accessed directly
5 }
6
7 class Merchant_Add_Module {
8
9 /**
10 * WooCommerce only.
11 *
12 */
13 public $wc_only = false;
14
15 /**
16 * Module section.
17 *
18 */
19 public $module_section = '';
20
21 /**
22 * Module id.
23 *
24 */
25 public $module_id = '';
26
27 /**
28 * Module default settings.
29 *
30 */
31 public $module_default_settings = array();
32
33 /**
34 * Module data.
35 *
36 */
37 public $module_data = array();
38
39 /**
40 * AI prompt examples shown in the "Ask AI" popup for this module.
41 *
42 * Subclasses set this in their constructor with a 'blurb' string
43 * and a 'prompts' array of example prompts.
44 *
45 * @var array
46 */
47 protected $ai_examples = array();
48
49 /**
50 * Module options.
51 *
52 */
53 public $module_options_path = '';
54
55 /**
56 * Whether the module has a shortcode or not.
57 *
58 * @var bool
59 */
60 public $has_shortcode = false;
61
62 protected $has_analytics = false;
63
64 /**
65 * Option group definitions cache.
66 *
67 * @var array<int, array<string, mixed>>|null
68 */
69 protected $option_groups = null;
70
71 /**
72 * Initialize the module's option group definitions.
73 *
74 * Subclasses override this to define their settings panels.
75 * Each element in the returned array corresponds to one settings
76 * section in the admin UI.
77 *
78 * @return array<int, array<string, mixed>> Array of option group arrays, each with 'module', 'title', and 'fields' keys.
79 */
80 protected function init_option_groups() {
81 return array();
82 }
83
84 /**
85 * Get the module's option group definitions.
86 *
87 * Returns cached definitions if available. On first call,
88 * initializes from the subclass and applies the filter for
89 * third-party extensibility.
90 *
91 * @return array<int, array<string, mixed>> Filtered option group definitions.
92 */
93 public function get_option_groups() {
94 if ( $this->option_groups !== null ) {
95 return $this->option_groups;
96 }
97
98 $groups = $this->init_option_groups();
99
100 /**
101 * Filter a module's option group definitions.
102 *
103 * Allows third-party plugins to add, remove, or modify
104 * the field definitions for any module.
105 *
106 * @param array<int, array<string, mixed>> $groups Array of option group arrays.
107 *
108 * @since 2.3.0
109 */
110 $this->option_groups = apply_filters(
111 "merchant_{$this->module_id}_option_groups",
112 $groups
113 );
114
115 return $this->option_groups;
116 }
117
118
119 /**
120 * Constructor.
121 *
122 */
123 public function __construct() {
124 // Add and expose the module into the plugin dashboard.
125 add_filter( 'merchant_modules', array( $this, 'add_module' ) );
126
127 // Add module options.
128 add_filter( 'merchant_module_file_path', array( $this, 'add_module_options' ), 10, 2 );
129
130 // Add class to body to identify if module is active or not.
131 add_filter( 'admin_body_class', array( $this, 'add_module_activation_status_class' ), 10, 2 );
132
133 // Handle modules list item class.
134 add_filter( "merchant_admin_module_{$this->module_id}_list_item_class", array( $this, 'modules_list_item_class' ) );
135
136 if ( $this->has_shortcode ) {
137 add_action( 'wp', array( $this, 'setup_product_object' ) );
138 add_shortcode( 'merchant_module_' . str_replace( '-', '_', $this->module_id ), array( $this, 'shortcode_handler' ) );
139 }
140
141 // Remove merchant shortcodes from Botiga product card short description
142 add_filter( 'botiga_loop_product_elements', function( $elements ) {
143 add_filter( 'get_the_excerpt', function( $excerpt ) {
144 return preg_replace( '/\[merchant_[^]]+]/', '', $excerpt );
145 } );
146
147 return $elements;
148 } );
149 }
150
151 /**
152 * Check if the module has analytics.
153 *
154 * @return bool
155 */
156 public function has_analytics() {
157 return $this->has_analytics;
158 }
159
160 /**
161 * Get the module's AI prompt examples.
162 *
163 * @return array Array with 'blurb' and 'prompts' keys, or an empty array if the module has none.
164 */
165 public function get_ai_examples() {
166 /**
167 * Filter a module's AI prompt examples.
168 *
169 * @param array $ai_examples Array with 'blurb' and 'prompts' keys.
170 *
171 * @since 2.3.0
172 */
173 return apply_filters( "merchant_{$this->module_id}_ai_examples", $this->ai_examples );
174 }
175
176 /**
177 * Get all analytics metrics and allow modules to filter them.
178 *
179 * @return array List of available metrics.
180 */
181 public function analytics_metrics() {
182 /**
183 * Hook: merchant_analytics_module_metrics
184 *
185 * @param array $metrics List of available metrics.
186 * @param string $module_id Module ID.
187 *
188 * @since 2.0
189 */
190 return apply_filters( 'merchant_analytics_module_metrics', $this->default_analytics_metrics(), $this->module_id, $this );
191 }
192
193 /**
194 * Get analytics metrics.
195 *
196 * @return array List of available metrics.
197 */
198 protected function default_analytics_metrics() {
199 return array(
200 'campaigns' => true,
201 'impressions' => true,
202 'clicks' => true,
203 'ctr' => true,
204 'revenue' => true,
205 'orders_count' => true,
206 'aov' => true,
207 );
208 }
209
210 /**
211 * Active modules class handler.
212 *
213 */
214 public function add_module_activation_status_class( $classes ) {
215 if ( ! $this->is_module_settings_page() ) {
216 return $classes;
217 }
218
219 if ( Merchant_Modules::is_module_active( $this->module_id ) ) {
220 $classes = $classes . ' merchant-module-enabled';
221 } else {
222 $classes = $classes . ' merchant-module-disabled';
223 }
224
225 return $classes;
226 }
227
228 /**
229 * Modules list item class.
230 *
231 * @param string $module_class
232 *
233 * @return string
234 */
235 public function modules_list_item_class( $module_class ) {
236 if ( $this->wc_only && ! class_exists( 'Woocommerce' ) ) {
237 $module_class = $module_class . ' merchant-module-wc-only';
238 }
239
240 return $module_class;
241 }
242
243 /**
244 * Is module settings page.
245 *
246 * @return bool
247 */
248 public function is_module_settings_page() {
249 return isset( $_GET['page'] ) && 'merchant' === $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
250 && isset( $_GET['module'] ) && $this->module_id === $_GET['module']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
251 }
252
253 /**
254 * Get module settings.
255 *
256 */
257 public function get_module_settings() {
258 $settings = get_option( 'merchant', array() );
259
260 // Default settings.
261 $defaults = $this->module_default_settings;
262
263 if ( empty( $settings[ $this->module_id ] ) ) {
264 $settings[ $this->module_id ] = $defaults;
265 }
266
267 // Parse settings with defaults.
268 // Todo: check if recursive_parse_args() works for all modules and remove the condition.
269 $settings = $this->module_id === 'product-labels' ? $this->recursive_parse_args( $settings[ $this->module_id ], $defaults ) : wp_parse_args( $settings[ $this->module_id ], $defaults );
270
271 /**
272 * Hook: merchant_module_settings
273 *
274 * @param array $settings Module settings.
275 * @param string $module_id Module ID.
276 *
277 * @since 1.9.16
278 */
279 $settings = apply_filters( 'merchant_module_settings', $settings, $this->module_id );
280
281 return $settings;
282 }
283
284 /**
285 * Update module settings.
286 *
287 * @param array $module_settings
288 *
289 * @return void
290 */
291 public function update_module_settings( $module_settings ) {
292 $settings = get_option( 'merchant', array() );
293
294 $settings[ $this->module_id ] = $module_settings;
295
296 /**
297 * Hook: merchant_module_settings_update
298 *
299 * @param array $settings Module settings.
300 * @param string $module_id Module ID.
301 *
302 * @since 2.0.0
303 */
304 $settings = apply_filters( 'merchant_module_settings_update', $settings, $this->module_id );
305
306 update_option( 'merchant', $settings );
307 }
308
309 /**
310 * Add module.
311 *
312 */
313 public function add_module( $modules ) {
314 $modules[ $this->module_section ]['modules'][ $this->module_id ] = $this->module_data;
315
316 return $modules;
317 }
318
319 /**
320 * Add module options.
321 *
322 * For migrated modules (those with option groups defined),
323 * renders settings directly from get_option_groups().
324 * Unmigrated modules fall back to including $module_options_path.
325 *
326 * @param string $module_path Default file path.
327 * @param string $merchant_module Module ID being rendered.
328 *
329 * @return string File path to include, or empty string if already rendered.
330 */
331 public function add_module_options( $module_path, $merchant_module ) {
332 if ( $this->module_id !== $merchant_module ) {
333 return $module_path;
334 }
335
336 $groups = $this->get_option_groups();
337
338 if ( ! empty( $groups ) ) {
339 $this->before_render_option_groups();
340
341 foreach ( $groups as $group ) {
342 Merchant_Admin_Options::create( $group );
343 }
344
345 $this->after_render_option_groups();
346
347 return '';
348 }
349
350 return $this->module_options_path;
351 }
352
353 /**
354 * Called before option groups are rendered in the admin.
355 *
356 * Override in subclasses that need to fire actions
357 * before settings fields (e.g., help banners).
358 *
359 * @return void
360 */
361 protected function before_render_option_groups() {}
362
363 /**
364 * Called after option groups are rendered in the admin.
365 *
366 * Override in subclasses that need to fire actions
367 * after settings fields.
368 *
369 * @return void
370 */
371 protected function after_render_option_groups() {}
372
373 /**
374 * Display error message if the shortcode is placed in the wrong place.
375 *
376 * @return mixed|null
377 */
378 public function shortcode_placement_error() {
379 /*
380 * translators: %s: module id
381 */
382 $message = __( 'The shortcode <strong>[merchant_module_%s]</strong> can only be used on single product pages.', 'merchant' );
383 $message = sprintf( $message, str_replace( '-', '_', $this->module_id ) );
384 $message = wp_kses( $message, array(
385 'strong' => array(),
386 ) );
387
388 /**
389 * Filter the shortcode error message html content.
390 *
391 * @param string $message_content
392 * @param string $module_id
393 *
394 * @since 1.8
395 */
396 return apply_filters( 'merchant_module_shortcode_error_message_html',
397 '<div class="merchant-shortcode-wrong-placement">' .
398 $message
399 . '</div>',
400 $this->module_id );
401 }
402
403 /**
404 * Check if shortcode is enabled.
405 *
406 * @return bool
407 */
408 public function is_shortcode_enabled() {
409
410 /**
411 * Hook 'merchant_{$this->module_id}_is_shortcode_enabled'
412 *
413 * @since 1.9.3
414 */
415 return apply_filters( "merchant_{$this->module_id}_is_shortcode_enabled", Merchant_Admin_Options::get( $this->module_id, 'use_shortcode', false ) );
416 }
417
418 /**
419 * Recursively merges user-defined arguments into default arguments.
420 *
421 * @param $args
422 * @param $defaults
423 *
424 * @return mixed
425 */
426 private function recursive_parse_args( $args, $defaults ) {
427 $result = $defaults;
428
429 foreach ( $args as $key => $value ) {
430 // If the value is an array and the corresponding default is also an array, merge them recursively.
431 if ( is_array( $value ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
432 $result[ $key ] = $this->recursive_parse_args( $value, $result[ $key ] );
433 } else {
434 $result[ $key ] = $value;
435 }
436 }
437
438 return $result;
439 }
440
441 /**
442 * Ensure $product is an object in the Breakdance builder editor.
443 * If $product is a string, convert it to a WooCommerce product object.
444 *
445 * @return void
446 */
447 public function setup_product_object() {
448 if ( ! class_exists( 'WooCommerce' ) ) {
449 return;
450 }
451
452 if ( ! is_product() ) {
453 return;
454 }
455
456 global $product;
457
458 // Check if $product is a string and not already an object
459 if ( ! is_object( $product ) && is_string( $product ) ) {
460 // Retrieve the product object by slug
461 $product_object = wc_get_product( get_page_by_path( $product, OBJECT, 'product' ) );
462
463 // Update global $product with the retrieved product object if found
464 if ( $product_object ) {
465 $product = $product_object;
466 }
467 }
468 }
469 }
470