OrderFormAddress   F
last analyzed

Complexity

Total Complexity 121

Size/Duplication

Total Lines 790
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
dl 0
loc 790
rs 1.81
c 0
b 0
f 0
wmc 121
lcom 1
cbo 22

15 Methods

Rating   Name   Duplication   Size   Complexity  
F __construct() 0 254 34
A orderHasFullyOperationalMember() 0 11 4
A orderDoesNotHaveFullyOperationalMember() 0 4 2
A saveAddress() 0 9 1
C saveAddressDetails() 0 77 16
A saveDataToSession() 0 10 1
A clearSessionData() 0 5 1
F createOrFindMember() 0 88 24
B memberShouldBeCreated() 0 32 8
C memberShouldBeSaved() 0 29 12
A memberShouldBeLoggedIn() 0 10 4
A uniqueMemberFieldCanBeUsed() 0 22 4
A anotherExistingMemberWithSameUniqueFieldValue() 0 29 3
A enteredEmailAddressDoesNotMatchLoggedInUser() 0 19 6
A validPasswordHasBeenEntered() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like OrderFormAddress 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 OrderFormAddress, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This class is the form for editing the Order Addresses.
5
 * It is also used to link the order to a member.
6
 *
7
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
8
 * @package: ecommerce
9
 * @sub-package: forms
10
 * @inspiration: Silverstripe Ltd, Jeremy
11
 **/
12
class OrderFormAddress extends Form
13
{
14
    /**
15
     * @var bool
16
     */
17
    protected $debug = false;
18
19
    /**
20
     * @var array
21
     */
22
    protected $debugArray = array();
23
24
    /**
25
     * the member attached to the order
26
     * this is not always the same as the loggedInMember.
27
     *
28
     * @var object (Member)
29
     */
30
    protected $orderMember = null;
31
32
    /**
33
     * the logged in member, if any
34
     * this is not always the same as the orderMember.
35
     *
36
     * @var object (Member)
37
     */
38
    protected $loggedInMember = null;
39
40
    /**
41
     * ID of the member that has just been created.
42
     *
43
     * @var int
44
     */
45
    protected $newlyCreatedMemberID = 0;
46
47
    /**
48
     * ID of the member that has just been created.
49
     *
50
     * @var Order
51
     */
52
    protected $order = null;
53
54
    /**
55
     * @param Controller
56
     * @param string
57
     */
58
    public function __construct(Controller $controller, $name)
59
    {
60
61
        //set basics
62
        $requiredFields = array();
63
64
        //requirements
65
        Requirements::javascript('ecommerce/javascript/EcomOrderFormAddress.js'); // LEAVE HERE - NOT EASY TO INCLUDE VIA TEMPLATE
66
        if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
67
            Requirements::javascript('ecommerce/javascript/EcomOrderFormShipping.js'); // LEAVE HERE - NOT EASY TO INCLUDE VIA TEMPLATE
68
        }
69
70
        //  ________________ 1) Order + Member + Address fields
71
72
73
        // define field lists ...
74
        $addressFieldsMember = FieldList::create();
75
        $addressFieldsBilling = FieldList::create();
76
        $addressFieldsShipping = null;
77
        $useShippingAddressField = null;
78
        $shippingAddressFirst = EcommerceConfig::get('OrderFormAddress', 'shipping_address_first');
79
80
        $addressFieldsMember->push(
81
            HeaderField::create(
82
                'AddressFieldsMemberHeading',
83
                _t('OrderFormAddress.Address_Fields_Member_Heading', 'Your Personal Details'),
84
                3
85
            )
86
        );
87
        //find member
88
        $this->order = ShoppingCart::current_order();
89
        $this->orderMember = $this->order->CreateOrReturnExistingMember(false);
90
        $this->loggedInMember = Member::currentUser();
91
92
        //strange security situation...
93
        if ($this->orderMember->exists() && $this->loggedInMember) {
94
            if ($this->orderMember->ID != $this->loggedInMember->ID) {
95
                if (!$this->loggedInMember->IsShopAdmin()) {
96
                    $this->loggedInMember->logOut();
97
                }
98
            }
99
        }
100
101
        //member fields
102
        if ($this->orderMember) {
103
            $memberFields = $this->orderMember->getEcommerceFields();
104
            $requiredFields = array_merge($requiredFields, $this->orderMember->getEcommerceRequiredFields());
105
            $addressFieldsMember->merge($memberFields);
106
        }
107
108
        //billing address field
109
        $billingAddress = $this->order->CreateOrReturnExistingAddress('BillingAddress');
110
        $billingAddressFields = $billingAddress->getFields($this->orderMember);
111
        $addressFieldsBilling->merge($billingAddressFields);
112
113
        $requiredFields = array_merge($requiredFields, $billingAddress->getRequiredFields());
114
115
        //HACK: move phone to member fields ..
116
        if ($addressFieldsMember) {
117
            if ($addressFieldsBilling) {
118
                if ($phoneField = $addressFieldsBilling->dataFieldByName('Phone')) {
119
                    $addressFieldsBilling->removeByName('Phone');
120
                    $addressFieldsMember->insertAfter('Email', $phoneField);
121
                }
122
            }
123
        }
124
125
126
        //shipping address field
127
128
        if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
129
            //add the important CHECKBOX
130
            $useShippingAddressField = FieldList::create(
131
                HeaderField::create(
132
                    'HasShippingAddressHeader',
133
                    _t('OrderFormAddress.HAS_SHIPPING_ADDRESS_HEADER', 'Delivery Option'),
134
                    3
135
                )
136
            );
137
            $useShippingAddress = $this->order ? $this->order->UseShippingAddress : 0;
0 ignored issues
show
Documentation introduced by
The property UseShippingAddress 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...
138
            if ($shippingAddressFirst) {
139
                $useShippingAddressField->push(
140
                    CheckboxField::create(
141
                        'UseDifferentShippingAddress',
142
                        _t('OrderForm.USE_DIFFERENT_SHIPPING_ADDRESS', 'I need to enter a separate billing address'),
143
                        $useShippingAddress
144
                    )
145
                );
146
                $useShippingAddressField->push(
147
                    HiddenField::create('UseShippingAddress', 'UseShippingAddress', $useShippingAddress)
148
                );
149
            } else {
150
                $useShippingAddressField->push(
151
                    CheckboxField::create(
152
                        'UseShippingAddress',
153
                        _t('OrderForm.USESHIPPINGADDRESS', 'Use separate shipping address'),
154
                        $useShippingAddress
155
                    )
156
                );
157
            }
158
159
            $addressFieldsShipping = FieldList::create();
160
161
            //$addressFieldsShipping->merge($useShippingAddressField);
162
            //now we can add the shipping fields
163
            $shippingAddress = $this->order->CreateOrReturnExistingAddress('ShippingAddress');
164
            $shippingAddressFields = $shippingAddress->getFields($this->orderMember);
165
            $requiredFields = array_merge($requiredFields, $shippingAddress->getRequiredFields());
166
            $addressFieldsShipping->merge($shippingAddressFields);
167
        }
168
169
        //create holder
170
        $allLeftFields = CompositeField::create();
171
        $allLeftFields->addExtraClass('leftOrder');
172
173
        //member fields holder
174
        $leftFieldsMember = CompositeField::create($addressFieldsMember);
175
        $leftFieldsMember->addExtraClass('leftOrderMember');
176
177
        //creating shipping fields holder
178
        $leftFieldsShipping = CompositeField::create($addressFieldsShipping);
179
        $leftFieldsShipping->addExtraClass('leftOrderShipping');
180
181
        //creating billing fields holder
182
        $leftFieldsBilling = CompositeField::create($addressFieldsBilling);
183
        $leftFieldsBilling->addExtraClass('leftOrderBilling');
184
185
        //adding member fields ...
186
        $allLeftFields->push($leftFieldsMember);
187
        if ($useShippingAddressField) {
188
            $leftFieldsShippingOptions = CompositeField::create($useShippingAddressField);
189
            $leftFieldsShippingOptions->addExtraClass('leftOrderShippingOptions');
190
            $allLeftFields->push($leftFieldsShippingOptions);
191
        }
192
        if ($shippingAddressFirst) {
193
            if ($addressFieldsShipping) {
194
                $allLeftFields->push($leftFieldsShipping);
195
            }
196
            $allLeftFields->push($leftFieldsBilling);
197
        } else {
198
            $allLeftFields->push($leftFieldsBilling);
199
            if ($addressFieldsShipping) {
200
                $allLeftFields->push($leftFieldsShipping);
201
            }
202
        }
203
204
        //  ________________  2) Log in / vs Create Account fields - RIGHT-HAND-SIDE fields
205
206
        $rightFields = CompositeField::create();
207
        $rightFields->addExtraClass('rightOrder');
208
        //to do: simplify
209
        if (EcommerceConfig::get('EcommerceRole', 'allow_customers_to_setup_accounts')) {
210
            if ($this->orderDoesNotHaveFullyOperationalMember()) {
211
                //general header
212
                if (!$this->loggedInMember) {
213
                    $rightFields->push(
214
                        //TODO: check EXACT link!!!
215
                        new LiteralField('MemberInfo', '<p class="message good">'._t('OrderForm.MEMBERINFO', 'If you already have an account then please').' <a href="Security/login/?BackURL=/'.urlencode(implode('/', $controller->getURLParams())).'">'._t('OrderForm.LOGIN', 'log in').'</a>.</p>')
216
                    );
217
                }
218
            } else {
219
                if ($this->loggedInMember) {
220
                    $rightFields->push(
221
                        new LiteralField(
222
                            'LoginNote',
223
                            '<p class="message good">'._t('Account.LOGGEDIN', 'You are logged in as ').
224
                            Convert::raw2xml($this->loggedInMember->FirstName).' '.
225
                            Convert::raw2xml($this->loggedInMember->Surname).
226
                            ' ('.Convert::raw2xml($this->loggedInMember->Email).').'.
227
                            ' <a href="/Security/logout/">'.
228
                            _t('Account.LOGOUTNOW', 'Log out?').
229
                            '</a>'.
230
                            '</p>'
231
                        )
232
                    );
233
                }
234
            }
235
            if ($this->orderMember->exists()) {
236
                if ($this->loggedInMember) {
237
                    if ($this->loggedInMember->ID !=  $this->orderMember->ID) {
238
                        $rightFields->push(
239
                            new LiteralField(
240
                                'OrderAddedTo',
241
                                '<p class="message good">'.
242
                                _t('Account.ORDERADDEDTO', 'Order will be added to ').
243
                                Convert::raw2xml($this->orderMember->FirstName).' '.
244
                                Convert::raw2xml($this->orderMember->Surname).' ('.
245
                                Convert::raw2xml($this->orderMember->Email).
246
                                ').</p>'
247
                            )
248
                        );
249
                    }
250
                }
251
            }
252
        }
253
254
        //  ________________  5) Put all the fields in one FieldList
255
256
        $fields = FieldList::create($rightFields, $allLeftFields);
257
258
        //  ________________  6) Actions and required fields creation + Final Form construction
259
260
        $nextButton = new FormAction('saveAddress', _t('OrderForm.NEXT', 'Next'));
261
        $nextButton->addExtraClass('next');
262
        $actions = FieldList::create($nextButton);
263
264
        $validator = OrderFormAddress_Validator::create($requiredFields);
265
266
        parent::__construct($controller, $name, $fields, $actions, $validator);
267
        $this->setAttribute('autocomplete', 'off');
268
        //extensions need to be set after __construct
269
        //extension point
270
        $this->extend('updateFields', $fields);
271
        $this->setFields($fields);
272
        $this->extend('updateActions', $actions);
273
        $this->setActions($actions);
274
        $this->extend('updateValidator', $validator);
275
        $this->setValidator($validator);
276
277
        //this needs to come after the extension calls
278
        foreach ($validator->getRequired() as $requiredField) {
279
            $field = $fields->dataFieldByName($requiredField);
280
            if ($field) {
281
                $field->addExtraClass('required');
282
            }
283
        }
284
285
        //  ________________  7)  Load saved data
286
287
        //we do this first so that Billing and Shipping Address can override this...
288
        if ($this->orderMember) {
289
            $this->loadDataFrom($this->orderMember);
290
        }
291
292
        if ($this->order) {
293
            $this->loadDataFrom($this->order);
294
            if ($billingAddress) {
295
                $this->loadDataFrom($billingAddress);
296
            }
297
            if (EcommerceConfig::get('OrderAddress', 'use_separate_shipping_address')) {
298
                if ($shippingAddress) {
299
                    $this->loadDataFrom($shippingAddress);
0 ignored issues
show
Bug introduced by
The variable $shippingAddress does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
300
                }
301
            }
302
        }
303
304
        //allow updating via decoration
305
        $oldData = Session::get("FormInfo.{$this->FormName()}.data");
306
        if ($oldData && (is_array($oldData) || is_object($oldData))) {
307
            $this->loadDataFrom($oldData);
0 ignored issues
show
Bug introduced by
It seems like $oldData can also be of type object; however, Form::loadDataFrom() does only seem to accept array|object<DataObject>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
308
        }
309
310
        $this->extend('updateOrderFormAddress', $this);
311
    }
312
313
    /**
314
     * Is there a member that is fully operational?
315
     * - saved
316
     * - has password.
317
     *
318
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
319
     */
320
    protected function orderHasFullyOperationalMember()
321
    {
322
        //orderMember is Created in __CONSTRUCT
323
        if ($this->orderMember) {
324
            if ($this->orderMember->exists()) {
325
                if ($this->orderMember->Password) {
326
                    return true;
327
                }
328
            }
329
        }
330
    }
331
332
    /**
333
     * Opposite of orderHasFullyOperationalMember method.
334
     *
335
     * @return bool
336
     */
337
    protected function orderDoesNotHaveFullyOperationalMember()
338
    {
339
        return $this->orderHasFullyOperationalMember() ? false : true;
340
    }
341
342
    /**
343
     * Process the items in the shopping cart from session,
344
     * creating a new {@link Order} record, and updating the
345
     * customer's details {@link Member} record.
346
     *
347
     * {@link Payment} instance is created, linked to the order,
348
     * and payment is processed {@link Payment::processPayment()}
349
     *
350
     * @param array       $data    Form request data submitted from OrderForm
351
     * @param Form        $form    Form object for this action
352
     * @param HTTPRequest $request Request object for this action
0 ignored issues
show
Documentation introduced by
Should the type for parameter $request not be SS_HTTPRequest?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
353
     */
354
    public function saveAddress(array $data, Form $form, SS_HTTPRequest $request)
355
    {
356
        $this->saveAddressDetails($data, $form, $request);
357
358
        $nextStepLink = CheckoutPage::find_next_step_link('orderformaddress');
359
        $this->controller->redirect($nextStepLink);
360
361
        return true;
362
    }
363
364
    /**
365
     * Process the items in the shopping cart from session,
366
     * creating a new {@link Order} record, and updating the
367
     * customer's details {@link Member} record.
368
     *
369
     * {@link Payment} instance is created, linked to the order,
370
     * and payment is processed {@link Payment::processPayment()}
371
     *
372
     * @param array       $data    Form request data submitted from OrderForm
373
     * @param Form        $form    Form object for this action
374
     * @param HTTPRequest $request Request object for this action
0 ignored issues
show
Documentation introduced by
Should the type for parameter $request not be SS_HTTPRequest?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
375
     */
376
    public function saveAddressDetails(array $data, Form $form, SS_HTTPRequest $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
377
    {
378
        Session::set('BillingEcommerceGeocodingFieldValue', empty($data['BillingEcommerceGeocodingField']) ? null : $data['BillingEcommerceGeocodingField']);
379
        Session::set('ShippingEcommerceGeocodingFieldValue', empty($data['ShippingEcommerceGeocodingField']) ? null : $data['ShippingEcommerceGeocodingField']);
380
        $this->saveDataToSession();
381
382
        $data = Convert::raw2sql($data);
383
        //check for cart items
384
        if (!$this->order) {
385
            $form->sessionMessage(_t('OrderForm.ORDERNOTFOUND', 'Your order could not be found.'), 'bad');
386
            $this->controller->redirectBack();
387
388
            return false;
389
        }
390
        if ($this->order && ($this->order->TotalItems($recalculate = true) < 1)) {
391
            // WE DO NOT NEED THE THING BELOW BECAUSE IT IS ALREADY IN THE TEMPLATE AND IT CAN LEAD TO SHOWING ORDER WITH ITEMS AND MESSAGE
392
            $form->sessionMessage(_t('OrderForm.NOITEMSINCART', 'Please add some items to your cart.'), 'bad');
393
            $this->controller->redirectBack();
394
395
            return false;
396
        }
397
398
        //----------- START BY SAVING INTO ORDER
399
        $form->saveInto($this->order);
400
        //----------- --------------------------------
401
402
        //MEMBER
403
        $this->orderMember = $this->createOrFindMember($data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by \Convert::raw2sql($data) on line 382 can also be of type string; however, OrderFormAddress::createOrFindMember() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
404
        if ($this->debug) {
405
            debug::log('debug array from OrderFormAddress:'.implode("\r\n<hr />", $this->debugArray));
406
        }
407
408
        if ($this->orderMember && is_object($this->orderMember)) {
409
            if ($this->memberShouldBeSaved($data)) {
0 ignored issues
show
Bug introduced by
It seems like $data defined by \Convert::raw2sql($data) on line 382 can also be of type string; however, OrderFormAddress::memberShouldBeSaved() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
410
                $form->saveInto($this->orderMember);
411
                $password = $this->validPasswordHasBeenEntered($data);
412
                if ($password) {
413
                    $this->orderMember->changePassword($password);
0 ignored issues
show
Bug introduced by
It seems like $password defined by $this->validPasswordHasBeenEntered($data) on line 411 can also be of type array; however, Member::changePassword() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
414
                }
415
                $this->orderMember->write();
416
            }
417
            if ($this->memberShouldBeLoggedIn($data)) {
0 ignored issues
show
Bug introduced by
It seems like $data defined by \Convert::raw2sql($data) on line 382 can also be of type string; however, OrderFormAddress::memberShouldBeLoggedIn() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
418
                $this->orderMember->LogIn();
419
            }
420
            //this causes ERRORS ....
421
            $this->order->MemberID = $this->orderMember->ID;
0 ignored issues
show
Documentation introduced by
The property MemberID 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...
422
            Session::set('Ecommerce_Member_For_Order', $this->orderMember->ID);
423
        }
424
425
        //BILLING ADDRESS
426
        if ($billingAddress = $this->order->CreateOrReturnExistingAddress('BillingAddress')) {
427
            $form->saveInto($billingAddress);
428
            // NOTE: write should return the new ID of the object
429
            $this->order->BillingAddressID = $billingAddress->write();
0 ignored issues
show
Documentation introduced by
The property BillingAddressID 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...
430
        }
431
432
        // SHIPPING ADDRESS
433
        if (isset($data['UseShippingAddress'])) {
434
            if ($data['UseShippingAddress']) {
435
                if ($shippingAddress = $this->order->CreateOrReturnExistingAddress('ShippingAddress')) {
436
                    $form->saveInto($shippingAddress);
437
                    // NOTE: write should return the new ID of the object
438
                    $this->order->ShippingAddressID = $shippingAddress->write();
0 ignored issues
show
Documentation introduced by
The property ShippingAddressID 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...
439
                }
440
            }
441
        }
442
443
        $this->extend('saveAddressExtension', $data, $form, $order, $this->orderMember);
444
445
        //SAVE ORDER
446
        $this->order->write();
447
448
        //----------------- CLEAR OLD DATA ------------------------------
449
        $this->clearSessionData(); //clears the stored session form data that might have been needed if validation failed
450
        //-----------------------------------------------
451
        return true;
452
    }
453
454
    /**
455
     * saves the form into session.
456
     *
457
     * @param array $data - data from form.
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
458
     */
459
    public function saveDataToSession()
460
    {
461
        $data = $this->getData();
462
        unset($data['AccountInfo']);
463
        unset($data['LoginDetails']);
464
        unset($data['LoggedInAsNote']);
465
        unset($data['PasswordCheck1']);
466
        unset($data['PasswordCheck2']);
467
        Session::set("FormInfo.{$this->FormName()}.data", $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a string.

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...
468
    }
469
470
    /**
471
     * clear the form data (after the form has been submitted and processed).
472
     */
473
    public function clearSessionData()
474
    {
475
        $this->clearMessage();
476
        Session::set("FormInfo.{$this->FormName()}.data", null);
477
    }
478
479
    /**
480
     * Works out the most likely member for the order after submission of the form.
481
     * It returns a member if appropriate.
482
     * 1. does the order already have a member that is not a shop-admin - if so - DONE.
483
     * 2. shop allows creation of member? - if NOT return NULL
484
     * A. is the logged in member the shop admin placing an order on behalf of someone else?
485
     * A1. is the email entered different from the admin email?
486
     * A2. attach to other member as new one or existing one.
487
     * 3. can the entered data be used? - if
488
     * 4. is there no member logged in yet? - If there is one return null, member is already linked to order.
489
     * 5. find member from data entered (even if not logged in)
490
     * 6. At this stage, if we dont have a member, we will create one!
491
     * 7. We do one last check to see if we are allowed to create one.
492
     *
493
     * @param array - form data - should include $data[uniqueField....] - e.g. $data["Email"]
494
     *
495
     * @return Member | Null
496
     **/
497
    protected function createOrFindMember(array $data)
498
    {
499
        //get the best available from order.
500
        $this->orderMember = $this->order->CreateOrReturnExistingMember(false);
501
        $orderPlacedByShopAdmin = ($this->loggedInMember && $this->loggedInMember->IsShopAdmin()) ? true : false;
502
        //1. does the order already have a member
503
        if ($this->orderMember->exists() && !$orderPlacedByShopAdmin) {
504
            if ($this->debug) {
505
                $this->debugArray[] = '1. the order already has a member';
506
            }
507
        } else {
508
            //special shop admin situation:
509
            if ($orderPlacedByShopAdmin) {
510
                if ($this->debug) {
511
                    $this->debugArray[] = 'A1. shop admin places order ';
512
                }
513
                //2. does email match shopadmin email
514
                if ($newEmail = $this->enteredEmailAddressDoesNotMatchLoggedInUser($data)) {
515
                    $this->orderMember = null;
516
                    if ($this->debug) {
517
                        $this->debugArray[] = 'A2. email does not match shopadmin email - reset orderMember';
518
                    }
519
                    $this->orderMember = $this->anotherExistingMemberWithSameUniqueFieldValue($data);
520
                    if ($this->orderMember) {
521
                        if ($this->debug) {
522
                            $this->debugArray[] = 'A3. the other member already exists';
523
                        }
524
                    } elseif ($this->memberShouldBeCreated($data)) {
525
                        if ($this->debug) {
526
                            $this->debugArray[] = 'A4. No other member found - creating new one';
527
                        }
528
                        $this->orderMember = Member::create();
529
                        $this->orderMember->Email = Convert::raw2sql($newEmail);
530
                        $this->orderMember->write($forceCreation = true);
531
                        $this->newlyCreatedMemberID = $this->orderMember->ID;
532
                    }
533
                }
534
            } else {
535
                if ($this->debug) {
536
                    $this->debugArray[] = '2. shop allows creation of member';
537
                }
538
                $this->orderMember = null;
539
540
                //3. can the entered data be used?
541
                //member that will be added does not exist somewhere else.
542
                if ($this->uniqueMemberFieldCanBeUsed($data)) {
543
                    if ($this->debug) {
544
                        $this->debugArray[] = '3. can the entered data be used?';
545
                    }
546
                    // 4. is there no member logged in yet?
547
                    //no logged in member
548
                    if (!$this->loggedInMember) {
549
                        if ($this->debug) {
550
                            $this->debugArray[] = '4. is there no member logged in yet?';
551
                        }
552
                        //5. find member from data entered (even if not logged in)
553
                        //another member with the same email?
554
555
                        if ($this->debug) {
556
                            $this->debugArray[] = '5. find member from data entered (even if not logged in)';
557
                        }
558
                        $this->orderMember = $this->anotherExistingMemberWithSameUniqueFieldValue($data);
559
560
                        //6. At this stage, if we dont have a member, we will create one!
561
                        //in case we still dont have a member AND we should create a member for every customer, then we do this below...
562
                        if (!$this->orderMember) {
563
                            if ($this->debug) {
564
                                $this->debugArray[] = '6. No other member found';
565
                            }
566
                            // 7. We do one last check to see if we are allowed to create one
567
                            //are we allowed to create a member?
568
                            if ($this->memberShouldBeCreated($data)) {
569
                                if ($this->debug) {
570
                                    $this->debugArray[] = '7. We do one last check to see if we are allowed to create one. CREATE NEW MEMBER';
571
                                }
572
                                $this->orderMember = $this->order->CreateOrReturnExistingMember(false);
573
                                $this->orderMember->write($forceCreation = true);
574
                                //this is safe because it is memberShouldBeCreated ...
575
                                $this->newlyCreatedMemberID = $this->orderMember->ID;
576
                            }
577
                        }
578
                    }
579
                }
580
            }
581
        }
582
583
        return $this->orderMember;
584
    }
585
586
    /**
587
     * Should a new member be created?
588
     *
589
     * @Todo: explain why password needs to be more than three characters...
590
     * @todo: create class that checks if password is good enough
591
     *
592
     * @param array - form data - should include $data[uniqueField....] - e.g. $data["Email"]
593
     *
594
     * @return bool
595
     **/
596
    protected function memberShouldBeCreated(array $data)
597
    {
598
        //shop admin and
599
        //data entered does not match shop admin and
600
        //data entered does not match existing member...
601
        //TRUE!
602
        if ($this->loggedInMember && $this->loggedInMember->IsShopAdmin()) {
603
            if ($this->enteredEmailAddressDoesNotMatchLoggedInUser($data)) {
604
                if ($this->anotherExistingMemberWithSameUniqueFieldValue($data)) {
0 ignored issues
show
Coding Style introduced by
The if-else statement can be simplified to return !$this->anotherEx...niqueFieldValue($data);.
Loading history...
605
                    return false;
606
                } else {
607
                    return true;
608
                }
609
            }
610
        }
611
        // already logged in or already created...
612
        // FALSE!
613
        elseif ($this->loggedInMember || $this->newlyCreatedMemberID) {
614
            return false;
615
        }
616
        // no other user exists with the email...
617
        // TRUE!
618
        else {
619
            if ($this->anotherExistingMemberWithSameUniqueFieldValue($data)) {
0 ignored issues
show
Coding Style introduced by
The if-else statement can be simplified to return !$this->anotherEx...niqueFieldValue($data);.
Loading history...
620
                return false;
621
            } else {
622
                return true;
623
            }
624
        }
625
        //defaults to FALSE...
626
        return false;
627
    }
628
629
    /**
630
     * @param array - form data - should include $data[uniqueField....] - e.g. $data["Email"]
631
     *
632
     * @return bool
633
     **/
634
    protected function memberShouldBeSaved(array $data)
635
    {
636
637
        //new members always need to be saved
638
        $newMember = (
639
            $this->memberShouldBeCreated($data) ||
640
            $this->newlyCreatedMemberID
641
        ) ? true : false;
642
643
        // existing logged in members need to be saved if they are updateable
644
        // AND do not match someone else...
645
        $updateableMember = (
646
            $this->loggedInMember &&
647
            !$this->anotherExistingMemberWithSameUniqueFieldValue($data) &&
648
            EcommerceConfig::get('EcommerceRole', 'automatically_update_member_details')
649
        ) ? true : false;
650
651
        // logged in member is shop admin and members are updateable...
652
        $memberIsShopAdmin = (
653
            $this->loggedInMember &&
654
            $this->loggedInMember->IsShopAdmin() &&
655
            EcommerceConfig::get('EcommerceRole', 'automatically_update_member_details')
656
        ) ? true : false;
657
        if ($newMember || $updateableMember || $memberIsShopAdmin) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $newMember || $up... || $memberIsShopAdmin;.
Loading history...
658
            return true;
659
        }
660
661
        return false;
662
    }
663
664
    /**
665
     * returns TRUE if
666
     * - the member is not logged in
667
     * - the member is new AND
668
     * - the password is valid.
669
     *
670
     * @param array - form data - should include $data[uniqueField....] - e.g. $data["Email"]
671
     *
672
     * @return bool
673
     **/
674
    protected function memberShouldBeLoggedIn(array $data)
675
    {
676
        if (!$this->loggedInMember) {
677
            if ($this->newlyCreatedMemberID && $this->validPasswordHasBeenEntered($data)) {
678
                return true;
679
            }
680
        }
681
682
        return false;
683
    }
684
685
    /**
686
     * returns TRUE if
687
     * - there is no existing member with the same value in the unique field
688
     * - OR the member is not logged in.
689
     * - OR the member is a Shop Admin (we assume they are placing an order on behalf of someone else).
690
     * returns FALSE if
691
     * - the unique field already exists in another member
692
     * - AND the member being "tested" is already logged in...
693
     * in that case the logged in member tries to take on another identity.
694
     * If you are not logged BUT the the unique field is used by an existing member then we can still
695
     * use the field - we just CAN NOT log in the member.
696
     * This method needs to be public because it is used by the OrderForm_Validator (see below).
697
     *
698
     * @param array - form data - should include $data[uniqueField....] - e.g. $data["Email"]
699
     *
700
     * @return bool
701
     **/
702
    public function uniqueMemberFieldCanBeUsed(array $data)
703
    {
704
        if ($this->loggedInMember && $this->anotherExistingMemberWithSameUniqueFieldValue($data)) {
705
            //there is an exception for shop admins
706
            //who can place an order on behalve of a customer.
707
            if ($this->loggedInMember->IsShopAdmin()) {
708
                //REMOVED PART:
709
                //but NOT when the member placing the Order is the ShopAdmin
710
                //AND there is another member with the same credentials.
711
                //because in that case the ShopAdmin is not placing an order
712
                //on behalf of someone else.
713
                //that is,
714
                //if($this->orderMember->ID == $this->loggedInMember->ID) {
715
                //	return false;
716
                //}
717
            } else {
718
                return false;
719
            }
720
        }
721
722
        return true;
723
    }
724
725
    /**
726
     * returns existing member if it already exists and it is not the logged-in one.
727
     * Based on the unique field (email)).
728
     *
729
     * @param array - form data - should include $data[uniqueField....] - e.g. $data["Email"]
730
     **/
731
    protected function anotherExistingMemberWithSameUniqueFieldValue(array $data)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
732
    {
733
        $uniqueFieldName = Member::get_unique_identifier_field();
0 ignored issues
show
Deprecated Code introduced by
The method Member::get_unique_identifier_field() has been deprecated with message: 4.0 Use the "Member.unique_identifier_field" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
734
        //The check below covers both Scenario 3 and 4....
735
        if (isset($data[$uniqueFieldName])) {
736
            if ($this->loggedInMember) {
737
                $currentUserID = $this->loggedInMember->ID;
738
            } else {
739
                $currentUserID = 0;
740
            }
741
            $uniqueFieldValue = $data[$uniqueFieldName];
742
            //no need to convert raw2sql as this has already been done.
743
            return Member::get()
744
                ->filter(
745
                    array(
746
                        $uniqueFieldName => $uniqueFieldValue,
747
                    )
748
                )
749
                ->exclude(
750
                    array(
751
                        'ID' => $currentUserID,
752
                    )
753
                )
754
                ->First();
755
        }
756
        user_error('No email data was set, suspicious transaction', E_USER_WARNING);
757
758
        return;
759
    }
760
761
    /**
762
     * returns the email if
763
     * - user is logged in already
764
     * - user's email in DB does not match email entered.
765
     *
766
     * @param array
767
     *
768
     * @return string | false
769
     */
770
    protected function enteredEmailAddressDoesNotMatchLoggedInUser($data)
771
    {
772
        if ($this->loggedInMember) {
773
            $DBUniqueFieldName = $this->loggedInMember->Email;
774
            if ($DBUniqueFieldName) {
775
                $uniqueFieldName = Member::get_unique_identifier_field();
0 ignored issues
show
Deprecated Code introduced by
The method Member::get_unique_identifier_field() has been deprecated with message: 4.0 Use the "Member.unique_identifier_field" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
776
                if (isset($data[$uniqueFieldName])) {
777
                    $enteredUniqueFieldName = $data[$uniqueFieldName];
778
                    if ($enteredUniqueFieldName) {
779
                        if ($DBUniqueFieldName != $enteredUniqueFieldName) {
780
                            return $enteredUniqueFieldName;
781
                        }
782
                    }
783
                }
784
            }
785
        }
786
787
        return false;
788
    }
789
790
    /**
791
     * Check if the password is good enough.
792
     *
793
     * @param data (from form)
794
     *
795
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be array|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
796
     */
797
    protected function validPasswordHasBeenEntered($data)
798
    {
799
        return ShopAccountForm_PasswordValidator::clean_password($data);
800
    }
801
}
802