Completed
Push — master ( ddb968...80e524 )
by Scott
02:15
created

Category::applyCustomOrder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
1
<?php namespace Bedard\Shop\Models;
2
3
use Lang;
4
use Model;
5
use October\Rain\Database\Builder;
6
7
/**
8
 * Category Model.
9
 */
10
class Category extends Model
11
{
12
    use \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_categories';
19
20
    /**
21
     * @var array Default attributes
22
     */
23
    public $attributes = [
24
        'description_html' => '',
25
        'description_plain' => '',
26
        'product_columns' => 4,
27
        'product_order' => '',
28
        'product_rows' => 3,
29
        'product_sort' => 'created_at:desc',
30
    ];
31
32
    /**
33
     * @var array Attribute casting
34
     */
35
    protected $casts = [
36
        'is_active' => 'boolean',
37
        'is_visible' => 'boolean',
38
    ];
39
40
    /**
41
     * @var array Guarded fields
42
     */
43
    protected $guarded = ['*'];
44
45
    /**
46
     * @var array Jsonable fields
47
     */
48
    protected $jsonable = [
49
        'product_order',
50
    ];
51
52
    /**
53
     * @var array Fillable fields
54
     */
55
    protected $fillable = [
56
        'is_active',
57
        'is_visible',
58
        'category_filters',
59
        'description_html',
60
        'description_plain',
61
        'name',
62
        'parent_id',
63
        'product_order',
64
        'product_sort',
65
        'product_sort_column',
66
        'product_sort_direction',
67
        'slug',
68
    ];
69
70
    /**
71
     * @var array Purgeable fields
72
     */
73
    protected $purgeable = [
74
        'category_filters',
75
        'filters_list',
76
        'product_sort',
77
    ];
78
79
    /**
80
     * @var array Relations
81
     */
82
    public $attachMany = [
83
        'thumbnails' => 'System\Models\File',
84
    ];
85
86
    public $belongsTo = [
87
        'parent' => 'Bedard\Shop\Models\Category',
88
    ];
89
90
    public $belongsToMany = [
91
        'discounts' => [
92
            'Bedard\Shop\Models\Discount',
93
            'table' => 'bedard_shop_category_discount',
94
        ],
95
        'products' => [
96
            'Bedard\Shop\Models\Product',
97
            'table' => 'bedard_shop_category_product',
98
            'pivot' => ['is_inherited'],
99
        ],
100
    ];
101
102
    public $hasMany = [
103
        'filters' => [
104
            'Bedard\Shop\Models\Filter',
105
        ],
106
    ];
107
108
    /**
109
     * @var array Validation
110
     */
111
    public $rules = [
112
        'name' => 'required',
113
        'product_columns' => 'required|integer|min:1',
114
        'product_rows' => 'required|integer|min:0',
115
        'slug' => 'required|unique:bedard_shop_categories',
116
    ];
117
118
    /**
119
     * After delete.
120
     *
121
     * @return void
122
     */
123
    public function afterDelete()
124
    {
125
        $this->syncInheritedProductsAfterDelete();
126
    }
127
128
    /**
129
     * After save.
130
     *
131
     * @return void
132
     */
133
    public function afterSave()
134
    {
135
        $this->saveFiltersRelationship();
136
        $this->syncInheritedProductsAfterSave();
137
    }
138
139
    protected function applyCustomOrder(Builder &$products)
140
    {
141
        $customOrder = "";
142
        foreach ($this->product_order as $index => $id) {
143
            $customOrder .= "when {$id} then {$index} ";
144
        }
145
146
        $products->orderByRaw("case id {$customOrder} else 'last' end");
147
    }
148
149
    /**
150
     * Apply filters to a products query.
151
     *
152
     * @param  \October\Rain\Database\Builder   $products
153
     * @return void
154
     */
155
    protected function applyProductFilters(Builder &$products)
156
    {
157
        $products->where(function ($query) {
158
            foreach ($this->filters as $filter) {
159
                $right = $filter->getRightClause();
160
                $query->where($filter->left, $filter->comparator, $right);
161
            }
162
        });
163
    }
164
165
    /**
166
     * Before save.
167
     *
168
     * @return void
169
     */
170
    public function beforeSave()
171
    {
172
        $this->setPlainDescription();
173
        $this->setNullParentId();
174
        $this->setProductSort();
175
    }
176
177
    /**
178
     * Delete filters that have the is_deleted flag.
179
     *
180
     * @param  array $filters
181
     * @return array
182
     */
183
    protected function deleteRelatedFilters(array $filters)
184
    {
185
        return array_filter($filters, function ($filter) {
186
            if ($filter['id'] !== null && $filter['is_deleted']) {
187
                Filter::find($filter['id'])->delete();
188
            }
189
190
            return ! $filter['is_deleted'];
191
        });
192
    }
193
194
    /**
195
     * Get the parent IDs of every category.
196
     *
197
     * @return array
198
     */
199
    public static function getParentCategoryIds()
200
    {
201
        $categories = self::select('id', 'parent_id')->get();
202
203
        $tree = [];
204
        foreach ($categories as $category) {
205
            $tree[$category->id] = self::walkParentCategories($categories, $category);
206
        }
207
208
        return $tree;
209
    }
210
211
    /**
212
     * Find the child ids of a parent category.
213
     *
214
     * @param  \Bedard\Shop\Models\Category|int     $parent
215
     * @param  \October\Rain\Database\Collection    $categories
216
     * @return array
217
     */
218
    public static function getChildIds($parent, \October\Rain\Database\Collection $categories = null)
219
    {
220
        if ($categories === null) {
221
            $categories = self::whereNotNull('parent_id')
222
                ->where('id', '<>', $parent)
223
                ->select('id', 'parent_id')
224
                ->get();
225
        }
226
227
        $children = [];
228
        foreach ($categories as $category) {
229
            if ($category->parent_id == $parent) {
230
                $children[] = $category->id;
231
                $children = array_merge($children, self::getChildIds($category->id, $categories));
232
            }
233
        }
234
235
        return $children;
236
    }
237
238
    public static function getParentIds($children, \October\Rain\Database\Collection $categories = null)
239
    {
240
        if (! is_array($children)) {
241
            $children = [$children];
242
        }
243
244
        if ($categories === null) {
245
            $categories = self::select('id', 'parent_id')->get();
246
        }
247
248
        $parents = [];
249
        foreach ($children as $child) {
250
            $category = $categories->filter(function ($model) use ($child) {
251
                return $model->id == $child;
252
            })->first();
253
254
            while ($category && $category->parent_id) {
255
                $parents[] = $category->parent_id;
256
                $category = $categories->filter(function ($model) use ($category) {
257
                    return $model->id == $category->parent_id;
258
                })->first();
259
            }
260
        }
261
262
        return array_unique($parents);
263
    }
264
265
    /**
266
     * Get options for the parent form field.
267
     *
268
     * @return array
269
     */
270
    public function getParentIdOptions()
271
    {
272
        $options = $this->exists
273
            ? self::where('id', '<>', $this->id)->isNotChildOf($this->id)->orderBy('name')->lists('name', 'id')
274
            : self::orderBy('name')->lists('name', 'id');
275
276
        array_unshift($options, '<em>'.Lang::get('bedard.shop::lang.categories.form.no_parent').'</em>');
277
278
        return $options;
279
    }
280
281
    /**
282
     * Query a category's products.
283
     *
284
     * @param  array $params
285
     * @return \October\Rain\Database\Collection
286
     */
287
    public function getProducts(array $params = [])
288
    {
289
        $products = Product::isEnabled();
290
291
        // select the appropriate columns
292
        if (array_key_exists('products_select', $params) && $params['products_select']) {
293
            $products->select($params['products_select']);
294
295
            if (in_array('price', $params['products_select'])) {
296
                $products->joinPrice();
297
            }
298
        }
299
300
        // grab products from filters or relationship
301
        if ($this->isFiltered()) {
302
            $this->applyProductFilters($products);
303
        } else {
304
            $products->appearingInCategory($this->id);
305
        }
306
307
        // sort the results
308
        if (array_key_exists('products_sort_column', $params) &&
309
            array_key_exists('products_sort_direction', $params)) {
310
            $products->orderBy($params['products_sort_column'], $params['products_sort_direction']);
311
        } elseif ($this->isCustomSorted()) {
312
            $this->applyCustomOrder($products);
313
        } else {
314
            $products->orderBy($this->product_sort_column, $this->product_sort_direction);
315
        }
316
317
        return $products->get();
318
    }
319
320
    /**
321
     * Determine if the category has a custom sort.
322
     *
323
     * @return bool
324
     */
325
    public function isCustomSorted()
326
    {
327
        return ! $this->product_sort_column && ! $this->product_sort_direction;
328
    }
329
330
    /**
331
     * Determine if the category is filtered.
332
     *
333
     * @return bool
334
     */
335
    public function isFiltered()
336
    {
337
        return $this->filters->count() > 0;
338
    }
339
340
    /**
341
     * Save filters relationship.
342
     *
343
     * @return void
344
     */
345
    protected function saveFiltersRelationship()
346
    {
347
        $filters = $this->getOriginalPurgeValue('category_filters');
348
349
        if (is_array($filters) && ! empty($filters)) {
350
            $filters = $this->deleteRelatedFilters($filters);
351
            $this->saveRelatedFilters($filters);
352
        }
353
    }
354
355
    /**
356
     * Save related filters.
357
     *
358
     * @param  array  $filters
359
     * @return void
360
     */
361
    protected function saveRelatedFilters(array $filters)
362
    {
363
        foreach ($filters as $filter) {
364
            $model = $filter['id'] !== null
365
                ? Filter::firstOrNew(['id' => $filter['id']])
366
                : new Filter;
367
368
            $filter['category_id'] = $this->id;
369
            $model->fill($filter);
370
            $model->save();
371
        }
372
    }
373
374
    /**
375
     * Query categories that are listed as active.
376
     *
377
     * @param  \October\Rain\Database\Builder   $query
378
     * @return \October\Rain\Database\Builder
379
     */
380
    public function scopeIsActive($query)
381
    {
382
        return $query->whereIsActive(true);
383
    }
384
385
    /**
386
     * Query categories that are listed as not active.
387
     *
388
     * @param  \October\Rain\Database\Builder   $query
389
     * @return \October\Rain\Database\Builder
390
     */
391
    public function scopeIsNotActive($query)
392
    {
393
        return $query->whereIsActive(false);
394
    }
395
396
    /**
397
     * Query categories that are children of another category.
398
     *
399
     * @param  \October\Rain\Database\Builder   $query
400
     * @param  \Bedard\Shop\Models\Category|int $parent
401
     * @return \October\Rain\Database\Builder
402
     */
403
    public function scopeIsChildOf($query, $parent)
404
    {
405
        return $query->whereIn('id', self::getChildIds($parent));
406
    }
407
408
    /**
409
     * Query categories that are not children of another category.
410
     *
411
     * @param  \October\Rain\Database\Builder   $query
412
     * @param  \Bedard\Shop\Models\Category|int $parent
413
     * @return \October\Rain\Database\Builder
414
     */
415
    public function scopeIsNotChildOf($query, $parent)
416
    {
417
        return $query->whereNotIn('id', self::getChildIds($parent));
418
    }
419
420
    /**
421
     * Query categories that are a parent of another category.
422
     *
423
     * @param  \October\Rain\Database\Builder   $query
424
     * @param  int                              $child
425
     * @return \October\Rain\Database\Builder
426
     */
427
    public function scopeIsParentOf($query, $child)
428
    {
429
        return $query->whereIn('id', self::getParentIds($child));
430
    }
431
432
    /**
433
     * Query categories that are not a parent of another category.
434
     *
435
     * @param  \October\Rain\Database\Builder   $query
436
     * @param  int                              $child
437
     * @return \October\Rain\Database\Builder
438
     */
439
    public function scopeIsNotParentOf($query, $child)
440
    {
441
        return $query->whereNotIn('id', self::getParentIds($child));
442
    }
443
444
    /**
445
     * Convert falsey parent id values to null.
446
     *
447
     * @return void
448
     */
449
    protected function setNullParentId()
450
    {
451
        if (! $this->parent_id) {
452
            $this->parent_id = null;
453
        }
454
    }
455
456
    /**
457
     * Set the plain text description_html.
458
     *
459
     * @return void
460
     */
461
    protected function setPlainDescription()
462
    {
463
        $this->description_plain = strip_tags($this->description_html);
464
    }
465
466
    /**
467
     * Set the product sort.
468
     *
469
     * @return void
470
     */
471
    protected function setProductSort()
472
    {
473
        $sort = $this->getOriginalPurgeValue('product_sort');
474
475
        if ($sort === 'custom') {
476
            $this->product_sort_column = null;
477
            $this->product_sort_direction = null;
478
        } else {
479
            $parts = explode(':', $this->getOriginalPurgeValue('product_sort'));
480
            $this->product_sort_column = $parts[0];
481
            $this->product_sort_direction = $parts[1];
482
        }
483
    }
484
485
    /**
486
     * If the model wasn't deleted in bulk, sync inherited categories.
487
     *
488
     * @return void
489
     */
490
    public function syncInheritedProductsAfterDelete()
491
    {
492
        if (! $this->dontSyncAfterDelete) {
493
            Product::syncAllInheritedCategories();
494
        }
495
    }
496
497
    /**
498
     * If the parent id has changed, resync all products.
499
     *
500
     * @return void
501
     */
502
    public function syncInheritedProductsAfterSave()
503
    {
504
        if ($this->isDirty('parent_id')) {
505
            Product::syncAllInheritedCategories();
506
        }
507
    }
508
509
    /**
510
     * Itterate over categories and update them with the given values.
511
     *
512
     * @param  array    $categories
513
     * @return void
514
     */
515
    public static function updateMany(array $categories)
516
    {
517
        foreach ($categories as $category) {
518
            $id = $category['id'];
519
            unset($category['id']);
520
            self::whereId($id)->update($category);
521
        }
522
    }
523
524
    /**
525
     * Walk up the category tree gathering parent IDs.
526
     *
527
     * @param  \October\Rain\Database\Collection    $categories     All categories
528
     * @param  \Bedard\Shop\Models\Category         $category       Current category being walked over
529
     * @param  array                                $tree           Tree of parent IDs
530
     * @return arrau
531
     */
532
    public static function walkParentCategories($categories, $category, $tree = [])
533
    {
534
        if ($category && $category->parent_id !== null) {
535
            $tree[] = $category->parent_id;
536
            $tree = array_merge($tree, self::walkParentCategories($categories, $categories->find($category->parent_id)));
537
        }
538
539
        return $tree;
540
    }
541
}
542