|
1
|
|
|
<?php namespace Bedard\Shop\Models; |
|
2
|
|
|
|
|
3
|
|
|
use DB; |
|
4
|
|
|
use Flash; |
|
5
|
|
|
use Lang; |
|
6
|
|
|
use Model; |
|
7
|
|
|
use October\Rain\Database\Builder; |
|
8
|
|
|
use October\Rain\Database\ModelException; |
|
9
|
|
|
use Queue; |
|
10
|
|
|
|
|
11
|
|
|
/** |
|
12
|
|
|
* Product Model. |
|
13
|
|
|
*/ |
|
14
|
|
|
class Product extends Model |
|
15
|
|
|
{ |
|
16
|
|
|
use \Bedard\Shop\Traits\Subqueryable, |
|
17
|
|
|
\October\Rain\Database\Traits\Purgeable, |
|
18
|
|
|
\October\Rain\Database\Traits\Validation; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* @var string The database table used by the model. |
|
22
|
|
|
*/ |
|
23
|
|
|
public $table = 'bedard_shop_products'; |
|
24
|
|
|
|
|
25
|
|
|
/** |
|
26
|
|
|
* @var array Default attributes |
|
27
|
|
|
*/ |
|
28
|
|
|
public $attributes = [ |
|
29
|
|
|
'base_price' => 0, |
|
30
|
|
|
'description_html' => '', |
|
31
|
|
|
'description_plain' => '', |
|
32
|
|
|
]; |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* @var array Attribute casting |
|
36
|
|
|
*/ |
|
37
|
|
|
protected $casts = [ |
|
38
|
|
|
'price' => 'float', |
|
39
|
|
|
'base_price' => 'float', |
|
40
|
|
|
]; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* @var array Guarded fields |
|
44
|
|
|
*/ |
|
45
|
|
|
protected $guarded = ['*']; |
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* @var array Fillable fields |
|
49
|
|
|
*/ |
|
50
|
|
|
protected $fillable = [ |
|
51
|
|
|
'base_price', |
|
52
|
|
|
'categoriesList', |
|
53
|
|
|
'description_html', |
|
54
|
|
|
'description_plain', |
|
55
|
|
|
'name', |
|
56
|
|
|
'optionsInventories', |
|
57
|
|
|
'slug', |
|
58
|
|
|
]; |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* @var array Purgeable fields |
|
62
|
|
|
*/ |
|
63
|
|
|
public $purgeable = [ |
|
64
|
|
|
'categoriesList', |
|
65
|
|
|
'optionsInventories', |
|
66
|
|
|
]; |
|
67
|
|
|
|
|
68
|
|
|
/** |
|
69
|
|
|
* @var array Relations |
|
70
|
|
|
*/ |
|
71
|
|
|
public $attachMany = [ |
|
72
|
|
|
'images' => 'System\Models\File', |
|
73
|
|
|
'thumbnails' => 'System\Models\File', |
|
74
|
|
|
]; |
|
75
|
|
|
|
|
76
|
|
|
public $belongsToMany = [ |
|
77
|
|
|
'categories' => [ |
|
78
|
|
|
'Bedard\Shop\Models\Category', |
|
79
|
|
|
'table' => 'bedard_shop_category_product', |
|
80
|
|
|
'conditions' => 'is_inherited = 0', |
|
81
|
|
|
], |
|
82
|
|
|
'discounts' => [ |
|
83
|
|
|
'Bedard\Shop\Models\Discount', |
|
84
|
|
|
'table' => 'bedard_shop_discount_product', |
|
85
|
|
|
], |
|
86
|
|
|
]; |
|
87
|
|
|
|
|
88
|
|
|
public $hasMany = [ |
|
89
|
|
|
'inventories' => [ |
|
90
|
|
|
'Bedard\Shop\Models\Inventory', |
|
91
|
|
|
'delete' => true, |
|
92
|
|
|
], |
|
93
|
|
|
'options' => [ |
|
94
|
|
|
'Bedard\Shop\Models\Option', |
|
95
|
|
|
'delete' => true, |
|
96
|
|
|
'order' => 'sort_order', |
|
97
|
|
|
], |
|
98
|
|
|
'prices' => [ |
|
99
|
|
|
'Bedard\Shop\Models\Price', |
|
100
|
|
|
'delete' => true, |
|
101
|
|
|
], |
|
102
|
|
|
]; |
|
103
|
|
|
|
|
104
|
|
|
public $hasOne = [ |
|
105
|
|
|
'current_price' => [ |
|
106
|
|
|
'Bedard\Shop\Models\Price', |
|
107
|
|
|
'scope' => 'isActive', |
|
108
|
|
|
'order' => 'price asc', |
|
109
|
|
|
], |
|
110
|
|
|
]; |
|
111
|
|
|
|
|
112
|
|
|
/** |
|
113
|
|
|
* @var array Validation |
|
114
|
|
|
*/ |
|
115
|
|
|
public $rules = [ |
|
116
|
|
|
'name' => 'required', |
|
117
|
|
|
'base_price' => 'required|numeric|min:0', |
|
118
|
|
|
'slug' => 'required|unique:bedard_shop_products', |
|
119
|
|
|
]; |
|
120
|
|
|
|
|
121
|
|
|
/** |
|
122
|
|
|
* After save. |
|
123
|
|
|
* |
|
124
|
|
|
* @return void |
|
125
|
|
|
*/ |
|
126
|
|
|
public function afterSave() |
|
127
|
|
|
{ |
|
128
|
|
|
$this->saveBasePrice(); |
|
129
|
|
|
$this->saveCategoryRelationships(); |
|
130
|
|
|
$this->saveOptionAndInventoryRelationships(); |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* After validate. |
|
135
|
|
|
* |
|
136
|
|
|
* @return void |
|
137
|
|
|
*/ |
|
138
|
|
|
public function afterValidate() |
|
139
|
|
|
{ |
|
140
|
|
|
$this->validateOptions(); |
|
141
|
|
|
$this->validateInventories(); |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
|
|
/** |
|
145
|
|
|
* Before save. |
|
146
|
|
|
* |
|
147
|
|
|
* @return void |
|
148
|
|
|
*/ |
|
149
|
|
|
public function beforeSave() |
|
150
|
|
|
{ |
|
151
|
|
|
$this->setPlainDescription(); |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
/** |
|
155
|
|
|
* Delete inventories that have the is_deleted flag. |
|
156
|
|
|
* |
|
157
|
|
|
* @param array $inventories |
|
158
|
|
|
* @return array |
|
159
|
|
|
*/ |
|
160
|
|
|
protected function deleteRelatedInventories($inventories) |
|
161
|
|
|
{ |
|
162
|
|
|
return array_filter($inventories, function ($inventory) { |
|
163
|
|
|
if ($inventory['id'] !== null && $inventory['is_deleted']) { |
|
164
|
|
|
Inventory::find($inventory['id'])->delete(); |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
return ! $inventory['is_deleted']; |
|
168
|
|
|
}); |
|
169
|
|
|
} |
|
170
|
|
|
|
|
171
|
|
|
/** |
|
172
|
|
|
* Delete options that have the is_deleted flag. |
|
173
|
|
|
* |
|
174
|
|
|
* @param array $options |
|
175
|
|
|
* @return array |
|
176
|
|
|
*/ |
|
177
|
|
|
protected function deleteRelatedOptions($options) |
|
178
|
|
|
{ |
|
179
|
|
|
return array_filter($options, function ($option) { |
|
180
|
|
|
if ($option['id'] !== null && $option['is_deleted']) { |
|
181
|
|
|
Option::find($option['id'])->delete(); |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
return ! $option['is_deleted']; |
|
185
|
|
|
}); |
|
186
|
|
|
} |
|
187
|
|
|
|
|
188
|
|
|
/** |
|
189
|
|
|
* Delete option values that have the is_deleted flag. |
|
190
|
|
|
* |
|
191
|
|
|
* @param array $optionValues |
|
192
|
|
|
* @return array |
|
193
|
|
|
*/ |
|
194
|
|
|
protected function deleteRelatedOptionValues($optionValues) |
|
195
|
|
|
{ |
|
196
|
|
|
return array_filter($optionValues, function($optionValue) { |
|
197
|
|
|
if ($optionValue['id'] !== null && $optionValue['is_deleted']) { |
|
198
|
|
|
OptionValue::find($optionValue['id'])->delete(); |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
return ! $optionValue['is_deleted']; |
|
202
|
|
|
}); |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* Get the categories options. |
|
207
|
|
|
* |
|
208
|
|
|
* @return array |
|
209
|
|
|
*/ |
|
210
|
|
|
public function getCategoriesListOptions() |
|
211
|
|
|
{ |
|
212
|
|
|
return Category::orderBy('name')->lists('name', 'id'); |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
/** |
|
216
|
|
|
* Get the categories that are directly related to this product. |
|
217
|
|
|
* |
|
218
|
|
|
* @return void |
|
219
|
|
|
*/ |
|
220
|
|
|
public function getCategoriesListAttribute() |
|
221
|
|
|
{ |
|
222
|
|
|
return $this->categories()->lists('id'); |
|
223
|
|
|
} |
|
224
|
|
|
|
|
225
|
|
|
/** |
|
226
|
|
|
* Update of create the related base price model. |
|
227
|
|
|
* |
|
228
|
|
|
* @return void |
|
229
|
|
|
*/ |
|
230
|
|
|
public function saveBasePrice() |
|
231
|
|
|
{ |
|
232
|
|
|
Price::updateOrCreate( |
|
233
|
|
|
['product_id' => $this->id, 'discount_id' => null], |
|
234
|
|
|
['price' => $this->base_price] |
|
235
|
|
|
); |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
/** |
|
239
|
|
|
* Sync the categories checkboxlist with the category relationship. |
|
240
|
|
|
* |
|
241
|
|
|
* @return void |
|
242
|
|
|
*/ |
|
243
|
|
|
public function saveCategoryRelationships() |
|
244
|
|
|
{ |
|
245
|
|
|
$categoryIds = $this->getOriginalPurgeValue('categoriesList'); |
|
246
|
|
|
|
|
247
|
|
|
if (is_array($categoryIds)) { |
|
248
|
|
|
$this->categories()->sync($categoryIds); |
|
249
|
|
|
} |
|
250
|
|
|
|
|
251
|
|
|
$this->syncInheritedCategories(); |
|
252
|
|
|
} |
|
253
|
|
|
|
|
254
|
|
|
/** |
|
255
|
|
|
* Save the options and inventories. |
|
256
|
|
|
* |
|
257
|
|
|
* @return void |
|
258
|
|
|
*/ |
|
259
|
|
|
public function saveOptionAndInventoryRelationships() |
|
260
|
|
|
{ |
|
261
|
|
|
$data = $this->getOriginalPurgeValue('optionsInventories'); |
|
262
|
|
|
|
|
263
|
|
|
if (is_array($data['options'])) { |
|
264
|
|
|
$options = $data['options']; |
|
265
|
|
|
$options = $this->deleteRelatedOptions($options); |
|
266
|
|
|
$this->saveRelatedOptions($options); |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
if (is_array($data['inventories'])) { |
|
270
|
|
|
$inventories = $data['inventories']; |
|
271
|
|
|
$inventories = $this->deleteRelatedInventories($inventories); |
|
272
|
|
|
$this->saveRelatedInventories($inventories); |
|
273
|
|
|
} |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
/** |
|
277
|
|
|
* Save an array of realted inventories. |
|
278
|
|
|
* |
|
279
|
|
|
* @param array $inventories |
|
280
|
|
|
* @return array |
|
281
|
|
|
*/ |
|
282
|
|
|
protected function saveRelatedInventories(array $inventories) |
|
283
|
|
|
{ |
|
284
|
|
|
foreach ($inventories as $inventory) { |
|
285
|
|
|
$model = $inventory['id'] !== null |
|
286
|
|
|
? Inventory::firstOrNew(['id' => $inventory['id']]) |
|
287
|
|
|
: new Inventory; |
|
288
|
|
|
|
|
289
|
|
|
$inventory['product_id'] = $this->id; |
|
290
|
|
|
$model->fill($inventory); |
|
291
|
|
|
$model->save(); |
|
292
|
|
|
} |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
/** |
|
296
|
|
|
* Save an array of related options. |
|
297
|
|
|
* |
|
298
|
|
|
* @param array $options |
|
299
|
|
|
* @return array |
|
300
|
|
|
*/ |
|
301
|
|
|
protected function saveRelatedOptions(array $options) |
|
302
|
|
|
{ |
|
303
|
|
|
foreach ($options as $index => &$option) { |
|
304
|
|
|
$model = $option['id'] !== null |
|
305
|
|
|
? Option::firstOrNew(['id' => $option['id']]) |
|
306
|
|
|
: new Option; |
|
307
|
|
|
|
|
308
|
|
|
$model->fill($option); |
|
309
|
|
|
$model->product_id = $this->id; |
|
310
|
|
|
$model->sort_order = $index; |
|
311
|
|
|
|
|
312
|
|
|
$sessionKey = uniqid('session_key', true); |
|
313
|
|
|
if (is_array($option['values'])) { |
|
314
|
|
|
$option['values'] = $this->deleteRelatedOptionValues($option['values']); |
|
315
|
|
|
$this->saveRelatedOptionValues($model, $option['values'], $sessionKey); |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
$model->save(null, $sessionKey); |
|
319
|
|
|
} |
|
320
|
|
|
|
|
321
|
|
|
return $options; |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
/** |
|
325
|
|
|
* Save related option values. |
|
326
|
|
|
* |
|
327
|
|
|
* @param Option $option |
|
328
|
|
|
* @param array $values |
|
329
|
|
|
* @param string $sessionKey |
|
330
|
|
|
* @return void |
|
331
|
|
|
*/ |
|
332
|
|
|
protected function saveRelatedOptionValues(Option &$option, array $values, $sessionKey) |
|
333
|
|
|
{ |
|
334
|
|
|
foreach ($values as $index => $value) { |
|
335
|
|
|
$model = array_key_exists('id', $value) && $value['id'] !== null |
|
336
|
|
|
? OptionValue::firstOrNew(['id' => $value['id']]) |
|
337
|
|
|
: new OptionValue; |
|
338
|
|
|
|
|
339
|
|
|
$model->fill($value); |
|
340
|
|
|
$model->sort_order = $index; |
|
341
|
|
|
$model->save(); |
|
342
|
|
|
|
|
343
|
|
|
$option->values()->add($model, $sessionKey); |
|
344
|
|
|
} |
|
345
|
|
|
} |
|
346
|
|
|
|
|
347
|
|
|
/** |
|
348
|
|
|
* Left joins a subquery containing the product price. |
|
349
|
|
|
* |
|
350
|
|
|
* @param \October\Rain\Database\Builder $query |
|
351
|
|
|
* @return \October\Rain\Database\Builder |
|
352
|
|
|
*/ |
|
353
|
|
|
public function scopeJoinPrice(Builder $query) |
|
354
|
|
|
{ |
|
355
|
|
|
$alias = 'prices'; |
|
356
|
|
|
$grammar = $query->getQuery()->getGrammar(); |
|
357
|
|
|
|
|
358
|
|
|
$subquery = Price::isActive() |
|
359
|
|
|
->addselect('bedard_shop_prices.product_id') |
|
360
|
|
|
->selectRaw('MIN('.$grammar->wrap('bedard_shop_prices.price').') as '.$grammar->wrap('price')) |
|
361
|
|
|
->groupBy('bedard_shop_prices.product_id'); |
|
362
|
|
|
|
|
363
|
|
|
return $query |
|
364
|
|
|
->addSelect($alias.'.price') |
|
365
|
|
|
->joinSubquery($subquery, $alias, 'bedard_shop_products.id', '=', $alias.'.product_id'); |
|
366
|
|
|
} |
|
367
|
|
|
|
|
368
|
|
|
/** |
|
369
|
|
|
* Set the plain text description_html. |
|
370
|
|
|
* |
|
371
|
|
|
* @return void |
|
372
|
|
|
*/ |
|
373
|
|
|
protected function setPlainDescription() |
|
374
|
|
|
{ |
|
375
|
|
|
$this->description_plain = strip_tags($this->description_html); |
|
376
|
|
|
} |
|
377
|
|
|
|
|
378
|
|
|
/** |
|
379
|
|
|
* Sync the inherited categories of all products. |
|
380
|
|
|
* |
|
381
|
|
|
* @return void |
|
382
|
|
|
*/ |
|
383
|
|
|
public static function syncAllInheritedCategories() |
|
384
|
|
|
{ |
|
385
|
|
|
Queue::push(function ($job) { |
|
386
|
|
|
$data = []; |
|
387
|
|
|
$products = Product::with('categories')->get(); |
|
388
|
|
|
$categoryTree = Category::getParentCategoryIds(); |
|
389
|
|
|
|
|
390
|
|
|
foreach ($products as $product) { |
|
391
|
|
|
$inheritedCategoryIds = []; |
|
392
|
|
|
foreach ($product->categories as $category) { |
|
393
|
|
|
if (array_key_exists($category->id, $categoryTree)) { |
|
394
|
|
|
$inheritedCategoryIds = array_merge($inheritedCategoryIds, $categoryTree[$category->id]); |
|
395
|
|
|
} |
|
396
|
|
|
} |
|
397
|
|
|
|
|
398
|
|
|
foreach (array_unique($inheritedCategoryIds) as $categoryId) { |
|
399
|
|
|
$data[] = [ |
|
400
|
|
|
'category_id' => $categoryId, |
|
401
|
|
|
'product_id' => $product->id, |
|
402
|
|
|
'is_inherited' => 1, |
|
403
|
|
|
]; |
|
404
|
|
|
} |
|
405
|
|
|
} |
|
406
|
|
|
|
|
407
|
|
|
DB::table('bedard_shop_category_product')->whereIsInherited(1)->delete(); |
|
408
|
|
|
DB::table('bedard_shop_category_product')->insert($data); |
|
409
|
|
|
Discount::syncAllPrices(); |
|
410
|
|
|
|
|
411
|
|
|
$job->delete(); |
|
412
|
|
|
}); |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
/** |
|
416
|
|
|
* Sync a product with it's inherited categories. |
|
417
|
|
|
* |
|
418
|
|
|
* @return void |
|
419
|
|
|
*/ |
|
420
|
|
|
public function syncInheritedCategories() |
|
421
|
|
|
{ |
|
422
|
|
|
$data = []; |
|
423
|
|
|
$categoryIds = $this->categories()->lists('id'); |
|
424
|
|
|
$parentIds = Category::isParentOf($categoryIds)->lists('id'); |
|
425
|
|
|
foreach ($parentIds as $parentId) { |
|
426
|
|
|
$data[] = [ |
|
427
|
|
|
'category_id' => $parentId, |
|
428
|
|
|
'product_id' => $this->id, |
|
429
|
|
|
'is_inherited' => true, |
|
430
|
|
|
]; |
|
431
|
|
|
} |
|
432
|
|
|
|
|
433
|
|
|
DB::table('bedard_shop_category_product')->whereProductId($this->id)->whereIsInherited(1)->delete(); |
|
434
|
|
|
DB::table('bedard_shop_category_product')->insert($data); |
|
435
|
|
|
|
|
436
|
|
|
$categoryScope = array_merge($categoryIds, $parentIds); |
|
437
|
|
|
Discount::syncProductPrice($this, $categoryScope); |
|
438
|
|
|
} |
|
439
|
|
|
|
|
440
|
|
|
/** |
|
441
|
|
|
* Validate inventories. |
|
442
|
|
|
* |
|
443
|
|
|
* @throws \October\Rain\Database\ModelException |
|
444
|
|
|
* @return void |
|
445
|
|
|
*/ |
|
446
|
|
View Code Duplication |
protected function validateInventories() |
|
|
|
|
|
|
447
|
|
|
{ |
|
448
|
|
|
if (! is_array($this->optionsInventories) || |
|
449
|
|
|
! is_array($this->optionsInventories['options'])) { |
|
450
|
|
|
return; |
|
451
|
|
|
} |
|
452
|
|
|
|
|
453
|
|
|
$takenValueCombinations = []; |
|
454
|
|
|
foreach ($this->optionsInventories['inventories'] as $inventory) { |
|
455
|
|
|
// validate the inventory |
|
456
|
|
|
$model = new Inventory($inventory); |
|
457
|
|
|
$model->validate(); |
|
458
|
|
|
|
|
459
|
|
|
// validate that the value combinations are unique |
|
460
|
|
|
sort($inventory['valueIds']); |
|
461
|
|
|
$valueCombination = json_encode($inventory['valueIds']); |
|
462
|
|
|
|
|
463
|
|
|
if (in_array($valueCombination, $takenValueCombinations)) { |
|
464
|
|
|
Flash::error(Lang::get('bedard.shop::lang.products.form.duplicate_inventories_error')); |
|
465
|
|
|
throw new ModelException($this); |
|
466
|
|
|
} |
|
467
|
|
|
|
|
468
|
|
|
$takenValueCombinations[] = $valueCombination; |
|
469
|
|
|
} |
|
470
|
|
|
} |
|
471
|
|
|
|
|
472
|
|
|
/** |
|
473
|
|
|
* Validate options. |
|
474
|
|
|
* |
|
475
|
|
|
* @throws \October\Rain\Database\ModelException |
|
476
|
|
|
* @return void |
|
477
|
|
|
*/ |
|
478
|
|
View Code Duplication |
protected function validateOptions() |
|
|
|
|
|
|
479
|
|
|
{ |
|
480
|
|
|
if (! is_array($this->optionsInventories) || |
|
481
|
|
|
! is_array($this->optionsInventories['options'])) { |
|
482
|
|
|
return; |
|
483
|
|
|
} |
|
484
|
|
|
|
|
485
|
|
|
$names = []; |
|
486
|
|
|
foreach ($this->optionsInventories['options'] as $option) { |
|
487
|
|
|
// validate the option |
|
488
|
|
|
$model = new Option($option); |
|
489
|
|
|
$model->validate(); |
|
490
|
|
|
|
|
491
|
|
|
// validate that names are unique |
|
492
|
|
|
$name = strtolower(trim($option['name'])); |
|
493
|
|
|
|
|
494
|
|
|
if (in_array($name, $names)) { |
|
495
|
|
|
Flash::error(Lang::get('bedard.shop::lang.products.form.duplicate_options_error')); |
|
496
|
|
|
throw new ModelException($this); |
|
497
|
|
|
} |
|
498
|
|
|
|
|
499
|
|
|
$names[] = $name; |
|
500
|
|
|
} |
|
501
|
|
|
} |
|
502
|
|
|
} |
|
503
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.