|
1
|
|
|
<?php namespace Bedard\Shop\Models; |
|
2
|
|
|
|
|
3
|
|
|
use DB; |
|
4
|
|
|
use Model; |
|
5
|
|
|
|
|
6
|
|
|
/** |
|
7
|
|
|
* Product Model. |
|
8
|
|
|
*/ |
|
9
|
|
|
class Product extends Model |
|
10
|
|
|
{ |
|
11
|
|
|
use \Bedard\Shop\Traits\Subqueryable, |
|
12
|
|
|
\October\Rain\Database\Traits\Purgeable, |
|
13
|
|
|
\October\Rain\Database\Traits\Validation; |
|
14
|
|
|
|
|
15
|
|
|
/** |
|
16
|
|
|
* @var string The database table used by the model. |
|
17
|
|
|
*/ |
|
18
|
|
|
public $table = 'bedard_shop_products'; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* @var array Default attributes |
|
22
|
|
|
*/ |
|
23
|
|
|
public $attributes = [ |
|
24
|
|
|
'base_price' => 0, |
|
25
|
|
|
'description_html' => '', |
|
26
|
|
|
'description_plain' => '', |
|
27
|
|
|
'is_enabled' => true, |
|
28
|
|
|
]; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* @var array Attribute casting |
|
32
|
|
|
*/ |
|
33
|
|
|
protected $casts = [ |
|
34
|
|
|
'base_price' => 'float', |
|
35
|
|
|
'id' => 'integer', |
|
36
|
|
|
'inventory_count' => 'integer', |
|
37
|
|
|
'is_enabled' => 'boolean', |
|
38
|
|
|
'price' => 'float', |
|
39
|
|
|
]; |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* @var array Guarded fields |
|
43
|
|
|
*/ |
|
44
|
|
|
protected $guarded = ['*']; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* @var array Fillable fields |
|
48
|
|
|
*/ |
|
49
|
|
|
protected $fillable = [ |
|
50
|
|
|
'base_price', |
|
51
|
|
|
'description_html', |
|
52
|
|
|
'description_plain', |
|
53
|
|
|
'is_enabled', |
|
54
|
|
|
'name', |
|
55
|
|
|
'options_inventories', |
|
56
|
|
|
'slug', |
|
57
|
|
|
]; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* @var array Purgeable fields |
|
61
|
|
|
*/ |
|
62
|
|
|
public $purgeable = [ |
|
63
|
|
|
'categories_field', |
|
64
|
|
|
'options_inventories', |
|
65
|
|
|
]; |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* @var array Relations |
|
69
|
|
|
*/ |
|
70
|
|
|
public $attachMany = [ |
|
71
|
|
|
'images' => [ |
|
72
|
|
|
'System\Models\File', |
|
73
|
|
|
], |
|
74
|
|
|
'thumbnails' => [ |
|
75
|
|
|
'System\Models\File', |
|
76
|
|
|
], |
|
77
|
|
|
]; |
|
78
|
|
|
|
|
79
|
|
|
public $belongsToMany = [ |
|
80
|
|
|
'categories' => [ |
|
81
|
|
|
'Bedard\Shop\Models\Category', |
|
82
|
|
|
'pivot' => ['is_inherited'], |
|
83
|
|
|
'table' => 'bedard_shop_category_product', |
|
84
|
|
|
], |
|
85
|
|
|
]; |
|
86
|
|
|
|
|
87
|
|
|
public $hasMany = [ |
|
88
|
|
|
'cartItems' => [ |
|
89
|
|
|
'Bedard\Shop\Models\CartItem', |
|
90
|
|
|
], |
|
91
|
|
|
'inventories' => [ |
|
92
|
|
|
'Bedard\Shop\Models\Inventory', |
|
93
|
|
|
'delete' => true, |
|
94
|
|
|
], |
|
95
|
|
|
'options' => [ |
|
96
|
|
|
'Bedard\Shop\Models\Option', |
|
97
|
|
|
'delete' => true, |
|
98
|
|
|
'order' => 'sort_order', |
|
99
|
|
|
], |
|
100
|
|
|
]; |
|
101
|
|
|
|
|
102
|
|
|
/** |
|
103
|
|
|
* @var array Validation |
|
104
|
|
|
*/ |
|
105
|
|
|
public $rules = [ |
|
106
|
|
|
'name' => 'required', |
|
107
|
|
|
'base_price' => 'required|numeric|min:0', |
|
108
|
|
|
'slug' => 'required|unique:bedard_shop_products', |
|
109
|
|
|
]; |
|
110
|
|
|
|
|
111
|
|
|
/** |
|
112
|
|
|
* After save. |
|
113
|
|
|
* |
|
114
|
|
|
* @return void |
|
115
|
|
|
*/ |
|
116
|
|
|
public function afterSave() |
|
117
|
|
|
{ |
|
118
|
|
|
$this->saveCategories(); |
|
119
|
|
|
$this->saveOptionsAndInventories(); |
|
120
|
|
|
} |
|
121
|
|
|
|
|
122
|
|
|
/** |
|
123
|
|
|
* After validate. |
|
124
|
|
|
* |
|
125
|
|
|
* @return void |
|
126
|
|
|
*/ |
|
127
|
|
|
public function afterValidate() |
|
128
|
|
|
{ |
|
129
|
|
|
$this->validateOptionsAndInventories(); |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
/** |
|
133
|
|
|
* Before save. |
|
134
|
|
|
* |
|
135
|
|
|
* @return void |
|
136
|
|
|
*/ |
|
137
|
|
|
public function beforeSave() |
|
138
|
|
|
{ |
|
139
|
|
|
$this->setPlainDescription(); |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
/** |
|
143
|
|
|
* Determine if the categories field contains new values. |
|
144
|
|
|
* |
|
145
|
|
|
* @return bool |
|
146
|
|
|
*/ |
|
147
|
|
|
protected function categoriesAreChanged($new) |
|
148
|
|
|
{ |
|
149
|
|
|
$old = $this->categoriesField ?: []; |
|
150
|
|
|
sort($old); |
|
151
|
|
|
sort($new); |
|
152
|
|
|
|
|
153
|
|
|
return ! count($old) || ! count($new) || $old != $new; |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* Format the price. |
|
158
|
|
|
* |
|
159
|
|
|
* @return string |
|
160
|
|
|
*/ |
|
161
|
|
|
public function formattedPrice() |
|
162
|
|
|
{ |
|
163
|
|
|
return number_format($this->base_price, 2); |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
/** |
|
167
|
|
|
* Get the categories that are not inherited. |
|
168
|
|
|
* |
|
169
|
|
|
* @return array |
|
170
|
|
|
*/ |
|
171
|
|
|
public function getCategoriesFieldAttribute() |
|
172
|
|
|
{ |
|
173
|
|
|
return $this->categories() |
|
174
|
|
|
->wherePivot('is_inherited', false) |
|
175
|
|
|
->lists('id'); |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
/** |
|
179
|
|
|
* List the category field options. |
|
180
|
|
|
* |
|
181
|
|
|
* @return array |
|
182
|
|
|
*/ |
|
183
|
|
|
public function getCategoriesFieldOptions() |
|
184
|
|
|
{ |
|
185
|
|
|
return Category::get()->sortBy('name')->lists('name', 'id'); |
|
186
|
|
|
} |
|
187
|
|
|
|
|
188
|
|
|
/** |
|
189
|
|
|
* Save related categories. |
|
190
|
|
|
* |
|
191
|
|
|
* @return void |
|
192
|
|
|
*/ |
|
193
|
|
|
protected function saveCategories() |
|
194
|
|
|
{ |
|
195
|
|
|
// if the categories haven't changed, do nothing |
|
196
|
|
|
$directIds = $this->getOriginalPurgeValue('categories_field') ?: []; |
|
197
|
|
|
if (! $this->categoriesAreChanged($directIds)) { |
|
198
|
|
|
return; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
// otherwise lets sync our categories. we first need to gather |
|
202
|
|
|
// the neccessary information, and create a few containers. |
|
203
|
|
|
$sync = []; |
|
204
|
|
|
$ancestorIds = []; |
|
205
|
|
|
$allCategories = Category::all(); |
|
206
|
|
|
|
|
207
|
|
|
// iterate over our direct ids |
|
208
|
|
|
foreach ($directIds as $directCategoryId) { |
|
209
|
|
|
// keep track of our direct ids for the eventual sync call. |
|
210
|
|
|
// we need to provide the pivot data, because by default |
|
211
|
|
|
// the sync function won't existing database entries. |
|
212
|
|
|
$sync[$directCategoryId] = ['is_inherited' => 0]; |
|
213
|
|
|
|
|
214
|
|
|
// add all of our direct category's parent ids to our ancestors |
|
215
|
|
|
$branchIds = $allCategories |
|
216
|
|
|
->find($directCategoryId) |
|
217
|
|
|
->getParents() |
|
218
|
|
|
->lists('id'); |
|
219
|
|
|
|
|
220
|
|
|
$ancestorIds = array_merge($ancestorIds, $branchIds); |
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
// iterate over our ancestor ids and set their is_inherited flag |
|
224
|
|
|
foreach ($ancestorIds as $ancestorId) { |
|
225
|
|
|
$sync[$ancestorId] = [ |
|
226
|
|
|
'is_inherited' => in_array($ancestorId, $directIds) ? 0 : 1, |
|
227
|
|
|
]; |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
|
|
// finally, sync our direct and ancestor categories |
|
231
|
|
|
$this->categories()->sync($sync); |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
/** |
|
235
|
|
|
* Save related inventories. |
|
236
|
|
|
* |
|
237
|
|
|
* @param array $inventories |
|
238
|
|
|
* @return void |
|
239
|
|
|
*/ |
|
240
|
|
|
protected function saveInventories($inventories) |
|
241
|
|
|
{ |
|
242
|
|
|
foreach ($inventories as $inventory) { |
|
243
|
|
|
if ($model = Inventory::find($inventory['id'])) { |
|
244
|
|
|
if (array_key_exists('_deleted', $inventory) && $inventory['_deleted']) { |
|
245
|
|
|
$model->delete(); |
|
246
|
|
|
} else { |
|
247
|
|
|
$model->fill($inventory); |
|
248
|
|
|
$model->product_id = $this->id; |
|
249
|
|
|
$model->save(); |
|
250
|
|
|
} |
|
251
|
|
|
} |
|
252
|
|
|
} |
|
253
|
|
|
} |
|
254
|
|
|
|
|
255
|
|
|
/** |
|
256
|
|
|
* Save related options. |
|
257
|
|
|
* |
|
258
|
|
|
* @param array $options |
|
259
|
|
|
* @return void |
|
260
|
|
|
*/ |
|
261
|
|
|
protected function saveOptions($options) |
|
262
|
|
|
{ |
|
263
|
|
|
foreach ($options as $index => $option) { |
|
264
|
|
View Code Duplication |
if ($model = Option::find($option['id'])) { |
|
|
|
|
|
|
265
|
|
|
if (array_key_exists('_deleted', $option) && $option['_deleted']) { |
|
266
|
|
|
$model->delete(); |
|
267
|
|
|
} else { |
|
268
|
|
|
$model->fill($option); |
|
269
|
|
|
$model->product_id = $this->id; |
|
270
|
|
|
$model->sort_order = $index; |
|
271
|
|
|
$model->save(); |
|
272
|
|
|
} |
|
273
|
|
|
} |
|
274
|
|
|
} |
|
275
|
|
|
} |
|
276
|
|
|
|
|
277
|
|
|
/** |
|
278
|
|
|
* Save related options and inventories. |
|
279
|
|
|
* |
|
280
|
|
|
* @return void |
|
281
|
|
|
*/ |
|
282
|
|
View Code Duplication |
protected function saveOptionsAndInventories() |
|
283
|
|
|
{ |
|
284
|
|
|
if (! $data = $this->getOriginalPurgeValue('options_inventories')) { |
|
285
|
|
|
return; |
|
286
|
|
|
} |
|
287
|
|
|
|
|
288
|
|
|
$data = json_decode($data, true); |
|
289
|
|
|
$this->saveOptions($data['options']); |
|
290
|
|
|
$this->saveInventories($data['inventories']); |
|
291
|
|
|
} |
|
292
|
|
|
|
|
293
|
|
|
/** |
|
294
|
|
|
* Fetch products in particular categories. |
|
295
|
|
|
* |
|
296
|
|
|
* @param \October\Rain\Database\Builder $query |
|
297
|
|
|
* @param array|string $slugs |
|
298
|
|
|
* @return \October\Rain\Database\Builder |
|
299
|
|
|
*/ |
|
300
|
|
|
public function scopeInCategories($query, $slugs) |
|
301
|
|
|
{ |
|
302
|
|
|
// if the slugs are a csv, explode them into an array |
|
303
|
|
|
if (is_string($slugs)) { |
|
304
|
|
|
$slugs = explode(',', $slugs); |
|
305
|
|
|
} |
|
306
|
|
|
|
|
307
|
|
|
return $query->whereHas('categories', function ($category) use ($slugs) { |
|
308
|
|
|
$category->whereIn('slug', array_map('trim', $slugs)); |
|
309
|
|
|
}); |
|
310
|
|
|
} |
|
311
|
|
|
|
|
312
|
|
|
/** |
|
313
|
|
|
* Select products that are enabled. |
|
314
|
|
|
* |
|
315
|
|
|
* @param \October\Rain\Database\Builder $query |
|
316
|
|
|
* @return \October\Rain\Database\Builder |
|
317
|
|
|
*/ |
|
318
|
|
|
public function scopeIsEnabled($query) |
|
319
|
|
|
{ |
|
320
|
|
|
return $query->whereIsEnabled(true); |
|
321
|
|
|
} |
|
322
|
|
|
|
|
323
|
|
|
/** |
|
324
|
|
|
* Joins the sum of available inventories. |
|
325
|
|
|
* |
|
326
|
|
|
* @param \October\Rain\Database\Builder $query |
|
327
|
|
|
* @return \October\Rain\Database\Builder |
|
328
|
|
|
*/ |
|
329
|
|
|
public function scopeJoinInventoryCount($query) |
|
330
|
|
|
{ |
|
331
|
|
|
$alias = 'inventories'; |
|
332
|
|
|
$grammar = $query->getQuery()->getGrammar(); |
|
333
|
|
|
|
|
334
|
|
|
$subquery = Inventory::addSelect('bedard_shop_inventories.product_id') |
|
335
|
|
|
->selectRaw('SUM('.$grammar->wrap('bedard_shop_inventories.quantity').') as '.$grammar->wrap('inventory_count')) |
|
336
|
|
|
->groupBy('bedard_shop_inventories.product_id'); |
|
337
|
|
|
|
|
338
|
|
|
return $query |
|
339
|
|
|
->addSelect($alias.'.inventory_count') |
|
340
|
|
|
->joinSubquery($subquery, $alias, 'bedard_shop_products.id', '=', $alias.'.product_id', 'leftJoin'); |
|
341
|
|
|
} |
|
342
|
|
|
|
|
343
|
|
|
/** |
|
344
|
|
|
* This exists to makes statuses sortable by assigning them a value. |
|
345
|
|
|
* |
|
346
|
|
|
* Disabled 0 |
|
347
|
|
|
* Enabled 1 |
|
348
|
|
|
* |
|
349
|
|
|
* @param \October\Rain\Database\Builder $query |
|
350
|
|
|
* @return \October\Rain\Database\Builder |
|
351
|
|
|
*/ |
|
352
|
|
|
public function scopeSelectStatus($query) |
|
353
|
|
|
{ |
|
354
|
|
|
$grammar = $query->getQuery()->getGrammar(); |
|
355
|
|
|
$price = $grammar->wrap('price'); |
|
|
|
|
|
|
356
|
|
|
$inventory = $grammar->wrap('inventory'); |
|
|
|
|
|
|
357
|
|
|
$is_enabled = $grammar->wrap('bedard_shop_products.is_enabled'); |
|
358
|
|
|
$base_price = $grammar->wrap('bedard_shop_products.base_price'); |
|
|
|
|
|
|
359
|
|
|
|
|
360
|
|
|
$subquery = 'CASE '. |
|
361
|
|
|
"WHEN {$is_enabled} = 1 THEN 1 ". |
|
362
|
|
|
'ELSE 0 '. |
|
363
|
|
|
'END'; |
|
364
|
|
|
|
|
365
|
|
|
return $query->selectSubquery($subquery, 'status'); |
|
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
|
|
|
protected function syncAllCategories() |
|
379
|
|
|
{ |
|
380
|
|
|
// delete all inherited categories |
|
381
|
|
|
DB::table('bedard_shop_category_product') |
|
382
|
|
|
->whereIsInherited(true) |
|
383
|
|
|
->delete(); |
|
384
|
|
|
|
|
385
|
|
|
// figure out what our branches are |
|
386
|
|
|
$branches = []; |
|
387
|
|
|
$categories = Category::select('id', 'parent_id')->get(); |
|
388
|
|
|
$categories->each(function ($category) use (&$branches, $categories) { |
|
389
|
|
|
$branches[$category->id] = Category::getParentIds($category->id, $categories); |
|
390
|
|
|
}); |
|
391
|
|
|
|
|
392
|
|
|
// itterate over each product and build up an insert object |
|
393
|
|
|
$insert = []; |
|
394
|
|
|
$products = self::with('categories')->select('id')->get(); |
|
395
|
|
|
foreach ($products as $product) { |
|
396
|
|
|
foreach ($product->categories as $category) { |
|
397
|
|
|
if (empty($branches[$category->id])) { |
|
398
|
|
|
continue; |
|
399
|
|
|
} |
|
400
|
|
|
|
|
401
|
|
|
foreach ($branches[$category->id] as $ancestorId) { |
|
402
|
|
|
$insert[] = [ |
|
403
|
|
|
'category_id' => $ancestorId, |
|
404
|
|
|
'product_id' => $product->id, |
|
405
|
|
|
'is_inherited' => true, |
|
406
|
|
|
]; |
|
407
|
|
|
} |
|
408
|
|
|
} |
|
409
|
|
|
} |
|
410
|
|
|
|
|
411
|
|
|
// insert the new inherited categories |
|
412
|
|
|
DB::table('bedard_shop_category_product')->insert($insert); |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
/** |
|
416
|
|
|
* Validate inventories. |
|
417
|
|
|
* |
|
418
|
|
|
* @param array $inventories |
|
419
|
|
|
* @return void |
|
420
|
|
|
*/ |
|
421
|
|
|
protected function validateInventories($inventories) |
|
422
|
|
|
{ |
|
423
|
|
|
// @todo |
|
424
|
|
|
} |
|
425
|
|
|
|
|
426
|
|
|
/** |
|
427
|
|
|
* Validate a product's options. |
|
428
|
|
|
* |
|
429
|
|
|
* @param array $options |
|
430
|
|
|
* @return void |
|
431
|
|
|
*/ |
|
432
|
|
|
protected function validateOptions($options) |
|
433
|
|
|
{ |
|
434
|
|
|
// validate each option individually |
|
435
|
|
|
foreach ($options as $option) { |
|
436
|
|
|
$model = new Option($option); |
|
437
|
|
|
$model->validate(); |
|
438
|
|
|
} |
|
439
|
|
|
|
|
440
|
|
|
// @todo: prevent duplicate options |
|
441
|
|
|
} |
|
442
|
|
|
|
|
443
|
|
|
/** |
|
444
|
|
|
* Call validate for options and inventories. |
|
445
|
|
|
* |
|
446
|
|
|
* @return void |
|
447
|
|
|
*/ |
|
448
|
|
View Code Duplication |
protected function validateOptionsAndInventories() |
|
449
|
|
|
{ |
|
450
|
|
|
if (! $data = $this->getOriginalPurgeValue('options_inventories')) { |
|
451
|
|
|
return; |
|
452
|
|
|
} |
|
453
|
|
|
|
|
454
|
|
|
$data = json_decode($data, true); |
|
455
|
|
|
$this->validateOptions($data['options']); |
|
456
|
|
|
$this->validateInventories($data['inventories']); |
|
457
|
|
|
} |
|
458
|
|
|
} |
|
459
|
|
|
|
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.