Passed
Pull Request — master (#735)
by Matt
03:17
created

Variation   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 152
c 4
b 0
f 0
dl 0
loc 322
rs 9.76
wmc 33

12 Methods

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

317
        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...
318
    }
319
320
    /**
321
     * Returns a link to the parent product of this variation (variations don't have their own pages)
322
     *
323
     * @param $action string
324
     *
325
     * @return string
326
     */
327
    public function Link($action = null) {
328
        return ($this->ProductID) ? $this->Product()->Link($action) : false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->ProductID ...->Link($action) : false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
329
    }
330
331
    public function createItem($quantity = 1, $filter = array())
332
    {
333
        $orderitem = self::config()->order_item;
334
        $item = new $orderitem();
335
        $item->ProductID = $this->ProductID;
336
        $item->ProductVariationID = $this->ID;
337
        //$item->ProductVariationVersion = $this->Version;
338
        if ($filter) {
339
            //TODO: make this a bit safer, perhaps intersect with allowed fields
340
            $item->update($filter);
341
        }
342
        $item->Quantity = $quantity;
343
        return $item;
344
    }
345
346
    public function sellingPrice()
347
    {
348
        $price = $this->Price;
349
        $this->extend('updateSellingPrice', $price);
350
351
        //prevent negative values
352
        $price = $price < 0 ? 0 : $price;
353
354
        // NOTE: Ideally, this would be dependent on the locale but as of
355
        // now the Silverstripe Currency field type has 2 hardcoded all over
356
        // the place. In the mean time there is an issue where the displayed
357
        // unit price can not exactly equal the multiplied price on an order
358
        // (i.e. if the calculated price is 3.145 it will display as 3.15.
359
        // so if I put 10 of them in my cart I will expect the price to be
360
        // 31.50 not 31.45).
361
        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...
362
    }
363
}
364