Product::saveOptions()   B
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 10
Ratio 66.67 %

Importance

Changes 0
Metric Value
dl 10
loc 15
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 4
nop 1
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'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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');
0 ignored issues
show
Unused Code introduced by
$price is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
356
        $inventory = $grammar->wrap('inventory');
0 ignored issues
show
Unused Code introduced by
$inventory is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
357
        $is_enabled = $grammar->wrap('bedard_shop_products.is_enabled');
358
        $base_price = $grammar->wrap('bedard_shop_products.base_price');
0 ignored issues
show
Unused Code introduced by
$base_price is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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