Completed
Pull Request — 2.0 (#506)
by Roman
18:39
created

Order   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 674
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 22

Test Coverage

Coverage 42.65%

Importance

Changes 9
Bugs 1 Features 2
Metric Value
wmc 80
c 9
b 1
f 2
lcom 4
cbo 22
dl 0
loc 674
ccs 90
cts 211
cp 0.4265
rs 1.3452

37 Methods

Rating   Name   Duplication   Size   Complexity  
A get_order_status_options() 0 8 2
B getCMSFields() 0 24 3
A getComponents() 0 14 4
A SubTotal() 0 8 2
A calculate() 0 8 2
A getModifier() 0 5 1
A setTotal() 0 4 1
A Total() 0 4 1
A GrandTotal() 0 4 1
A TotalOutstanding() 0 7 2
A getStatusI18N() 0 4 1
A Link() 0 7 2
B canCancel() 0 15 6
A canPay() 0 10 4
A canDelete() 0 4 1
A canView() 0 4 1
A canEdit() 0 4 1
A canCreate() 0 4 1
A Currency() 0 4 1
A getLatestEmail() 0 7 4
A getName() 0 6 3
A getTitle() 0 4 1
A getShippingAddress() 0 4 1
A getBillingAddress() 0 8 3
B getAddress() 0 15 6
A getAddressesDiffer() 0 4 2
A IsSent() 0 4 1
A IsProcessing() 0 4 2
A IsPaid() 0 4 2
A IsCart() 0 4 1
A generateReference() 0 13 2
A getReference() 0 4 2
A onBeforeWrite() 0 13 4
A onBeforeDelete() 0 8 1
B debug() 0 21 5
A provideI18nEntities() 0 15 2
A getDefaultSearchContext() 0 52 1

How to fix   Complexity   

Complex Class

Complex classes like Order 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Order, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * The order class is a databound object for handling Orders
5
 * within SilverStripe.
6
 *
7
 * @property string|float Currency
8
 * @property string Reference
9
 * @property string Placed
10
 * @property string Paid
11
 * @property string ReceiptSent
12
 * @property string Printed
13
 * @property string Dispatched
14
 * @property string Status
15
 * @property string FirstName
16
 * @property string Surname
17
 * @property string Email
18
 * @property string Notes
19
 * @property string IPAddress
20
 * @property string|bool SeparateBillingAddress
21
 * @property string Locale
22
 * @property string|int MemberID
23
 * @property string|int ShippingAddressID
24
 * @property string|int BillingAddressID
25
 * @method Member|ShopMember Member
26
 * @method Address BillingAddress
27
 * @method Address ShippingAddress
28
 * @method OrderItem[]|HasManyList Items
29
 * @method OrderModifier[]|HasManyList Modifiers
30
 * @method OrderStatusLog[]|HasManyList OrderStatusLogs
31
 *
32
 * @package shop
33
 */
34
class Order extends DataObject
0 ignored issues
show
Complexity introduced by
This class has 22 fields which exceeds the configured maximum of 15.

Too many fields generally indicate a class which does too much and does not follow the single responsibility principle.

We suggest taking a look at the “Code” section for further suggestions on how to fix this.

Loading history...
Complexity introduced by
This class has a complexity of 80 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
35
{
36
    /**
37
     * Status codes and what they mean:
38
     *
39
     * Unpaid (default): Order created but no successful payment by customer yet
40
     * Query: Order not being processed yet (customer has a query, or could be out of stock)
41
     * Paid: Order successfully paid for by customer
42
     * Processing: Order paid for, package is currently being processed before shipping to customer
43
     * Sent: Order paid for, processed for shipping, and now sent to the customer
44
     * Complete: Order completed (paid and shipped). Customer assumed to have received their goods
45
     * AdminCancelled: Order cancelled by the administrator
46
     * MemberCancelled: Order cancelled by the customer (Member)
47
     */
48
    private static $db                = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
49
        'Total'                  => 'Currency',
50
        'Reference'              => 'Varchar', //allow for customised order numbering schemes
51
        //status
52
        'Placed'                 => "SS_Datetime", //date the order was placed (went from Cart to Order)
53
        'Paid'                   => 'SS_Datetime', //no outstanding payment left
54
        'ReceiptSent'            => 'SS_Datetime', //receipt emailed to customer
55
        'Printed'                => 'SS_Datetime',
56
        'Dispatched'             => 'SS_Datetime', //products have been sent to customer
57
        'Status'                 => "Enum('Unpaid,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart','Cart')",
58
        //customer (for guest orders)
59
        'FirstName'              => 'Varchar',
60
        'Surname'                => 'Varchar',
61
        'Email'                  => 'Varchar',
62
        'Notes'                  => 'Text',
63
        'IPAddress'              => 'Varchar(15)',
64
        //separate shipping
65
        'SeparateBillingAddress' => 'Boolean',
66
        // keep track of customer locale
67
        'Locale'                 => 'DBLocale',
68
    );
69
70
    private static $has_one           = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
71
        'Member'          => 'Member',
72
        'ShippingAddress' => 'Address',
73
        'BillingAddress'  => 'Address',
74
    );
75
76
    private static $has_many          = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
77
        'Items'           => 'OrderItem',
78
        'Modifiers'       => 'OrderModifier',
79
        'OrderStatusLogs' => 'OrderStatusLog',
80
    );
81
82
    private static $defaults          = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
83
        'Status' => 'Cart',
84
    );
85
86
    private static $casting           = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
87
        'FullBillingAddress'  => 'Text',
88
        'FullShippingAddress' => 'Text',
89
        'Total'               => 'Currency',
90
        'SubTotal'            => 'Currency',
91
        'TotalPaid'           => 'Currency',
92
        'Shipping'            => 'Currency',
93
        'TotalOutstanding'    => 'Currency',
94
    );
95
96
    private static $summary_fields    = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
97
        'Reference'   => 'Order No',
98
        'Placed'      => 'Date',
99
        'Name'        => 'Customer',
100
        'LatestEmail' => 'Email',
101
        'Total'       => 'Total',
102
        'Status'      => 'Status',
103
    );
104
105
    private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
106
        'Reference' => array(),
107
        'FirstName' => array(
108
            'title' => 'Customer Name',
109
        ),
110
        'Email'     => array(
111
            'title' => 'Customer Email',
112
        ),
113
        'Status'    => array(
114
            'filter' => 'ExactMatchFilter',
115
            'field'  => 'CheckboxSetField',
116
        ),
117
    );
118
119
    private static $singular_name     = "Order";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
120
121
    private static $plural_name       = "Orders";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
122
123
    private static $default_sort      = "\"Placed\" DESC, \"Created\" DESC";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
124
125
    /**
126
     * Statuses for orders that have been placed.
127
     */
128
    private static $placed_status = array(
129
        'Paid',
130
        'Unpaid',
131
        'Processing',
132
        'Sent',
133
        'Complete',
134
        'MemberCancelled',
135
        'AdminCancelled',
136
    );
137
138
    /**
139
     * Statuses for which an order can be paid for
140
     */
141
    private static $payable_status = array(
142
        'Cart',
143
        'Unpaid',
144
        'Processing',
145
        'Sent',
146
    );
147
148
    /**
149
     * Statuses that shouldn't show in user account.
150
     */
151
    private static $hidden_status = array('Cart');
152
153
    /**
154
     * Flags to determine when an order can be cancelled.
155
     */
156
    private static $cancel_before_payment    = true;
157
158
    private static $cancel_before_processing = false;
159
160
    private static $cancel_before_sending    = false;
161
162
    private static $cancel_after_sending     = false;
163
164
    /**
165
     * Place an order before payment processing begins
166
     *
167
     * @var boolean
168
     */
169
    private static $place_before_payment = false;
170
171
    /**
172
     * Modifiers represent the additional charges or
173
     * deductions associated to an order, such as
174
     * shipping, taxes, vouchers etc.
175
     */
176
    private static $modifiers            = array();
177
178
    private static $rounding_precision   = 2;
179
180
    private static $reference_id_padding = 5;
181
182
    /**
183
     * @var boolean Will allow completion of orders with GrandTotal=0,
184
     * which could be the case for orders paid with loyalty points or vouchers.
185
     * Will send the "Paid" date on the order, even though no actual payment was taken.
186
     * Will trigger the payment related extension points:
187
     * Order->onPayment, OrderItem->onPayment, Order->onPaid.
188
     */
189
    private static $allow_zero_order_total = false;
190
191
    public static function get_order_status_options()
192
    {
193
        $values = array();
194
        foreach (singleton('Order')->dbObject('Status')->enumValues(false) as $value) {
195
            $values[$value] = _t('Order.STATUS_' . strtoupper($value), $value);
196
        }
197
        return $values;
198
    }
199
200
    /**
201
     * Create CMS fields for cms viewing and editing orders
202
     */
203
    public function getCMSFields()
204
    {
205
        $fields = FieldList::create(TabSet::create('Root', Tab::create('Main')));
206
        $fs = "<div class=\"field\">";
207
        $fe = "</div>";
208
        $parts = array(
209
            DropdownField::create("Status", _t('Order.db_Status', "Status"), self::get_order_status_options()),
210
            LiteralField::create('Customer', $fs . $this->renderWith("OrderAdmin_Customer") . $fe),
211
            LiteralField::create('Addresses', $fs . $this->renderWith("OrderAdmin_Addresses") . $fe),
212
            LiteralField::create('Content', $fs . $this->renderWith("OrderAdmin_Content") . $fe),
213
        );
214
        if ($this->Notes) {
215
            $parts[] = LiteralField::create('Notes', $fs . $this->renderWith("OrderAdmin_Notes") . $fe);
216
        }
217
        $fields->addFieldsToTab('Root.Main', $parts);
218
        $this->extend('updateCMSFields', $fields);
219
        if ($payments = $fields->fieldByName("Root.Payments.Payments")) {
220
            $fields->removeByName("Payments");
221
            $fields->insertAfter($payments, "Content");
222
            $payments->addExtraClass("order-payments");
223
        }
224
225
        return $fields;
226
    }
227
228
    /**
229
     * Adjust scafolded search context
230
     *
231
     * @return SearchContext the updated search context
232
     */
233
    public function getDefaultSearchContext()
234
    {
235
        $context = parent::getDefaultSearchContext();
236
        $fields = $context->getFields();
237
        $fields->push(
238
            ListboxField::create("Status", _t('Order.db_Status', "Status"))
239
                ->setSource(
240
                    array_combine(
241
                        self::config()->placed_status,
242
                        self::config()->placed_status
243
                    )
244
                )
245
                ->setMultiple(true)
246
        );
247
248
        // add date range filtering
249
        $fields->insertBefore(
250
            DateField::create("DateFrom", _t('Order.DateFrom', "Date from"))
251
                ->setConfig('showcalendar', true),
252
            'Status'
0 ignored issues
show
Documentation introduced by
'Status' is of type string, but the function expects a object<FormField>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
253
        );
254
        $fields->insertBefore(
255
            DateField::create("DateTo", _t('Order.DateTo', "Date to"))
256
                ->setConfig('showcalendar', true),
257
            'Status'
0 ignored issues
show
Documentation introduced by
'Status' is of type string, but the function expects a object<FormField>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
258
        );
259
260
        // get the array, to maniplulate name, and fullname seperately
261
        $filters = $context->getFilters();
262
        $filters['DateFrom'] = GreaterThanFilter::create('Placed');
263
        $filters['DateTo'] = LessThanFilter::create('Placed');
264
265
        // filter customer need to use a bunch of different sources
266
        $filters['FirstName'] = new MultiFieldPartialMatchFilter(
267
            'FirstName', false,
268
            array('SplitWords'),
269
            array(
270
                'Surname',
271
                'Member.FirstName',
272
                'Member.Surname',
273
                'BillingAddress.FirstName',
274
                'BillingAddress.Surname',
275
                'ShippingAddress.FirstName',
276
                'ShippingAddress.Surname',
277
            )
278
        );
279
280
        $context->setFilters($filters);
281
282
        $this->extend('updateDefaultSearchContext', $context);
283
        return $context;
284
    }
285
286
    /**
287
     * Hack for swapping out relation list with OrderItemList
288
     */
289 36
    public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = null)
290
    {
291 36
        $components = parent::getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = null);
292 36
        if ($componentName === "Items" && get_class($components) !== "UnsavedRelationList") {
293 33
            $query = $components->dataQuery();
294 33
            $components = OrderItemList::create("OrderItem", "OrderID");
295 33
            if ($this->model) {
296 33
                $components->setDataModel($this->model);
297 33
            }
298 33
            $components->setDataQuery($query);
299 33
            $components = $components->forForeignID($this->ID);
300 33
        }
301 36
        return $components;
302
    }
303
304
    /**
305
     * Returns the subtotal of the items for this order.
306
     */
307 19
    public function SubTotal()
308
    {
309 19
        if ($this->Items()->exists()) {
0 ignored issues
show
Documentation Bug introduced by
The method Items does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
310 14
            return $this->Items()->SubTotal();
0 ignored issues
show
Documentation Bug introduced by
The method Items does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
311
        }
312
313 7
        return 0;
314
    }
315
316
    /**
317
     * Calculate the total
318
     *
319
     * @return the final total
320
     */
321 15
    public function calculate()
322
    {
323 15
        if (!$this->IsCart()) {
324
            return $this->Total;
0 ignored issues
show
Documentation introduced by
The property Total does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
325
        }
326 15
        $calculator = new OrderTotalCalculator($this);
327 15
        return $this->Total = $calculator->calculate();
0 ignored issues
show
Documentation introduced by
The property Total does not exist on object<Order>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
328
    }
329
330
    /**
331
     * This is needed to maintain backwards compatiability with
332
     * some subsystems using modifiers. eg discounts
333
     */
334
    public function getModifier($className, $forcecreate = false)
335
    {
336
        $calculator = new OrderTotalCalculator($this);
337
        return $calculator->getModifier($className, $forcecreate);
338
    }
339
340
    /**
341
     * Enforce rounding precision when setting total
342
     */
343 67
    public function setTotal($val)
344
    {
345 67
        $this->setField("Total", round($val, self::$rounding_precision));
346 67
    }
347
348
    /**
349
     * Get final value of order.
350
     * Retrieves value from DataObject's record array.
351
     */
352 15
    public function Total()
353
    {
354 15
        return $this->getField("Total");
355
    }
356
357
    /**
358
     * Alias for Total.
359
     */
360 13
    public function GrandTotal()
361
    {
362 13
        return $this->Total();
363
    }
364
365
    /**
366
     * Calculate how much is left to be paid on the order.
367
     * Enforces rounding precision.
368
     *
369
     * Payments that have been authorized via a non-manual gateway should count towards the total paid amount.
370
     * However, it's possible to exclude these by setting the $includeAuthorized parameter to false, which is
371
     * useful to determine the status of the Order. Order status should only change to 'Paid' when all
372
     * payments are 'Captured'.
373
     *
374
     * @param bool $includeAuthorized whether or not to include authorized payments (excluding manual payments)
375
     * @return float
376
     */
377 11
    public function TotalOutstanding($includeAuthorized = true)
378
    {
379 11
        return round(
380 11
            $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->TotalPaid()),
0 ignored issues
show
Documentation Bug introduced by
The method TotalPaidOrAuthorized does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method TotalPaid does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
381 11
            self::config()->rounding_precision
382 11
        );
383
    }
384
385
    /**
386
     * Get the order status. This will return a localized value if available.
387
     *
388
     * @return string the payment status
389
     */
390 2
    public function getStatusI18N()
391
    {
392 2
        return _t('Order.STATUS_' . strtoupper($this->Status), $this->Status);
393
    }
394
395
    /**
396
     * Get the link for finishing order processing.
397
     */
398 2
    public function Link()
399
    {
400 2
        if (Member::currentUser()) {
401 1
            return Controller::join_links(AccountPage::find_link(), 'order', $this->ID);
402
        }
403 1
        return CheckoutPage::find_link(false, "order", $this->ID);
404
    }
405
406
    /**
407
     * Returns TRUE if the order can be cancelled
408
     * PRECONDITION: Order is in the DB.
409
     *
410
     * @return boolean
411
     */
412 1
    public function canCancel()
413
    {
414 1
        switch ($this->Status) {
415 1
            case 'Unpaid' :
416 1
                return self::config()->cancel_before_payment;
417
            case 'Paid' :
418
                return self::config()->cancel_before_processing;
419
            case 'Processing' :
420
                return self::config()->cancel_before_sending;
421
            case 'Sent' :
422
            case 'Complete' :
423
                return self::config()->cancel_after_sending;
424
        }
425
        return false;
426
    }
427
428
    /**
429
     * Check if an order can be paid for.
430
     *
431
     * @return boolean
432
     */
433 2
    public function canPay($member = null)
0 ignored issues
show
Unused Code introduced by
The parameter $member is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
434
    {
435 2
        if (!in_array($this->Status, self::config()->payable_status)) {
436
            return false;
437
        }
438 2
        if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) {
439 2
            return true;
440
        }
441
        return false;
442
    }
443
444
    /*
445
     * Prevent deleting orders.
446
     * @return boolean
447
     */
448
    public function canDelete($member = null)
449
    {
450
        return false;
451
    }
452
453
    /**
454
     * Check if an order can be viewed.
455
     *
456
     * @return boolean
457
     */
458
    public function canView($member = null)
459
    {
460
        return true;
461
    }
462
463
    /**
464
     * Check if an order can be edited.
465
     *
466
     * @return boolean
467
     */
468
    public function canEdit($member = null)
469
    {
470
        return true;
471
    }
472
473
    /**
474
     * Prevent standard creation of orders.
475
     *
476
     * @return boolean
477
     */
478
    public function canCreate($member = null, $context = array())
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
479
    {
480
        return false;
481
    }
482
483
    /**
484
     * Return the currency of this order.
485
     * Note: this is a fixed value across the entire site.
486
     *
487
     * @return string
488
     */
489 3
    public function Currency()
490
    {
491 3
        return ShopConfig::get_site_currency();
492
    }
493
494
    /**
495
     * Get the latest email for this order.
496
     */
497 3
    public function getLatestEmail()
498
    {
499 3
        if ($this->MemberID && ($this->Member()->LastEdited > $this->LastEdited || !$this->Email)) {
0 ignored issues
show
Documentation Bug introduced by
The method Member does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
500 3
            return $this->Member()->Email;
0 ignored issues
show
Documentation Bug introduced by
The method Member does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
501
        }
502
        return $this->getField('Email');
503
    }
504
505
    /**
506
     * Gets the name of the customer.
507
     */
508
    public function getName()
509
    {
510
        $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName;
0 ignored issues
show
Documentation Bug introduced by
The method Member does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
511
        $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname;
0 ignored issues
show
Documentation Bug introduced by
The method Member does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
512
        return implode(" ", array_filter(array($firstname, $surname)));
513
    }
514
515
    public function getTitle()
516
    {
517
        return $this->Reference . " - " . $this->dbObject('Placed')->Nice();
518
    }
519
520
    /**
521
     * Get shipping address, or member default shipping address.
522
     */
523 1
    public function getShippingAddress()
524
    {
525 1
        return $this->getAddress('Shipping');
526
    }
527
528
    /**
529
     * Get billing address, if marked to use seperate address, otherwise use shipping address,
530
     * or the member default billing address.
531
     */
532 1
    public function getBillingAddress()
533
    {
534 1
        if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) {
535 1
            return $this->getShippingAddress();
536
        } else {
537
            return $this->getAddress('Billing');
538
        }
539
    }
540
541
    /**
542
     * @param string $type - Billing or Shipping
543
     * @return Address
544
     * @throws Exception
545
     */
546 1
    protected function getAddress($type)
547
    {
548 1
        $address = $this->getComponent($type . 'Address');
549
550 1
        if (!$address || !$address->exists() && $this->Member()) {
0 ignored issues
show
Documentation Bug introduced by
The method Member does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
551 1
            $address = $this->Member()->{"Default${type}Address"}();
0 ignored issues
show
Documentation Bug introduced by
The method Member does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
552 1
        }
553
554
        if (empty($address->Surname) && empty($address->FirstName)) {
555
            $address->FirstName = $this->FirstName;
556
            $address->Surname = $this->Surname;
557
        }
558
559
        return $address;
560
    }
561
562
    /**
563
     * Check if the two addresses saved differ.
564
     *
565
     * @return boolean
566
     */
567
    public function getAddressesDiffer()
568
    {
569
        return $this->SeparateBillingAddress || $this->ShippingAddressID !== $this->BillingAddressID;
570
    }
571
572
    /**
573
     * Has this order been sent to the customer?
574
     * (at "Sent" status).
575
     *
576
     * @return boolean
577
     */
578
    public function IsSent()
579
    {
580 1
        return $this->Status == 'Sent';
581
    }
582
583
    /**
584
     * Is this order currently being processed?
585
     * (at "Sent" OR "Processing" status).
586
     *
587
     * @return boolean
588
     */
589
    public function IsProcessing()
590
    {
591 1
        return $this->IsSent() || $this->Status == 'Processing';
592
    }
593
594
    /**
595
     * Return whether this Order has been paid for (Status == Paid)
596
     * or Status == Processing, where it's been paid for, but is
597
     * currently in a processing state.
598
     *
599
     * @return boolean
600
     */
601
    public function IsPaid()
602
    {
603 1
        return (boolean)$this->Paid || $this->Status == 'Paid';
604
    }
605
606
    public function IsCart()
607
    {
608 84
        return $this->Status == 'Cart';
609
    }
610
611
    /**
612
     * Create a unique reference identifier string for this order.
613
     */
614
    public function generateReference()
615
    {
616 67
        $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT);
617 67
        $this->extend('generateReference', $reference);
618 67
        $candidate = $reference;
619
        //prevent generating references that are the same
620 67
        $count = 0;
621 67
        while (DataObject::get_one('Order', "\"Reference\" = '$candidate'")) {
622 67
            $count++;
623 67
            $candidate = $reference . "" . $count;
624 67
        }
625 67
        $this->Reference = $candidate;
626 67
    }
627
628
    /**
629
     * Get the reference for this order, or fall back to order ID.
630
     */
631
    public function getReference()
632
    {
633 67
        return $this->getField('Reference') ? $this->getField('Reference') : $this->ID;
634
    }
635
636
    /**
637
     * Force creating an order reference
638
     */
639
    public function onBeforeWrite()
640
    {
641 87
        parent::onBeforeWrite();
642 87
        if (!$this->getField("Reference") && in_array($this->Status, self::$placed_status)) {
643 67
            $this->generateReference();
644 67
        }
645
646
        // While the order is unfinished/cart, always store the current locale with the order.
647
        // We do this everytime an order is saved, because the user might change locale (language-switch).
648 87
        if ($this->Status == 'Cart') {
649 87
            $this->Locale = ShopTools::get_current_locale();
650 87
        }
651 87
    }
652
653
    /**
654
     * delete attributes, statuslogs, and payments
655
     */
656
    public function onBeforeDelete()
657
    {
658 1
        $this->Items()->removeAll();
0 ignored issues
show
Documentation Bug introduced by
The method Items does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
659 1
        $this->Modifiers()->removeAll();
0 ignored issues
show
Documentation Bug introduced by
The method Modifiers does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
660 1
        $this->OrderStatusLogs()->removeAll();
0 ignored issues
show
Documentation Bug introduced by
The method OrderStatusLogs does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
661 1
        $this->Payments()->removeAll();
0 ignored issues
show
Documentation Bug introduced by
The method Payments does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
662 1
        parent::onBeforeDelete();
663 1
    }
664
665
    public function debug()
666
    {
667
        $val = "<div class='order'><h1>$this->class</h1>\n<ul>\n";
668
        if ($this->record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
669
            foreach ($this->record as $fieldName => $fieldVal) {
670
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
671
            }
672
        }
673
        $val .= "</ul>\n";
674
        $val .= "<div class='items'><h2>Items</h2>";
675
        if ($items = $this->Items()) {
0 ignored issues
show
Documentation Bug introduced by
The method Items does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Unused Code introduced by
$items 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...
676
            $val .= $this->Items()->debug();
0 ignored issues
show
Documentation Bug introduced by
The method Items does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
677
        }
678
        $val .= "</div><div class='modifiers'><h2>Modifiers</h2>";
679
        if ($modifiers = $this->Modifiers()) {
0 ignored issues
show
Documentation Bug introduced by
The method Modifiers does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
680
            $val .= $modifiers->debug();
681
        }
682
        $val .= "</div></div>";
683
684
        return $val;
685
    }
686
687
    /**
688
     * Provide i18n entities for the order class
689
     *
690
     * @return array
691
     */
692
    public function provideI18nEntities()
693
    {
694
        $entities = parent::provideI18nEntities();
695
696
        // collect all the payment status values
697
        foreach ($this->dbObject('Status')->enumValues() as $value) {
698
            $key = strtoupper($value);
699
            $entities["Order.STATUS_$key"] = array(
700
                $value,
701
                "Translation of the order status '$value'",
702
            );
703
        }
704
705
        return $entities;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $entities; (array<*,array>) is incompatible with the return type of the parent method DataObject::provideI18nEntities of type array<*,array<array|inte...double|string|boolean>>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
706
    }
707
}
708