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

Items::add()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 27
rs 8.5806
cc 4
eloc 19
nc 2
nop 9

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
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