Completed
Push — master ( 613b2e...a2195f )
by Nicolaas
03:28
created

EcommerceTaskCartCleanup::run()   F

Complexity

Conditions 51
Paths > 20000

Size

Total Lines 300
Code Lines 203

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 51
dl 0
loc 300
rs 2
c 0
b 0
f 0
eloc 203
nc 2377728
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
4
/**
5
 * @description: cleans up old (abandonned) carts...
6
 *
7
 *
8
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
9
 * @package: ecommerce
10
 * @sub-package: tasks
11
 * @inspiration: Silverstripe Ltd, Jeremy
12
 **/
13
class EcommerceTaskCartCleanup extends BuildTask
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
14
{
15
    /**
16
     * Standard SS Variable
17
     * TODO: either remove or add to all tasks.
18
     */
19
    private static $allowed_actions = array(
0 ignored issues
show
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
20
        '*' => 'ADMIN',
21
        '*' => 'SHOPADMIN',
22
    );
23
24
    protected $title = 'Clear old carts';
25
26
    protected $description = 'Deletes abandonned carts (add ?limit=xxxx to the end of the URL to set the number of records (xxx = number of records) to be deleted in one load).';
27
28
    /**
29
     * Output feedback about task?
30
     *
31
     * @var bool
32
     */
33
    public $verbose = false;
34
35
    /**
36
     * run in verbose mode.
37
     */
38
    public static function run_on_demand()
39
    {
40
        $obj = new self();
41
        $obj->verbose = true;
42
        $obj->run(null);
43
    }
44
45
    /**
46
     * runs the task without output.
47
     */
48
    public function runSilently()
49
    {
50
        $this->verbose = false;
51
52
        return $this->run(null);
53
    }
54
    /**
55
     *@return int - number of carts destroyed
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|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...
56
     **/
57
    public function run($request)
58
    {
59
        if ($this->verbose) {
60
            $this->flush();
61
            $countAll = DB::query('SELECT COUNT("ID") FROM "Order"')->value();
62
            DB::alteration_message("<h2>deleting empty and abandonned carts (total cart count = $countAll)</h2>.");
63
        }
64
65
        $neverDeleteIfLinkedToMember = EcommerceConfig::get('EcommerceTaskCartCleanup', 'never_delete_if_linked_to_member');
66
        $maximumNumberOfObjectsDeleted = EcommerceConfig::get('EcommerceTaskCartCleanup', 'maximum_number_of_objects_deleted');
67
68
        //LIMITS ...
69
        if ($this->verbose && $request) {
70
            $limitFromGetVar = $request->getVar('limit');
71
            if ($limitFromGetVar) {
72
                $maximumNumberOfObjectsDeleted = intval($limitFromGetVar);
73
            }
74
        }
75
        $limit = '0, '.$maximumNumberOfObjectsDeleted;
0 ignored issues
show
Unused Code introduced by
$limit 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...
76
77
        //sort
78
        $sort = '"Order"."Created" ASC';
79
80
        //join
81
        $leftMemberJoin = 'LEFT JOIN Member ON "Member"."ID" = "Order"."MemberID"';
82
        $joinShort = '"Member"."ID" = "Order"."MemberID"';
83
84
        //ABANDONNED CARTS
85
        $clearMinutes = EcommerceConfig::get('EcommerceTaskCartCleanup', 'clear_minutes');
86
        $createdStepID = OrderStep::get_status_id_from_code('CREATED');
87
        $time = strtotime('-'.$clearMinutes.' minutes');
88
        $where = '"StatusID" = '.$createdStepID." AND UNIX_TIMESTAMP(\"Order\".\"LastEdited\") < '$time'";
89
        if ($neverDeleteIfLinkedToMember) {
90
            $userStatement = 'or have a user associated with it';
91
            $withoutMemberWhere = ' AND "Member"."ID" IS NULL ';
92
            $withMemberWhere = ' OR "Member"."ID" IS NOT NULL ';
93
            $memberDeleteNote = '(Carts linked to a member will NEVER be deleted)';
94
        } else {
95
            $userStatement = '';
96
            $withoutMemberWhere = '  ';
97
            $withMemberWhere = '';
98
            $memberDeleteNote = '(We will also delete carts in this category that are linked to a member)';
99
        }
100
        $oldCarts = Order::get()
101
            ->where($where.$withoutMemberWhere)
102
            ->sort($sort)
103
            ->limit($maximumNumberOfObjectsDeleted);
104
        $oldCarts = $oldCarts->leftJoin('Member', $joinShort);
105
        if ($neverDeleteIfLinkedToMember) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
106
        }
107
        if ($oldCarts->count()) {
108
            $count = 0;
109
            if ($this->verbose) {
110
                $this->flush();
111
                $totalToDeleteSQLObject = DB::query('
112
                    SELECT COUNT(*)
113
                    FROM "Order"
114
                        '.$leftMemberJoin.'
115
                    WHERE '
116
                        .$where
117
                        .$withoutMemberWhere
118
                    .';'
119
                );
120
                $totalToDelete = $totalToDeleteSQLObject->value();
121
                DB::alteration_message('
122
                        <h2>Total number of abandonned carts: '.$totalToDelete.'</h2>
123
                        <br /><b>number of records deleted at one time:</b> '.$maximumNumberOfObjectsDeleted.'
124
                        <br /><b>Criteria:</b> last edited '.$clearMinutes.' (~'.round($clearMinutes / 60 / 24, 2)." days)
125
                        minutes ago or more $memberDeleteNote", 'created');
126
            }
127
            foreach ($oldCarts as $oldCart) {
128
                $count++;
129
                if ($this->verbose) {
130
                    $this->flush();
131
                    DB::alteration_message("$count ... deleting abandonned order #".$oldCart->ID, 'deleted');
132
                }
133
                $this->deleteObject($oldCart);
134
            }
135
        }
136
        if ($this->verbose) {
137
            $this->flush();
138
            $timeLegible = date('Y-m-d H:i:s', $time);
139
            $countCart = DB::query('SELECT COUNT("ID") FROM "Order" WHERE "StatusID" = '.$createdStepID.' ')->value();
140
            $countCartWithinTimeLimit = DB::query('
141
                SELECT COUNT("Order"."ID")
142
                FROM "Order"
143
                    '.$leftMemberJoin.'
144
                WHERE "StatusID" = '.$createdStepID.'
145
                AND
146
                (
147
                    UNIX_TIMESTAMP("Order"."LastEdited") >= '.$time .'
148
                    '.$withMemberWhere.'
149
                );
150
            ')->value();
151
            DB::alteration_message("
152
                    $countCart Orders are still in the CREATED cart state (not submitted),
153
                    $countCartWithinTimeLimit of them are within the time limit (last edited after $timeLegible)
154
                    ".$userStatement." so they are not deleted.",
155
                'created'
156
            );
157
        }
158
159
        //EMPTY ORDERS
160
        $clearMinutes = EcommerceConfig::get('EcommerceTaskCartCleanup', 'clear_minutes_empty_carts');
161
        $time = strtotime('-'.$clearMinutes.' minutes');
162
        $where = "\"StatusID\" = 0 AND UNIX_TIMESTAMP(\"Order\".\"LastEdited\") < '$time'";
163
        $oldCarts = Order::get()
164
            ->where($where)
165
            ->sort($sort)
166
            ->limit($maximumNumberOfObjectsDeleted);
167
        $oldCarts = $oldCarts->leftJoin('Member', $joinShort);
168
        if ($oldCarts->count()) {
169
            $count = 0;
170
            if ($this->verbose) {
171
                $this->flush();
172
                $totalToDelete = DB::query('
173
                    SELECT COUNT(*)
174
                    FROM "Order"
175
                        '.$leftMemberJoin.'
176
                    WHERE '
177
                        .$where
178
                        .$withoutMemberWhere
179
                    .';'
180
                )->value();
181
                DB::alteration_message('
182
                        <h2>Total number of empty carts: '.$totalToDelete.'</h2>
183
                        <br /><b>number of records deleted at one time:</b> '.$maximumNumberOfObjectsDeleted."
184
                        <br /><b>Criteria:</b> there are no order items and
185
                        the order was last edited $clearMinutes minutes ago $memberDeleteNote", 'created');
186
            }
187
            foreach ($oldCarts as $oldCart) {
188
                ++$count;
189
                if ($this->verbose) {
190
                    $this->flush();
191
                    DB::alteration_message("$count ... deleting empty order #".$oldCart->ID, 'deleted');
192
                }
193
                $this->deleteObject($oldCart);
194
            }
195
        }
196
        if ($this->verbose) {
197
            $this->flush();
198
            $timeLegible = date('Y-m-d H:i:s', $time);
199
            $countCart = DB::query('
200
                SELECT COUNT("Order"."ID")
201
                FROM "Order"
202
                    '.$leftMemberJoin.'
203
                WHERE "StatusID" = 0 '
204
            )->value();
205
            $countCartWithinTimeLimit = DB::query('
206
                SELECT COUNT("Order"."ID")
207
                FROM "Order"
208
                    '.$leftMemberJoin.'
209
                WHERE "StatusID" = 0 AND
210
                (
211
                    UNIX_TIMESTAMP("Order"."LastEdited") >= '.$time.'
212
                    '.$withMemberWhere.'
213
                )'
214
            )->value();
215
            DB::alteration_message("
216
                    $countCart Orders are without status at all,
217
                    $countCartWithinTimeLimit are within the time limit (last edited after $timeLegible)
218
                    ".$userStatement."so they are not deleted yet.",
219
                'created'
220
            );
221
        }
222
223
        $oneToMany = EcommerceConfig::get('EcommerceTaskCartCleanup', 'one_to_many_classes');
224
        $oneToOne = EcommerceConfig::get('EcommerceTaskCartCleanup', 'one_to_one_classes');
225
        $manyToMany = EcommerceConfig::get('EcommerceTaskCartCleanup', 'many_to_many_classes');
226
        if (!is_array($oneToOne)) {
227
            $oneToOne = array();
228
        }
229
        if (!is_array($oneToMany)) {
230
            $oneToMany = array();
231
        }
232
        if (!is_array($manyToMany)) {
233
            $manyToMany = array();
234
        }
235
236
        /***********************************************
237
        //CLEANING ONE-TO-ONES
238
        ************************************************/
239
        if ($this->verbose) {
240
            $this->flush();
241
            DB::alteration_message('<h2>Checking one-to-one relationships</h2>.');
242
        }
243
        if (count($oneToOne)) {
244
            foreach ($oneToOne as $orderFieldName => $className) {
245
                if (!in_array($className, $oneToMany) && !in_array($className, $manyToMany)) {
246
                    if ($this->verbose) {
247
                        $this->flush();
248
                        DB::alteration_message("looking for $className objects without link to order.");
249
                    }
250
                    $rows = DB::query("
251
                        SELECT \"$className\".\"ID\"
252
                        FROM \"$className\"
253
                            LEFT JOIN \"Order\"
254
                                ON \"Order\".\"$orderFieldName\" = \"$className\".\"ID\"
255
                        WHERE \"Order\".\"ID\" IS NULL
256
                        LIMIT 0, ".$maximumNumberOfObjectsDeleted);
257
                    //the code below is a bit of a hack, but because of the one-to-one relationship we
258
                    //want to check both sides....
259
                    $oneToOneIDArray = array();
260
                    if ($rows) {
261
                        foreach ($rows as $row) {
262
                            $oneToOneIDArray[$row['ID']] = $row['ID'];
263
                        }
264
                    }
265
                    if (count($oneToOneIDArray)) {
266
                        $unlinkedObjects = $className::get()
267
                            ->filter(array('ID' => $oneToOneIDArray));
268
                        if ($unlinkedObjects->count()) {
269
                            foreach ($unlinkedObjects as $unlinkedObject) {
270
                                if ($this->verbose) {
271
                                    $this->flush();
272
                                    DB::alteration_message('Deleting '.$unlinkedObject->ClassName.' with ID #'.$unlinkedObject->ID.' because it does not appear to link to an order.', 'deleted');
273
                                }
274
                                $this->deleteObject($unlinkedObject);
275
                            }
276
                        } else {
277
                            if ($this->verbose) {
278
                                $this->flush();
279
                                DB::alteration_message("No objects where found for $className even though there appear to be missing links.", 'created');
280
                            }
281
                        }
282
                    } elseif ($this->verbose) {
283
                        $this->flush();
284
                        DB::alteration_message("All references in Order to $className are valid.", 'created');
285
                    }
286
                    if ($this->verbose) {
287
                        $this->flush();
288
                        $countAll = DB::query("SELECT COUNT(\"ID\") FROM \"$className\"")->value();
289
                        $countUnlinkedOnes = DB::query("SELECT COUNT(\"$className\".\"ID\") FROM \"$className\" LEFT JOIN \"Order\" ON \"$className\".\"ID\" = \"Order\".\"$orderFieldName\" WHERE \"Order\".\"ID\" IS NULL")->value();
290
                        DB::alteration_message("In total there are $countAll $className ($orderFieldName), of which there are $countUnlinkedOnes not linked to an order. ", 'created');
291
                        if ($countUnlinkedOnes) {
292
                            DB::alteration_message("There should be NO $orderFieldName ($className) without link to Order - un error is suspected", 'deleted');
293
                        }
294
                    }
295
                }
296
            }
297
        }
298
299
        /***********************************************
300
        //CLEANING ONE-TO-MANY
301
        *************************************************/
302
303
        //one order has many other things so we increase the ability to delete stuff
304
        $maximumNumberOfObjectsDeleted = $maximumNumberOfObjectsDeleted * 25;
305
        if ($this->verbose) {
306
            $this->flush();
307
            DB::alteration_message('<h2>Checking one-to-many relationships</h2>.');
308
        }
309
        if (count($oneToMany)) {
310
            foreach ($oneToMany as $classWithOrderID => $classWithLastEdited) {
311
                if (!in_array($classWithLastEdited, $oneToOne) && !in_array($classWithLastEdited, $manyToMany)) {
312
                    if ($this->verbose) {
313
                        $this->flush();
314
                        DB::alteration_message("looking for $classWithOrderID objects without link to order.");
315
                    }
316
                    $rows = DB::query("
317
                        SELECT \"$classWithOrderID\".\"ID\"
318
                        FROM \"$classWithOrderID\"
319
                            LEFT JOIN \"Order\"
320
                                ON \"Order\".\"ID\" = \"$classWithOrderID\".\"OrderID\"
321
                        WHERE \"Order\".\"ID\" IS NULL
322
                        LIMIT 0, ".$maximumNumberOfObjectsDeleted);
323
                    $oneToManyIDArray = array();
324
                    if ($rows) {
325
                        foreach ($rows as $row) {
326
                            $oneToManyIDArray[$row['ID']] = $row['ID'];
327
                        }
328
                    }
329
                    if (count($oneToManyIDArray)) {
330
                        $unlinkedObjects = $classWithLastEdited::get()
331
                            ->filter(array('ID' => $oneToManyIDArray));
332
                        if ($unlinkedObjects->count()) {
333
                            foreach ($unlinkedObjects as $unlinkedObject) {
334
                                if ($this->verbose) {
335
                                    DB::alteration_message('Deleting '.$unlinkedObject->ClassName.' with ID #'.$unlinkedObject->ID.' because it does not appear to link to an order.', 'deleted');
336
                                }
337
                                $this->deleteObject($unlinkedObject);
338
                            }
339
                        } elseif ($this->verbose) {
340
                            $this->flush();
341
                            DB::alteration_message("$classWithLastEdited objects could not be found even though they were referenced.", 'deleted');
342
                        }
343
                    } elseif ($this->verbose) {
344
                        $this->flush();
345
                        DB::alteration_message("All $classWithLastEdited objects have a reference to a valid order.", 'created');
346
                    }
347
                    if ($this->verbose) {
348
                        $this->flush();
349
                        $countAll = DB::query("SELECT COUNT(\"ID\") FROM \"$classWithLastEdited\"")->value();
350
                        $countUnlinkedOnes = DB::query("SELECT COUNT(\"$classWithOrderID\".\"ID\") FROM \"$classWithOrderID\" LEFT JOIN \"Order\" ON \"$classWithOrderID\".\"OrderID\" = \"Order\".\"ID\" WHERE \"Order\".\"ID\" IS NULL")->value();
351
                        DB::alteration_message("In total there are $countAll $classWithOrderID ($classWithLastEdited), of which there are $countUnlinkedOnes not linked to an order. ", 'created');
352
                    }
353
                }
354
            }
355
        }
356
    }
357
358
    private function flush()
359
    {
360
        if ((php_sapi_name() === 'cli')) {
361
            echo "\n";
362
        } else {
363
            ob_flush();
364
            flush();
365
        }
366
    }
367
368
    private function deleteObject($objectToDelete)
369
    {
370
        $objectToDelete->delete();
371
        $objectToDelete->destroy();
372
    }
373
}
374