Passed
Pull Request — master (#169)
by
unknown
01:59
created

Algolia_Send_Products   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Importance

Changes 20
Bugs 0 Features 8
Metric Value
eloc 154
c 20
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_SETTINGS', array(
71
    'enabled' => 'Enable indexing of attributes',
72
    'visibility' => 'Visibility',
73
    'variation' => 'Used for variations',
74
    'list' => 'Valid Attributes',
75
    'interp' => 'Numeric Interpolation'
76
));
77
define('ATTRIBUTES_ENABLED', '_attributes_enabled');
78
define('ATTRIBUTES_VISIBILITY', '_attributes_visibility');
79
define('ATTRIBUTES_VISIBILITY_STATES', array('all', 'visible', 'hidden'));
80
define('ATTRIBUTES_VARIATION', '_attributes_variation');
81
define('ATTRIBUTES_VARIATION_STATES', array('all', 'used', 'notused'));
82
define('ATTRIBUTES_LIST', '_attributes_list');
83
define('ATTRIBUTES_INTERP', '_attributes_interp');
84
85
86
if (!class_exists('Algolia_Send_Products')) {
87
    /**
88
     * Algolia WooIndexer main class
89
     */
90
    // TODO Rename class "Algolia_Send_Products" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.
91
    class Algolia_Send_Products
92
    {
93
        const PLUGIN_NAME      = 'Algolia Woo Indexer';
94
        const PLUGIN_TRANSIENT = 'algowoo-plugin-notice';
95
96
        /**
97
         * The Algolia instance
98
         *
99
         * @var \Algolia\AlgoliaSearch\SearchClient
100
         */
101
        private static $algolia = null;
102
103
        /**
104
         * Check if we can connect to Algolia, if not, handle the exception, display an error and then return
105
         */
106
        public static function can_connect_to_algolia()
107
        {
108
            try {
109
                self::$algolia->listApiKeys();
110
            } catch (\Algolia\AlgoliaSearch\Exceptions\UnreachableException $error) {
111
                add_action(
112
                    'admin_notices',
113
                    function () {
114
                        echo '<div class="error notice">
115
                            <p>' . esc_html__('An error has been encountered. Please check your application ID and API key. ', 'algolia-woo-indexer') . '</p>
116
						</div>';
117
                    }
118
                );
119
                return;
120
            }
121
        }
122
123
        /**
124
         * check if the field is enabled and shall be sent
125
         *
126
         * @param  mixed $field name of field to be checked according to BASIC_FIELDS 
127
         * @return boolean true if enable, false is not enabled
128
         */
129
        public static function is_basic_field_enabled($field)
130
        {
131
            $fieldValue = get_option(ALGOWOO_DB_OPTION . BASIC_FIELD_PREFIX . $field);
132
            return $fieldValue;
133
        }
134
135
        /**
136
         * helper function to add a field to a record while checking their state
137
         *
138
         * @param  array $record existing record where the field and value shall be added to 
139
         * @param  string $field name of field to be checked according to BASIC_FIELDS 
140
         * @param  mixed $value data to be added to the record array named to $field
141
         * @param  boolean $skip_basic_field_validation set to true if it is not a basic field to skip validation 
142
         * @return array $record previous passed $record with added field data
143
         */
144
        public static function add_to_record($record, $field, $value, $skip_basic_field_validation = false)
145
        {
146
            /**
147
             *  only if enabled or validation skipped and not empty
148
             */
149
            if ((!self::is_basic_field_enabled($field) && !$skip_basic_field_validation) || empty($value)) {
150
                return $record;
151
            }
152
153
            $record[$field] = $value;
154
            return $record;
155
        }
156
157
        /**
158
         * Get sale price or regular price based on product type
159
         *
160
         * @param  mixed $product Product to check   
161
         * @return array ['sale_price' => $sale_price,'regular_price' => $regular_price] Array with regular price and sale price
162
         */
163
        public static function get_product_type_price($product)
164
        {
165
            $sale_price = 0;
166
            $regular_price = 0;
167
            if ($product->is_type('simple')) {
168
                $sale_price     =  $product->get_sale_price();
169
                $regular_price  =  $product->get_regular_price();
170
            } elseif ($product->is_type('variable')) {
171
                $sale_price     =  $product->get_variation_sale_price('min', true);
172
                $regular_price  =  $product->get_variation_regular_price('max', true);
173
            }
174
            return array(
175
                'sale_price' => $sale_price,
176
                'regular_price' => $regular_price
177
            );
178
        }
179
180
181
        /**
182
         * Checks if stock management is enabled and if so, returns quantity and status
183
         *
184
         * @param  mixed $product Product to check   
185
         * @return array ['stock_quantity' => $stock_quantity,'stock_status' => $stock_status] Array with quantity and status. if stock management is disabled, false will be returned,
186
         */
187
        public static function get_product_stock_data($product)
188
        {
189
            if ($product->get_manage_stock()) {
190
                return array(
191
                    'stock_quantity' => $product->get_stock_quantity(),
192
                    'stock_status' => $product->get_stock_status()
193
                );
194
            }
195
            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...
196
        }
197
198
        /**
199
         * Get product tags
200
         *
201
         * @param  mixed $product Product to check   
202
         * @return array ['tag1', 'tag2', ...] simple array with associated tags
203
         */
204
        public static function get_product_tags($product)
205
        {
206
            $tags = get_the_terms($product->get_id(), 'product_tag');
207
            $term_array = array();
208
            if (is_array($tags)) {
0 ignored issues
show
introduced by
The condition is_array($tags) is always false.
Loading history...
209
                foreach ($tags as $tag) {
210
                    $name = get_term($tag)->name;
211
                    array_push($term_array, $name);
212
                }
213
            }
214
            return $term_array;
215
        }
216
217
        /**
218
         * Get product categories
219
         *
220
         * @param  mixed $product Product to check   
221
         * @return array ['tag1', 'tag2', ...] simple array with associated categories
222
         */
223
        public static function get_product_categories($product)
224
        {
225
            $categories = get_the_terms($product->get_id(), 'product_cat');
226
            $term_array = array();
227
            foreach ($categories as $category) {
0 ignored issues
show
Bug introduced by
The expression $categories of type false is not traversable.
Loading history...
228
                $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...
229
                $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...
230
                array_push($term_array, array(
231
                    "name" => $name,
232
                    "slug" => $slug
233
                ));
234
            }
235
            return $term_array;
236
        }
237
238
        /**
239
         * Get attributes from product
240
         *
241
         * @param  mixed $product Product to check   
242
         * @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.
243
         */
244
        public static function get_product_attributes($product)
245
        {
246
            /**
247
             * ensure that attrobutes are actually enabled
248
             */
249
            $attributes_enabled = (int) get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_ENABLED);
250
            if ($attributes_enabled !== 1) {
251
                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...
252
            }
253
254
            /**
255
             * gather data and settings
256
             */
257
            $rawAttributes = $product->get_attributes("edit");
258
            $setting_visibility = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_VISIBILITY);
259
            $setting_variation = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_VARIATION);
260
            $setting_ids = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_LIST);
261
            $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

261
            $setting_ids = explode(",", /** @scrutinizer ignore-type */ $setting_ids);
Loading history...
262
            $setting_ids_interp = get_option(ALGOWOO_DB_OPTION . ATTRIBUTES_INTERP);
263
            $setting_ids_interp = explode(",", $setting_ids_interp);
264
265
            if (!$rawAttributes) {
266
                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...
267
            }
268
269
            $attributes = [];
270
            foreach ($rawAttributes as $attribute) {
271
                /**
272
                 * skip variation attributes
273
                 */
274
                if ($attribute->get_variation()) {
275
                    continue;
276
                }
277
278
                /**
279
                 * ensure that the setting_visibility is respected
280
                 */
281
                $visibility = $attribute["visible"];
282
                if (
283
                    ($setting_visibility ===  "visible" && $visibility === false) ||
284
                    ($setting_visibility ===  "hidden" && $visibility === true)
285
                ) {
286
                    continue;
287
                }
288
289
                /**
290
                 * ensure that the variation is respected
291
                 */
292
                $variation = $attribute["variation"];
293
                if (
294
                    ($setting_variation ===  "used" && $variation === false) ||
295
                    ($setting_variation ===  "notused" && $variation === true)
296
                ) {
297
                    continue;
298
                }
299
300
                /**
301
                 * ensure that taxonomy is whitelisted
302
                 */
303
                $id = $attribute->get_id();
304
                if (!in_array($id, $setting_ids)) {
305
                    continue;
306
                }
307
308
                $name = $attribute->get_name();
309
                if ($attribute->is_taxonomy()) {
310
                    $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

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