Completed
Push — master ( 8a92d6...46e7ad )
by Scott
02:08
created

Product::saveRelatedOptions()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 22
rs 8.9197
cc 4
eloc 14
nc 5
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
    ];
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()
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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()
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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