Variation   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 153
c 5
b 0
f 0
dl 0
loc 324
rs 9.76
wmc 33

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getCategoryIDs() 0 3 2
A getTitle() 0 18 4
A getCategories() 0 3 2
A addLink() 0 3 1
A Item() 0 11 2
A IsInCart() 0 3 2
A canPurchase() 0 11 4
A onBeforeWrite() 0 12 5
B getCMSFields() 0 101 5
A Link() 0 3 2
A createItem() 0 13 2
A sellingPrice() 0 16 2
1
<?php
2
3
namespace SilverShop\Model\Variation;
4
5
use SilverShop\Cart\ShoppingCart;
6
use SilverShop\Model\Buyable;
7
use SilverShop\Model\Order;
8
use SilverShop\Page\Product;
9
use SilverStripe\AssetAdmin\Forms\UploadField;
10
use SilverStripe\Assets\Image;
11
use SilverStripe\Forms\FieldList;
12
use SilverStripe\Forms\LiteralField;
13
use SilverStripe\Forms\TextField;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\ORM\FieldType\DBCurrency;
17
use SilverStripe\ORM\FieldType\DBDecimal;
18
use SilverStripe\ORM\ManyManyList;
19
use SilverStripe\Versioned\Versioned;
20
21
/**
22
 * Product Variation
23
 *
24
 * Provides a means for specifying many variations on a product.
25
 * Used in combination with ProductAttributes, such as color, size.
26
 * A variation will specify one particular combination, such as red, and large.
27
 *
28
 * @property string $InternalItemID
29
 * @property DBCurrency $Price
30
 * @property DBDecimal $Weight
31
 * @property DBDecimal $Height
32
 * @property DBDecimal $Width
33
 * @property DBDecimal $Depth
34
 * @property int $ProductID
35
 * @property int $ImageID
36
 * @method   Product Product()
37
 * @method   Image Image()
38
 * @method   AttributeValue[]|ManyManyList AttributeValues()
39
 */
40
class Variation extends DataObject implements Buyable
41
{
42
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
43
        'Sort' => 'Int',
44
        'InternalItemID' => 'Varchar(30)',
45
        'Price' => 'Currency(19,4)',
46
47
        //physical properties
48
        //TODO: Move to an extension
49
        'Weight' => 'Decimal(12,5)',
50
        'Height' => 'Decimal(12,5)',
51
        'Width' => 'Decimal(12,5)',
52
        'Depth' => 'Decimal(12,5)'
53
    ];
54
55
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
56
        'Product' => Product::class,
57
        'Image' => Image::class
58
    ];
59
60
    private static $owns = [
0 ignored issues
show
introduced by
The private property $owns is not used, and could be removed.
Loading history...
61
        'Image'
62
    ];
63
64
    private static $many_many = [
0 ignored issues
show
introduced by
The private property $many_many is not used, and could be removed.
Loading history...
65
        'AttributeValues' => AttributeValue::class
66
    ];
67
68
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
69
        'Title' => 'Text',
70
        'Price' => 'Currency'
71
    ];
72
73
    private static $versioning = [
0 ignored issues
show
introduced by
The private property $versioning is not used, and could be removed.
Loading history...
74
        'Live'
75
    ];
76
77
    private static $extensions = [
0 ignored issues
show
introduced by
The private property $extensions is not used, and could be removed.
Loading history...
78
        Versioned::class . '.versioned'
79
    ];
80
81
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
82
        'InternalItemID' => 'Product Code',
83
        //'Product.Title' => 'Product',
84
        'Title' => 'Variation',
85
        'Price' => 'Price'
86
    ];
87
88
    private static $searchable_fields = [
0 ignored issues
show
introduced by
The private property $searchable_fields is not used, and could be removed.
Loading history...
89
        'Product.Title',
90
        'InternalItemID'
91
    ];
92
93
    private static $indexes = [
0 ignored issues
show
introduced by
The private property $indexes is not used, and could be removed.
Loading history...
94
        'InternalItemID' => true,
95
        'LastEdited' => true
96
    ];
97
98
    private static $singular_name = 'Variation';
0 ignored issues
show
introduced by
The private property $singular_name is not used, and could be removed.
Loading history...
99
100
    private static $plural_name = 'Variations';
0 ignored issues
show
introduced by
The private property $plural_name is not used, and could be removed.
Loading history...
101
102
    private static $default_sort = 'InternalItemID';
0 ignored issues
show
introduced by
The private property $default_sort is not used, and could be removed.
Loading history...
103
104
    private static $order_item = OrderItem::class;
105
106
    private static $table_name = 'SilverShop_Variation';
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
107
108
    /**
109
     * @config
110
     * @var bool
111
     */
112
    private static $title_has_label = true;
113
114
    /**
115
     * @config
116
     * @var string
117
     */
118
    private static $title_separator = ':';
119
120
    /**
121
     * @config
122
     * @var string
123
     */
124
    private static $title_glue = ', ';
125
126
    public function getCMSFields()
127
    {
128
        $fields = FieldList::create(
129
            TextField::create('InternalItemID', _t('SilverShop\Page\Product.Code', 'Product Code')),
130
            TextField::create('Price', _t('SilverShop\Page\Product.db_BasePrice', 'Price'))
131
        );
132
        //add attributes dropdowns
133
        $attributes = $this->Product()->VariationAttributeTypes();
134
        if ($attributes->exists()) {
135
            foreach ($attributes as $attribute) {
136
                if ($field = $attribute->getDropDownField()) {
137
                    if ($value = $this->AttributeValues()->find('TypeID', $attribute->ID)) {
138
                        $field->setValue($value->ID);
139
                    }
140
                    $fields->push($field);
141
                } else {
142
                    $fields->push(
143
                        LiteralField::create(
144
                            'novalues' . $attribute->Name,
145
                            '<p class="message warning">' .
146
                            _t(
147
                                __CLASS__ . '.NoAttributeValuesMessage',
148
                                '{attribute} has no values to choose from. You can create them in the "Products" &#62; "Product Attribute Type" section of the CMS.',
149
                                'Warning that will be shown if an attribute doesn\'t have any values',
150
                                ['attribute' => $attribute->Name]
151
                            ) .
152
                            '</p>'
153
                        )
154
                    );
155
                }
156
                //TODO: allow setting custom values here, rather than visiting the products section
157
            }
158
        } else {
159
            $fields->push(
160
                LiteralField::create(
161
                    'savefirst',
162
                    '<p class="message warning">' .
163
                    _t(
164
                        __CLASS__ . '.MustSaveFirstMessage',
165
                        'You can choose variation attributes after saving for the first time, if they exist.'
166
                    ) .
167
                    '</p>'
168
                )
169
            );
170
        }
171
        $fields->push(
172
            UploadField::create('Image', _t('SilverShop\Page\Product.Image', 'Product Image'))
173
        );
174
175
        //physical measurement units
176
        $fieldSubstitutes = [
177
            'LengthUnit' => Product::config()->length_unit
178
        ];
179
180
        //physical measurements
181
        $fields->push(
182
            TextField::create(
183
                'Weight',
184
                _t(
185
                    'SilverShop\Page\Product.WeightWithUnit',
186
                    'Weight ({WeightUnit})',
187
                    '',
188
                    [
189
                        'WeightUnit' => Product::config()->weight_unit
190
                    ]
191
                ),
192
                '',
193
                12
194
            )
195
        );
196
197
        $fields->push(
198
            TextField::create(
199
                'Height',
200
                _t('SilverShop\Page\Product.HeightWithUnit', 'Height ({LengthUnit})', '', $fieldSubstitutes),
201
                '',
202
                12
203
            )
204
        );
205
206
        $fields->push(
207
            TextField::create(
208
                'Width',
209
                _t('SilverShop\Page\Product.WidthWithUnit', 'Width ({LengthUnit})', '', $fieldSubstitutes),
210
                '',
211
                12
212
            )
213
        );
214
215
        $fields->push(
216
            TextField::create(
217
                'Depth',
218
                _t('SilverShop\Page\Product.DepthWithUnit', 'Depth ({LengthUnit})', '', $fieldSubstitutes),
219
                '',
220
                12
221
            )
222
        );
223
224
        $this->extend('updateCMSFields', $fields);
225
226
        return $fields;
227
    }
228
229
    /**
230
     * Save selected attributes - somewhat of a hack.
231
     */
232
    public function onBeforeWrite()
233
    {
234
        parent::onBeforeWrite();
235
236
        if (isset($_POST['ProductAttributes']) && is_array($_POST['ProductAttributes'])) {
237
            $this->AttributeValues()->setByIDList(array_values($_POST['ProductAttributes']));
238
        }
239
240
        $img = $this->Image();
241
242
        if ($img && $img->exists()) {
243
            $img->doPublish();
244
        }
245
    }
246
247
    public function getTitle()
248
    {
249
        $values = $this->AttributeValues();
250
        if ($values->exists()) {
251
            $labelvalues = [];
252
            foreach ($values as $value) {
253
                if (self::config()->title_has_label) {
254
                    $labelvalues[] = $value->Type()->Label . self::config()->title_separator . $value->Value;
255
                } else {
256
                    $labelvalues[] = $value->Value;
257
                }
258
            }
259
260
            $title = implode(self::config()->title_glue, $labelvalues);
261
        }
262
        $this->extend('updateTitle', $title);
263
264
        return $title;
265
    }
266
267
    public function getCategoryIDs()
268
    {
269
        return $this->Product() ? $this->Product()->getCategoryIDs() : [];
270
    }
271
272
    public function getCategories()
273
    {
274
        return $this->Product() ? $this->Product()->getCategories() : ArrayList::create();
275
    }
276
277
    public function canPurchase($member = null, $quantity = 1)
278
    {
279
        $allowpurchase = false;
280
        if ($product = $this->Product()) {
281
            $allowpurchase =
282
                ($this->sellingPrice() > 0 || Product::config()->allow_zero_price) && $product->AllowPurchase;
283
        }
284
285
        $permissions = $this->extend('canPurchase', $member, $quantity);
286
        $permissions[] = $allowpurchase;
287
        return min($permissions);
288
    }
289
290
    /*
291
     * Returns if the product variation is already in the shopping cart.
292
     * @return boolean
293
     */
294
    public function IsInCart()
295
    {
296
        return $this->Item() && $this->Item()->Quantity > 0;
297
    }
298
299
    /*
300
     * Returns the order item which contains the product variation
301
     * @return  OrderItem
302
     */
303
    public function Item()
304
    {
305
        $filter = [];
306
        $this->extend('updateItemFilter', $filter);
307
        $item = ShoppingCart::singleton()->get($this, $filter);
308
        if (!$item) {
309
            //return dummy item so that we can still make use of Item
310
            $item = $this->createItem(0);
311
        }
312
        $this->extend('updateDummyItem', $item);
313
        return $item;
314
    }
315
316
    public function addLink()
317
    {
318
        return $this->Item()->addLink($this->ProductID, $this->ID);
0 ignored issues
show
Unused Code introduced by
The call to SilverShop\Model\OrderItem::addLink() has too many arguments starting with $this->ProductID. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

318
        return $this->Item()->/** @scrutinizer ignore-call */ addLink($this->ProductID, $this->ID);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
319
    }
320
321
    /**
322
     * Returns a link to the parent product of this variation (variations don't have their own pages)
323
     *
324
     * @param $action string
325
     *
326
     * @return string|false
327
     */
328
    public function Link($action = null)
329
    {
330
        return ($this->ProductID) ? $this->Product()->Link($action) : false;
331
    }
332
333
    public function createItem($quantity = 1, $filter = [])
334
    {
335
        $orderitem = self::config()->order_item;
336
        $item = new $orderitem();
337
        $item->ProductID = $this->ProductID;
338
        $item->ProductVariationID = $this->ID;
339
        //$item->ProductVariationVersion = $this->Version;
340
        if ($filter) {
341
            //TODO: make this a bit safer, perhaps intersect with allowed fields
342
            $item->update($filter);
343
        }
344
        $item->Quantity = $quantity;
345
        return $item;
346
    }
347
348
    public function sellingPrice()
349
    {
350
        $price = $this->Price;
351
        $this->extend('updateSellingPrice', $price);
352
353
        //prevent negative values
354
        $price = $price < 0 ? 0 : $price;
355
356
        // NOTE: Ideally, this would be dependent on the locale but as of
357
        // now the Silverstripe Currency field type has 2 hardcoded all over
358
        // the place. In the mean time there is an issue where the displayed
359
        // unit price can not exactly equal the multiplied price on an order
360
        // (i.e. if the calculated price is 3.145 it will display as 3.15.
361
        // so if I put 10 of them in my cart I will expect the price to be
362
        // 31.50 not 31.45).
363
        return round($price, Order::config()->rounding_precision);
0 ignored issues
show
Bug Best Practice introduced by
The expression return round($price, Sil...()->rounding_precision) returns the type double which is incompatible with the return type mandated by SilverShop\Model\Buyable::sellingPrice() of SilverShop\ORM\FieldType\ShopCurrency.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
364
    }
365
}
366