Passed
Pull Request — master (#169)
by
unknown
02:04
created

Algolia_Send_Products   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Importance

Changes 19
Bugs 0 Features 8
Metric Value
eloc 154
c 19
b 0
f 8
dl 0
loc 405
rs 8.48
wmc 49

9 Methods

Rating   Name   Duplication   Size   Complexity  
D get_product_attributes() 0 97 21
C send_products_to_algolia() 0 147 11
A get_product_tags() 0 11 3
A get_product_type_price() 0 14 3
A add_to_record() 0 11 4
A get_product_categories() 0 13 2
A is_basic_field_enabled() 0 4 1
A get_product_stock_data() 0 9 2
A can_connect_to_algolia() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like Algolia_Send_Products often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Algolia_Send_Products, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Algolia Woo Indexer class for sending products
5
 * Called from main plugin file algolia-woo-indexer.php
6
 *
7
 * @package algolia-woo-indexer
8
 */
9
10
namespace Algowoo;
11
12
use \Algowoo\Algolia_Check_Requirements as Algolia_Check_Requirements;
0 ignored issues
show
Bug introduced by
The type \Algowoo\Algolia_Check_Requirements was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Bug introduced by
This use statement conflicts with another class in this namespace, Algowoo\Algolia_Check_Requirements. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
13
14
/**
15
 * Abort if this file is called directly
16
 */
17
if (!defined('ABSPATH')) {
18
    exit;
19
}
20
21
/**
22
 * Include plugin file if function is_plugin_active does not exist
23
 */
24
if (!function_exists('is_plugin_active')) {
25
    require_once(ABSPATH . '/wp-admin/includes/plugin.php');
26
}
27
28
/**
29
 * Define the plugin version and the database table name
30
 */
31
define('ALGOWOO_DB_OPTION', '_algolia_woo_indexer');
32
define('ALGOWOO_CURRENT_DB_VERSION', '0.3');
33
34
/**
35
 * Define application constants
36
 */
37
define('CHANGE_ME', 'change me');
38
39
/**
40
 * Define list of fields available to index
41
 */
42
define('BASIC_FIELDS', array(
43
    'product_name',
44
    'permalink',
45
    'tags',
46
    'categories',
47
    'short_description',
48
    'long_description',
49
    'excerpt',
50
    'product_image',
51
    'regular_price',
52
    'sale_price',
53
    'on_sale',
54
    "stock_quantity",
55
    "stock_status"
56
));
57
58
/**
59
 * Database table names
60
 */
61
define('INDEX_NAME', '_index_name');
62
define('AUTOMATICALLY_SEND_NEW_PRODUCTS', '_automatically_send_new_products');
63
define('BASIC_FIELD_PREFIX', '_field_');
64
define('ALGOLIA_APP_ID', '_application_id');
65
define('ALGOLIA_API_KEY', '_admin_api_key');
66
67
/**
68
 * constants for attributes
69
 */
70
define('ATTRIBUTES_ENABLED', '_attributes_enabled');
71
define('ATTRIBUTES_VISIBILITY', '_attributes_visibility');
72
define('ATTRIBUTES_VISIBILITY_STATES', array('all', 'visible', 'hidden'));
73
define('ATTRIBUTES_VARIATION', '_attributes_variation');
74
define('ATTRIBUTES_VARIATION_STATES', array('all', 'used', 'notused'));
75
define('ATTRIBUTES_LIST', '_attributes_list');
76
define('ATTRIBUTES_LIST_INTERPOLATE', '_attributes_list_interpolate');
77
78
79
if (!class_exists('Algolia_Send_Products')) {
80
    /**
81
     * Algolia WooIndexer main class
82
     */
83
    // TODO Rename class "Algolia_Send_Products" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.
84
    class Algolia_Send_Products
85
    {
86
        const PLUGIN_NAME      = 'Algolia Woo Indexer';
87
        const PLUGIN_TRANSIENT = 'algowoo-plugin-notice';
88
89
        /**
90
         * The Algolia instance
91
         *
92
         * @var \Algolia\AlgoliaSearch\SearchClient
93
         */
94
        private static $algolia = null;
95
96
        /**
97
         * Check if we can connect to Algolia, if not, handle the exception, display an error and then return
98
         */
99
        public static function can_connect_to_algolia()
100
        {
101
            try {
102
                self::$algolia->listApiKeys();
103
            } catch (\Algolia\AlgoliaSearch\Exceptions\UnreachableException $error) {
104
                add_action(
105
                    'admin_notices',
106
                    function () {
107
                        echo '<div class="error notice">
108
                            <p>' . esc_html__('An error has been encountered. Please check your application ID and API key. ', 'algolia-woo-indexer') . '</p>
109
						</div>';
110
                    }
111
                );
112
                return;
113
            }
114
        }
115
116
        /**
117
         * check if the field is enabled and shall be sent
118
         *
119
         * @param  mixed $field name of field to be checked according to BASIC_FIELDS 
120
         * @return boolean true if enable, false is not enabled
121
         */
122
        public static function is_basic_field_enabled($field)
123
        {
124
            $fieldValue = get_option(ALGOWOO_DB_OPTION . BASIC_FIELD_PREFIX . $field);
125
            return $fieldValue;
126
        }
127
128
        /**
129
         * helper function to add a field to a record while checking their state
130
         *
131
         * @param  array $record existing record where the field and value shall be added to 
132
         * @param  string $field name of field to be checked according to BASIC_FIELDS 
133
         * @param  mixed $value data to be added to the record array named to $field
134
         * @param  boolean $skip_basic_field_validation set to true if it is not a basic field to skip validation 
135
         * @return array $record previous passed $record with added field data
136
         */
137
        public static function add_to_record($record, $field, $value, $skip_basic_field_validation = false)
138
        {
139
            /**
140
             *  only if enabled or validation skipped and not empty
141
             */
142
            if ((!self::is_basic_field_enabled($field) && !$skip_basic_field_validation) || empty($value)) {
143
                return $record;
144
            }
145
146
            $record[$field] = $value;
147
            return $record;
148
        }
149
150
        /**
151
         * Get sale price or regular price based on product type
152
         *
153
         * @param  mixed $product Product to check   
154
         * @return array ['sale_price' => $sale_price,'regular_price' => $regular_price] Array with regular price and sale price
155
         */
156
        public static function get_product_type_price($product)
157
        {
158
            $sale_price = 0;
159
            $regular_price = 0;
160
            if ($product->is_type('simple')) {
161
                $sale_price     =  $product->get_sale_price();
162
                $regular_price  =  $product->get_regular_price();
163
            } elseif ($product->is_type('variable')) {
164
                $sale_price     =  $product->get_variation_sale_price('min', true);
165
                $regular_price  =  $product->get_variation_regular_price('max', true);
166
            }
167
            return array(
168
                'sale_price' => $sale_price,
169
                'regular_price' => $regular_price
170
            );
171
        }
172
173
174
        /**
175
         * Checks if stock management is enabled and if so, returns quantity and status
176
         *
177
         * @param  mixed $product Product to check   
178
         * @return array ['stock_quantity' => $stock_quantity,'stock_status' => $stock_status] Array with quantity and status. if stock management is disabled, false will be returned,
179
         */
180
        public static function get_product_stock_data($product)
181
        {
182
            if ($product->get_manage_stock()) {
183
                return array(
184
                    'stock_quantity' => $product->get_stock_quantity(),
185
                    'stock_status' => $product->get_stock_status()
186
                );
187
            }
188
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
189
        }
190
191
        /**
192
         * Get product tags
193
         *
194
         * @param  mixed $product Product to check   
195
         * @return array ['tag1', 'tag2', ...] simple array with associated tags
196
         */
197
        public static function get_product_tags($product)
198
        {
199
            $tags = get_the_terms($product->get_id(), 'product_tag');
200
            $term_array = array();
201
            if (is_array($tags)) {
0 ignored issues
show
introduced by
The condition is_array($tags) is always false.
Loading history...
202
                foreach ($tags as $tag) {
203
                    $name = get_term($tag)->name;
204
                    array_push($term_array, $name);
205
                }
206
            }
207
            return $term_array;
208
        }
209
210
        /**
211
         * Get product categories
212
         *
213
         * @param  mixed $product Product to check   
214
         * @return array ['tag1', 'tag2', ...] simple array with associated categories
215
         */
216
        public static function get_product_categories($product)
217
        {
218
            $categories = get_the_terms($product->get_id(), 'product_cat');
219
            $term_array = array();
220
            foreach ($categories as $category) {
0 ignored issues
show
Bug introduced by
The expression $categories of type false is not traversable.
Loading history...
221
                $name = get_term($category)->name;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on WP_Error.
Loading history...
222
                $slug = get_term($category)->slug;
0 ignored issues
show
Bug introduced by
The property slug does not seem to exist on WP_Error.
Loading history...
223
                array_push($term_array, array(
224
                    "name" => $name,
225
                    "slug" => $slug
226
                ));
227
            }
228
            return $term_array;
229
        }
230
231
        /**
232
         * Get attributes from product
233
         *
234
         * @param  mixed $product Product to check   
235
         * @return array ['pa_name' => ['value1', 'value2']] Array with key set to the product attribute internal name and values as array. returns false if not attributes found.
236
         */
237
        public static function get_product_attributes($product)
238
        {
239
            /**
240
             * ensure that attrobutes are actually enabled
241
             */
242
            $attributes_enabled = (int) get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_ENABLED);
243
            if ($attributes_enabled !== 1) {
244
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
245
            }
246
247
            /**
248
             * gather data and settings
249
             */
250
            $rawAttributes = $product->get_attributes("edit");
251
            $setting_visibility = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_VISIBILITY);
252
            $setting_variation = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_VARIATION);
253
            $setting_ids = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_LIST);
254
            $setting_ids = explode(",", $setting_ids);
0 ignored issues
show
Bug introduced by
It seems like $setting_ids can also be of type false; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

254
            $setting_ids = explode(",", /** @scrutinizer ignore-type */ $setting_ids);
Loading history...
255
            $setting_ids_interpolate = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_LIST_INTERPOLATE);
256
            $setting_ids_interpolate = explode(",", $setting_ids_interpolate);
257
258
            if (!$rawAttributes) {
259
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
260
            }
261
262
            $attributes = [];
263
            foreach ($rawAttributes as $attribute) {
264
                /**
265
                 * skip variation attributes
266
                 */
267
                if ($attribute->get_variation()) {
268
                    continue;
269
                }
270
271
                /**
272
                 * ensure that the setting_visibility is respected
273
                 */
274
                $visibility = $attribute["visible"];
275
                if (
276
                    ($setting_visibility ===  "visible" && $visibility === false) ||
277
                    ($setting_visibility ===  "hidden" && $visibility === true)
278
                ) {
279
                    continue;
280
                }
281
282
                /**
283
                 * ensure that the variation is respected
284
                 */
285
                $variation = $attribute["variation"];
286
                if (
287
                    ($setting_variation ===  "used" && $variation === false) ||
288
                    ($setting_variation ===  "notused" && $variation === true)
289
                ) {
290
                    continue;
291
                }
292
293
                /**
294
                 * ensure that taxonomy is whitelisted
295
                 */
296
                $id = $attribute->get_id();
297
                if (!in_array($id, $setting_ids)) {
298
                    continue;
299
                }
300
301
                $name = $attribute->get_name();
302
                if ($attribute->is_taxonomy()) {
303
                    $terms = wp_get_post_terms($product->get_id(), $name, 'all');
0 ignored issues
show
Bug introduced by
'all' of type string is incompatible with the type array expected by parameter $args of wp_get_post_terms(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

303
                    $terms = wp_get_post_terms($product->get_id(), $name, /** @scrutinizer ignore-type */ 'all');
Loading history...
304
                    $tax_terms = array();
305
306
                    /**
307
                     * interpolate all values when specified to interpolate
308
                     */
309
                    if (in_array($id, $setting_ids_interpolate)) {
310
                        $integers = array();
311
                        foreach ($terms as $term) {
312
                            array_push($integers, (int) $term->name);
313
                        }
314
                        if (count($integers) > 0) {
315
                            for ($i = min($integers); $i <= max($integers); $i++) {
316
                                array_push($tax_terms, $i);
317
                            }
318
                        }
319
                    }
320
321
                    /**
322
                     * normal mixed content case 
323
                     */
324
                    if (!in_array($id, $setting_ids_interpolate)) {
325
                        foreach ($terms as $term) {
326
                            $single_term = esc_html($term->name);
327
                            array_push($tax_terms, $single_term);
328
                        }
329
                    }
330
                }
331
                $attributes[$name] = $tax_terms;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tax_terms does not seem to be defined for all execution paths leading up to this point.
Loading history...
332
            }
333
            return $attributes;
334
        }
335
336
        /**
337
         * Send WooCommerce products to Algolia
338
         *
339
         * @param Int $id Product to send to Algolia if we send only a single product
340
         * @return void
341
         */
342
        public static function send_products_to_algolia($id = '')
343
        {
344
            /**
345
             * Remove classes from plugin URL and autoload Algolia with Composer
346
             */
347
348
            $base_plugin_directory = str_replace('classes', '', dirname(__FILE__));
349
            require_once $base_plugin_directory . '/vendor/autoload.php';
350
351
            /**
352
             * Fetch the required variables from the Settings API
353
             */
354
355
            $algolia_application_id = get_option(ALGOWOO_DB_OPTION . ALGOLIA_APP_ID);
356
            $algolia_application_id = is_string($algolia_application_id) ? $algolia_application_id : CHANGE_ME;
357
358
            $algolia_api_key        = get_option(ALGOWOO_DB_OPTION . ALGOLIA_API_KEY);
359
            $algolia_api_key        = is_string($algolia_api_key) ? $algolia_api_key : CHANGE_ME;
360
361
            $algolia_index_name     = get_option(ALGOWOO_DB_OPTION . INDEX_NAME);
362
            $algolia_index_name        = is_string($algolia_index_name) ? $algolia_index_name : CHANGE_ME;
363
364
            /**
365
             * Display admin notice and return if not all values have been set
366
             */
367
368
            Algolia_Check_Requirements::check_algolia_input_values($algolia_application_id, $algolia_api_key, $algolia_index_name);
369
370
            /**
371
             * Initiate the Algolia client
372
             */
373
            self::$algolia = \Algolia\AlgoliaSearch\SearchClient::create($algolia_application_id, $algolia_api_key);
374
375
            /**
376
             * Check if we can connect, if not, handle the exception, display an error and then return
377
             */
378
            self::can_connect_to_algolia();
379
380
            /**
381
             * Initialize the search index and set the name to the option from the database
382
             */
383
            $index = self::$algolia->initIndex($algolia_index_name);
384
385
            /**
386
             * Setup arguments for sending all products to Algolia
387
             *
388
             * Limit => -1 means we send all products
389
             */
390
            $arguments = array(
391
                'status'   => 'publish',
392
                'limit'    => -1,
393
                'paginate' => false,
394
            );
395
396
            /**
397
             * Setup arguments for sending only a single product
398
             */
399
            if (isset($id) && '' !== $id) {
400
                $arguments = array(
401
                    'status'   => 'publish',
402
                    'include'  => array($id),
403
                    'paginate' => false,
404
                );
405
            }
406
407
            /**
408
             * Fetch all products from WooCommerce
409
             *
410
             * @see https://docs.woocommerce.com/wc-apidocs/function-wc_get_products.html
411
             */
412
            $products =
413
                /** @scrutinizer ignore-call */
414
                wc_get_products($arguments);
415
416
            if (empty($products)) {
417
                return;
418
            }
419
            $records = array();
420
            $record  = array();
421
422
            foreach ($products as $product) {
423
                /**
424
                 * Set sale price or regular price based on product type
425
                 */
426
                $product_type_price = self::get_product_type_price($product);
427
                $sale_price = $product_type_price['sale_price'];
428
                $regular_price = $product_type_price['regular_price'];
429
430
431
432
433
                /**
434
                 * always add objectID (mandatory field for algolia)
435
                 */
436
                $record['objectID'] = $product->get_id();
437
438
                /**
439
                 * Extract image from $product->get_image()
440
                 */
441
                if (self::is_basic_field_enabled("product_image")) {
442
                    preg_match('/<img(.*)src(.*)=(.*)"(.*)"/U', $product->get_image(), $result);
443
                    $record["product_image"] = array_pop($result);
444
                }
445
446
                $record = self::add_to_record($record, 'product_name', $product->get_name());
447
                $record = self::add_to_record($record, 'short_description', $product->get_short_description());
448
                $record = self::add_to_record($record, 'long_description', $product->get_description());
449
                $record = self::add_to_record($record, 'excerpt', get_the_excerpt($product->get_id()));
450
                $record = self::add_to_record($record, 'regular_price', $regular_price);
451
                $record = self::add_to_record($record, 'sale_price', $sale_price);
452
                $record = self::add_to_record($record, 'on_sale', $product->is_on_sale());
453
                $record = self::add_to_record($record, 'permalink', $product->get_permalink());
454
                $record = self::add_to_record($record, 'categories', self::get_product_categories($product));
455
                $record = self::add_to_record($record, 'tags', self::get_product_tags($product));
456
                $record = self::add_to_record($record, 'attributes', self::get_product_attributes($product), true);
457
458
459
460
                /**
461
                 * Add stock information if stock management is on
462
                 */
463
                $stock_data = self::get_product_stock_data($product);
464
                if (is_array($stock_data)) {
465
                    $record = self::add_to_record($record, 'stock_quantity', $stock_data['stock_quantity']);
466
                    $record = self::add_to_record($record, 'stock_status', $stock_data['stock_status']);
467
                }
468
469
                $records[] = $record;
470
            }
471
472
            wp_reset_postdata();
473
474
            /**
475
             * Send the information to Algolia and save the result
476
             * If result is NullResponse, print an error message
477
             */
478
            $result = $index->saveObjects($records);
479
480
            if ('Algolia\AlgoliaSearch\Response\NullResponse' === get_class($result)) {
481
                wp_die(esc_html__('No response from the server. Please check your settings and try again', 'algolia_woo_indexer_settings'));
482
            }
483
484
            /**
485
             * Display success message
486
             */
487
            echo '<div class="notice notice-success is-dismissible">
488
					 	<p>' . esc_html__('Product(s) sent to Algolia.', 'algolia-woo-indexer') . '</p>
489
				  		</div>';
490
        }
491
    }
492
}
493