Total Complexity | 43 |
Total Lines | 454 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Product often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Product, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
56 | class Product extends Page implements Buyable |
||
57 | { |
||
58 | private static $db = [ |
||
|
|||
59 | 'InternalItemID' => 'Varchar(30)', //ie SKU, ProductID etc (internal / existing recognition of product) |
||
60 | 'Model' => 'Varchar(30)', |
||
61 | |||
62 | 'BasePrice' => 'Currency(19,4)', // Base retail price the item is marked at. |
||
63 | |||
64 | //physical properties |
||
65 | // TODO: Move these to an extension (used in Variations as well) |
||
66 | 'Weight' => 'Decimal(12,5)', |
||
67 | 'Height' => 'Decimal(12,5)', |
||
68 | 'Width' => 'Decimal(12,5)', |
||
69 | 'Depth' => 'Decimal(12,5)', |
||
70 | |||
71 | 'Featured' => 'Boolean', |
||
72 | 'AllowPurchase' => 'Boolean', |
||
73 | |||
74 | 'Popularity' => 'Float' //storage for CalculateProductPopularity task |
||
75 | ]; |
||
76 | |||
77 | private static $has_one = [ |
||
78 | 'Image' => Image::class, |
||
79 | ]; |
||
80 | |||
81 | private static $owns = [ |
||
82 | 'Image' |
||
83 | ]; |
||
84 | |||
85 | private static $many_many = [ |
||
86 | 'ProductCategories' => ProductCategory::class, |
||
87 | ]; |
||
88 | |||
89 | private static $defaults = [ |
||
90 | 'AllowPurchase' => true, |
||
91 | 'ShowInMenus' => false, |
||
92 | ]; |
||
93 | |||
94 | private static $casting = [ |
||
95 | 'Price' => 'Currency', |
||
96 | ]; |
||
97 | |||
98 | private static $summary_fields = [ |
||
99 | 'InternalItemID', |
||
100 | 'Title', |
||
101 | 'BasePrice.NiceOrEmpty', |
||
102 | 'IsPurchaseable.Nice', |
||
103 | ]; |
||
104 | |||
105 | private static $searchable_fields = [ |
||
106 | 'InternalItemID', |
||
107 | 'Title', |
||
108 | 'Featured', |
||
109 | ]; |
||
110 | |||
111 | private static $table_name = 'SilverShop_Product'; |
||
112 | |||
113 | private static $singular_name = 'Product'; |
||
114 | |||
115 | private static $plural_name = 'Products'; |
||
116 | |||
117 | private static $icon = 'silvershop/core: client/dist/images/icons/package.gif'; |
||
118 | |||
119 | private static $default_parent = ProductCategory::class; |
||
120 | |||
121 | private static $default_sort = '"Title" ASC'; |
||
122 | |||
123 | private static $global_allow_purchase = true; |
||
124 | |||
125 | private static $allow_zero_price = false; |
||
126 | |||
127 | private static $order_item = OrderItem::class; |
||
128 | |||
129 | // Physical Measurement |
||
130 | private static $weight_unit = 'kg'; |
||
131 | |||
132 | private static $length_unit = 'cm'; |
||
133 | |||
134 | private static $indexes = [ |
||
135 | 'Featured' => true, |
||
136 | 'AllowPurchase' => true, |
||
137 | 'InternalItemID' => true, |
||
138 | ]; |
||
139 | |||
140 | /** |
||
141 | * Add product fields to CMS |
||
142 | * |
||
143 | * @return FieldList updated field list |
||
144 | */ |
||
145 | public function getCMSFields() |
||
146 | { |
||
147 | $self = $this; |
||
148 | |||
149 | $this->beforeUpdateCMSFields( |
||
150 | function (FieldList $fields) use ($self) { |
||
151 | $fields->fieldByName('Root.Main.Title') |
||
152 | ->setTitle(_t(__CLASS__ . '.PageTitle', 'Product Title')); |
||
153 | |||
154 | $fields->addFieldsToTab( |
||
155 | 'Root.Main', |
||
156 | [ |
||
157 | TextField::create('InternalItemID', _t(__CLASS__ . '.InternalItemID', 'Product Code/SKU'), '', 30), |
||
158 | DropdownField::create('ParentID', _t(__CLASS__ . '.Category', 'Category'), $self->getCategoryOptions()) |
||
159 | ->setDescription(_t(__CLASS__ . '.CategoryDescription', 'This is the parent page or default category.')), |
||
160 | TextField::create('Model', _t(__CLASS__ . '.Model', 'Model'), '', 30), |
||
161 | CheckboxField::create('Featured', _t(__CLASS__ . '.Featured', 'Featured Product')), |
||
162 | CheckboxField::create('AllowPurchase', _t(__CLASS__ . '.AllowPurchase', 'Allow product to be purchased'), 1), |
||
163 | ] |
||
164 | ); |
||
165 | |||
166 | $fields->addFieldsToTab( |
||
167 | 'Root.Pricing', |
||
168 | [ |
||
169 | TextField::create('BasePrice', $this->fieldLabel('BasePrice')) |
||
170 | ->setDescription(_t(__CLASS__ . '.PriceDesc', 'Base price to sell this product at.')) |
||
171 | ->setMaxLength(12), |
||
172 | ] |
||
173 | ); |
||
174 | |||
175 | $fieldSubstitutes = [ |
||
176 | 'LengthUnit' => $self::config()->length_unit |
||
177 | ]; |
||
178 | |||
179 | $fields->addFieldsToTab( |
||
180 | 'Root.Shipping', |
||
181 | [ |
||
182 | TextField::create( |
||
183 | 'Weight', |
||
184 | _t( |
||
185 | __CLASS__ . '.WeightWithUnit', |
||
186 | 'Weight ({WeightUnit})', |
||
187 | '', |
||
188 | [ |
||
189 | 'WeightUnit' => self::config()->weight_unit |
||
190 | ] |
||
191 | ), |
||
192 | '', |
||
193 | 12 |
||
194 | ), |
||
195 | TextField::create( |
||
196 | 'Height', |
||
197 | _t(__CLASS__ . '.HeightWithUnit', 'Height ({LengthUnit})', '', $fieldSubstitutes), |
||
198 | '', |
||
199 | 12 |
||
200 | ), |
||
201 | TextField::create( |
||
202 | 'Width', |
||
203 | _t(__CLASS__ . '.WidthWithUnit', 'Width ({LengthUnit})', '', $fieldSubstitutes), |
||
204 | '', |
||
205 | 12 |
||
206 | ), |
||
207 | TextField::create( |
||
208 | 'Depth', |
||
209 | _t(__CLASS__ . '.DepthWithUnit', 'Depth ({LengthUnit})', '', $fieldSubstitutes), |
||
210 | '', |
||
211 | 12 |
||
212 | ), |
||
213 | ] |
||
214 | ); |
||
215 | |||
216 | if (!$fields->dataFieldByName('Image')) { |
||
217 | $fields->addFieldToTab( |
||
218 | 'Root.Images', |
||
219 | UploadField::create('Image', _t(__CLASS__ . '.Image', 'Product Image')) |
||
220 | ); |
||
221 | } |
||
222 | } |
||
223 | ); |
||
224 | |||
225 | return parent::getCMSFields(); |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * Add missing translations to the fieldLabels |
||
230 | */ |
||
231 | public function fieldLabels($includerelations = true) |
||
232 | { |
||
233 | $labels = parent::fieldLabels($includerelations); |
||
234 | |||
235 | $labels['Title'] = _t(__CLASS__ . '.PageTitle', 'Product Title'); |
||
236 | $labels['IsPurchaseable'] = $labels['IsPurchaseable.Nice'] = _t(__CLASS__ . '.IsPurchaseable', 'Is Purchaseable'); |
||
237 | $labels['BasePrice.NiceOrEmpty'] = _t(__CLASS__ . '.db_BasePrice', 'Price'); |
||
238 | |||
239 | return $labels; |
||
240 | } |
||
241 | |||
242 | /** |
||
243 | * Helper function for generating list of categories to select from. |
||
244 | * |
||
245 | * @return array categories |
||
246 | */ |
||
247 | private function getCategoryOptions() |
||
248 | { |
||
249 | $categories = ProductCategory::get()->map('ID', 'NestedTitle')->toArray(); |
||
250 | $categories = [ |
||
251 | 0 => _t('SilverStripe\CMS\Model\SiteTree.PARENTTYPE_ROOT', 'Top-level page'), |
||
252 | ] + $categories; |
||
253 | if ($this->ParentID && !($this->Parent() instanceof ProductCategory)) { |
||
254 | $categories = [ |
||
255 | $this->ParentID => $this->Parent()->Title . ' (' . $this->Parent()->i18n_singular_name() . ')', |
||
256 | ] + $categories; |
||
257 | } |
||
258 | |||
259 | return $categories; |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * Helper function for generating a list of additional categories excluding the main parent. |
||
264 | * |
||
265 | * @return array categories |
||
266 | */ |
||
267 | private function getCategoryOptionsNoParent() |
||
268 | { |
||
269 | $ancestors = $this->getAncestors()->map('ID', 'ID'); |
||
270 | $categories = ProductCategory::get(); |
||
271 | if (!empty($ancestors)) { |
||
272 | $categories->filter('ID:not', $ancestors); |
||
273 | } |
||
274 | return $categories->map('ID', 'NestedTitle')->toArray(); |
||
275 | } |
||
276 | |||
277 | /** |
||
278 | * Get ids of all categories that this product appears in. |
||
279 | * |
||
280 | * @return array ids list |
||
281 | */ |
||
282 | public function getCategoryIDs() |
||
293 | } |
||
294 | |||
295 | /** |
||
296 | * Get all categories that this product appears in. |
||
297 | * |
||
298 | * @return DataList category data list |
||
299 | */ |
||
300 | public function getCategories() |
||
301 | { |
||
302 | return ProductCategory::get()->byIDs($this->getCategoryIDs()); |
||
303 | } |
||
304 | |||
305 | /** |
||
306 | * Conditions for whether a product can be purchased: |
||
307 | * - global allow purchase is enabled |
||
308 | * - product AllowPurchase field is true |
||
309 | * - if variations, then one of them needs to be purchasable |
||
310 | * - if not variations, selling price must be above 0 |
||
311 | * |
||
312 | * Other conditions may be added by decorating with the canPurchase function |
||
313 | * |
||
314 | * @param Member $member |
||
315 | * @param int $quantity |
||
316 | * |
||
317 | * @return boolean |
||
318 | */ |
||
319 | public function canPurchase($member = null, $quantity = 1) |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * Returns the purchaseable flag as `DBBoolean`. Useful for templates or summaries. |
||
346 | * @return DBBoolean |
||
347 | */ |
||
348 | public function IsPurchaseable() |
||
349 | { |
||
350 | return DBBoolean::create_field(DBBoolean::class, $this->canPurchase()); |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Returns if the product is already in the shopping cart. |
||
355 | * |
||
356 | * @return boolean |
||
357 | */ |
||
358 | public function IsInCart() |
||
359 | { |
||
360 | $item = $this->Item(); |
||
361 | return $item && $item->exists() && $item->Quantity > 0; |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * Returns the order item which contains the product |
||
366 | * |
||
367 | * @return OrderItem |
||
368 | */ |
||
369 | public function Item() |
||
370 | { |
||
371 | $filter = array(); |
||
372 | $this->extend('updateItemFilter', $filter); |
||
373 | $item = ShoppingCart::singleton()->get($this, $filter); |
||
374 | if (!$item) { |
||
375 | //return dummy item so that we can still make use of Item |
||
376 | $item = $this->createItem(); |
||
377 | } |
||
378 | $this->extend('updateDummyItem', $item); |
||
379 | return $item; |
||
380 | } |
||
381 | |||
382 | /** |
||
383 | * @see Buyable::createItem() |
||
384 | */ |
||
385 | public function createItem($quantity = 1, $filter = null) |
||
386 | { |
||
387 | $orderitem = self::config()->order_item; |
||
388 | $item = new $orderitem(); |
||
389 | $item->ProductID = $this->ID; |
||
390 | if ($filter) { |
||
391 | //TODO: make this a bit safer, perhaps intersect with allowed fields |
||
392 | $item->update($filter); |
||
393 | } |
||
394 | $item->Quantity = $quantity; |
||
395 | return $item; |
||
396 | } |
||
397 | |||
398 | /** |
||
399 | * The raw retail price the visitor will get when they |
||
400 | * add to cart. Can include discounts or markups on the base price. |
||
401 | */ |
||
402 | public function sellingPrice() |
||
403 | { |
||
404 | $price = $this->BasePrice; |
||
405 | //TODO: this is not ideal, because prices manipulations will not happen in a known order |
||
406 | $this->extend('updateSellingPrice', $price); |
||
407 | //prevent negative values |
||
408 | $price = $price < 0 ? 0 : $price; |
||
409 | |||
410 | // NOTE: Ideally, this would be dependent on the locale but as of |
||
411 | // now the Silverstripe Currency field type has 2 hardcoded all over |
||
412 | // the place. In the mean time there is an issue where the displayed |
||
413 | // unit price can not exactly equal the multiplied price on an order |
||
414 | // (i.e. if the calculated price is 3.145 it will display as 3.15. |
||
415 | // so if I put 10 of them in my cart I will expect the price to be |
||
416 | // 31.50 not 31.45). |
||
417 | return round($price, Order::config()->rounding_precision); |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * This value is cased to Currency in temlates. |
||
422 | */ |
||
423 | public function getPrice() |
||
424 | { |
||
425 | return $this->sellingPrice(); |
||
426 | } |
||
427 | |||
428 | public function setPrice($price) |
||
429 | { |
||
430 | $price = $price < 0 ? 0 : $price; |
||
431 | $this->setField('BasePrice', $price); |
||
432 | } |
||
433 | |||
434 | /** |
||
435 | * Allow orphaned products to be viewed. |
||
436 | */ |
||
437 | public function isOrphaned() |
||
438 | { |
||
439 | return false; |
||
440 | } |
||
441 | |||
442 | public function Link($action = null) |
||
443 | { |
||
444 | $link = parent::Link($action); |
||
445 | $this->extend('updateLink', $link); |
||
446 | return $link; |
||
447 | } |
||
448 | |||
449 | /** |
||
450 | * If the product does not have an image, and a default image |
||
451 | * is defined in SiteConfig, return that instead. |
||
452 | * |
||
453 | * @return Image |
||
454 | * @throws \Exception |
||
455 | */ |
||
456 | public function Image() |
||
457 | { |
||
458 | $image = $this->getComponent('Image'); |
||
459 | $this->extend('updateImage', $image); |
||
460 | |||
461 | if ($image && $image->exists()) { |
||
462 | return $image; |
||
463 | } |
||
464 | $image = SiteConfig::current_site_config()->DefaultProductImage(); |
||
465 | if ($image && $image->exists()) { |
||
466 | return $image; |
||
467 | } |
||
468 | return null; |
||
469 | } |
||
470 | |||
471 | /** |
||
472 | * Link to add this product to cart. |
||
473 | * |
||
474 | * @return string link |
||
475 | */ |
||
476 | public function addLink() |
||
479 | } |
||
480 | |||
481 | /** |
||
482 | * Link to remove one of this product from cart. |
||
483 | * |
||
484 | * @return string link |
||
485 | */ |
||
486 | public function removeLink() |
||
489 | } |
||
490 | |||
491 | /** |
||
492 | * Link to remove all of this product from cart. |
||
493 | * |
||
494 | * @return string link |
||
495 | */ |
||
496 | public function removeallLink() |
||
499 | } |
||
500 | |||
501 | /** |
||
502 | * Get the form class to use to edit this product in the frontend |
||
503 | * @return string FQCN |
||
504 | */ |
||
505 | public function getFormClass() |
||
510 | } |
||
511 | } |
||
512 |