Passed
Push — master ( a4754c...e0c6ec )
by Nazar
05:32
created

Items::set()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 3
nop 10
dl 0
loc 36
rs 8.439
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
 * @copyright Copyright (c) 2014-2017, 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
 * @method static $this instance($check = false)
48
 */
49
class Items {
50
	use CRUD_helpers {
51
		search as crud_search;
52
	}
53
	use
54
		Singleton;
55
56
	const DEFAULT_IMAGE = 'modules/Shop/assets/img/no-image.svg';
57
58
	protected $data_model                  = [
59
		'id'         => 'int:0',
60
		'date'       => 'int:0',
61
		'category'   => 'int:0',
62
		'price'      => 'float',
63
		'in_stock'   => 'int:0',
64
		'soon'       => 'int:0..1',
65
		'listed'     => 'int:0..1',
66
		'attributes' => [
67
			'data_model' => [
68
				'id'            => 'int:0',
69
				'attribute'     => 'int:0',
70
				'numeric_value' => 'float',
71
				'string_value'  => 'text',
72
				'text_value'    => 'html',
73
				'lang'          => 'text' // Some attributes are language-dependent, some aren't, so we'll handle that manually
74
			]
75
		],
76
		'images'     => [
77
			'data_model' => [
78
				'id'    => 'int:0',
79
				'image' => 'text'
80
			]
81
		],
82
		'videos'     => [
83
			'data_model' => [
84
				'id'     => 'int:0',
85
				'video'  => 'text',
86
				'poster' => 'text',
87
				'type'   => 'text'
88
			]
89
		],
90
		'tags'       => [
91
			'data_model'     => [
92
				'id'  => 'int:0',
93
				'tag' => 'int:0'
94
			],
95
			'language_field' => 'lang'
96
		]
97
	];
98
	protected $table                       = '[prefix]shop_items';
99
	protected $data_model_files_tag_prefix = 'Shop/items';
100
	/**
101
	 * @var Prefix
102
	 */
103
	protected $cache;
104
105
	protected function construct () {
106
		$this->cache = new Prefix('Shop/items');
107
	}
108
	/**
109
	 * Returns database index
110
	 *
111
	 * @return int
112
	 */
113
	protected function cdb () {
114
		return Config::instance()->module('Shop')->db('shop');
115
	}
116
	/**
117
	 * Get item
118
	 *
119
	 * @param int|int[] $id
120
	 *
121
	 * @return array|false
122
	 */
123
	public function get ($id) {
124
		if (is_array($id)) {
125
			return array_map([$this, 'get'], $id);
126
		}
127
		$L    = Language::instance();
128
		$id   = (int)$id;
129
		$data = $this->cache->get(
130
			"$id/$L->clang",
131
			function () use ($id, $L) {
132
				$data = $this->read($id);
133
				if (!$data) {
0 ignored issues
show
introduced by
The condition ! $data can never be false.
Loading history...
134
					return false;
135
				}
136
				$data['attributes']  = $this->read_attributes_processing($data['attributes'], $L->clang);
137
				$category            = Categories::instance()->get($data['category']);
138
				$data['title']       = $data['attributes'][$category['title_attribute']];
139
				$data['description'] = $data['attributes'][$category['description_attribute']] ?? '';
140
				$data['tags']        = $this->read_tags_processing($data['tags']);
141
				return $data;
142
			}
143
		);
144
		if (!Event::instance()->fire(
145
			'Shop/Items/get',
146
			[
147
				'data' => &$data
148
			]
149
		)
150
		) {
151
			return false;
152
		}
153
		return $data;
154
	}
155
	/**
156
	 * Transform normalized attributes structure back into simple initial structure
157
	 *
158
	 * @param array  $attributes
159
	 * @param string $clang
160
	 *
161
	 * @return array
162
	 */
163
	protected function read_attributes_processing ($attributes, $clang) {
164
		/**
165
		 * Select language-independent attributes and ones that are set for current language
166
		 */
167
		$filtered_attributes = array_filter(
168
			$attributes,
169
			function ($attribute) use ($clang) {
170
				return !$attribute['lang'] || $attribute['lang'] == $clang;
171
			}
172
		);
173
		$existing_attributes = array_column($filtered_attributes, 'attribute');
174
		/**
175
		 * Now fill other existing attributes that are missing for current language
176
		 */
177
		foreach ($attributes as $attribute) {
178
			if (!in_array($attribute['attribute'], $existing_attributes)) {
179
				$existing_attributes[] = $attribute['attribute'];
180
				$filtered_attributes[] = $attribute;
181
			}
182
		}
183
		/**
184
		 * We have attributes of different types, so, here is normalization for that
185
		 */
186
		$Attributes = Attributes::instance();
187
		foreach ($filtered_attributes as &$value) {
188
			$attribute = $Attributes->get($value['attribute']);
189
			if ($attribute) {
0 ignored issues
show
introduced by
The condition $attribute can never be true.
Loading history...
190
				$value['value'] = $value[$this->attribute_type_to_value_field($attribute['type'])];
191
			} else {
192
				$value['value'] = $value['text_value'];
193
				if (!strlen($value['value'])) {
194
					$value['value'] = $value['string_value'];
195
				}
196
				if (!strlen($value['value'])) {
197
					$value['value'] = $value['numeric_value'];
198
				}
199
			}
200
		}
201
		return array_column($filtered_attributes, 'value', 'attribute');
202
	}
203
	/**
204
	 * Transform tags ids back into array of strings
205
	 *
206
	 * @param int[] $tags
207
	 *
208
	 * @return string[]
209
	 */
210
	protected function read_tags_processing ($tags) {
211
		return array_column(Tags::instance()->get($tags) ?: [], 'text');
212
	}
213
	/**
214
	 * Get item data for specific user (price might be adjusted, some items may be restricted and so on)
215
	 *
216
	 * @param int|int[] $id
217
	 * @param bool|int  $user
218
	 *
219
	 * @return array|false
220
	 */
221
	public function get_for_user ($id, $user = false) {
222
		if (is_array($id)) {
223
			foreach ($id as $index => &$i) {
224
				$i = $this->get_for_user($i, $user);
225
				if ($i === false) {
0 ignored issues
show
introduced by
The condition $i === false can never be false.
Loading history...
226
					unset($id[$index]);
227
				}
228
			}
229
			return $id;
230
		}
231
		$user = (int)$user ?: User::instance()->id;
232
		$data = $this->get($id);
233
		if (!Event::instance()->fire(
234
			'Shop/Items/get_for_user',
235
			[
236
				'data' => &$data,
237
				'user' => $user
238
			]
239
		)
240
		) {
241
			return false;
242
		}
243
		return $data;
244
	}
245
	/**
246
	 * Get array of all items
247
	 *
248
	 * @return int[] Array of items ids
249
	 */
250
	public function get_all () {
251
		return $this->cache->get(
252
			'all',
253
			function () {
254
				return $this->crud_search([], 1, PHP_INT_MAX, 'id', true) ?: [];
255
			}
256
		);
257
	}
258
	/**
259
	 * Items search
260
	 *
261
	 * @param mixed[] $search_parameters Array in form [attribute => value], [attribute => [value, value]], [attribute => [from => value, to => value]],
262
	 *                                   [property => value], [tag] or mixed; if `total_count => 1` element is present - total number of found rows will be
263
	 *                                   returned instead of rows themselves
264
	 * @param int     $page
265
	 * @param int     $count
266
	 * @param string  $order_by
267
	 * @param bool    $asc
268
	 *
269
	 * @return array|false|int
270
	 */
271
	public function search ($search_parameters = [], $page = 1, $count = 20, $order_by = 'id', $asc = false) {
272
		if (!isset($this->data_model[$order_by])) {
273
			return false;
274
		}
275
		$Attributes   = Attributes::instance();
276
		$L            = Language::instance();
277
		$joins        = '';
278
		$join_params  = [];
279
		$join_index   = 0;
280
		$where        = [];
281
		$where_params = [];
282
		foreach ($search_parameters as $key => $details) {
283
			if (isset($this->data_model[$key])) { // Property
284
				$where[]        = "`i`.`$key` = ?";
285
				$where_params[] = $details;
286
			} elseif (is_numeric($key)) { // Tag
287
				$joins .=
288
					"INNER JOIN `{$this->table}_tags` AS `t`
289
					ON
290
						`i`.`id`	= `t`.`id` AND
291
						`t`.`tag`	= ?";
292
				$where_params[] = $details;
293
			} else { // Attribute
294
				$field = @$this->attribute_type_to_value_field($Attributes->get($key)['type']);
295
				if (!$field || empty($details)) {
296
					continue;
297
				}
298
				$join_params[] = $key;
299
				++$join_index;
300
				$joins .=
301
					"INNER JOIN `{$this->table}_attributes` AS `a$join_index`
302
					ON
303
						`i`.`id`					= `a$join_index`.`id` AND
304
						`a$join_index`.`attribute`	= ? AND
305
						(
306
							`a$join_index`.`lang`	= '$L->clang' OR
307
							`a$join_index`.`lang`	= ''
308
						)";
309
				if (is_array($details)) {
310
					if (isset($details['from']) || isset($details['to'])) {
311
						/** @noinspection NotOptimalIfConditionsInspection */
312
						if (isset($details['from'])) {
313
							$joins .= "AND `a$join_index`.`$field`	>= ?";
314
							$join_params[] = $details['from'];
315
						}
316
						/** @noinspection NotOptimalIfConditionsInspection */
317
						if (isset($details['to'])) {
318
							$joins .= "AND `a$join_index`.`$field`	<= ?";
319
							$join_params[] = $details['to'];
320
						}
321
					} else {
322
						$on = [];
323
						foreach ($details as $d) {
324
							$on[]          = "`a$join_index`.`$field` = ?";
325
							$join_params[] = $d;
326
						}
327
						$on = implode(' OR ', $on);
328
						$joins .= "AND ($on)";
329
						unset($on, $d);
330
					}
331
				} else {
332
					switch ($field) {
333
						case 'numeric_value':
334
							$joins .= "AND `a$join_index`.`$field` = ?";
335
							$join_params[] = $details;
336
							break;
337
						case 'string_value':
338
							$joins .= "AND `a$join_index`.`$field` LIKE ?";
339
							$join_params[] = $details.'%';
340
							break;
341
						default:
342
							$joins .= "AND MATCH (`a$join_index`.`$field`) AGAINST (? IN BOOLEAN MODE) > 0";
343
							$join_params[] = $details;
344
					}
345
				}
346
			}
347
		}
348
		return $this->search_do('i', @$search_parameters['total_count'], $where, $where_params, $joins, $join_params, $page, $count, $order_by, $asc);
349
	}
350
	/**
351
	 * @param int $type
352
	 *
353
	 * @return false|string
354
	 */
355
	protected function attribute_type_to_value_field ($type) {
356
		switch ($type) {
357
			/**
358
			 * 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
359
			 * column for faster search
360
			 */
361
			case Attributes::TYPE_INT_SET:
362
			case Attributes::TYPE_INT_RANGE:
363
			case Attributes::TYPE_FLOAT_SET:
364
			case Attributes::TYPE_FLOAT_RANGE:
365
			case Attributes::TYPE_SWITCH:
366
			case Attributes::TYPE_STRING_SET:
367
			case Attributes::TYPE_COLOR_SET:
368
				return 'numeric_value';
369
			case Attributes::TYPE_STRING:
370
				return 'string_value';
371
			case Attributes::TYPE_TEXT:
372
				return 'text_value';
373
			default:
374
				return false;
375
		}
376
	}
377
	/**
378
	 * Add new item
379
	 *
380
	 * @param int      $category
381
	 * @param float    $price
382
	 * @param int      $in_stock
383
	 * @param int      $soon
384
	 * @param int      $listed
385
	 * @param array    $attributes
386
	 * @param string[] $images
387
	 * @param array[]  $videos
388
	 * @param string[] $tags
389
	 *
390
	 * @return false|int Id of created item on success of <b>false</> on failure
391
	 */
392
	public function add ($category, $price, $in_stock, $soon, $listed, $attributes, $images, $videos, $tags) {
393
		$L  = Language::instance();
394
		$id = $this->create(
395
			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

395
			/** @scrutinizer ignore-type */ time(),
Loading history...
396
			$category,
397
			$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

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