Completed
Push — master ( 2676d9...a4ebee )
by Nazar
04:30
created

Items   C

Complexity

Total Complexity 79

Size/Duplication

Total Lines 561
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11
Metric Value
wmc 79
lcom 1
cbo 11
dl 0
loc 561
rs 5.1632

16 Methods

Rating   Name   Duplication   Size   Complexity  
A construct() 0 3 1
A cdb() 0 3 1
B get() 0 35 6
C read_attributes_processing() 0 40 8
A read_tags_processing() 0 3 2
B get_for_user() 0 24 6
A get_all() 0 11 2
C search() 0 77 15
C attribute_type_to_value_field() 0 22 10
B add() 0 27 4
C prepare_attributes() 0 43 7
A prepare_images() 0 8 1
C prepare_videos() 0 23 7
A prepare_tags() 0 3 2
B set() 0 39 5
A del() 0 17 2

How to fix   Complexity   

Complex Class

Complex classes like Items 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Items, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package   Shop
4
 * @category  modules
5
 * @author    Nazar Mokrynskyi <[email protected]>
6
 * @copyright Copyright (c) 2014-2016, Nazar Mokrynskyi
7
 * @license   MIT License, see license.txt
8
 */
9
namespace cs\modules\Shop;
10
use
11
	cs\Cache\Prefix,
12
	cs\Config,
13
	cs\Event,
14
	cs\Language,
15
	cs\User,
16
	cs\CRUD_helpers,
17
	cs\Singleton;
18
19
/**
20
 * Provides next events:<br>
21
 *  Shop/Items/get<code>
22
 *  [
23
 *   'data' => &$data
24
 *  ]</code>
25
 *
26
 *  Shop/Items/get_for_user<code>
27
 *  [
28
 *   'data' => &$data,
29
 *   'user' => $user
30
 *  ]</code>
31
 *
32
 *  Shop/Items/add<code>
33
 *  [
34
 *   'id' => $id
35
 *  ]</code>
36
 *
37
 *  Shop/Items/set<code>
38
 *  [
39
 *   'id' => $id
40
 *  ]</code>
41
 *
42
 *  Shop/Items/del<code>
43
 *  [
44
 *   'id' => $id
45
 *  ]</code>
46
 */
47
class Items {
48
	use CRUD_helpers {
49
		search as crud_search;
50
	}
51
	use
52
		Singleton;
53
54
	const DEFAULT_IMAGE = 'components/modules/Shop/includes/img/no-image.svg';
55
56
	protected $data_model                  = [
57
		'id'         => 'int',
58
		'date'       => 'int',
59
		'category'   => 'int',
60
		'price'      => 'float',
61
		'in_stock'   => 'int',
62
		'soon'       => 'int:0..1',
63
		'listed'     => 'int:0..1',
64
		'attributes' => [
65
			'data_model' => [
66
				'id'            => 'int',
67
				'attribute'     => 'int',
68
				'numeric_value' => 'float',
69
				'string_value'  => 'text',
70
				'text_value'    => 'html',
71
				'lang'          => 'text' // Some attributes are language-dependent, some aren't, so we'll handle that manually
72
			]
73
		],
74
		'images'     => [
75
			'data_model' => [
76
				'id'    => 'int',
77
				'image' => 'text'
78
			]
79
		],
80
		'videos'     => [
81
			'data_model' => [
82
				'id'     => 'int',
83
				'video'  => 'text',
84
				'poster' => 'text',
85
				'type'   => 'text'
86
			]
87
		],
88
		'tags'       => [
89
			'data_model'     => [
90
				'id'  => 'int',
91
				'tag' => 'html'
92
			],
93
			'language_field' => 'lang'
94
		]
95
	];
96
	protected $table                       = '[prefix]shop_items';
97
	protected $data_model_files_tag_prefix = 'Shop/items';
98
	/**
99
	 * @var Prefix
100
	 */
101
	protected $cache;
102
103
	protected function construct () {
104
		$this->cache = new Prefix('Shop/items');
105
	}
106
	/**
107
	 * Returns database index
108
	 *
109
	 * @return int
110
	 */
111
	protected function cdb () {
112
		return Config::instance()->module('Shop')->db('shop');
113
	}
114
	/**
115
	 * Get item
116
	 *
117
	 * @param int|int[] $id
118
	 *
119
	 * @return array|false
120
	 */
121
	function get ($id) {
122
		if (is_array($id)) {
123
			foreach ($id as &$i) {
124
				$i = $this->get($i);
125
			}
126
			return $id;
127
		}
128
		$L    = Language::instance();
129
		$id   = (int)$id;
130
		$data = $this->cache->get(
131
			"$id/$L->clang",
132
			function () use ($id, $L) {
133
				$data = $this->read($id);
134
				if (!$data) {
135
					return false;
136
				}
137
				$data['attributes']  = $this->read_attributes_processing($data['attributes'], $L->clang);
138
				$category            = Categories::instance()->get($data['category']);
139
				$data['title']       = $data['attributes'][$category['title_attribute']];
140
				$data['description'] = @$data['attributes'][$category['description_attribute']] ?: '';
141
				$data['tags']        = $this->read_tags_processing($data['tags']);
142
				return $data;
143
			}
144
		);
145
		if (!Event::instance()->fire(
146
			'Shop/Items/get',
147
			[
148
				'data' => &$data
149
			]
150
		)
151
		) {
152
			return false;
153
		}
154
		return $data;
155
	}
156
	/**
157
	 * Transform normalized attributes structure back into simple initial structure
158
	 *
159
	 * @param array  $attributes
160
	 * @param string $clang
161
	 *
162
	 * @return array
163
	 */
164
	protected function read_attributes_processing ($attributes, $clang) {
165
		/**
166
		 * Select language-independent attributes and ones that are set for current language
167
		 */
168
		$filtered_attributes = array_filter(
169
			$attributes,
170
			function ($attribute) use ($clang) {
171
				return !$attribute['lang'] || $attribute['lang'] == $clang;
172
			}
173
		);
174
		$existing_attributes = array_column($filtered_attributes, 'attribute');
175
		/**
176
		 * Now fill other existing attributes that are missing for current language
177
		 */
178
		foreach ($attributes as $attribute) {
179
			if (!in_array($attribute['attribute'], $existing_attributes)) {
180
				$existing_attributes[] = $attribute['attribute'];
181
				$filtered_attributes[] = $attribute;
182
			}
183
		}
184
		/**
185
		 * We have attributes of different types, so, here is normalization for that
186
		 */
187
		$Attributes = Attributes::instance();
188
		foreach ($filtered_attributes as &$value) {
189
			$attribute = $Attributes->get($value['attribute']);
190
			if ($attribute) {
191
				$value['value'] = $value[$this->attribute_type_to_value_field($attribute['type'])];
192
			} else {
193
				$value['value'] = $value['text_value'];
194
				if (!strlen($value['value'])) {
195
					$value['value'] = $value['string_value'];
196
				}
197
				if (!strlen($value['value'])) {
198
					$value['value'] = $value['numeric_value'];
199
				}
200
			}
201
		}
202
		return array_column($filtered_attributes, 'value', 'attribute');
203
	}
204
	/**
205
	 * Transform tags ids back into array of strings
206
	 *
207
	 * @param int[] $tags
208
	 *
209
	 * @return string[]
210
	 */
211
	protected function read_tags_processing ($tags) {
212
		return array_column(Tags::instance()->get($tags) ?: [], 'text');
213
	}
214
	/**
215
	 * Get item data for specific user (price might be adjusted, some items may be restricted and so on)
216
	 *
217
	 * @param int|int[] $id
218
	 * @param bool|int  $user
219
	 *
220
	 * @return array|false
221
	 */
222
	function get_for_user ($id, $user = false) {
223
		if (is_array($id)) {
224
			foreach ($id as $index => &$i) {
225
				$i = $this->get_for_user($i, $user);
226
				if ($i === false) {
227
					unset($id[$index]);
228
				}
229
			}
230
			return $id;
231
		}
232
		$user = (int)$user ?: User::instance()->id;
233
		$data = $this->get($id);
234
		if (!Event::instance()->fire(
235
			'Shop/Items/get_for_user',
236
			[
237
				'data' => &$data,
238
				'user' => $user
239
			]
240
		)
241
		) {
242
			return false;
243
		}
244
		return $data;
245
	}
246
	/**
247
	 * Get array of all items
248
	 *
249
	 * @return int[] Array of items ids
250
	 */
251
	function get_all () {
252
		return $this->cache->get(
253
			'all',
254
			function () {
255
				return $this->db()->qfas(
256
					"SELECT `id`
257
				FROM `$this->table`"
258
				) ?: [];
259
			}
260
		);
261
	}
262
	/**
263
	 * Items search
264
	 *
265
	 * @param mixed[] $search_parameters Array in form [attribute => value], [attribute => [value, value]], [attribute => [from => value, to => value]],
266
	 *                                   [property => value], [tag] or mixed; if `total_count => 1` element is present - total number of found rows will be
267
	 *                                   returned instead of rows themselves
268
	 * @param int     $page
269
	 * @param int     $count
270
	 * @param string  $order_by
271
	 * @param bool    $asc
272
	 *
273
	 * @return array|false|string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|array[]|integer|integer[]|string|string[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
274
	 */
275
	function search ($search_parameters = [], $page = 1, $count = 20, $order_by = 'id', $asc = false) {
276
		if (!isset($this->data_model[$order_by])) {
277
			return false;
278
		}
279
		$Attributes   = Attributes::instance();
280
		$L            = Language::instance();
281
		$joins        = '';
282
		$join_params  = [];
283
		$join_index   = 0;
284
		$where        = [];
285
		$where_params = [];
286
		foreach ($search_parameters as $key => $details) {
287
			if (isset($this->data_model[$key])) { // Property
288
				$where[]        = "`i`.`$key` = '%s'";
289
				$where_params[] = $details;
290
			} elseif (is_numeric($key)) { // Tag
291
				$joins .=
292
					"INNER JOIN `{$this->table}_tags` AS `t`
293
					ON
294
						`i`.`id`	= `t`.`id` AND
295
						`t`.`tag`	= '%s'";
296
				$where_params[] = $details;
297
			} else { // Attribute
298
				$field = @$this->attribute_type_to_value_field($Attributes->get($key)['type']);
299
				if (!$field || empty($details)) {
300
					continue;
301
				}
302
				$join_params[] = $key;
303
				++$join_index;
304
				$joins .=
305
					"INNER JOIN `{$this->table}_attributes` AS `a$join_index`
306
					ON
307
						`i`.`id`					= `a$join_index`.`id` AND
308
						`a$join_index`.`attribute`	= '%s' AND
309
						(
310
							`a$join_index`.`lang`	= '$L->clang' OR
311
							`a$join_index`.`lang`	= ''
312
						)";
313
				if (is_array($details)) {
314
					if (isset($details['from']) || isset($details['to'])) {
315
						/** @noinspection NotOptimalIfConditionsInspection */
316
						if (isset($details['from'])) {
317
							$joins .= "AND `a$join_index`.`$field`	>= '%s'";
318
							$join_params[] = $details['from'];
319
						}
320
						/** @noinspection NotOptimalIfConditionsInspection */
321
						if (isset($details['to'])) {
322
							$joins .= "AND `a$join_index`.`$field`	<= '%s'";
323
							$join_params[] = $details['to'];
324
						}
325
					} else {
326
						$on = [];
327
						foreach ($details as $d) {
328
							$on[]          = "`a$join_index`.`$field` = '%s'";
329
							$join_params[] = $d;
330
						}
331
						$on = implode(' OR ', $on);
332
						$joins .= "AND ($on)";
333
						unset($on, $d);
334
					}
335
				} else {
336
					switch ($field) {
337
						case 'numeric_value':
338
							$joins .= "AND `a$join_index`.`$field` = '%s'";
339
							break;
340
						case 'string_value':
341
							$joins .= "AND `a$join_index`.`$field` LIKE '%s%%'";
342
							break;
343
						default:
344
							$joins .= "AND MATCH (`a$join_index`.`$field`) AGAINST ('%s' IN BOOLEAN MODE) > 0";
345
					}
346
					$join_params[] = $details;
347
				}
348
			}
349
		}
350
		return $this->search_do('i', @$search_parameters['total_count'], $where, $where_params, $joins, $join_params, $page, $count, $order_by, $asc);
351
	}
352
	/**
353
	 * @param int $type
354
	 *
355
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
356
	 */
357
	protected function attribute_type_to_value_field ($type) {
358
		switch ($type) {
359
			/**
360
			 * For numeric values and value sets (each value have its own index in set and does not depend on language) value is stored in numeric
361
			 * column for faster search
362
			 */
363
			case Attributes::TYPE_INT_SET:
364
			case Attributes::TYPE_INT_RANGE:
365
			case Attributes::TYPE_FLOAT_SET:
366
			case Attributes::TYPE_FLOAT_RANGE:
367
			case Attributes::TYPE_SWITCH:
368
			case Attributes::TYPE_STRING_SET:
369
			case Attributes::TYPE_COLOR_SET:
370
				return 'numeric_value';
371
			case Attributes::TYPE_STRING:
372
				return 'string_value';
373
			case Attributes::TYPE_TEXT:
374
				return 'text_value';
375
			default:
376
				return false;
377
		}
378
	}
379
	/**
380
	 * Add new item
381
	 *
382
	 * @param int      $category
383
	 * @param float    $price
384
	 * @param int      $in_stock
385
	 * @param int      $soon
386
	 * @param int      $listed
387
	 * @param array    $attributes
388
	 * @param string[] $images
389
	 * @param array[]  $videos
390
	 * @param string[] $tags
391
	 *
392
	 * @return false|int Id of created item on success of <b>false</> on failure
1 ignored issue
show
Documentation introduced by
Should the return type not be integer|string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
393
	 */
394
	function add ($category, $price, $in_stock, $soon, $listed, $attributes, $images, $videos, $tags) {
395
		$L  = Language::instance();
396
		$id = $this->create(
397
			[
398
				time(),
399
				$category,
400
				$price,
401
				$in_stock,
402
				$soon && !$in_stock ? 1 : 0,
403
				$listed,
404
				$this->prepare_attributes($attributes, $category, $L->clang),
405
				$this->prepare_images($images),
406
				$this->prepare_videos($videos),
407
				$this->prepare_tags($tags)
408
			]
409
		);
410
		if ($id) {
411
			unset($this->cache->all);
412
			Event::instance()->fire(
413
				'Shop/Items/add',
414
				[
415
					'id' => $id
416
				]
417
			);
418
		}
419
		return $id;
420
	}
421
	/**
422
	 * Normalize attributes array structure
423
	 *
424
	 * @param array  $attributes
425
	 * @param int    $category
426
	 * @param string $clang
427
	 *
428
	 * @return array
429
	 */
430
	protected function prepare_attributes ($attributes, $category, $clang) {
431
		$Attributes      = Attributes::instance();
432
		$title_attribute = Categories::instance()->get($category)['title_attribute'];
433
		foreach ($attributes as $attribute => &$value) {
434
			$attribute_data = $Attributes->get($attribute);
435
			if (!$attribute_data) {
436
				unset($attributes[$attribute]);
437
				continue;
438
			}
439
			$value_type = [
440
				'numeric' => 0,
441
				'string'  => '',
442
				'text'    => ''
443
			];
444
			$lang       = '';
445
			switch ($this->attribute_type_to_value_field($attribute_data['type'])) {
446
				case 'numeric_value':
447
					$value_type['numeric'] = $value;
448
					break;
449
				case 'string_value':
450
					$value_type['string'] = xap($value);
451
					/**
452
					 * Multilingual feature only for title attribute
453
					 */
454
					if ($attribute_data['id'] == $title_attribute) {
455
						$lang = $clang;
456
					}
457
					break;
458
				case 'text_value':
459
					$value_type['text'] = xap($value, true, true);
460
					$lang               = $clang;
461
					break;
462
			}
463
			$value = [
464
				$attribute_data['id'],
465
				$value_type['numeric'],
466
				$value_type['string'],
467
				$value_type['text'],
468
				$lang
469
			];
470
		}
471
		return array_values($attributes);
472
	}
473
	/**
474
	 * Filter images to remove non-URL elements
475
	 *
476
	 * @param array $images
477
	 *
478
	 * @return array
479
	 */
480
	protected function prepare_images ($images) {
481
		return array_filter(
482
			$images,
483
			function ($image) {
484
				return filter_var($image, FILTER_VALIDATE_URL);
485
			}
486
		);
487
	}
488
	/**
489
	 * Normalize videos array structure
490
	 *
491
	 * @param array[] $videos
492
	 *
493
	 * @return array[]
494
	 */
495
	protected function prepare_videos ($videos) {
496
		if (!$videos || !is_array($videos)) {
497
			return [];
498
		}
499
		$videos = array_flip_3d($videos);
500
		foreach ($videos as $i => &$video) {
501
			if (!@$video['video']) {
502
				unset($videos[$i]);
503
			}
504
			if (
505
				$video['type'] == 'iframe' &&
506
				preg_match('#(http[s]?:)?//[^\s"\'>]+#ims', $video['video'], $match)
507
			) {
508
				$video['video'] = $match[0];
509
			}
510
			$video = [
511
				$video['video'],
512
				$video['poster'],
513
				$video['type']
514
			];
515
		}
516
		return $videos;
517
	}
518
	/**
519
	 * Transform array of string tags into array of their ids
520
	 *
521
	 * @param string[] $tags
522
	 *
523
	 * @return int[]
524
	 */
525
	protected function prepare_tags ($tags) {
526
		return Tags::instance()->add($tags) ?: [];
527
	}
528
	/**
529
	 * Set data of specified item
530
	 *
531
	 * @param int      $id
532
	 * @param int      $category
533
	 * @param float    $price
534
	 * @param int      $in_stock
535
	 * @param int      $soon
536
	 * @param int      $listed
537
	 * @param array    $attributes
538
	 * @param string[] $images
539
	 * @param array[]  $videos
540
	 * @param string[] $tags
541
	 *
542
	 * @return bool
543
	 */
544
	function set ($id, $category, $price, $in_stock, $soon, $listed, $attributes, $images, $videos, $tags) {
545
		$id   = (int)$id;
546
		$data = $this->get($id);
547
		if (!$data) {
548
			return false;
549
		}
550
		$L      = Language::instance();
551
		$result = $this->update(
552
			[
553
				$id,
554
				$data['date'],
555
				$category,
556
				$price,
557
				$in_stock,
558
				$soon && !$in_stock ? 1 : 0,
559
				$listed,
560
				$this->prepare_attributes($attributes, $category, $L->clang),
561
				$this->prepare_images($images),
562
				$this->prepare_videos($videos),
563
				$this->prepare_tags($tags)
564
			]
565
		);
566
		if ($result) {
567
			/**
568
			 * Attributes processing
569
			 */
570
			unset(
571
				$this->cache->{"$id/$L->clang"},
572
				$this->cache->all
573
			);
574
			Event::instance()->fire(
575
				'Shop/Items/set',
576
				[
577
					'id' => $id
578
				]
579
			);
580
		}
581
		return $result;
582
	}
583
	/**
584
	 * Delete specified item
585
	 *
586
	 * @param int $id
587
	 *
588
	 * @return bool
589
	 */
590
	function del ($id) {
591
		$id     = (int)$id;
592
		$result = $this->delete($id);
593
		if ($result) {
594
			unset(
595
				$this->cache->$id,
596
				$this->cache->all
597
			);
598
			Event::instance()->fire(
599
				'Shop/Items/del',
600
				[
601
					'id' => $id
602
				]
603
			);
604
		}
605
		return $result;
606
	}
607
}
608