Completed
Push — master ( c5384d...c1ead9 )
by Nazar
04:21
created

Items::set()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 37
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 37
rs 8.439
cc 5
eloc 26
nc 3
nop 10

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-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
 * @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 = 'components/modules/Shop/includes/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
	function get ($id) {
124
		if (is_array($id)) {
125
			foreach ($id as &$i) {
126
				$i = $this->get($i);
127
			}
128
			return $id;
129
		}
130
		$L    = Language::instance();
131
		$id   = (int)$id;
132
		$data = $this->cache->get(
133
			"$id/$L->clang",
134
			function () use ($id, $L) {
135
				$data = $this->read($id);
136
				if (!$data) {
137
					return false;
138
				}
139
				$data['attributes']  = $this->read_attributes_processing($data['attributes'], $L->clang);
140
				$category            = Categories::instance()->get($data['category']);
141
				$data['title']       = $data['attributes'][$category['title_attribute']];
142
				$data['description'] = @$data['attributes'][$category['description_attribute']] ?: '';
143
				$data['tags']        = $this->read_tags_processing($data['tags']);
144
				return $data;
145
			}
146
		);
147
		if (!Event::instance()->fire(
148
			'Shop/Items/get',
149
			[
150
				'data' => &$data
151
			]
152
		)
153
		) {
154
			return false;
155
		}
156
		return $data;
157
	}
158
	/**
159
	 * Transform normalized attributes structure back into simple initial structure
160
	 *
161
	 * @param array  $attributes
162
	 * @param string $clang
163
	 *
164
	 * @return array
165
	 */
166
	protected function read_attributes_processing ($attributes, $clang) {
167
		/**
168
		 * Select language-independent attributes and ones that are set for current language
169
		 */
170
		$filtered_attributes = array_filter(
171
			$attributes,
172
			function ($attribute) use ($clang) {
173
				return !$attribute['lang'] || $attribute['lang'] == $clang;
174
			}
175
		);
176
		$existing_attributes = array_column($filtered_attributes, 'attribute');
177
		/**
178
		 * Now fill other existing attributes that are missing for current language
179
		 */
180
		foreach ($attributes as $attribute) {
181
			if (!in_array($attribute['attribute'], $existing_attributes)) {
182
				$existing_attributes[] = $attribute['attribute'];
183
				$filtered_attributes[] = $attribute;
184
			}
185
		}
186
		/**
187
		 * We have attributes of different types, so, here is normalization for that
188
		 */
189
		$Attributes = Attributes::instance();
190
		foreach ($filtered_attributes as &$value) {
191
			$attribute = $Attributes->get($value['attribute']);
192
			if ($attribute) {
193
				$value['value'] = $value[$this->attribute_type_to_value_field($attribute['type'])];
194
			} else {
195
				$value['value'] = $value['text_value'];
196
				if (!strlen($value['value'])) {
197
					$value['value'] = $value['string_value'];
198
				}
199
				if (!strlen($value['value'])) {
200
					$value['value'] = $value['numeric_value'];
201
				}
202
			}
203
		}
204
		return array_column($filtered_attributes, 'value', 'attribute');
205
	}
206
	/**
207
	 * Transform tags ids back into array of strings
208
	 *
209
	 * @param int[] $tags
210
	 *
211
	 * @return string[]
212
	 */
213
	protected function read_tags_processing ($tags) {
214
		return array_column(Tags::instance()->get($tags) ?: [], 'text');
215
	}
216
	/**
217
	 * Get item data for specific user (price might be adjusted, some items may be restricted and so on)
218
	 *
219
	 * @param int|int[] $id
220
	 * @param bool|int  $user
221
	 *
222
	 * @return array|false
223
	 */
224
	function get_for_user ($id, $user = false) {
225
		if (is_array($id)) {
226
			foreach ($id as $index => &$i) {
227
				$i = $this->get_for_user($i, $user);
228
				if ($i === false) {
229
					unset($id[$index]);
230
				}
231
			}
232
			return $id;
233
		}
234
		$user = (int)$user ?: User::instance()->id;
235
		$data = $this->get($id);
236
		if (!Event::instance()->fire(
237
			'Shop/Items/get_for_user',
238
			[
239
				'data' => &$data,
240
				'user' => $user
241
			]
242
		)
243
		) {
244
			return false;
245
		}
246
		return $data;
247
	}
248
	/**
249
	 * Get array of all items
250
	 *
251
	 * @return int[] Array of items ids
252
	 */
253
	function get_all () {
254
		return $this->cache->get(
255
			'all',
256
			function () {
257
				return $this->search([], 1, PHP_INT_MAX, 'id', true) ?: [];
258
			}
259
		);
260
	}
261
	/**
262
	 * Items search
263
	 *
264
	 * @param mixed[] $search_parameters Array in form [attribute => value], [attribute => [value, value]], [attribute => [from => value, to => value]],
265
	 *                                   [property => value], [tag] or mixed; if `total_count => 1` element is present - total number of found rows will be
266
	 *                                   returned instead of rows themselves
267
	 * @param int     $page
268
	 * @param int     $count
269
	 * @param string  $order_by
270
	 * @param bool    $asc
271
	 *
272
	 * @return array|false|string
273
	 */
274
	function search ($search_parameters = [], $page = 1, $count = 20, $order_by = 'id', $asc = false) {
275
		if (!isset($this->data_model[$order_by])) {
276
			return false;
277
		}
278
		$Attributes   = Attributes::instance();
279
		$L            = Language::instance();
280
		$joins        = '';
281
		$join_params  = [];
282
		$join_index   = 0;
283
		$where        = [];
284
		$where_params = [];
285
		foreach ($search_parameters as $key => $details) {
286
			if (isset($this->data_model[$key])) { // Property
287
				$where[]        = "`i`.`$key` = '%s'";
288
				$where_params[] = $details;
289
			} elseif (is_numeric($key)) { // Tag
290
				$joins .=
291
					"INNER JOIN `{$this->table}_tags` AS `t`
292
					ON
293
						`i`.`id`	= `t`.`id` AND
294
						`t`.`tag`	= '%s'";
295
				$where_params[] = $details;
296
			} else { // Attribute
297
				$field = @$this->attribute_type_to_value_field($Attributes->get($key)['type']);
298
				if (!$field || empty($details)) {
299
					continue;
300
				}
301
				$join_params[] = $key;
302
				++$join_index;
303
				$joins .=
304
					"INNER JOIN `{$this->table}_attributes` AS `a$join_index`
305
					ON
306
						`i`.`id`					= `a$join_index`.`id` AND
307
						`a$join_index`.`attribute`	= '%s' AND
308
						(
309
							`a$join_index`.`lang`	= '$L->clang' OR
310
							`a$join_index`.`lang`	= ''
311
						)";
312
				if (is_array($details)) {
313
					if (isset($details['from']) || isset($details['to'])) {
314
						/** @noinspection NotOptimalIfConditionsInspection */
315
						if (isset($details['from'])) {
316
							$joins .= "AND `a$join_index`.`$field`	>= '%s'";
317
							$join_params[] = $details['from'];
318
						}
319
						/** @noinspection NotOptimalIfConditionsInspection */
320
						if (isset($details['to'])) {
321
							$joins .= "AND `a$join_index`.`$field`	<= '%s'";
322
							$join_params[] = $details['to'];
323
						}
324
					} else {
325
						$on = [];
326
						foreach ($details as $d) {
327
							$on[]          = "`a$join_index`.`$field` = '%s'";
328
							$join_params[] = $d;
329
						}
330
						$on = implode(' OR ', $on);
331
						$joins .= "AND ($on)";
332
						unset($on, $d);
333
					}
334
				} else {
335
					switch ($field) {
336
						case 'numeric_value':
337
							$joins .= "AND `a$join_index`.`$field` = '%s'";
338
							break;
339
						case 'string_value':
340
							$joins .= "AND `a$join_index`.`$field` LIKE '%s%%'";
341
							break;
342
						default:
343
							$joins .= "AND MATCH (`a$join_index`.`$field`) AGAINST ('%s' IN BOOLEAN MODE) > 0";
344
					}
345
					$join_params[] = $details;
346
				}
347
			}
348
		}
349
		return $this->search_do('i', @$search_parameters['total_count'], $where, $where_params, $joins, $join_params, $page, $count, $order_by, $asc);
350
	}
351
	/**
352
	 * @param int $type
353
	 *
354
	 * @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...
355
	 */
356
	protected function attribute_type_to_value_field ($type) {
357
		switch ($type) {
358
			/**
359
			 * 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
360
			 * column for faster search
361
			 */
362
			case Attributes::TYPE_INT_SET:
363
			case Attributes::TYPE_INT_RANGE:
364
			case Attributes::TYPE_FLOAT_SET:
365
			case Attributes::TYPE_FLOAT_RANGE:
366
			case Attributes::TYPE_SWITCH:
367
			case Attributes::TYPE_STRING_SET:
368
			case Attributes::TYPE_COLOR_SET:
369
				return 'numeric_value';
370
			case Attributes::TYPE_STRING:
371
				return 'string_value';
372
			case Attributes::TYPE_TEXT:
373
				return 'text_value';
374
			default:
375
				return false;
376
		}
377
	}
378
	/**
379
	 * Add new item
380
	 *
381
	 * @param int      $category
382
	 * @param float    $price
383
	 * @param int      $in_stock
384
	 * @param int      $soon
385
	 * @param int      $listed
386
	 * @param array    $attributes
387
	 * @param string[] $images
388
	 * @param array[]  $videos
389
	 * @param string[] $tags
390
	 *
391
	 * @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...
392
	 */
393
	function add ($category, $price, $in_stock, $soon, $listed, $attributes, $images, $videos, $tags) {
394
		$L  = Language::instance();
395
		$id = $this->create(
396
			time(),
397
			$category,
398
			$price,
399
			$in_stock,
400
			$soon && !$in_stock ? 1 : 0,
401
			$listed,
402
			$this->prepare_attributes($attributes, $category, $L->clang),
403
			$this->prepare_images($images),
404
			$this->prepare_videos($videos),
405
			$this->prepare_tags($tags)
406
		);
407
		if ($id) {
408
			unset($this->cache->all);
409
			Event::instance()->fire(
410
				'Shop/Items/add',
411
				[
412
					'id' => $id
413
				]
414
			);
415
		}
416
		return $id;
417
	}
418
	/**
419
	 * Normalize attributes array structure
420
	 *
421
	 * @param array  $attributes
422
	 * @param int    $category
423
	 * @param string $clang
424
	 *
425
	 * @return array
426
	 */
427
	protected function prepare_attributes ($attributes, $category, $clang) {
428
		$Attributes      = Attributes::instance();
429
		$title_attribute = Categories::instance()->get($category)['title_attribute'];
430
		foreach ($attributes as $attribute => &$value) {
431
			$attribute_data = $Attributes->get($attribute);
432
			if (!$attribute_data) {
433
				unset($attributes[$attribute]);
434
				continue;
435
			}
436
			$value_type = [
437
				'numeric' => 0,
438
				'string'  => '',
439
				'text'    => ''
440
			];
441
			$lang       = '';
442
			switch ($this->attribute_type_to_value_field($attribute_data['type'])) {
443
				case 'numeric_value':
444
					$value_type['numeric'] = $value;
445
					break;
446
				case 'string_value':
447
					$value_type['string'] = xap($value);
448
					/**
449
					 * Multilingual feature only for title attribute
450
					 */
451
					if ($attribute_data['id'] == $title_attribute) {
452
						$lang = $clang;
453
					}
454
					break;
455
				case 'text_value':
456
					$value_type['text'] = xap($value, true, true);
457
					$lang               = $clang;
458
					break;
459
			}
460
			$value = [
461
				$attribute_data['id'],
462
				$value_type['numeric'],
463
				$value_type['string'],
464
				$value_type['text'],
465
				$lang
466
			];
467
		}
468
		return array_values($attributes);
469
	}
470
	/**
471
	 * Filter images to remove non-URL elements
472
	 *
473
	 * @param array $images
474
	 *
475
	 * @return array
476
	 */
477
	protected function prepare_images ($images) {
478
		return array_filter(
479
			$images,
480
			function ($image) {
481
				return filter_var($image, FILTER_VALIDATE_URL);
482
			}
483
		);
484
	}
485
	/**
486
	 * Normalize videos array structure
487
	 *
488
	 * @param array[] $videos
489
	 *
490
	 * @return array[]
491
	 */
492
	protected function prepare_videos ($videos) {
493
		if (!$videos || !is_array($videos)) {
494
			return [];
495
		}
496
		$videos = array_flip_3d($videos);
497
		foreach ($videos as $i => &$video) {
498
			if (!@$video['video']) {
499
				unset($videos[$i]);
500
			}
501
			if (
502
				$video['type'] == 'iframe' &&
503
				preg_match('#(http[s]?:)?//[^\s"\'>]+#ims', $video['video'], $match)
504
			) {
505
				$video['video'] = $match[0];
506
			}
507
			$video = [
508
				$video['video'],
509
				$video['poster'],
510
				$video['type']
511
			];
512
		}
513
		return $videos;
514
	}
515
	/**
516
	 * Transform array of string tags into array of their ids
517
	 *
518
	 * @param string[] $tags
519
	 *
520
	 * @return int[]
521
	 */
522
	protected function prepare_tags ($tags) {
523
		return Tags::instance()->add($tags) ?: [];
524
	}
525
	/**
526
	 * Set data of specified item
527
	 *
528
	 * @param int      $id
529
	 * @param int      $category
530
	 * @param float    $price
531
	 * @param int      $in_stock
532
	 * @param int      $soon
533
	 * @param int      $listed
534
	 * @param array    $attributes
535
	 * @param string[] $images
536
	 * @param array[]  $videos
537
	 * @param string[] $tags
538
	 *
539
	 * @return bool
540
	 */
541
	function set ($id, $category, $price, $in_stock, $soon, $listed, $attributes, $images, $videos, $tags) {
542
		$id   = (int)$id;
543
		$data = $this->get($id);
544
		if (!$data) {
545
			return false;
546
		}
547
		$L      = Language::instance();
548
		$result = $this->update(
549
			$id,
550
			$data['date'],
551
			$category,
552
			$price,
553
			$in_stock,
554
			$soon && !$in_stock ? 1 : 0,
555
			$listed,
556
			$this->prepare_attributes($attributes, $category, $L->clang),
557
			$this->prepare_images($images),
558
			$this->prepare_videos($videos),
559
			$this->prepare_tags($tags)
560
		);
561
		if ($result) {
562
			/**
563
			 * Attributes processing
564
			 */
565
			unset(
566
				$this->cache->{"$id/$L->clang"},
567
				$this->cache->all
568
			);
569
			Event::instance()->fire(
570
				'Shop/Items/set',
571
				[
572
					'id' => $id
573
				]
574
			);
575
		}
576
		return $result;
577
	}
578
	/**
579
	 * Delete specified item
580
	 *
581
	 * @param int $id
582
	 *
583
	 * @return bool
584
	 */
585
	function del ($id) {
586
		$id     = (int)$id;
587
		$result = $this->delete($id);
588
		if ($result) {
589
			unset(
590
				$this->cache->$id,
591
				$this->cache->all
592
			);
593
			Event::instance()->fire(
594
				'Shop/Items/del',
595
				[
596
					'id' => $id
597
				]
598
			);
599
		}
600
		return $result;
601
	}
602
}
603