Completed
Push — master ( 2554b1...252880 )
by Scott
02:18
created

Product::scopeIsNotEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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
        'is_enabled' => true,
33
    ];
34
35
    /**
36
     * @var array Attribute casting
37
     */
38
    protected $casts = [
39
        'price' => 'float',
40
        'base_price' => 'float',
41
        'is_enabled' => 'boolean',
42
    ];
43
44
    /**
45
     * @var array Guarded fields
46
     */
47
    protected $guarded = ['*'];
48
49
    /**
50
     * @var array Fillable fields
51
     */
52
    protected $fillable = [
53
        'base_price',
54
        'categoriesList',
55
        'description_html',
56
        'description_plain',
57
        'is_enabled',
58
        'name',
59
        'optionsInventories',
60
        'slug',
61
    ];
62
63
    /**
64
     * @var array Purgeable fields
65
     */
66
    public $purgeable = [
67
        'categoriesList',
68
        'optionsInventories',
69
    ];
70
71
    /**
72
     * @var array Relations
73
     */
74
    public $attachMany = [
75
        'images' => 'System\Models\File',
76
        'thumbnails' => 'System\Models\File',
77
    ];
78
79
    public $belongsToMany = [
80
        'categories' => [
81
            'Bedard\Shop\Models\Category',
82
            'table' => 'bedard_shop_category_product',
83
            'conditions' => 'is_inherited = 0',
84
        ],
85
        'discounts' => [
86
            'Bedard\Shop\Models\Discount',
87
            'table' => 'bedard_shop_discount_product',
88
        ],
89
        'inherited_categories' => [
90
            'Bedard\Shop\Models\Category',
91
            'table' => 'bedard_shop_category_product',
92
            'conditions' => 'is_inherited = 1',
93
        ],
94
    ];
95
96
    public $hasMany = [
97
        'inventories' => [
98
            'Bedard\Shop\Models\Inventory',
99
            'delete' => true,
100
        ],
101
        'options' => [
102
            'Bedard\Shop\Models\Option',
103
            'delete' => true,
104
            'order' => 'sort_order',
105
        ],
106
        'prices' => [
107
            'Bedard\Shop\Models\Price',
108
            'delete' => true,
109
        ],
110
    ];
111
112
    public $hasOne = [
113
        'current_price' => [
114
            'Bedard\Shop\Models\Price',
115
            'scope' => 'isActive',
116
            'order' => 'price asc',
117
        ],
118
    ];
119
120
    /**
121
     * @var array Validation
122
     */
123
    public $rules = [
124
        'name' => 'required',
125
        'base_price' => 'required|numeric|min:0',
126
        'slug' => 'required|unique:bedard_shop_products',
127
    ];
128
129
    /**
130
     * After save.
131
     *
132
     * @return void
133
     */
134
    public function afterSave()
135
    {
136
        $this->saveBasePrice();
137
        $this->saveCategoryRelationships();
138
        $this->saveOptionAndInventoryRelationships();
139
    }
140
141
    /**
142
     * After validate.
143
     *
144
     * @return void
145
     */
146
    public function afterValidate()
147
    {
148
        $this->validateOptions();
149
        $this->validateInventories();
150
    }
151
152
    /**
153
     * Before save.
154
     *
155
     * @return void
156
     */
157
    public function beforeSave()
158
    {
159
        $this->setPlainDescription();
160
    }
161
162
    /**
163
     * Delete inventories that have the is_deleted flag.
164
     *
165
     * @param  array $inventories
166
     * @return array
167
     */
168
    protected function deleteRelatedInventories($inventories)
169
    {
170
        return array_filter($inventories, function ($inventory) {
171
            if ($inventory['id'] !== null && $inventory['is_deleted']) {
172
                Inventory::find($inventory['id'])->delete();
173
            }
174
175
            return ! $inventory['is_deleted'];
176
        });
177
    }
178
179
    /**
180
     * Delete options that have the is_deleted flag.
181
     *
182
     * @param  array $options
183
     * @return array
184
     */
185
    protected function deleteRelatedOptions(array $options)
186
    {
187
        return array_filter($options, function ($option) {
188
            if ($option['id'] !== null && $option['is_deleted']) {
189
                Option::find($option['id'])->delete();
190
            }
191
192
            return ! $option['is_deleted'];
193
        });
194
    }
195
196
    /**
197
     * Delete option values that have the is_deleted flag.
198
     *
199
     * @param  array $optionValues
200
     * @return array
201
     */
202
    protected function deleteRelatedOptionValues(array $optionValues)
203
    {
204
        return array_filter($optionValues, function ($optionValue) {
205
            if ($optionValue['id'] !== null && $optionValue['is_deleted']) {
206
                OptionValue::find($optionValue['id'])->delete();
207
            }
208
209
            return ! $optionValue['is_deleted'];
210
        });
211
    }
212
213
    /**
214
     * Get the categories options.
215
     *
216
     * @return array
217
     */
218
    public function getCategoriesListOptions()
219
    {
220
        return Category::orderBy('name')->lists('name', 'id');
221
    }
222
223
    /**
224
     * Get the categories that are directly related to this product.
225
     *
226
     * @return void
227
     */
228
    public function getCategoriesListAttribute()
229
    {
230
        return $this->categories()->lists('id');
231
    }
232
233
    /**
234
     * Update of create the related base price model.
235
     *
236
     * @return void
237
     */
238
    public function saveBasePrice()
239
    {
240
        Price::updateOrCreate(
241
            ['product_id' => $this->id, 'discount_id' => null],
242
            ['price' => $this->base_price]
243
        );
244
    }
245
246
    /**
247
     * Sync the categories checkboxlist with the category relationship.
248
     *
249
     * @return void
250
     */
251
    public function saveCategoryRelationships()
252
    {
253
        $categoryIds = $this->getOriginalPurgeValue('categoriesList');
254
255
        if (is_array($categoryIds)) {
256
            $this->categories()->sync($categoryIds);
257
        }
258
259
        $this->syncInheritedCategories();
260
    }
261
262
    /**
263
     * Save the options and inventories.
264
     *
265
     * @return void
266
     */
267
    public function saveOptionAndInventoryRelationships()
268
    {
269
        $data = $this->getOriginalPurgeValue('optionsInventories');
270
271
        if (is_array($data['inventories'])) {
272
            $inventories = $data['inventories'];
273
            $inventories = $this->deleteRelatedInventories($inventories);
274
            $this->saveRelatedInventories($inventories);
275
        }
276
277
        if (is_array($data['options'])) {
278
            $options = $data['options'];
279
            $options = $this->deleteRelatedOptions($options);
280
            $this->saveRelatedOptions($options);
281
        }
282
    }
283
284
    /**
285
     * Save an array of realted inventories.
286
     *
287
     * @param  array  $inventories
288
     * @return void
289
     */
290
    protected function saveRelatedInventories(array $inventories)
291
    {
292
        foreach ($inventories as $inventory) {
293
            $model = $inventory['id'] !== null
294
                ? Inventory::firstOrNew(['id' => $inventory['id']])
295
                : new Inventory;
296
297
            $inventory['product_id'] = $this->id;
298
            $model->fill($inventory);
299
            $model->save();
300
            $model->optionValues()->sync($inventory['valueIds']);
301
        }
302
    }
303
304
    /**
305
     * Save an array of related options.
306
     *
307
     * @param  array  $options
308
     * @return array
309
     */
310
    protected function saveRelatedOptions(array $options)
311
    {
312
        foreach ($options as $index => &$option) {
313
            $model = $option['id'] !== null
314
                ? Option::firstOrNew(['id' => $option['id']])
315
                : new Option;
316
317
            $model->fill($option);
318
            $model->product_id = $this->id;
319
            $model->sort_order = $index;
320
321
            $sessionKey = uniqid('session_key', true);
322
            if (is_array($option['values'])) {
323
                $option['values'] = $this->deleteRelatedOptionValues($option['values']);
324
                $this->saveRelatedOptionValues($model, $option['values'], $sessionKey);
325
            }
326
327
            $model->save(null, $sessionKey);
328
        }
329
330
        return $options;
331
    }
332
333
    /**
334
     * Save related option values.
335
     *
336
     * @param  Option $option
337
     * @param  array  $values
338
     * @param  string $sessionKey
339
     * @return void
340
     */
341
    protected function saveRelatedOptionValues(Option &$option, array $values, $sessionKey)
342
    {
343
        foreach ($values as $index => $value) {
344
            $model = array_key_exists('id', $value) && $value['id'] !== null
345
                ? OptionValue::firstOrNew(['id' => $value['id']])
346
                : new OptionValue;
347
348
            $model->fill($value);
349
            $model->sort_order = $index;
350
            $model->save();
351
352
            $option->values()->add($model, $sessionKey);
353
        }
354
    }
355
356
    /**
357
     * Select products appearing in a particular category.
358
     *
359
     * @param  \October\Rain\Database\Builder   $query      
360
     * @param  integer                          $categoryId
361
     * @return \October\Rain\Database\Builder
362
     */
363
    public function scopeAppearingInCategory(Builder $query, $categoryId)
364
    {
365
        return $query->where(function($q) use ($categoryId) {
366
            return $q->whereHas('categories', function($category) use ($categoryId) {
367
                    $category->whereId($categoryId);
368
                })->orWhereHas('inherited_categories', function($category) use ($categoryId) {
369
                    $category->whereId($categoryId);
370
                });
371
        });
372
    }
373
374
375
    /**
376
     * Select products that are enabled.
377
     *
378
     * @param  \October\Rain\Database\Builder   $query
379
     * @return \October\Rain\Database\Builder
380
     */
381
    public function scopeIsEnabled(Builder $query)
382
    {
383
        return $query->whereIsEnabled(true);
384
    }
385
386
    /**
387
     * Select products that are not enabled
388
     *
389
     * @param  \October\Rain\Database\Builder   $query
390
     * @return \October\Rain\Database\Builder
391
     */
392
    public function scopeIsNotEnabled(Builder $query)
393
    {
394
        return $query->whereIsEnabled(false);
395
    }
396
397
    /**
398
     * Left joins a subquery containing the available inventory.
399
     *
400
     * @param  \October\Rain\Database\Builder   $query
401
     * @return \October\Rain\Database\Builder
402
     */
403 View Code Duplication
    public function scopeJoinInventory(Builder $query)
404
    {
405
        $alias = 'inventories';
406
        $grammar = $query->getQuery()->getGrammar();
407
408
        $subquery = Inventory::addSelect('bedard_shop_inventories.product_id')
409
            ->selectRaw('SUM('.$grammar->wrap('bedard_shop_inventories.quantity').') as '.$grammar->wrap('inventory'))
410
            ->groupBy('bedard_shop_inventories.product_id');
411
412
        return $query
413
            ->addSelect($alias.'.inventory')
414
            ->joinSubquery($subquery, $alias, 'bedard_shop_products.id', '=', $alias.'.product_id', 'leftJoin');
415
    }
416
417
    /**
418
     * Left joins a subquery containing the product price.
419
     *
420
     * @param  \October\Rain\Database\Builder   $query
421
     * @return \October\Rain\Database\Builder
422
     */
423 View Code Duplication
    public function scopeJoinPrice(Builder $query)
424
    {
425
        $alias = 'prices';
426
        $grammar = $query->getQuery()->getGrammar();
427
428
        $subquery = Price::isActive()
429
            ->addselect('bedard_shop_prices.product_id')
430
            ->selectRaw('MIN('.$grammar->wrap('bedard_shop_prices.price').') as '.$grammar->wrap('price'))
431
            ->groupBy('bedard_shop_prices.product_id');
432
433
        return $query
434
            ->addSelect($alias.'.price')
435
            ->joinSubquery($subquery, $alias, 'bedard_shop_products.id', '=', $alias.'.product_id');
436
    }
437
438
    /**
439
     * This exists to makes statuses sortable by assigning them a value.
440
     *
441
     * Disabled     -2
442
     * Out of stock -1
443
     * Normal        0
444
     * Discounted    1
445
     *
446
     * @param  \October\Rain\Database\Builder   $query
447
     * @return \October\Rain\Database\Builder
448
     */
449
    public function scopeSelectStatus($query)
450
    {
451
        $grammar = $query->getQuery()->getGrammar();
452
453
        $price = $grammar->wrap('price');
454
        $inventory = $grammar->wrap('inventory');
455
        $is_enabled = $grammar->wrap('bedard_shop_products.is_enabled');
456
        $base_price = $grammar->wrap('bedard_shop_products.base_price');
457
458
        $subquery = 'CASE '.
459
            "WHEN {$is_enabled} = 0 THEN -2 ".
460
            "WHEN ({$inventory} IS NULL or {$inventory} = 0) THEN -1 ".
461
            "WHEN {$price} < {$base_price} THEN 1 ".
462
            'ELSE 0 '.
463
        'END';
464
465
        return $query->selectSubquery($subquery, 'status');
466
    }
467
468
    /**
469
     * Set the plain text description_html.
470
     *
471
     * @return void
472
     */
473
    protected function setPlainDescription()
474
    {
475
        $this->description_plain = strip_tags($this->description_html);
476
    }
477
478
    /**
479
     * Sync the inherited categories of all products.
480
     *
481
     * @return void
482
     */
483
    public static function syncAllInheritedCategories()
484
    {
485
        Queue::push(function ($job) {
486
            $data = [];
487
            $products = Product::with('categories')->get();
488
            $categoryTree = Category::getParentCategoryIds();
489
490
            foreach ($products as $product) {
491
                $inheritedCategoryIds = [];
492
                foreach ($product->categories as $category) {
493
                    if (array_key_exists($category->id, $categoryTree)) {
494
                        $inheritedCategoryIds = array_merge($inheritedCategoryIds, $categoryTree[$category->id]);
495
                    }
496
                }
497
498
                foreach (array_unique($inheritedCategoryIds) as $categoryId) {
499
                    $data[] = [
500
                        'category_id' => $categoryId,
501
                        'product_id' => $product->id,
502
                        'is_inherited' => 1,
503
                    ];
504
                }
505
            }
506
507
            DB::table('bedard_shop_category_product')->whereIsInherited(1)->delete();
508
            DB::table('bedard_shop_category_product')->insert($data);
509
            Discount::syncAllPrices();
510
511
            $job->delete();
512
        });
513
    }
514
515
    /**
516
     * Sync a product with it's inherited categories.
517
     *
518
     * @return void
519
     */
520
    public function syncInheritedCategories()
521
    {
522
        $data = [];
523
        $categoryIds = $this->categories()->lists('id');
524
        $parentIds = Category::isParentOf($categoryIds)->lists('id');
525
        foreach ($parentIds as $parentId) {
526
            $data[] = [
527
                'category_id' => $parentId,
528
                'product_id' => $this->id,
529
                'is_inherited' => true,
530
            ];
531
        }
532
533
        DB::table('bedard_shop_category_product')->whereProductId($this->id)->whereIsInherited(1)->delete();
534
        DB::table('bedard_shop_category_product')->insert($data);
535
536
        $categoryScope = array_merge($categoryIds, $parentIds);
537
        Discount::syncProductPrice($this, $categoryScope);
538
    }
539
540
    /**
541
     * Validate inventories.
542
     *
543
     * @throws \October\Rain\Database\ModelException
544
     * @return void
545
     */
546
    protected function validateInventories()
547
    {
548
        if (! is_array($this->optionsInventories) ||
549
            ! is_array($this->optionsInventories['options'])) {
550
            return;
551
        }
552
553
        $takenValueCombinations = [];
554
        foreach ($this->optionsInventories['inventories'] as $inventory) {
555
            // validate the inventory
556
            $model = new Inventory($inventory);
557
            $model->validate();
558
559
            // validate that the value combinations are unique
560
            sort($inventory['valueIds']);
561
            $valueCombination = json_encode($inventory['valueIds']);
562
563
            if (in_array($valueCombination, $takenValueCombinations) && ! $inventory['is_deleted']) {
564
                Flash::error(Lang::get('bedard.shop::lang.products.form.duplicate_inventories_error'));
565
                throw new ModelException($this);
566
            }
567
568
            $takenValueCombinations[] = $valueCombination;
569
        }
570
    }
571
572
    /**
573
     * Validate options.
574
     *
575
     * @throws \October\Rain\Database\ModelException
576
     * @return void
577
     */
578
    protected function validateOptions()
579
    {
580
        if (! is_array($this->optionsInventories) ||
581
            ! is_array($this->optionsInventories['options'])) {
582
            return;
583
        }
584
585
        $names = [];
586
        foreach ($this->optionsInventories['options'] as $option) {
587
            // validate the option
588
            $model = new Option($option);
589
            $model->validate();
590
591
            // validate that names are unique
592
            $name = strtolower(trim($option['name']));
593
594
            if (in_array($name, $names)) {
595
                Flash::error(Lang::get('bedard.shop::lang.products.form.duplicate_options_error'));
596
                throw new ModelException($this);
597
            }
598
599
            $names[] = $name;
600
        }
601
    }
602
}
603