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

Algolia_Send_Products   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Importance

Changes 21
Bugs 0 Features 8
Metric Value
eloc 152
c 21
b 0
f 8
dl 0
loc 389
rs 8.5599
wmc 48

9 Methods

Rating   Name   Duplication   Size   Complexity  
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
D get_product_attributes() 0 81 20
C send_products_to_algolia() 0 147 11

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
                $visibility = $attribute["visible"];
273
                $variation = $attribute["variation"];
274
                $id = $attribute->get_id();
275
                /**
276
                 * skip variable related attributes,
277
                 * ensure that taxonomy is whitelisted and
278
                 * ensure that the visibility and variation is respected
279
                 */
280
                if (
281
                    $attribute->get_variation() ||
282
                    !in_array($id, $setting_ids) ||
283
                    ($setting_visibility ===  "visible" && $visibility === false) ||
284
                    ($setting_visibility ===  "hidden" && $visibility === true) ||
285
                    ($setting_variation ===  "used" && $variation === false) ||
286
                    ($setting_variation ===  "notused" && $variation === true)
287
                ) {
288
                    continue;
289
                }
290
291
292
                $name = $attribute->get_name();
293
                if ($attribute->is_taxonomy()) {
294
                    $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

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