Items::add()   B
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 2
nop 9
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * @package  Shop
4
 * @category modules
5
 * @author   Nazar Mokrynskyi <[email protected]>
6
 * @license  0BSD
7
 */
8
namespace cs\modules\Shop;
9
use
10
	cs\Cache\Prefix,
11
	cs\Config,
12
	cs\Event,
13
	cs\Language,
14
	cs\User,
15
	cs\CRUD_helpers,
16
	cs\Singleton;
17
18
/**
19
 * Provides next events:<br>
20
 *  Shop/Items/get<code>
21
 *  [
22
 *   'data' => &$data
23
 *  ]</code>
24
 *
25
 *  Shop/Items/get_for_user<code>
26
 *  [
27
 *   'data' => &$data,
28
 *   'user' => $user
29
 *  ]</code>
30
 *
31
 *  Shop/Items/add<code>
32
 *  [
33
 *   'id' => $id
34
 *  ]</code>
35
 *
36
 *  Shop/Items/set<code>
37
 *  [
38
 *   'id' => $id
39
 *  ]</code>
40
 *
41
 *  Shop/Items/del<code>
42
 *  [
43
 *   'id' => $id
44
 *  ]</code>
45
 *
46
 * @method static $this instance($check = false)
47
 */
48
class Items {
49
	use CRUD_helpers {
50
		search as crud_search;
51
	}
52
	use
53
		Singleton;
54
55
	const DEFAULT_IMAGE = 'modules/Shop/assets/img/no-image.svg';
56
57
	protected $data_model                  = [
58
		'id'         => 'int:0',
59
		'date'       => 'int:0',
60
		'category'   => 'int:0',
61
		'price'      => 'float',
62
		'in_stock'   => 'int:0',
63
		'soon'       => 'int:0..1',
64
		'listed'     => 'int:0..1',
65
		'attributes' => [
66
			'data_model' => [
67
				'id'            => 'int:0',
68
				'attribute'     => 'int:0',
69
				'numeric_value' => 'float',
70
				'string_value'  => 'text',
71
				'text_value'    => 'html',
72
				'lang'          => 'text' // Some attributes are language-dependent, some aren't, so we'll handle that manually
73
			]
74
		],
75
		'images'     => [
76
			'data_model' => [
77
				'id'    => 'int:0',
78
				'image' => 'text'
79
			]
80
		],
81
		'videos'     => [
82
			'data_model' => [
83
				'id'     => 'int:0',
84
				'video'  => 'text',
85
				'poster' => 'text',
86
				'type'   => 'text'
87
			]
88
		],
89
		'tags'       => [
90
			'data_model'     => [
91
				'id'  => 'int:0',
92
				'tag' => 'int:0'
93
			],
94
			'language_field' => 'lang'
95
		]
96
	];
97
	protected $table                       = '[prefix]shop_items';
98
	protected $data_model_files_tag_prefix = 'Shop/items';
99
	/**
100
	 * @var Prefix
101
	 */
102
	protected $cache;
103
104
	protected function construct () {
105
		$this->cache = new Prefix('Shop/items');
106
	}
107
	/**
108
	 * Returns database index
109
	 *
110
	 * @return int
111
	 */
112
	protected function cdb () {
113
		return Config::instance()->module('Shop')->db('shop');
114
	}
115
	/**
116
	 * Get item
117
	 *
118
	 * @param int|int[] $id
119
	 *
120
	 * @return array|false
121
	 */
122
	public function get ($id) {
123
		if (is_array($id)) {
124
			return array_map([$this, 'get'], $id);
125
		}
126
		$L    = Language::instance();
127
		$id   = (int)$id;
128
		$data = $this->cache->get(
129
			"$id/$L->clang",
130
			function () use ($id, $L) {
131
				$data = $this->read($id);
132
				if (!$data) {
133
					return false;
134
				}
135
				$data['attributes']  = $this->read_attributes_processing($data['attributes'], $L->clang);
136
				$category            = Categories::instance()->get($data['category']);
137
				$data['title']       = $data['attributes'][$category['title_attribute']];
138
				$data['description'] = $data['attributes'][$category['description_attribute']] ?? '';
139
				$data['tags']        = $this->read_tags_processing($data['tags']);
0 ignored issues
show
Bug introduced by
It seems like $data['tags'] can also be of type string; however, parameter $tags of cs\modules\Shop\Items::read_tags_processing() does only seem to accept integer[], 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

139
				$data['tags']        = $this->read_tags_processing(/** @scrutinizer ignore-type */ $data['tags']);
Loading history...
140
				return $data;
141
			}
142
		);
143
		if (!Event::instance()->fire(
144
			'Shop/Items/get',
145
			[
146
				'data' => &$data
147
			]
148
		)
149
		) {
150
			return false;
151
		}
152
		return $data;
153
	}
154
	/**
155
	 * Transform normalized attributes structure back into simple initial structure
156
	 *
157
	 * @param array  $attributes
158
	 * @param string $clang
159
	 *
160
	 * @return array
161
	 */
162
	protected function read_attributes_processing ($attributes, $clang) {
163
		/**
164
		 * Select language-independent attributes and ones that are set for current language
165
		 */
166
		$filtered_attributes = array_filter(
167
			$attributes,
168
			function ($attribute) use ($clang) {
169
				return !$attribute['lang'] || $attribute['lang'] == $clang;
170
			}
171
		);
172
		$existing_attributes = array_column($filtered_attributes, 'attribute');
173
		/**
174
		 * Now fill other existing attributes that are missing for current language
175
		 */
176
		foreach ($attributes as $attribute) {
177
			if (!in_array($attribute['attribute'], $existing_attributes)) {
178
				$existing_attributes[] = $attribute['attribute'];
179
				$filtered_attributes[] = $attribute;
180
			}
181
		}
182
		/**
183
		 * We have attributes of different types, so, here is normalization for that
184
		 */
185
		$Attributes = Attributes::instance();
186
		foreach ($filtered_attributes as &$value) {
187
			$attribute = $Attributes->get($value['attribute']);
188
			if ($attribute) {
189
				$value['value'] = $value[$this->attribute_type_to_value_field($attribute['type'])];
190
			} else {
191
				$value['value'] = $value['text_value'];
192
				if (!strlen($value['value'])) {
193
					$value['value'] = $value['string_value'];
194
				}
195
				if (!strlen($value['value'])) {
196
					$value['value'] = $value['numeric_value'];
197
				}
198
			}
199
		}
200
		return array_column($filtered_attributes, 'value', 'attribute');
201
	}
202
	/**
203
	 * Transform tags ids back into array of strings
204
	 *
205
	 * @param int[] $tags
206
	 *
207
	 * @return string[]
208
	 */
209
	protected function read_tags_processing ($tags) {
210
		return array_column(Tags::instance()->get($tags) ?: [], 'text');
211
	}
212
	/**
213
	 * Get item data for specific user (price might be adjusted, some items may be restricted and so on)
214
	 *
215
	 * @param int|int[] $id
216
	 * @param bool|int  $user
217
	 *
218
	 * @return array|false
219
	 */
220
	public function get_for_user ($id, $user = false) {
221
		if (is_array($id)) {
222
			foreach ($id as $index => &$i) {
223
				$i = $this->get_for_user($i, $user);
224
				if ($i === false) {
225
					unset($id[$index]);
226
				}
227
			}
228
			return $id;
229
		}
230
		$user = (int)$user ?: User::instance()->id;
231
		$data = $this->get($id);
232
		if (!Event::instance()->fire(
233
			'Shop/Items/get_for_user',
234
			[
235
				'data' => &$data,
236
				'user' => $user
237
			]
238
		)
239
		) {
240
			return false;
241
		}
242
		return $data;
243
	}
244
	/**
245
	 * Get array of all items
246
	 *
247
	 * @return int[] Array of items ids
248
	 */
249
	public function get_all () {
250
		return $this->cache->get(
251
			'all',
252
			function () {
253
				return $this->crud_search([], 1, PHP_INT_MAX, 'id', true) ?: [];
254
			}
255
		);
256
	}
257
	/**
258
	 * Items search
259
	 *
260
	 * @param mixed[] $search_parameters Array in form [attribute => value], [attribute => [value, value]], [attribute => [from => value, to => value]],
261
	 *                                   [property => value], [tag] or mixed; if `total_count => 1` element is present - total number of found rows will be
262
	 *                                   returned instead of rows themselves
263
	 * @param int     $page
264
	 * @param int     $count
265
	 * @param string  $order_by
266
	 * @param bool    $asc
267
	 *
268
	 * @return array|false|int
269
	 */
270
	public function search ($search_parameters = [], $page = 1, $count = 20, $order_by = 'id', $asc = false) {
271
		if (!isset($this->data_model[$order_by])) {
272
			return false;
273
		}
274
		$Attributes   = Attributes::instance();
275
		$L            = Language::instance();
276
		$joins        = '';
277
		$join_params  = [];
278
		$join_index   = 0;
279
		$where        = [];
280
		$where_params = [];
281
		foreach ($search_parameters as $key => $details) {
282
			if (isset($this->data_model[$key])) { // Property
283
				$where[]        = "`i`.`$key` = ?";
284
				$where_params[] = $details;
285
			} elseif (is_numeric($key)) { // Tag
286
				$joins .=
287
					"INNER JOIN `{$this->table}_tags` AS `t`
288
					ON
289
						`i`.`id`	= `t`.`id` AND
290
						`t`.`tag`	= ?";
291
				$where_params[] = $details;
292
			} else { // Attribute
293
				$field = @$this->attribute_type_to_value_field($Attributes->get($key)['type']);
294
				if (!$field || empty($details)) {
295
					continue;
296
				}
297
				$join_params[] = $key;
298
				++$join_index;
299
				$joins .=
300
					"INNER JOIN `{$this->table}_attributes` AS `a$join_index`
301
					ON
302
						`i`.`id`					= `a$join_index`.`id` AND
303
						`a$join_index`.`attribute`	= ? AND
304
						(
305
							`a$join_index`.`lang`	= '$L->clang' OR
306
							`a$join_index`.`lang`	= ''
307
						)";
308
				if (is_array($details)) {
309
					if (isset($details['from']) || isset($details['to'])) {
310
						/** @noinspection NotOptimalIfConditionsInspection */
311
						if (isset($details['from'])) {
312
							$joins .= "AND `a$join_index`.`$field`	>= ?";
313
							$join_params[] = $details['from'];
314
						}
315
						/** @noinspection NotOptimalIfConditionsInspection */
316
						if (isset($details['to'])) {
317
							$joins .= "AND `a$join_index`.`$field`	<= ?";
318
							$join_params[] = $details['to'];
319
						}
320
					} else {
321
						$on = [];
322
						foreach ($details as $d) {
323
							$on[]          = "`a$join_index`.`$field` = ?";
324
							$join_params[] = $d;
325
						}
326
						$on = implode(' OR ', $on);
327
						$joins .= "AND ($on)";
328
						unset($on, $d);
329
					}
330
				} else {
331
					switch ($field) {
332
						case 'numeric_value':
333
							$joins .= "AND `a$join_index`.`$field` = ?";
334
							$join_params[] = $details;
335
							break;
336
						case 'string_value':
337
							$joins .= "AND `a$join_index`.`$field` LIKE ?";
338
							$join_params[] = $details.'%';
339
							break;
340
						default:
341
							$joins .= "AND MATCH (`a$join_index`.`$field`) AGAINST (? IN BOOLEAN MODE) > 0";
342
							$join_params[] = $details;
343
					}
344
				}
345
			}
346
		}
347
		return $this->search_do('i', @$search_parameters['total_count'], $where, $where_params, $joins, $join_params, $page, $count, $order_by, $asc);
348
	}
349
	/**
350
	 * @param int $type
351
	 *
352
	 * @return false|string
353
	 */
354
	protected function attribute_type_to_value_field ($type) {
355
		switch ($type) {
356
			/**
357
			 * 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
358
			 * column for faster search
359
			 */
360
			case Attributes::TYPE_INT_SET:
361
			case Attributes::TYPE_INT_RANGE:
362
			case Attributes::TYPE_FLOAT_SET:
363
			case Attributes::TYPE_FLOAT_RANGE:
364
			case Attributes::TYPE_SWITCH:
365
			case Attributes::TYPE_STRING_SET:
366
			case Attributes::TYPE_COLOR_SET:
367
				return 'numeric_value';
368
			case Attributes::TYPE_STRING:
369
				return 'string_value';
370
			case Attributes::TYPE_TEXT:
371
				return 'text_value';
372
			default:
373
				return false;
374
		}
375
	}
376
	/**
377
	 * Add new item
378
	 *
379
	 * @param int      $category
380
	 * @param float    $price
381
	 * @param int      $in_stock
382
	 * @param int      $soon
383
	 * @param int      $listed
384
	 * @param array    $attributes
385
	 * @param string[] $images
386
	 * @param array[]  $videos
387
	 * @param string[] $tags
388
	 *
389
	 * @return false|int Id of created item on success of <b>false</> on failure
390
	 */
391
	public function add ($category, $price, $in_stock, $soon, $listed, $attributes, $images, $videos, $tags) {
392
		$L  = Language::instance();
393
		$id = $this->create(
394
			time(),
0 ignored issues
show
Bug introduced by
time() of type integer is incompatible with the type array expected by parameter $arguments of cs\modules\Shop\Items::create(). ( Ignorable by Annotation )

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

394
			/** @scrutinizer ignore-type */ time(),
Loading history...
395
			$category,
396
			$price,
0 ignored issues
show
Bug introduced by
$price of type double is incompatible with the type array expected by parameter $arguments of cs\modules\Shop\Items::create(). ( Ignorable by Annotation )

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

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